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