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