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