1 /*
   2  * Copyright (c) 1996, 2012, 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 package sun.tools.jar;
  27 
  28 import java.io.*;
  29 import java.nio.file.Path;
  30 import java.nio.file.Files;
  31 import java.util.*;
  32 import java.util.zip.*;
  33 import java.util.jar.*;
  34 import java.util.jar.Manifest;
  35 import java.text.MessageFormat;
  36 import sun.misc.JarIndex;
  37 import static sun.misc.JarIndex.INDEX_NAME;
  38 import static java.util.jar.JarFile.MANIFEST_NAME;
  39 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
  40 
  41 /**
  42  * This class implements a simple utility for creating files in the JAR
  43  * (Java Archive) file format. The JAR format is based on the ZIP file
  44  * format, with optional meta-information stored in a MANIFEST entry.
  45  */
  46 public
  47 class Main {
  48     String program;
  49     PrintStream out, err;
  50     String fname, mname, ename;
  51     String zname = "";
  52     String[] files;
  53     String rootjar = null;
  54 
  55     // An entryName(path)->File map generated during "expand", it helps to
  56     // decide whether or not an existing entry in a jar file needs to be
  57     // replaced, during the "update" operation.
  58     Map<String, File> entryMap = new HashMap<String, File>();
  59 
  60     // All files need to be added/updated.
  61     Set<File> entries = new LinkedHashSet<File>();
  62 
  63     // Directories specified by "-C" operation.
  64     Set<String> paths = new HashSet<String>();
  65 
  66     /*
  67      * cflag: create
  68      * uflag: update
  69      * xflag: xtract
  70      * tflag: table
  71      * vflag: verbose
  72      * flag0: no zip compression (store only)
  73      * Mflag: DO NOT generate a manifest file (just ZIP)
  74      * iflag: generate jar index
  75      */
  76     boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag;
  77 
  78     static final String MANIFEST_DIR = "META-INF/";
  79     static final String VERSION = "1.0";
  80 
  81     private static ResourceBundle rsrc;
  82 
  83     /**
  84      * If true, maintain compatibility with JDK releases prior to 6.0 by
  85      * timestamping extracted files with the time at which they are extracted.
  86      * Default is to use the time given in the archive.
  87      */
  88     private static final boolean useExtractionTime =
  89         Boolean.getBoolean("sun.tools.jar.useExtractionTime");
  90 
  91     /**
  92      * Initialize ResourceBundle
  93      */
  94     static {
  95         try {
  96             rsrc = ResourceBundle.getBundle("sun.tools.jar.resources.jar");
  97         } catch (MissingResourceException e) {
  98             throw new Error("Fatal: Resource for jar is missing");
  99         }
 100     }
 101 
 102     private String getMsg(String key) {
 103         try {
 104             return (rsrc.getString(key));
 105         } catch (MissingResourceException e) {
 106             throw new Error("Error in message file");
 107         }
 108     }
 109 
 110     private String formatMsg(String key, String arg) {
 111         String msg = getMsg(key);
 112         String[] args = new String[1];
 113         args[0] = arg;
 114         return MessageFormat.format(msg, (Object[]) args);
 115     }
 116 
 117     private String formatMsg2(String key, String arg, String arg1) {
 118         String msg = getMsg(key);
 119         String[] args = new String[2];
 120         args[0] = arg;
 121         args[1] = arg1;
 122         return MessageFormat.format(msg, (Object[]) args);
 123     }
 124 
 125     public Main(PrintStream out, PrintStream err, String program) {
 126         this.out = out;
 127         this.err = err;
 128         this.program = program;
 129     }
 130 
 131     /**
 132      * Creates a new empty temporary file in the same directory as the
 133      * specified file.  A variant of File.createTempFile.
 134      */
 135     private static File createTempFileInSameDirectoryAs(File file)
 136         throws IOException {
 137         File dir = file.getParentFile();
 138         if (dir == null)
 139             dir = new File(".");
 140         return File.createTempFile("jartmp", null, dir);
 141     }
 142 
 143     private boolean ok;
 144 
 145     /**
 146      * Starts main program with the specified arguments.
 147      */
 148     public synchronized boolean run(String args[]) {
 149         ok = true;
 150         if (!parseArgs(args)) {
 151             return false;
 152         }
 153         try {
 154             if (cflag || uflag) {
 155                 if (fname != null) {
 156                     // The name of the zip file as it would appear as its own
 157                     // zip file entry. We use this to make sure that we don't
 158                     // add the zip file to itself.
 159                     zname = fname.replace(File.separatorChar, '/');
 160                     if (zname.startsWith("./")) {
 161                         zname = zname.substring(2);
 162                     }
 163                 }
 164             }
 165             if (cflag) {
 166                 Manifest manifest = null;
 167                 InputStream in = null;
 168 
 169                 if (!Mflag) {
 170                     if (mname != null) {
 171                         in = new FileInputStream(mname);
 172                         manifest = new Manifest(new BufferedInputStream(in));
 173                     } else {
 174                         manifest = new Manifest();
 175                     }
 176                     addVersion(manifest);
 177                     addCreatedBy(manifest);
 178                     if (isAmbiguousMainClass(manifest)) {
 179                         if (in != null) {
 180                             in.close();
 181                         }
 182                         return false;
 183                     }
 184                     if (ename != null) {
 185                         addMainClass(manifest, ename);
 186                     }
 187                 }
 188                 OutputStream out;
 189                 if (fname != null) {
 190                     out = new FileOutputStream(fname);
 191                 } else {
 192                     out = new FileOutputStream(FileDescriptor.out);
 193                     if (vflag) {
 194                         // Disable verbose output so that it does not appear
 195                         // on stdout along with file data
 196                         // error("Warning: -v option ignored");
 197                         vflag = false;
 198                     }
 199                 }
 200                 expand(null, files, false);
 201                 create(new BufferedOutputStream(out, 4096), manifest);
 202                 if (in != null) {
 203                     in.close();
 204                 }
 205                 out.close();
 206             } else if (uflag) {
 207                 File inputFile = null, tmpFile = null;
 208                 FileInputStream in;
 209                 FileOutputStream out;
 210                 if (fname != null) {
 211                     inputFile = new File(fname);
 212                     tmpFile = createTempFileInSameDirectoryAs(inputFile);
 213                     in = new FileInputStream(inputFile);
 214                     out = new FileOutputStream(tmpFile);
 215                 } else {
 216                     in = new FileInputStream(FileDescriptor.in);
 217                     out = new FileOutputStream(FileDescriptor.out);
 218                     vflag = false;
 219                 }
 220                 InputStream manifest = (!Mflag && (mname != null)) ?
 221                     (new FileInputStream(mname)) : null;
 222                 expand(null, files, true);
 223                 boolean updateOk = update(in, new BufferedOutputStream(out),
 224                                           manifest, null);
 225                 if (ok) {
 226                     ok = updateOk;
 227                 }
 228                 in.close();
 229                 out.close();
 230                 if (manifest != null) {
 231                     manifest.close();
 232                 }
 233                 if (fname != null) {
 234                     // on Win32, we need this delete
 235                     inputFile.delete();
 236                     if (!tmpFile.renameTo(inputFile)) {
 237                         tmpFile.delete();
 238                         throw new IOException(getMsg("error.write.file"));
 239                     }
 240                     tmpFile.delete();
 241                 }
 242             } else if (tflag) {
 243                 replaceFSC(files);
 244                 if (fname != null) {
 245                     list(fname, files);
 246                 } else {
 247                     InputStream in = new FileInputStream(FileDescriptor.in);
 248                     try{
 249                         list(new BufferedInputStream(in), files);
 250                     } finally {
 251                         in.close();
 252                     }
 253                 }
 254             } else if (xflag) {
 255                 replaceFSC(files);
 256                 if (fname != null && files != null) {
 257                     extract(fname, files);
 258                 } else {
 259                     InputStream in = (fname == null)
 260                         ? new FileInputStream(FileDescriptor.in)
 261                         : new FileInputStream(fname);
 262                     try {
 263                         extract(new BufferedInputStream(in), files);
 264                     } finally {
 265                         in.close();
 266                     }
 267                 }
 268             } else if (iflag) {
 269                 genIndex(rootjar, files);
 270             }
 271         } catch (IOException e) {
 272             fatalError(e);
 273             ok = false;
 274         } catch (Error ee) {
 275             ee.printStackTrace();
 276             ok = false;
 277         } catch (Throwable t) {
 278             t.printStackTrace();
 279             ok = false;
 280         }
 281         out.flush();
 282         err.flush();
 283         return ok;
 284     }
 285 
 286     /**
 287      * Parses command line arguments.
 288      */
 289     boolean parseArgs(String args[]) {
 290         /* Preprocess and expand @file arguments */
 291         try {
 292             args = CommandLine.parse(args);
 293         } catch (FileNotFoundException e) {
 294             fatalError(formatMsg("error.cant.open", e.getMessage()));
 295             return false;
 296         } catch (IOException e) {
 297             fatalError(e);
 298             return false;
 299         }
 300         /* parse flags */
 301         int count = 1;
 302         try {
 303             String flags = args[0];
 304             if (flags.startsWith("-")) {
 305                 flags = flags.substring(1);
 306             }
 307             for (int i = 0; i < flags.length(); i++) {
 308                 switch (flags.charAt(i)) {
 309                 case 'c':
 310                     if (xflag || tflag || uflag || iflag) {
 311                         usageError();
 312                         return false;
 313                     }
 314                     cflag = true;
 315                     break;
 316                 case 'u':
 317                     if (cflag || xflag || tflag || iflag) {
 318                         usageError();
 319                         return false;
 320                     }
 321                     uflag = true;
 322                     break;
 323                 case 'x':
 324                     if (cflag || uflag || tflag || iflag) {
 325                         usageError();
 326                         return false;
 327                     }
 328                     xflag = true;
 329                     break;
 330                 case 't':
 331                     if (cflag || uflag || xflag || iflag) {
 332                         usageError();
 333                         return false;
 334                     }
 335                     tflag = true;
 336                     break;
 337                 case 'M':
 338                     Mflag = true;
 339                     break;
 340                 case 'v':
 341                     vflag = true;
 342                     break;
 343                 case 'f':
 344                     fname = args[count++];
 345                     break;
 346                 case 'm':
 347                     mname = args[count++];
 348                     break;
 349                 case '0':
 350                     flag0 = true;
 351                     break;
 352                 case 'i':
 353                     if (cflag || uflag || xflag || tflag) {
 354                         usageError();
 355                         return false;
 356                     }
 357                     // do not increase the counter, files will contain rootjar
 358                     rootjar = args[count++];
 359                     iflag = true;
 360                     break;
 361                 case 'e':
 362                      ename = args[count++];
 363                      break;
 364                 default:
 365                     error(formatMsg("error.illegal.option",
 366                                 String.valueOf(flags.charAt(i))));
 367                     usageError();
 368                     return false;
 369                 }
 370             }
 371         } catch (ArrayIndexOutOfBoundsException e) {
 372             usageError();
 373             return false;
 374         }
 375         if (!cflag && !tflag && !xflag && !uflag && !iflag) {
 376             error(getMsg("error.bad.option"));
 377             usageError();
 378             return false;
 379         }
 380         /* parse file arguments */
 381         int n = args.length - count;
 382         if (n > 0) {
 383             int k = 0;
 384             String[] nameBuf = new String[n];
 385             try {
 386                 for (int i = count; i < args.length; i++) {
 387                     if (args[i].equals("-C")) {
 388                         /* change the directory */
 389                         String dir = args[++i];
 390                         dir = (dir.endsWith(File.separator) ?
 391                                dir : (dir + File.separator));
 392                         dir = dir.replace(File.separatorChar, '/');
 393                         while (dir.indexOf("//") > -1) {
 394                             dir = dir.replace("//", "/");
 395                         }
 396                         paths.add(dir.replace(File.separatorChar, '/'));
 397                         nameBuf[k++] = dir + args[++i];
 398                     } else {
 399                         nameBuf[k++] = args[i];
 400                     }
 401                 }
 402             } catch (ArrayIndexOutOfBoundsException e) {
 403                 usageError();
 404                 return false;
 405             }
 406             files = new String[k];
 407             System.arraycopy(nameBuf, 0, files, 0, k);
 408         } else if (cflag && (mname == null)) {
 409             error(getMsg("error.bad.cflag"));
 410             usageError();
 411             return false;
 412         } else if (uflag) {
 413             if ((mname != null) || (ename != null)) {
 414                 /* just want to update the manifest */
 415                 return true;
 416             } else {
 417                 error(getMsg("error.bad.uflag"));
 418                 usageError();
 419                 return false;
 420             }
 421         }
 422         return true;
 423     }
 424 
 425     /**
 426      * Expands list of files to process into full list of all files that
 427      * can be found by recursively descending directories.
 428      */
 429     void expand(File dir, String[] files, boolean isUpdate) {
 430         if (files == null) {
 431             return;
 432         }
 433         for (int i = 0; i < files.length; i++) {
 434             File f;
 435             if (dir == null) {
 436                 f = new File(files[i]);
 437             } else {
 438                 f = new File(dir, files[i]);
 439             }
 440             if (f.isFile()) {
 441                 if (entries.add(f)) {
 442                     if (isUpdate)
 443                         entryMap.put(entryName(f.getPath()), f);
 444                 }
 445             } else if (f.isDirectory()) {
 446                 if (entries.add(f)) {
 447                     if (isUpdate) {
 448                         String dirPath = f.getPath();
 449                         dirPath = (dirPath.endsWith(File.separator)) ? dirPath :
 450                             (dirPath + File.separator);
 451                         entryMap.put(entryName(dirPath), f);
 452                     }
 453                     expand(f, f.list(), isUpdate);
 454                 }
 455             } else {
 456                 error(formatMsg("error.nosuch.fileordir", String.valueOf(f)));
 457                 ok = false;
 458             }
 459         }
 460     }
 461 
 462     /**
 463      * Creates a new JAR file.
 464      */
 465     void create(OutputStream out, Manifest manifest)
 466         throws IOException
 467     {
 468         ZipOutputStream zos = new JarOutputStream(out);
 469         if (flag0) {
 470             zos.setMethod(ZipOutputStream.STORED);
 471         }
 472         if (manifest != null) {
 473             if (vflag) {
 474                 output(getMsg("out.added.manifest"));
 475             }
 476             ZipEntry e = new ZipEntry(MANIFEST_DIR);
 477             e.setTime(System.currentTimeMillis());
 478             e.setSize(0);
 479             e.setCrc(0);
 480             zos.putNextEntry(e);
 481             e = new ZipEntry(MANIFEST_NAME);
 482             e.setTime(System.currentTimeMillis());
 483             if (flag0) {
 484                 crc32Manifest(e, manifest);
 485             }
 486             zos.putNextEntry(e);
 487             manifest.write(zos);
 488             zos.closeEntry();
 489         }
 490         for (File file: entries) {
 491             addFile(zos, file);
 492         }
 493         zos.close();
 494     }
 495 
 496     private char toUpperCaseASCII(char c) {
 497         return (c < 'a' || c > 'z') ? c : (char) (c + 'A' - 'a');
 498     }
 499 
 500     /**
 501      * Compares two strings for equality, ignoring case.  The second
 502      * argument must contain only upper-case ASCII characters.
 503      * We don't want case comparison to be locale-dependent (else we
 504      * have the notorious "turkish i bug").
 505      */
 506     private boolean equalsIgnoreCase(String s, String upper) {
 507         assert upper.toUpperCase(java.util.Locale.ENGLISH).equals(upper);
 508         int len;
 509         if ((len = s.length()) != upper.length())
 510             return false;
 511         for (int i = 0; i < len; i++) {
 512             char c1 = s.charAt(i);
 513             char c2 = upper.charAt(i);
 514             if (c1 != c2 && toUpperCaseASCII(c1) != c2)
 515                 return false;
 516         }
 517         return true;
 518     }
 519 
 520     /**
 521      * Updates an existing jar file.
 522      */
 523     boolean update(InputStream in, OutputStream out,
 524                    InputStream newManifest,
 525                    JarIndex jarIndex) throws IOException
 526     {
 527         ZipInputStream zis = new ZipInputStream(in);
 528         ZipOutputStream zos = new JarOutputStream(out);
 529         ZipEntry e = null;
 530         boolean foundManifest = false;
 531         boolean updateOk = true;
 532 
 533         if (jarIndex != null) {
 534             addIndex(jarIndex, zos);
 535         }
 536 
 537         // put the old entries first, replace if necessary
 538         while ((e = zis.getNextEntry()) != null) {
 539             String name = e.getName();
 540 
 541             boolean isManifestEntry = equalsIgnoreCase(name, MANIFEST_NAME);
 542 
 543             if ((jarIndex != null && equalsIgnoreCase(name, INDEX_NAME))
 544                 || (Mflag && isManifestEntry)) {
 545                 continue;
 546             } else if (isManifestEntry && ((newManifest != null) ||
 547                         (ename != null))) {
 548                 foundManifest = true;
 549                 if (newManifest != null) {
 550                     // Don't read from the newManifest InputStream, as we
 551                     // might need it below, and we can't re-read the same data
 552                     // twice.
 553                     FileInputStream fis = new FileInputStream(mname);
 554                     boolean ambiguous = isAmbiguousMainClass(new Manifest(fis));
 555                     fis.close();
 556                     if (ambiguous) {
 557                         return false;
 558                     }
 559                 }
 560 
 561                 // Update the manifest.
 562                 Manifest old = new Manifest(zis);
 563                 if (newManifest != null) {
 564                     old.read(newManifest);
 565                 }
 566                 updateManifest(old, zos);
 567             } else {
 568                 if (!entryMap.containsKey(name)) { // copy the old stuff
 569                     // do our own compression
 570                     ZipEntry e2 = new ZipEntry(name);
 571                     e2.setMethod(e.getMethod());
 572                     e2.setTime(e.getTime());
 573                     e2.setComment(e.getComment());
 574                     e2.setExtra(e.getExtra());
 575                     if (e.getMethod() == ZipEntry.STORED) {
 576                         e2.setSize(e.getSize());
 577                         e2.setCrc(e.getCrc());
 578                     }
 579                     zos.putNextEntry(e2);
 580                     copy(zis, zos);
 581                 } else { // replace with the new files
 582                     File f = entryMap.get(name);
 583                     addFile(zos, f);
 584                     entryMap.remove(name);
 585                     entries.remove(f);
 586                 }
 587             }
 588         }
 589 
 590         // add the remaining new files
 591         for (File f: entries) {
 592             addFile(zos, f);
 593         }
 594         if (!foundManifest) {
 595             if (newManifest != null) {
 596                 Manifest m = new Manifest(newManifest);
 597                 updateOk = !isAmbiguousMainClass(m);
 598                 if (updateOk) {
 599                     updateManifest(m, zos);
 600                 }
 601             } else if (ename != null) {
 602                 updateManifest(new Manifest(), zos);
 603             }
 604         }
 605         zis.close();
 606         zos.close();
 607         return updateOk;
 608     }
 609 
 610 
 611     private void addIndex(JarIndex index, ZipOutputStream zos)
 612         throws IOException
 613     {
 614         ZipEntry e = new ZipEntry(INDEX_NAME);
 615         e.setTime(System.currentTimeMillis());
 616         if (flag0) {
 617             CRC32OutputStream os = new CRC32OutputStream();
 618             index.write(os);
 619             os.updateEntry(e);
 620         }
 621         zos.putNextEntry(e);
 622         index.write(zos);
 623         zos.closeEntry();
 624     }
 625 
 626     private void updateManifest(Manifest m, ZipOutputStream zos)
 627         throws IOException
 628     {
 629         addVersion(m);
 630         addCreatedBy(m);
 631         if (ename != null) {
 632             addMainClass(m, ename);
 633         }
 634         ZipEntry e = new ZipEntry(MANIFEST_NAME);
 635         e.setTime(System.currentTimeMillis());
 636         if (flag0) {
 637             crc32Manifest(e, m);
 638         }
 639         zos.putNextEntry(e);
 640         m.write(zos);
 641         if (vflag) {
 642             output(getMsg("out.update.manifest"));
 643         }
 644     }
 645 
 646 
 647     private String entryName(String name) {
 648         name = name.replace(File.separatorChar, '/');
 649         String matchPath = "";
 650         for (String path : paths) {
 651             if (name.startsWith(path)
 652                 && (path.length() > matchPath.length())) {
 653                 matchPath = path;
 654             }
 655         }
 656         name = name.substring(matchPath.length());
 657 
 658         if (name.startsWith("/")) {
 659             name = name.substring(1);
 660         } else if (name.startsWith("./")) {
 661             name = name.substring(2);
 662         }
 663         return name;
 664     }
 665 
 666     private void addVersion(Manifest m) {
 667         Attributes global = m.getMainAttributes();
 668         if (global.getValue(Attributes.Name.MANIFEST_VERSION) == null) {
 669             global.put(Attributes.Name.MANIFEST_VERSION, VERSION);
 670         }
 671     }
 672 
 673     private void addCreatedBy(Manifest m) {
 674         Attributes global = m.getMainAttributes();
 675         if (global.getValue(new Attributes.Name("Created-By")) == null) {
 676             String javaVendor = System.getProperty("java.vendor");
 677             String jdkVersion = System.getProperty("java.version");
 678             global.put(new Attributes.Name("Created-By"), jdkVersion + " (" +
 679                         javaVendor + ")");
 680         }
 681     }
 682 
 683     private void addMainClass(Manifest m, String mainApp) {
 684         Attributes global = m.getMainAttributes();
 685 
 686         // overrides any existing Main-Class attribute
 687         global.put(Attributes.Name.MAIN_CLASS, mainApp);
 688     }
 689 
 690     private boolean isAmbiguousMainClass(Manifest m) {
 691         if (ename != null) {
 692             Attributes global = m.getMainAttributes();
 693             if ((global.get(Attributes.Name.MAIN_CLASS) != null)) {
 694                 error(getMsg("error.bad.eflag"));
 695                 usageError();
 696                 return true;
 697             }
 698         }
 699         return false;
 700     }
 701 
 702     /**
 703      * Adds a new file entry to the ZIP output stream.
 704      */
 705     void addFile(ZipOutputStream zos, File file) throws IOException {
 706         String name = file.getPath();
 707         boolean isDir = file.isDirectory();
 708         if (isDir) {
 709             name = name.endsWith(File.separator) ? name :
 710                 (name + File.separator);
 711         }
 712         name = entryName(name);
 713 
 714         if (name.equals("") || name.equals(".") || name.equals(zname)) {
 715             return;
 716         } else if ((name.equals(MANIFEST_DIR) || name.equals(MANIFEST_NAME))
 717                    && !Mflag) {
 718             if (vflag) {
 719                 output(formatMsg("out.ignore.entry", name));
 720             }
 721             return;
 722         }
 723 
 724         long size = isDir ? 0 : file.length();
 725 
 726         if (vflag) {
 727             out.print(formatMsg("out.adding", name));
 728         }
 729         ZipEntry e = new ZipEntry(name);
 730         e.setTime(file.lastModified());
 731         if (size == 0) {
 732             e.setMethod(ZipEntry.STORED);
 733             e.setSize(0);
 734             e.setCrc(0);
 735         } else if (flag0) {
 736             crc32File(e, file);
 737         }
 738         zos.putNextEntry(e);
 739         if (!isDir) {
 740             copy(file, zos);
 741         }
 742         zos.closeEntry();
 743         /* report how much compression occurred. */
 744         if (vflag) {
 745             size = e.getSize();
 746             long csize = e.getCompressedSize();
 747             out.print(formatMsg2("out.size", String.valueOf(size),
 748                         String.valueOf(csize)));
 749             if (e.getMethod() == ZipEntry.DEFLATED) {
 750                 long ratio = 0;
 751                 if (size != 0) {
 752                     ratio = ((size - csize) * 100) / size;
 753                 }
 754                 output(formatMsg("out.deflated", String.valueOf(ratio)));
 755             } else {
 756                 output(getMsg("out.stored"));
 757             }
 758         }
 759     }
 760 
 761     /**
 762      * A buffer for use only by copy(InputStream, OutputStream).
 763      * Not as clean as allocating a new buffer as needed by copy,
 764      * but significantly more efficient.
 765      */
 766     private byte[] copyBuf = new byte[8192];
 767 
 768     /**
 769      * Copies all bytes from the input stream to the output stream.
 770      * Does not close or flush either stream.
 771      *
 772      * @param from the input stream to read from
 773      * @param to the output stream to write to
 774      * @throws IOException if an I/O error occurs
 775      */
 776     private void copy(InputStream from, OutputStream to) throws IOException {
 777         int n;
 778         while ((n = from.read(copyBuf)) != -1)
 779             to.write(copyBuf, 0, n);
 780     }
 781 
 782     /**
 783      * Copies all bytes from the input file to the output stream.
 784      * Does not close or flush the output stream.
 785      *
 786      * @param from the input file to read from
 787      * @param to the output stream to write to
 788      * @throws IOException if an I/O error occurs
 789      */
 790     private void copy(File from, OutputStream to) throws IOException {
 791         InputStream in = new FileInputStream(from);
 792         try {
 793             copy(in, to);
 794         } finally {
 795             in.close();
 796         }
 797     }
 798 
 799     /**
 800      * Copies all bytes from the input stream to the output file.
 801      * Does not close the input stream.
 802      *
 803      * @param from the input stream to read from
 804      * @param to the output file to write to
 805      * @throws IOException if an I/O error occurs
 806      */
 807     private void copy(InputStream from, File to) throws IOException {
 808         OutputStream out = new FileOutputStream(to);
 809         try {
 810             copy(from, out);
 811         } finally {
 812             out.close();
 813         }
 814     }
 815 
 816     /**
 817      * Computes the crc32 of a Manifest.  This is necessary when the
 818      * ZipOutputStream is in STORED mode.
 819      */
 820     private void crc32Manifest(ZipEntry e, Manifest m) throws IOException {
 821         CRC32OutputStream os = new CRC32OutputStream();
 822         m.write(os);
 823         os.updateEntry(e);
 824     }
 825 
 826     /**
 827      * Computes the crc32 of a File.  This is necessary when the
 828      * ZipOutputStream is in STORED mode.
 829      */
 830     private void crc32File(ZipEntry e, File f) throws IOException {
 831         CRC32OutputStream os = new CRC32OutputStream();
 832         copy(f, os);
 833         if (os.n != f.length()) {
 834             throw new JarException(formatMsg(
 835                         "error.incorrect.length", f.getPath()));
 836         }
 837         os.updateEntry(e);
 838     }
 839 
 840     void replaceFSC(String files[]) {
 841         if (files != null) {
 842             for (int i = 0; i < files.length; i++) {
 843                 files[i] = files[i].replace(File.separatorChar, '/');
 844             }
 845         }
 846     }
 847 
 848     @SuppressWarnings("serial")
 849     Set<ZipEntry> newDirSet() {
 850         return new HashSet<ZipEntry>() {
 851             public boolean add(ZipEntry e) {
 852                 return ((e == null || useExtractionTime) ? false : super.add(e));
 853             }};
 854     }
 855 
 856     void updateLastModifiedTime(Set<ZipEntry> zes) throws IOException {
 857         for (ZipEntry ze : zes) {
 858             long lastModified = ze.getTime();
 859             if (lastModified != -1) {
 860                 File f = new File(ze.getName().replace('/', File.separatorChar));
 861                 f.setLastModified(lastModified);
 862             }
 863         }
 864     }
 865 
 866     /**
 867      * Extracts specified entries from JAR file.
 868      */
 869     void extract(InputStream in, String files[]) throws IOException {
 870         ZipInputStream zis = new ZipInputStream(in);
 871         ZipEntry e;
 872         // Set of all directory entries specified in archive.  Disallows
 873         // null entries.  Disallows all entries if using pre-6.0 behavior.
 874         Set<ZipEntry> dirs = newDirSet();
 875         while ((e = zis.getNextEntry()) != null) {
 876             if (files == null) {
 877                 dirs.add(extractFile(zis, e));
 878             } else {
 879                 String name = e.getName();
 880                 for (String file : files) {
 881                     if (name.startsWith(file)) {
 882                         dirs.add(extractFile(zis, e));
 883                         break;
 884                     }
 885                 }
 886             }
 887         }
 888 
 889         // Update timestamps of directories specified in archive with their
 890         // timestamps as given in the archive.  We do this after extraction,
 891         // instead of during, because creating a file in a directory changes
 892         // that directory's timestamp.
 893         updateLastModifiedTime(dirs);
 894     }
 895 
 896     /**
 897      * Extracts specified entries from JAR file, via ZipFile.
 898      */
 899     void extract(String fname, String files[]) throws IOException {
 900         ZipFile zf = new ZipFile(fname);
 901         Set<ZipEntry> dirs = newDirSet();
 902         Enumeration<? extends ZipEntry> zes = zf.entries();
 903         while (zes.hasMoreElements()) {
 904             ZipEntry e = zes.nextElement();
 905             InputStream is;
 906             if (files == null) {
 907                 dirs.add(extractFile(zf.getInputStream(e), e));
 908             } else {
 909                 String name = e.getName();
 910                 for (String file : files) {
 911                     if (name.startsWith(file)) {
 912                         dirs.add(extractFile(zf.getInputStream(e), e));
 913                         break;
 914                     }
 915                 }
 916             }
 917         }
 918         zf.close();
 919         updateLastModifiedTime(dirs);
 920     }
 921 
 922     /**
 923      * Extracts next entry from JAR file, creating directories as needed.  If
 924      * the entry is for a directory which doesn't exist prior to this
 925      * invocation, returns that entry, otherwise returns null.
 926      */
 927     ZipEntry extractFile(InputStream is, ZipEntry e) throws IOException {
 928         ZipEntry rc = null;
 929         String name = e.getName();
 930         File f = new File(e.getName().replace('/', File.separatorChar));
 931         if (e.isDirectory()) {
 932             if (f.exists()) {
 933                 if (!f.isDirectory()) {
 934                     throw new IOException(formatMsg("error.create.dir",
 935                         f.getPath()));
 936                 }
 937             } else {
 938                 if (!f.mkdirs()) {
 939                     throw new IOException(formatMsg("error.create.dir",
 940                         f.getPath()));
 941                 } else {
 942                     rc = e;
 943                 }
 944             }
 945 
 946             if (vflag) {
 947                 output(formatMsg("out.create", name));
 948             }
 949         } else {
 950             if (f.getParent() != null) {
 951                 File d = new File(f.getParent());
 952                 if (!d.exists() && !d.mkdirs() || !d.isDirectory()) {
 953                     throw new IOException(formatMsg(
 954                         "error.create.dir", d.getPath()));
 955                 }
 956             }
 957             try {
 958                 copy(is, f);
 959             } finally {
 960                 if (is instanceof ZipInputStream)
 961                     ((ZipInputStream)is).closeEntry();
 962                 else
 963                     is.close();
 964             }
 965             if (vflag) {
 966                 if (e.getMethod() == ZipEntry.DEFLATED) {
 967                     output(formatMsg("out.inflated", name));
 968                 } else {
 969                     output(formatMsg("out.extracted", name));
 970                 }
 971             }
 972         }
 973         if (!useExtractionTime) {
 974             long lastModified = e.getTime();
 975             if (lastModified != -1) {
 976                 f.setLastModified(lastModified);
 977             }
 978         }
 979         return rc;
 980     }
 981 
 982     /**
 983      * Lists contents of JAR file.
 984      */
 985     void list(InputStream in, String files[]) throws IOException {
 986         ZipInputStream zis = new ZipInputStream(in);
 987         ZipEntry e;
 988         while ((e = zis.getNextEntry()) != null) {
 989             /*
 990              * In the case of a compressed (deflated) entry, the entry size
 991              * is stored immediately following the entry data and cannot be
 992              * determined until the entry is fully read. Therefore, we close
 993              * the entry first before printing out its attributes.
 994              */
 995             zis.closeEntry();
 996             printEntry(e, files);
 997         }
 998     }
 999 
1000     /**
1001      * Lists contents of JAR file, via ZipFile.
1002      */
1003     void list(String fname, String files[]) throws IOException {
1004         ZipFile zf = new ZipFile(fname);
1005         Enumeration<? extends ZipEntry> zes = zf.entries();
1006         while (zes.hasMoreElements()) {
1007             printEntry(zes.nextElement(), files);
1008         }
1009         zf.close();
1010     }
1011 
1012     /**
1013      * Outputs the class index table to the INDEX.LIST file of the
1014      * root jar file.
1015      */
1016     void dumpIndex(String rootjar, JarIndex index) throws IOException {
1017         File jarFile = new File(rootjar);
1018         Path jarPath = jarFile.toPath();
1019         Path tmpPath = createTempFileInSameDirectoryAs(jarFile).toPath();
1020         try {
1021             if (update(Files.newInputStream(jarPath),
1022                        Files.newOutputStream(tmpPath),
1023                        null, index)) {
1024                 try {
1025                     Files.move(tmpPath, jarPath, REPLACE_EXISTING);
1026                 } catch (IOException e) {
1027                     throw new IOException(getMsg("error.write.file"), e);
1028                 }
1029             }
1030         } finally {
1031             Files.deleteIfExists(tmpPath);
1032         }
1033     }
1034 
1035     private HashSet<String> jarPaths = new HashSet<String>();
1036 
1037     /**
1038      * Generates the transitive closure of the Class-Path attribute for
1039      * the specified jar file.
1040      */
1041     List<String> getJarPath(String jar) throws IOException {
1042         List<String> files = new ArrayList<String>();
1043         files.add(jar);
1044         jarPaths.add(jar);
1045 
1046         // take out the current path
1047         String path = jar.substring(0, Math.max(0, jar.lastIndexOf('/') + 1));
1048 
1049         // class path attribute will give us jar file name with
1050         // '/' as separators, so we need to change them to the
1051         // appropriate one before we open the jar file.
1052         JarFile rf = new JarFile(jar.replace('/', File.separatorChar));
1053 
1054         if (rf != null) {
1055             Manifest man = rf.getManifest();
1056             if (man != null) {
1057                 Attributes attr = man.getMainAttributes();
1058                 if (attr != null) {
1059                     String value = attr.getValue(Attributes.Name.CLASS_PATH);
1060                     if (value != null) {
1061                         StringTokenizer st = new StringTokenizer(value);
1062                         while (st.hasMoreTokens()) {
1063                             String ajar = st.nextToken();
1064                             if (!ajar.endsWith("/")) {  // it is a jar file
1065                                 ajar = path.concat(ajar);
1066                                 /* check on cyclic dependency */
1067                                 if (! jarPaths.contains(ajar)) {
1068                                     files.addAll(getJarPath(ajar));
1069                                 }
1070                             }
1071                         }
1072                     }
1073                 }
1074             }
1075         }
1076         rf.close();
1077         return files;
1078     }
1079 
1080     /**
1081      * Generates class index file for the specified root jar file.
1082      */
1083     void genIndex(String rootjar, String[] files) throws IOException {
1084         List<String> jars = getJarPath(rootjar);
1085         int njars = jars.size();
1086         String[] jarfiles;
1087 
1088         if (njars == 1 && files != null) {
1089             // no class-path attribute defined in rootjar, will
1090             // use command line specified list of jars
1091             for (int i = 0; i < files.length; i++) {
1092                 jars.addAll(getJarPath(files[i]));
1093             }
1094             njars = jars.size();
1095         }
1096         jarfiles = jars.toArray(new String[njars]);
1097         JarIndex index = new JarIndex(jarfiles);
1098         dumpIndex(rootjar, index);
1099     }
1100 
1101     /**
1102      * Prints entry information, if requested.
1103      */
1104     void printEntry(ZipEntry e, String[] files) throws IOException {
1105         if (files == null) {
1106             printEntry(e);
1107         } else {
1108             String name = e.getName();
1109             for (String file : files) {
1110                 if (name.startsWith(file)) {
1111                     printEntry(e);
1112                     return;
1113                 }
1114             }
1115         }
1116     }
1117 
1118     /**
1119      * Prints entry information.
1120      */
1121     void printEntry(ZipEntry e) throws IOException {
1122         if (vflag) {
1123             StringBuilder sb = new StringBuilder();
1124             String s = Long.toString(e.getSize());
1125             for (int i = 6 - s.length(); i > 0; --i) {
1126                 sb.append(' ');
1127             }
1128             sb.append(s).append(' ').append(new Date(e.getTime()).toString());
1129             sb.append(' ').append(e.getName());
1130             output(sb.toString());
1131         } else {
1132             output(e.getName());
1133         }
1134     }
1135 
1136     /**
1137      * Prints usage message.
1138      */
1139     void usageError() {
1140         error(getMsg("usage"));
1141     }
1142 
1143     /**
1144      * A fatal exception has been caught.  No recovery possible
1145      */
1146     void fatalError(Exception e) {
1147         e.printStackTrace();
1148     }
1149 
1150     /**
1151      * A fatal condition has been detected; message is "s".
1152      * No recovery possible
1153      */
1154     void fatalError(String s) {
1155         error(program + ": " + s);
1156     }
1157 
1158     /**
1159      * Print an output message; like verbose output and the like
1160      */
1161     protected void output(String s) {
1162         out.println(s);
1163     }
1164 
1165     /**
1166      * Print an error mesage; like something is broken
1167      */
1168     protected void error(String s) {
1169         err.println(s);
1170     }
1171 
1172     /**
1173      * Main routine to start program.
1174      */
1175     public static void main(String args[]) {
1176         Main jartool = new Main(System.out, System.err, "jar");
1177         System.exit(jartool.run(args) ? 0 : 1);
1178     }
1179 
1180     /**
1181      * An OutputStream that doesn't send its output anywhere, (but could).
1182      * It's here to find the CRC32 of an input file, necessary for STORED
1183      * mode in ZIP.
1184      */
1185     private static class CRC32OutputStream extends java.io.OutputStream {
1186         final CRC32 crc = new CRC32();
1187         long n = 0;
1188 
1189         CRC32OutputStream() {}
1190 
1191         public void write(int r) throws IOException {
1192             crc.update(r);
1193             n++;
1194         }
1195 
1196         public void write(byte[] b, int off, int len) throws IOException {
1197             crc.update(b, off, len);
1198             n += len;
1199         }
1200 
1201         /**
1202          * Updates a ZipEntry which describes the data read by this
1203          * output stream, in STORED mode.
1204          */
1205         public void updateEntry(ZipEntry e) {
1206             e.setMethod(ZipEntry.STORED);
1207             e.setSize(n);
1208             e.setCrc(crc.getValue());
1209         }
1210     }
1211 }