1 /*
   2  * Copyright (c) 2009, 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import java.io.*;
  25 import java.nio.charset.Charset;
  26 import java.text.MessageFormat;
  27 import java.util.*;
  28 import java.util.zip.CRC32;
  29 import java.util.zip.ZipEntry;
  30 import java.util.zip.ZipException;
  31 import java.util.zip.ZipFile;
  32 import java.util.zip.ZipInputStream;
  33 import java.util.zip.ZipOutputStream;
  34 
  35 /**
  36  * A stripped-down version of Jar tool with a "-encoding" option to
  37  * support non-UTF8 encoidng for entry name and comment.
  38  */
  39 public class zip {
  40     String program;
  41     PrintStream out, err;
  42     String fname;
  43     String zname = "";
  44     String[] files;
  45     Charset cs = Charset.forName("UTF-8");
  46 
  47     Map<String, File> entryMap = new HashMap<>();
  48     Set<File> entries = new LinkedHashSet<>();
  49     List<String> paths = new ArrayList<>();
  50 
  51     CRC32 crc32 = new CRC32();
  52     /*
  53      * cflag: create
  54      * uflag: update
  55      * xflag: xtract
  56      * tflag: table
  57      * vflag: verbose
  58      * flag0: no zip compression (store only)
  59      */
  60     boolean cflag, uflag, xflag, tflag, vflag, flag0;
  61 
  62     private static ResourceBundle rsrc;
  63     static {
  64         try {
  65             // just use the jar message
  66             rsrc = ResourceBundle.getBundle("sun.tools.jar.resources.jar");
  67         } catch (MissingResourceException e) {
  68             throw new Error("Fatal: Resource for jar is missing");
  69         }
  70     }
  71 
  72     public zip(PrintStream out, PrintStream err, String program) {
  73         this.out = out;
  74         this.err = err;
  75         this.program = program;
  76     }
  77 
  78     private boolean ok;
  79 
  80     public synchronized boolean run(String args[]) {
  81         ok = true;
  82         if (!parseArgs(args)) {
  83             return false;
  84         }
  85         try {
  86             if (cflag || uflag) {
  87                 if (fname != null) {
  88                     zname = fname.replace(File.separatorChar, '/');
  89                     if (zname.startsWith("./")) {
  90                         zname = zname.substring(2);
  91                     }
  92                 }
  93             }
  94             if (cflag) {
  95                 OutputStream out;
  96                 if (fname != null) {
  97                     out = new FileOutputStream(fname);
  98                 } else {
  99                     out = new FileOutputStream(FileDescriptor.out);
 100                     if (vflag) {
 101                          vflag = false;
 102                     }
 103                 }
 104                 expand(null, files, false);
 105                 create(new BufferedOutputStream(out, 4096));
 106                 out.close();
 107             } else if (uflag) {
 108                 File inputFile = null, tmpFile = null;
 109                 FileInputStream in;
 110                 FileOutputStream out;
 111                 if (fname != null) {
 112                     inputFile = new File(fname);
 113                     String path = inputFile.getParent();
 114                     tmpFile = File.createTempFile("tmp", null,
 115                               new File((path == null) ? "." : path));
 116                     in = new FileInputStream(inputFile);
 117                     out = new FileOutputStream(tmpFile);
 118                 } else {
 119                     in = new FileInputStream(FileDescriptor.in);
 120                     out = new FileOutputStream(FileDescriptor.out);
 121                     vflag = false;
 122                 }
 123                 expand(null, files, true);
 124                 boolean updateOk = update(in, new BufferedOutputStream(out));
 125                 if (ok) {
 126                     ok = updateOk;
 127                 }
 128                 in.close();
 129                 out.close();
 130                 if (fname != null) {
 131                     inputFile.delete();
 132                     if (!tmpFile.renameTo(inputFile)) {
 133                         tmpFile.delete();
 134                         throw new IOException(getMsg("error.write.file"));
 135                     }
 136                     tmpFile.delete();
 137                 }
 138             } else if (tflag) {
 139                 replaceFSC(files);
 140                 if (fname != null) {
 141                     list(fname, files);
 142                 } else {
 143                     InputStream in = new FileInputStream(FileDescriptor.in);
 144                     try{
 145                         list(new BufferedInputStream(in), files);
 146                     } finally {
 147                         in.close();
 148                     }
 149                 }
 150             } else if (xflag) {
 151                 replaceFSC(files);
 152                 if (fname != null && files != null) {
 153                     extract(fname, files);
 154                 } else {
 155                     InputStream in = (fname == null)
 156                         ? new FileInputStream(FileDescriptor.in)
 157                         : new FileInputStream(fname);
 158                     try {
 159                         extract(new BufferedInputStream(in), files);
 160                     } finally {
 161                         in.close();
 162                     }
 163                 }
 164             }
 165         } catch (IOException e) {
 166             fatalError(e);
 167             ok = false;
 168         } catch (Error ee) {
 169             ee.printStackTrace();
 170             ok = false;
 171         } catch (Throwable t) {
 172             t.printStackTrace();
 173             ok = false;
 174         }
 175         out.flush();
 176         err.flush();
 177         return ok;
 178     }
 179 
 180 
 181     boolean parseArgs(String args[]) {
 182         try {
 183             args = parse(args);
 184         } catch (FileNotFoundException e) {
 185             fatalError(formatMsg("error.cant.open", e.getMessage()));
 186             return false;
 187         } catch (IOException e) {
 188             fatalError(e);
 189             return false;
 190         }
 191         int count = 1;
 192         try {
 193             String flags = args[0];
 194             if (flags.startsWith("-")) {
 195                 flags = flags.substring(1);
 196             }
 197             for (int i = 0; i < flags.length(); i++) {
 198                 switch (flags.charAt(i)) {
 199                 case 'c':
 200                     if (xflag || tflag || uflag) {
 201                         usageError();
 202                         return false;
 203                     }
 204                     cflag = true;
 205                     break;
 206                 case 'u':
 207                     if (cflag || xflag || tflag) {
 208                         usageError();
 209                         return false;
 210                     }
 211                     uflag = true;
 212                     break;
 213                 case 'x':
 214                     if (cflag || uflag || tflag) {
 215                         usageError();
 216                         return false;
 217                     }
 218                     xflag = true;
 219                     break;
 220                 case 't':
 221                     if (cflag || uflag || xflag) {
 222                         usageError();
 223                         return false;
 224                     }
 225                     tflag = true;
 226                     break;
 227                 case 'v':
 228                     vflag = true;
 229                     break;
 230                 case 'f':
 231                     fname = args[count++];
 232                     break;
 233                 case '0':
 234                     flag0 = true;
 235                     break;
 236                 default:
 237                     error(formatMsg("error.illegal.option",
 238                                 String.valueOf(flags.charAt(i))));
 239                     usageError();
 240                     return false;
 241                 }
 242             }
 243         } catch (ArrayIndexOutOfBoundsException e) {
 244             usageError();
 245             return false;
 246         }
 247         if (!cflag && !tflag && !xflag && !uflag) {
 248             error(getMsg("error.bad.option"));
 249             usageError();
 250             return false;
 251         }
 252         /* parse file arguments */
 253         int n = args.length - count;
 254         if (n > 0) {
 255             int k = 0;
 256             String[] nameBuf = new String[n];
 257             try {
 258                 for (int i = count; i < args.length; i++) {
 259                     if (args[i].equals("-encoding")) {
 260                         cs = Charset.forName(args[++i]);
 261                     } else if (args[i].equals("-C")) {
 262                         /* change the directory */
 263                         String dir = args[++i];
 264                         dir = (dir.endsWith(File.separator) ?
 265                                dir : (dir + File.separator));
 266                         dir = dir.replace(File.separatorChar, '/');
 267                         while (dir.indexOf("//") > -1) {
 268                             dir = dir.replace("//", "/");
 269                         }
 270                         paths.add(dir.replace(File.separatorChar, '/'));
 271                         nameBuf[k++] = dir + args[++i];
 272                     } else {
 273                         nameBuf[k++] = args[i];
 274                     }
 275                 }
 276             } catch (ArrayIndexOutOfBoundsException e) {
 277                 e.printStackTrace();
 278                 usageError();
 279                 return false;
 280             }
 281             if (k != 0) {
 282                 files = new String[k];
 283                 System.arraycopy(nameBuf, 0, files, 0, k);
 284             }
 285         } else if (cflag || uflag) {
 286             error(getMsg("error.bad.uflag"));
 287             usageError();
 288             return false;
 289         }
 290         return true;
 291     }
 292 
 293     void expand(File dir, String[] files, boolean isUpdate) {
 294         if (files == null) {
 295             return;
 296         }
 297         for (int i = 0; i < files.length; i++) {
 298             File f;
 299             if (dir == null) {
 300                 f = new File(files[i]);
 301             } else {
 302                 f = new File(dir, files[i]);
 303             }
 304             if (f.isFile()) {
 305                 if (entries.add(f)) {
 306                     if (isUpdate)
 307                         entryMap.put(entryName(f.getPath()), f);
 308                 }
 309             } else if (f.isDirectory()) {
 310                 if (entries.add(f)) {
 311                     if (isUpdate) {
 312                         String dirPath = f.getPath();
 313                         dirPath = (dirPath.endsWith(File.separator)) ? dirPath :
 314                             (dirPath + File.separator);
 315                         entryMap.put(entryName(dirPath), f);
 316                     }
 317                     expand(f, f.list(), isUpdate);
 318                 }
 319             } else {
 320                 error(formatMsg("error.nosuch.fileordir", String.valueOf(f)));
 321                 ok = false;
 322             }
 323         }
 324     }
 325 
 326     void create(OutputStream out) throws IOException
 327     {
 328         try (ZipOutputStream zos = new ZipOutputStream(out, cs)) {
 329             if (flag0) {
 330                 zos.setMethod(ZipOutputStream.STORED);
 331             }
 332             for (File file: entries) {
 333                 addFile(zos, file);
 334             }
 335         }
 336     }
 337 
 338     boolean update(InputStream in, OutputStream out) throws IOException {
 339         try (ZipInputStream zis = new ZipInputStream(in, cs);
 340              ZipOutputStream zos = new ZipOutputStream(out, cs))
 341         {
 342             ZipEntry e = null;
 343             byte[] buf = new byte[1024];
 344             int n = 0;
 345 
 346             // put the old entries first, replace if necessary
 347             while ((e = zis.getNextEntry()) != null) {
 348                 String name = e.getName();
 349                 if (!entryMap.containsKey(name)) { // copy the old stuff
 350                     // do our own compression
 351                     ZipEntry e2 = new ZipEntry(name);
 352                     e2.setMethod(e.getMethod());
 353                     e2.setTime(e.getTime());
 354                     e2.setComment(e.getComment());
 355                     e2.setExtra(e.getExtra());
 356                     if (e.getMethod() == ZipEntry.STORED) {
 357                         e2.setSize(e.getSize());
 358                         e2.setCrc(e.getCrc());
 359                     }
 360                     zos.putNextEntry(e2);
 361                     while ((n = zis.read(buf, 0, buf.length)) != -1) {
 362                         zos.write(buf, 0, n);
 363                     }
 364                 } else { // replace with the new files
 365                     File f = entryMap.get(name);
 366                     addFile(zos, f);
 367                     entryMap.remove(name);
 368                     entries.remove(f);
 369                 }
 370             }
 371 
 372             // add the remaining new files
 373             for (File f : entries) {
 374                 addFile(zos, f);
 375             }
 376         }
 377         return true;
 378     }
 379 
 380     private String entryName(String name) {
 381         name = name.replace(File.separatorChar, '/');
 382         String matchPath = "";
 383         for (String path : paths) {
 384             if (name.startsWith(path) && (path.length() > matchPath.length())) {
 385                 matchPath = path;
 386             }
 387         }
 388         name = name.substring(matchPath.length());
 389 
 390         if (name.startsWith("/")) {
 391             name = name.substring(1);
 392         } else if (name.startsWith("./")) {
 393             name = name.substring(2);
 394         }
 395         return name;
 396     }
 397 
 398     void addFile(ZipOutputStream zos, File file) throws IOException {
 399         String name = file.getPath();
 400         boolean isDir = file.isDirectory();
 401         if (isDir) {
 402             name = name.endsWith(File.separator) ? name :
 403                 (name + File.separator);
 404         }
 405         name = entryName(name);
 406 
 407         if (name.equals("") || name.equals(".") || name.equals(zname)) {
 408             return;
 409         }
 410 
 411         long size = isDir ? 0 : file.length();
 412 
 413         if (vflag) {
 414             out.print(formatMsg("out.adding", name));
 415         }
 416         ZipEntry e = new ZipEntry(name);
 417         e.setTime(file.lastModified());
 418         if (size == 0) {
 419             e.setMethod(ZipEntry.STORED);
 420             e.setSize(0);
 421             e.setCrc(0);
 422         } else if (flag0) {
 423             e.setSize(size);
 424             e.setMethod(ZipEntry.STORED);
 425             crc32File(e, file);
 426         }
 427         zos.putNextEntry(e);
 428         if (!isDir) {
 429             byte[] buf = new byte[8192];
 430             int len;
 431             InputStream is = new BufferedInputStream(new FileInputStream(file));
 432             while ((len = is.read(buf, 0, buf.length)) != -1) {
 433                 zos.write(buf, 0, len);
 434             }
 435             is.close();
 436         }
 437         zos.closeEntry();
 438         /* report how much compression occurred. */
 439         if (vflag) {
 440             size = e.getSize();
 441             long csize = e.getCompressedSize();
 442             out.print(formatMsg2("out.size", String.valueOf(size),
 443                         String.valueOf(csize)));
 444             if (e.getMethod() == ZipEntry.DEFLATED) {
 445                 long ratio = 0;
 446                 if (size != 0) {
 447                     ratio = ((size - csize) * 100) / size;
 448                 }
 449                 output(formatMsg("out.deflated", String.valueOf(ratio)));
 450             } else {
 451                 output(getMsg("out.stored"));
 452             }
 453         }
 454     }
 455 
 456     private void crc32File(ZipEntry e, File f) throws IOException {
 457         InputStream is = new BufferedInputStream(new FileInputStream(f));
 458         byte[] buf = new byte[8192];
 459         crc32.reset();
 460         int r = 0;
 461         int nread = 0;
 462         long len = f.length();
 463         while ((r = is.read(buf)) != -1) {
 464             nread += r;
 465             crc32.update(buf, 0, r);
 466         }
 467         is.close();
 468         if (nread != (int) len) {
 469             throw new ZipException(formatMsg(
 470                         "error.incorrect.length", f.getPath()));
 471         }
 472         e.setCrc(crc32.getValue());
 473     }
 474 
 475     void replaceFSC(String files[]) {
 476         if (files != null) {
 477             for (String file : files) {
 478                 file = file.replace(File.separatorChar, '/');
 479             }
 480         }
 481     }
 482 
 483     Set<ZipEntry> newDirSet() {
 484         return new HashSet<ZipEntry>() {
 485             private static final long serialVersionUID = 4547977575248028254L;
 486 
 487             public boolean add(ZipEntry e) {
 488                 return (e == null || super.add(e));
 489             }};
 490     }
 491 
 492     void updateLastModifiedTime(Set<ZipEntry> zes) throws IOException {
 493         for (ZipEntry ze : zes) {
 494             long lastModified = ze.getTime();
 495             if (lastModified != -1) {
 496                 File f = new File(ze.getName().replace('/', File.separatorChar));
 497                 f.setLastModified(lastModified);
 498             }
 499         }
 500     }
 501 
 502     void extract(InputStream in, String files[]) throws IOException {
 503         ZipInputStream zis = new ZipInputStream(in, cs);
 504         ZipEntry e;
 505         Set<ZipEntry> dirs = newDirSet();
 506         while ((e = zis.getNextEntry()) != null) {
 507             if (files == null) {
 508                 dirs.add(extractFile(zis, e));
 509             } else {
 510                 String name = e.getName();
 511                 for (String file : files) {
 512                     if (name.startsWith(file)) {
 513                         dirs.add(extractFile(zis, e));
 514                         break;
 515                     }
 516                 }
 517             }
 518         }
 519         updateLastModifiedTime(dirs);
 520     }
 521 
 522     void extract(String fname, String files[]) throws IOException {
 523         try (ZipFile zf = new ZipFile(fname, cs)) {
 524             Set<ZipEntry> dirs = newDirSet();
 525             Enumeration<? extends ZipEntry> zes = zf.entries();
 526             while (zes.hasMoreElements()) {
 527                 ZipEntry e = zes.nextElement();
 528                 if (files == null) {
 529                     dirs.add(extractFile(zf.getInputStream(e), e));
 530                 } else {
 531                     String name = e.getName();
 532                     for (String file : files) {
 533                         if (name.startsWith(file)) {
 534                             dirs.add(extractFile(zf.getInputStream(e), e));
 535                             break;
 536                         }
 537                     }
 538                 }
 539             }
 540             updateLastModifiedTime(dirs);
 541         }
 542     }
 543 
 544     ZipEntry extractFile(InputStream is, ZipEntry e) throws IOException {
 545         ZipEntry rc = null;
 546         String name = e.getName();
 547         File f = new File(e.getName().replace('/', File.separatorChar));
 548         if (e.isDirectory()) {
 549             if (f.exists()) {
 550                 if (!f.isDirectory()) {
 551                     throw new IOException(formatMsg("error.create.dir",
 552                         f.getPath()));
 553                 }
 554             } else {
 555                 if (!f.mkdirs()) {
 556                     throw new IOException(formatMsg("error.create.dir",
 557                         f.getPath()));
 558                 } else {
 559                     rc = e;
 560                 }
 561             }
 562             if (vflag) {
 563                 output(formatMsg("out.create", name));
 564             }
 565         } else {
 566             if (f.getParent() != null) {
 567                 File d = new File(f.getParent());
 568                 if (!d.exists() && !d.mkdirs() || !d.isDirectory()) {
 569                     throw new IOException(formatMsg(
 570                         "error.create.dir", d.getPath()));
 571                 }
 572             }
 573             OutputStream os = new FileOutputStream(f);
 574             byte[] b = new byte[8192];
 575             int len;
 576             try {
 577                 while ((len = is.read(b, 0, b.length)) != -1) {
 578                     os.write(b, 0, len);
 579                 }
 580             } finally {
 581                 if (is instanceof ZipInputStream)
 582                     ((ZipInputStream)is).closeEntry();
 583                 else
 584                     is.close();
 585                 os.close();
 586             }
 587             if (vflag) {
 588                 if (e.getMethod() == ZipEntry.DEFLATED) {
 589                     output(formatMsg("out.inflated", name));
 590                 } else {
 591                     output(formatMsg("out.extracted", name));
 592                 }
 593             }
 594         }
 595         long lastModified = e.getTime();
 596         if (lastModified != -1) {
 597             f.setLastModified(lastModified);
 598         }
 599         return rc;
 600     }
 601 
 602     void list(InputStream in, String files[]) throws IOException {
 603         ZipInputStream zis = new ZipInputStream(in, cs);
 604         ZipEntry e;
 605         while ((e = zis.getNextEntry()) != null) {
 606             zis.closeEntry();
 607             printEntry(e, files);
 608         }
 609     }
 610 
 611     void list(String fname, String files[]) throws IOException {
 612         try (ZipFile zf = new ZipFile(fname, cs)) {
 613             Enumeration<? extends ZipEntry> zes = zf.entries();
 614             while (zes.hasMoreElements()) {
 615                 printEntry(zes.nextElement(), files);
 616             }
 617         }
 618     }
 619 
 620     void printEntry(ZipEntry e, String[] files) throws IOException {
 621         if (files == null) {
 622             printEntry(e);
 623         } else {
 624             String name = e.getName();
 625             for (String file : files) {
 626                 if (name.startsWith(file)) {
 627                     printEntry(e);
 628                     return;
 629                 }
 630             }
 631         }
 632     }
 633 
 634     void printEntry(ZipEntry e) throws IOException {
 635         if (vflag) {
 636             StringBuilder sb = new StringBuilder();
 637             String s = Long.toString(e.getSize());
 638             for (int i = 6 - s.length(); i > 0; --i) {
 639                 sb.append(' ');
 640             }
 641             sb.append(s).append(' ').append(new Date(e.getTime()).toString());
 642             sb.append(' ').append(e.getName());
 643             output(sb.toString());
 644         } else {
 645             output(e.getName());
 646         }
 647     }
 648 
 649     void usageError() {
 650         error(
 651         "Usage: zip {ctxu}[vf0] [zip-file] [-encoding encname][-C dir] files ...\n" +
 652         "Options:\n" +
 653         "   -c  create new archive\n" +
 654         "   -t  list table of contents for archive\n" +
 655         "   -x  extract named (or all) files from archive\n" +
 656         "   -u  update existing archive\n" +
 657         "   -v  generate verbose output on standard output\n" +
 658         "   -f  specify archive file name\n" +
 659         "   -0  store only; use no ZIP compression\n" +
 660         "   -C  change to the specified directory and include the following file\n" +
 661         "If any file is a directory then it is processed recursively.\n");
 662     }
 663 
 664     void fatalError(Exception e) {
 665         e.printStackTrace();
 666     }
 667 
 668 
 669     void fatalError(String s) {
 670         error(program + ": " + s);
 671     }
 672 
 673 
 674     protected void output(String s) {
 675         out.println(s);
 676     }
 677 
 678     protected void error(String s) {
 679         err.println(s);
 680     }
 681 
 682     private String getMsg(String key) {
 683         try {
 684             return (rsrc.getString(key));
 685         } catch (MissingResourceException e) {
 686             throw new Error("Error in message file");
 687         }
 688     }
 689 
 690     private String formatMsg(String key, String arg) {
 691         String msg = getMsg(key);
 692         String[] args = new String[1];
 693         args[0] = arg;
 694         return MessageFormat.format(msg, (Object[]) args);
 695     }
 696 
 697     private String formatMsg2(String key, String arg, String arg1) {
 698         String msg = getMsg(key);
 699         String[] args = new String[2];
 700         args[0] = arg;
 701         args[1] = arg1;
 702         return MessageFormat.format(msg, (Object[]) args);
 703     }
 704 
 705     public static String[] parse(String[] args) throws IOException
 706     {
 707         ArrayList<String> newArgs = new ArrayList<String>(args.length);
 708         for (int i = 0; i < args.length; i++) {
 709             String arg = args[i];
 710             if (arg.length() > 1 && arg.charAt(0) == '@') {
 711                 arg = arg.substring(1);
 712                 if (arg.charAt(0) == '@') {
 713                     newArgs.add(arg);
 714                 } else {
 715                     loadCmdFile(arg, newArgs);
 716                 }
 717             } else {
 718                 newArgs.add(arg);
 719             }
 720         }
 721         return newArgs.toArray(new String[newArgs.size()]);
 722     }
 723 
 724     private static void loadCmdFile(String name, List<String> args) throws IOException
 725     {
 726         Reader r = new BufferedReader(new FileReader(name));
 727         StreamTokenizer st = new StreamTokenizer(r);
 728         st.resetSyntax();
 729         st.wordChars(' ', 255);
 730         st.whitespaceChars(0, ' ');
 731         st.commentChar('#');
 732         st.quoteChar('"');
 733         st.quoteChar('\'');
 734         while (st.nextToken() != StreamTokenizer.TT_EOF) {
 735             args.add(st.sval);
 736         }
 737         r.close();
 738     }
 739 
 740     public static void main(String args[]) {
 741         zip z = new zip(System.out, System.err, "zip");
 742         System.exit(z.run(args) ? 0 : 1);
 743     }
 744 }