1 /*
   2  * Copyright 1996-2009 Sun Microsystems, Inc.  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.  Sun designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
  22  * CA 95054 USA or visit www.sun.com if you need additional information or
  23  * have any 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) {
 310                         usageError();
 311                         return false;
 312                     }
 313                     cflag = true;
 314                     break;
 315                 case 'u':
 316                     if (cflag || xflag || tflag) {
 317                         usageError();
 318                         return false;
 319                     }
 320                     uflag = true;
 321                     break;
 322                 case 'x':
 323                     if (cflag || uflag || tflag) {
 324                         usageError();
 325                         return false;
 326                     }
 327                     xflag = true;
 328                     break;
 329                 case 't':
 330                     if (cflag || uflag || xflag) {
 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                     // do not increase the counter, files will contain rootjar
 353                     rootjar = args[count++];
 354                     iflag = true;
 355                     break;
 356                 case 'e':
 357                      ename = args[count++];
 358                      break;
 359                 default:
 360                     error(formatMsg("error.illegal.option",
 361                                 String.valueOf(flags.charAt(i))));
 362                     usageError();
 363                     return false;
 364                 }
 365             }
 366         } catch (ArrayIndexOutOfBoundsException e) {
 367             usageError();
 368             return false;
 369         }
 370         if (!cflag && !tflag && !xflag && !uflag && !iflag) {
 371             error(getMsg("error.bad.option"));
 372             usageError();
 373             return false;
 374         }
 375         /* parse file arguments */
 376         int n = args.length - count;
 377         if (n > 0) {
 378             int k = 0;
 379             String[] nameBuf = new String[n];
 380             try {
 381                 for (int i = count; i < args.length; i++) {
 382                     if (args[i].equals("-C")) {
 383                         /* change the directory */
 384                         String dir = args[++i];
 385                         dir = (dir.endsWith(File.separator) ?
 386                                dir : (dir + File.separator));
 387                         dir = dir.replace(File.separatorChar, '/');
 388                         while (dir.indexOf("//") > -1) {
 389                             dir = dir.replace("//", "/");
 390                         }
 391                         paths.add(dir.replace(File.separatorChar, '/'));
 392                         nameBuf[k++] = dir + args[++i];
 393                     } else {
 394                         nameBuf[k++] = args[i];
 395                     }
 396                 }
 397             } catch (ArrayIndexOutOfBoundsException e) {
 398                 usageError();
 399                 return false;
 400             }
 401             files = new String[k];
 402             System.arraycopy(nameBuf, 0, files, 0, k);
 403         } else if (cflag && (mname == null)) {
 404             error(getMsg("error.bad.cflag"));
 405             usageError();
 406             return false;
 407         } else if (uflag) {
 408             if ((mname != null) || (ename != null)) {
 409                 /* just want to update the manifest */
 410                 return true;
 411             } else {
 412                 error(getMsg("error.bad.uflag"));
 413                 usageError();
 414                 return false;
 415             }
 416         }
 417         return true;
 418     }
 419 
 420     /**
 421      * Expands list of files to process into full list of all files that
 422      * can be found by recursively descending directories.
 423      */
 424     void expand(File dir, String[] files, boolean isUpdate) {
 425         if (files == null) {
 426             return;
 427         }
 428         for (int i = 0; i < files.length; i++) {
 429             File f;
 430             if (dir == null) {
 431                 f = new File(files[i]);
 432             } else {
 433                 f = new File(dir, files[i]);
 434             }
 435             if (f.isFile()) {
 436                 if (entries.add(f)) {
 437                     if (isUpdate)
 438                         entryMap.put(entryName(f.getPath()), f);
 439                 }
 440             } else if (f.isDirectory()) {
 441                 if (entries.add(f)) {
 442                     if (isUpdate) {
 443                         String dirPath = f.getPath();
 444                         dirPath = (dirPath.endsWith(File.separator)) ? dirPath :
 445                             (dirPath + File.separator);
 446                         entryMap.put(entryName(dirPath), f);
 447                     }
 448                     expand(f, f.list(), isUpdate);
 449                 }
 450             } else {
 451                 error(formatMsg("error.nosuch.fileordir", String.valueOf(f)));
 452                 ok = false;
 453             }
 454         }
 455     }
 456 
 457     /**
 458      * Creates a new JAR file.
 459      */
 460     void create(OutputStream out, Manifest manifest)
 461         throws IOException
 462     {
 463         ZipOutputStream zos = new JarOutputStream(out);
 464         if (flag0) {
 465             zos.setMethod(ZipOutputStream.STORED);
 466         }
 467         if (manifest != null) {
 468             if (vflag) {
 469                 output(getMsg("out.added.manifest"));
 470             }
 471             ZipEntry e = new ZipEntry(MANIFEST_DIR);
 472             e.setTime(System.currentTimeMillis());
 473             e.setSize(0);
 474             e.setCrc(0);
 475             zos.putNextEntry(e);
 476             e = new ZipEntry(MANIFEST_NAME);
 477             e.setTime(System.currentTimeMillis());
 478             if (flag0) {
 479                 crc32Manifest(e, manifest);
 480             }
 481             zos.putNextEntry(e);
 482             manifest.write(zos);
 483             zos.closeEntry();
 484         }
 485         for (File file: entries) {
 486             addFile(zos, file);
 487         }
 488         zos.close();
 489     }
 490 
 491     private char toUpperCaseASCII(char c) {
 492         return (c < 'a' || c > 'z') ? c : (char) (c + 'A' - 'a');
 493     }
 494 
 495     /**
 496      * Compares two strings for equality, ignoring case.  The second
 497      * argument must contain only upper-case ASCII characters.
 498      * We don't want case comparison to be locale-dependent (else we
 499      * have the notorious "turkish i bug").
 500      */
 501     private boolean equalsIgnoreCase(String s, String upper) {
 502         assert upper.toUpperCase(java.util.Locale.ENGLISH).equals(upper);
 503         int len;
 504         if ((len = s.length()) != upper.length())
 505             return false;
 506         for (int i = 0; i < len; i++) {
 507             char c1 = s.charAt(i);
 508             char c2 = upper.charAt(i);
 509             if (c1 != c2 && toUpperCaseASCII(c1) != c2)
 510                 return false;
 511         }
 512         return true;
 513     }
 514 
 515     /**
 516      * Updates an existing jar file.
 517      */
 518     boolean update(InputStream in, OutputStream out,
 519                    InputStream newManifest,
 520                    JarIndex jarIndex) throws IOException
 521     {
 522         ZipInputStream zis = new ZipInputStream(in);
 523         ZipOutputStream zos = new JarOutputStream(out);
 524         ZipEntry e = null;
 525         boolean foundManifest = false;
 526         boolean updateOk = true;
 527 
 528         if (jarIndex != null) {
 529             addIndex(jarIndex, zos);
 530         }
 531 
 532         // put the old entries first, replace if necessary
 533         while ((e = zis.getNextEntry()) != null) {
 534             String name = e.getName();
 535 
 536             boolean isManifestEntry = equalsIgnoreCase(name, MANIFEST_NAME);
 537 
 538             if ((jarIndex != null && equalsIgnoreCase(name, INDEX_NAME))
 539                 || (Mflag && isManifestEntry)) {
 540                 continue;
 541             } else if (isManifestEntry && ((newManifest != null) ||
 542                         (ename != null))) {
 543                 foundManifest = true;
 544                 if (newManifest != null) {
 545                     // Don't read from the newManifest InputStream, as we
 546                     // might need it below, and we can't re-read the same data
 547                     // twice.
 548                     FileInputStream fis = new FileInputStream(mname);
 549                     boolean ambiguous = isAmbiguousMainClass(new Manifest(fis));
 550                     fis.close();
 551                     if (ambiguous) {
 552                         return false;
 553                     }
 554                 }
 555 
 556                 // Update the manifest.
 557                 Manifest old = new Manifest(zis);
 558                 if (newManifest != null) {
 559                     old.read(newManifest);
 560                 }
 561                 updateManifest(old, zos);
 562             } else {
 563                 if (!entryMap.containsKey(name)) { // copy the old stuff
 564                     // do our own compression
 565                     ZipEntry e2 = new ZipEntry(name);
 566                     e2.setMethod(e.getMethod());
 567                     e2.setTime(e.getTime());
 568                     e2.setComment(e.getComment());
 569                     e2.setExtra(e.getExtra());
 570                     if (e.getMethod() == ZipEntry.STORED) {
 571                         e2.setSize(e.getSize());
 572                         e2.setCrc(e.getCrc());
 573                     }
 574                     zos.putNextEntry(e2);
 575                     copy(zis, zos);
 576                 } else { // replace with the new files
 577                     File f = entryMap.get(name);
 578                     addFile(zos, f);
 579                     entryMap.remove(name);
 580                     entries.remove(f);
 581                 }
 582             }
 583         }
 584 
 585         // add the remaining new files
 586         for (File f: entries) {
 587             addFile(zos, f);
 588         }
 589         if (!foundManifest) {
 590             if (newManifest != null) {
 591                 Manifest m = new Manifest(newManifest);
 592                 updateOk = !isAmbiguousMainClass(m);
 593                 if (updateOk) {
 594                     updateManifest(m, zos);
 595                 }
 596             } else if (ename != null) {
 597                 updateManifest(new Manifest(), zos);
 598             }
 599         }
 600         zis.close();
 601         zos.close();
 602         return updateOk;
 603     }
 604 
 605 
 606     private void addIndex(JarIndex index, ZipOutputStream zos)
 607         throws IOException
 608     {
 609         ZipEntry e = new ZipEntry(INDEX_NAME);
 610         e.setTime(System.currentTimeMillis());
 611         if (flag0) {
 612             CRC32OutputStream os = new CRC32OutputStream();
 613             index.write(os);
 614             os.updateEntry(e);
 615         }
 616         zos.putNextEntry(e);
 617         index.write(zos);
 618         zos.closeEntry();
 619     }
 620 
 621     private void updateManifest(Manifest m, ZipOutputStream zos)
 622         throws IOException
 623     {
 624         addVersion(m);
 625         addCreatedBy(m);
 626         if (ename != null) {
 627             addMainClass(m, ename);
 628         }
 629         ZipEntry e = new ZipEntry(MANIFEST_NAME);
 630         e.setTime(System.currentTimeMillis());
 631         if (flag0) {
 632             crc32Manifest(e, m);
 633         }
 634         zos.putNextEntry(e);
 635         m.write(zos);
 636         if (vflag) {
 637             output(getMsg("out.update.manifest"));
 638         }
 639     }
 640 
 641 
 642     private String entryName(String name) {
 643         name = name.replace(File.separatorChar, '/');
 644         String matchPath = "";
 645         for (String path : paths) {
 646             if (name.startsWith(path)
 647                 && (path.length() > matchPath.length())) {
 648                 matchPath = path;
 649             }
 650         }
 651         name = name.substring(matchPath.length());
 652 
 653         if (name.startsWith("/")) {
 654             name = name.substring(1);
 655         } else if (name.startsWith("./")) {
 656             name = name.substring(2);
 657         }
 658         return name;
 659     }
 660 
 661     private void addVersion(Manifest m) {
 662         Attributes global = m.getMainAttributes();
 663         if (global.getValue(Attributes.Name.MANIFEST_VERSION) == null) {
 664             global.put(Attributes.Name.MANIFEST_VERSION, VERSION);
 665         }
 666     }
 667 
 668     private void addCreatedBy(Manifest m) {
 669         Attributes global = m.getMainAttributes();
 670         if (global.getValue(new Attributes.Name("Created-By")) == null) {
 671             String javaVendor = System.getProperty("java.vendor");
 672             String jdkVersion = System.getProperty("java.version");
 673             global.put(new Attributes.Name("Created-By"), jdkVersion + " (" +
 674                         javaVendor + ")");
 675         }
 676     }
 677 
 678     private void addMainClass(Manifest m, String mainApp) {
 679         Attributes global = m.getMainAttributes();
 680 
 681         // overrides any existing Main-Class attribute
 682         global.put(Attributes.Name.MAIN_CLASS, mainApp);
 683     }
 684 
 685     private boolean isAmbiguousMainClass(Manifest m) {
 686         if (ename != null) {
 687             Attributes global = m.getMainAttributes();
 688             if ((global.get(Attributes.Name.MAIN_CLASS) != null)) {
 689                 error(getMsg("error.bad.eflag"));
 690                 usageError();
 691                 return true;
 692             }
 693         }
 694         return false;
 695     }
 696 
 697     /**
 698      * Adds a new file entry to the ZIP output stream.
 699      */
 700     void addFile(ZipOutputStream zos, File file) throws IOException {
 701         String name = file.getPath();
 702         boolean isDir = file.isDirectory();
 703         if (isDir) {
 704             name = name.endsWith(File.separator) ? name :
 705                 (name + File.separator);
 706         }
 707         name = entryName(name);
 708 
 709         if (name.equals("") || name.equals(".") || name.equals(zname)) {
 710             return;
 711         } else if ((name.equals(MANIFEST_DIR) || name.equals(MANIFEST_NAME))
 712                    && !Mflag) {
 713             if (vflag) {
 714                 output(formatMsg("out.ignore.entry", name));
 715             }
 716             return;
 717         }
 718 
 719         long size = isDir ? 0 : file.length();
 720 
 721         if (vflag) {
 722             out.print(formatMsg("out.adding", name));
 723         }
 724         ZipEntry e = new ZipEntry(name);
 725         e.setTime(file.lastModified());
 726         if (size == 0) {
 727             e.setMethod(ZipEntry.STORED);
 728             e.setSize(0);
 729             e.setCrc(0);
 730         } else if (flag0) {
 731             crc32File(e, file);
 732         }
 733         zos.putNextEntry(e);
 734         if (!isDir) {
 735             copy(file, zos);
 736         }
 737         zos.closeEntry();
 738         /* report how much compression occurred. */
 739         if (vflag) {
 740             size = e.getSize();
 741             long csize = e.getCompressedSize();
 742             out.print(formatMsg2("out.size", String.valueOf(size),
 743                         String.valueOf(csize)));
 744             if (e.getMethod() == ZipEntry.DEFLATED) {
 745                 long ratio = 0;
 746                 if (size != 0) {
 747                     ratio = ((size - csize) * 100) / size;
 748                 }
 749                 output(formatMsg("out.deflated", String.valueOf(ratio)));
 750             } else {
 751                 output(getMsg("out.stored"));
 752             }
 753         }
 754     }
 755 
 756     /**
 757      * A buffer for use only by copy(InputStream, OutputStream).
 758      * Not as clean as allocating a new buffer as needed by copy,
 759      * but significantly more efficient.
 760      */
 761     private byte[] copyBuf = new byte[8192];
 762 
 763     /**
 764      * Copies all bytes from the input stream to the output stream.
 765      * Does not close or flush either stream.
 766      *
 767      * @param from the input stream to read from
 768      * @param to the output stream to write to
 769      * @throws IOException if an I/O error occurs
 770      */
 771     private void copy(InputStream from, OutputStream to) throws IOException {
 772         int n;
 773         while ((n = from.read(copyBuf)) != -1)
 774             to.write(copyBuf, 0, n);
 775     }
 776 
 777     /**
 778      * Copies all bytes from the input file to the output stream.
 779      * Does not close or flush the output stream.
 780      *
 781      * @param from the input file to read from
 782      * @param to the output stream to write to
 783      * @throws IOException if an I/O error occurs
 784      */
 785     private void copy(File from, OutputStream to) throws IOException {
 786         InputStream in = new FileInputStream(from);
 787         try {
 788             copy(in, to);
 789         } finally {
 790             in.close();
 791         }
 792     }
 793 
 794     /**
 795      * Copies all bytes from the input stream to the output file.
 796      * Does not close the input stream.
 797      *
 798      * @param from the input stream to read from
 799      * @param to the output file to write to
 800      * @throws IOException if an I/O error occurs
 801      */
 802     private void copy(InputStream from, File to) throws IOException {
 803         OutputStream out = new FileOutputStream(to);
 804         try {
 805             copy(from, out);
 806         } finally {
 807             out.close();
 808         }
 809     }
 810 
 811     /**
 812      * Computes the crc32 of a Manifest.  This is necessary when the
 813      * ZipOutputStream is in STORED mode.
 814      */
 815     private void crc32Manifest(ZipEntry e, Manifest m) throws IOException {
 816         CRC32OutputStream os = new CRC32OutputStream();
 817         m.write(os);
 818         os.updateEntry(e);
 819     }
 820 
 821     /**
 822      * Computes the crc32 of a File.  This is necessary when the
 823      * ZipOutputStream is in STORED mode.
 824      */
 825     private void crc32File(ZipEntry e, File f) throws IOException {
 826         CRC32OutputStream os = new CRC32OutputStream();
 827         copy(f, os);
 828         if (os.n != f.length()) {
 829             throw new JarException(formatMsg(
 830                         "error.incorrect.length", f.getPath()));
 831         }
 832         os.updateEntry(e);
 833     }
 834 
 835     void replaceFSC(String files[]) {
 836         if (files != null) {
 837             for (String file : files) {
 838                 file = file.replace(File.separatorChar, '/');
 839             }
 840         }
 841     }
 842 
 843     @SuppressWarnings("serial")
 844     Set<ZipEntry> newDirSet() {
 845         return new HashSet<ZipEntry>() {
 846             public boolean add(ZipEntry e) {
 847                 return ((e == null || useExtractionTime) ? false : super.add(e));
 848             }};
 849     }
 850 
 851     void updateLastModifiedTime(Set<ZipEntry> zes) throws IOException {
 852         for (ZipEntry ze : zes) {
 853             long lastModified = ze.getTime();
 854             if (lastModified != -1) {
 855                 File f = new File(ze.getName().replace('/', File.separatorChar));
 856                 f.setLastModified(lastModified);
 857             }
 858         }
 859     }
 860 
 861     /**
 862      * Extracts specified entries from JAR file.
 863      */
 864     void extract(InputStream in, String files[]) throws IOException {
 865         ZipInputStream zis = new ZipInputStream(in);
 866         ZipEntry e;
 867         // Set of all directory entries specified in archive.  Disallows
 868         // null entries.  Disallows all entries if using pre-6.0 behavior.
 869         Set<ZipEntry> dirs = newDirSet();
 870         while ((e = zis.getNextEntry()) != null) {
 871             if (files == null) {
 872                 dirs.add(extractFile(zis, e));
 873             } else {
 874                 String name = e.getName();
 875                 for (String file : files) {
 876                     if (name.startsWith(file)) {
 877                         dirs.add(extractFile(zis, e));
 878                         break;
 879                     }
 880                 }
 881             }
 882         }
 883 
 884         // Update timestamps of directories specified in archive with their
 885         // timestamps as given in the archive.  We do this after extraction,
 886         // instead of during, because creating a file in a directory changes
 887         // that directory's timestamp.
 888         updateLastModifiedTime(dirs);
 889     }
 890 
 891     /**
 892      * Extracts specified entries from JAR file, via ZipFile.
 893      */
 894     void extract(String fname, String files[]) throws IOException {
 895         ZipFile zf = new ZipFile(fname);
 896         Set<ZipEntry> dirs = newDirSet();
 897         Enumeration<? extends ZipEntry> zes = zf.entries();
 898         while (zes.hasMoreElements()) {
 899             ZipEntry e = zes.nextElement();
 900             InputStream is;
 901             if (files == null) {
 902                 dirs.add(extractFile(zf.getInputStream(e), e));
 903             } else {
 904                 String name = e.getName();
 905                 for (String file : files) {
 906                     if (name.startsWith(file)) {
 907                         dirs.add(extractFile(zf.getInputStream(e), e));
 908                         break;
 909                     }
 910                 }
 911             }
 912         }
 913         zf.close();
 914         updateLastModifiedTime(dirs);
 915     }
 916 
 917     /**
 918      * Extracts next entry from JAR file, creating directories as needed.  If
 919      * the entry is for a directory which doesn't exist prior to this
 920      * invocation, returns that entry, otherwise returns null.
 921      */
 922     ZipEntry extractFile(InputStream is, ZipEntry e) throws IOException {
 923         ZipEntry rc = null;
 924         String name = e.getName();
 925         File f = new File(e.getName().replace('/', File.separatorChar));
 926         if (e.isDirectory()) {
 927             if (f.exists()) {
 928                 if (!f.isDirectory()) {
 929                     throw new IOException(formatMsg("error.create.dir",
 930                         f.getPath()));
 931                 }
 932             } else {
 933                 if (!f.mkdirs()) {
 934                     throw new IOException(formatMsg("error.create.dir",
 935                         f.getPath()));
 936                 } else {
 937                     rc = e;
 938                 }
 939             }
 940 
 941             if (vflag) {
 942                 output(formatMsg("out.create", name));
 943             }
 944         } else {
 945             if (f.getParent() != null) {
 946                 File d = new File(f.getParent());
 947                 if (!d.exists() && !d.mkdirs() || !d.isDirectory()) {
 948                     throw new IOException(formatMsg(
 949                         "error.create.dir", d.getPath()));
 950                 }
 951             }
 952             try {
 953                 copy(is, f);
 954             } finally {
 955                 if (is instanceof ZipInputStream)
 956                     ((ZipInputStream)is).closeEntry();
 957                 else
 958                     is.close();
 959             }
 960             if (vflag) {
 961                 if (e.getMethod() == ZipEntry.DEFLATED) {
 962                     output(formatMsg("out.inflated", name));
 963                 } else {
 964                     output(formatMsg("out.extracted", name));
 965                 }
 966             }
 967         }
 968         if (!useExtractionTime) {
 969             long lastModified = e.getTime();
 970             if (lastModified != -1) {
 971                 f.setLastModified(lastModified);
 972             }
 973         }
 974         return rc;
 975     }
 976 
 977     /**
 978      * Lists contents of JAR file.
 979      */
 980     void list(InputStream in, String files[]) throws IOException {
 981         ZipInputStream zis = new ZipInputStream(in);
 982         ZipEntry e;
 983         while ((e = zis.getNextEntry()) != null) {
 984             /*
 985              * In the case of a compressed (deflated) entry, the entry size
 986              * is stored immediately following the entry data and cannot be
 987              * determined until the entry is fully read. Therefore, we close
 988              * the entry first before printing out its attributes.
 989              */
 990             zis.closeEntry();
 991             printEntry(e, files);
 992         }
 993     }
 994 
 995     /**
 996      * Lists contents of JAR file, via ZipFile.
 997      */
 998     void list(String fname, String files[]) throws IOException {
 999         ZipFile zf = new ZipFile(fname);
1000         Enumeration<? extends ZipEntry> zes = zf.entries();
1001         while (zes.hasMoreElements()) {
1002             printEntry(zes.nextElement(), files);
1003         }
1004         zf.close();
1005     }
1006 
1007     /**
1008      * Outputs the class index table to the INDEX.LIST file of the
1009      * root jar file.
1010      */
1011     void dumpIndex(String rootjar, JarIndex index) throws IOException {
1012         File jarFile = new File(rootjar);
1013         Path jarPath = jarFile.toPath();
1014         Path tmpPath = createTempFileInSameDirectoryAs(jarFile).toPath();
1015         try {
1016             if (update(jarPath.newInputStream(),
1017                        tmpPath.newOutputStream(),
1018                        null, index)) {
1019                 try {
1020                     tmpPath.moveTo(jarPath, REPLACE_EXISTING);
1021                 } catch (IOException e) {
1022                     throw new IOException(getMsg("error.write.file"), e);
1023                 }
1024             }
1025         } finally {
1026             tmpPath.deleteIfExists();
1027         }
1028     }
1029 
1030     private HashSet<String> jarPaths = new HashSet<String>();
1031 
1032     /**
1033      * Generates the transitive closure of the Class-Path attribute for
1034      * the specified jar file.
1035      */
1036     List<String> getJarPath(String jar) throws IOException {
1037         List<String> files = new ArrayList<String>();
1038         files.add(jar);
1039         jarPaths.add(jar);
1040 
1041         // take out the current path
1042         String path = jar.substring(0, Math.max(0, jar.lastIndexOf('/') + 1));
1043 
1044         // class path attribute will give us jar file name with
1045         // '/' as separators, so we need to change them to the
1046         // appropriate one before we open the jar file.
1047         JarFile rf = new JarFile(jar.replace('/', File.separatorChar));
1048 
1049         if (rf != null) {
1050             Manifest man = rf.getManifest();
1051             if (man != null) {
1052                 Attributes attr = man.getMainAttributes();
1053                 if (attr != null) {
1054                     String value = attr.getValue(Attributes.Name.CLASS_PATH);
1055                     if (value != null) {
1056                         StringTokenizer st = new StringTokenizer(value);
1057                         while (st.hasMoreTokens()) {
1058                             String ajar = st.nextToken();
1059                             if (!ajar.endsWith("/")) {  // it is a jar file
1060                                 ajar = path.concat(ajar);
1061                                 /* check on cyclic dependency */
1062                                 if (! jarPaths.contains(ajar)) {
1063                                     files.addAll(getJarPath(ajar));
1064                                 }
1065                             }
1066                         }
1067                     }
1068                 }
1069             }
1070         }
1071         rf.close();
1072         return files;
1073     }
1074 
1075     /**
1076      * Generates class index file for the specified root jar file.
1077      */
1078     void genIndex(String rootjar, String[] files) throws IOException {
1079         List<String> jars = getJarPath(rootjar);
1080         int njars = jars.size();
1081         String[] jarfiles;
1082 
1083         if (njars == 1 && files != null) {
1084             // no class-path attribute defined in rootjar, will
1085             // use command line specified list of jars
1086             for (int i = 0; i < files.length; i++) {
1087                 jars.addAll(getJarPath(files[i]));
1088             }
1089             njars = jars.size();
1090         }
1091         jarfiles = jars.toArray(new String[njars]);
1092         JarIndex index = new JarIndex(jarfiles);
1093         dumpIndex(rootjar, index);
1094     }
1095 
1096     /**
1097      * Prints entry information, if requested.
1098      */
1099     void printEntry(ZipEntry e, String[] files) throws IOException {
1100         if (files == null) {
1101             printEntry(e);
1102         } else {
1103             String name = e.getName();
1104             for (String file : files) {
1105                 if (name.startsWith(file)) {
1106                     printEntry(e);
1107                     return;
1108                 }
1109             }
1110         }
1111     }
1112 
1113     /**
1114      * Prints entry information.
1115      */
1116     void printEntry(ZipEntry e) throws IOException {
1117         if (vflag) {
1118             StringBuilder sb = new StringBuilder();
1119             String s = Long.toString(e.getSize());
1120             for (int i = 6 - s.length(); i > 0; --i) {
1121                 sb.append(' ');
1122             }
1123             sb.append(s).append(' ').append(new Date(e.getTime()).toString());
1124             sb.append(' ').append(e.getName());
1125             output(sb.toString());
1126         } else {
1127             output(e.getName());
1128         }
1129     }
1130 
1131     /**
1132      * Prints usage message.
1133      */
1134     void usageError() {
1135         error(getMsg("usage"));
1136     }
1137 
1138     /**
1139      * A fatal exception has been caught.  No recovery possible
1140      */
1141     void fatalError(Exception e) {
1142         e.printStackTrace();
1143     }
1144 
1145     /**
1146      * A fatal condition has been detected; message is "s".
1147      * No recovery possible
1148      */
1149     void fatalError(String s) {
1150         error(program + ": " + s);
1151     }
1152 
1153     /**
1154      * Print an output message; like verbose output and the like
1155      */
1156     protected void output(String s) {
1157         out.println(s);
1158     }
1159 
1160     /**
1161      * Print an error mesage; like something is broken
1162      */
1163     protected void error(String s) {
1164         err.println(s);
1165     }
1166 
1167     /**
1168      * Main routine to start program.
1169      */
1170     public static void main(String args[]) {
1171         Main jartool = new Main(System.out, System.err, "jar");
1172         System.exit(jartool.run(args) ? 0 : 1);
1173     }
1174 
1175     /**
1176      * An OutputStream that doesn't send its output anywhere, (but could).
1177      * It's here to find the CRC32 of an input file, necessary for STORED
1178      * mode in ZIP.
1179      */
1180     private static class CRC32OutputStream extends java.io.OutputStream {
1181         final CRC32 crc = new CRC32();
1182         long n = 0;
1183 
1184         CRC32OutputStream() {}
1185 
1186         public void write(int r) throws IOException {
1187             crc.update(r);
1188             n++;
1189         }
1190 
1191         public void write(byte[] b, int off, int len) throws IOException {
1192             crc.update(b, off, len);
1193             n += len;
1194         }
1195 
1196         /**
1197          * Updates a ZipEntry which describes the data read by this
1198          * output stream, in STORED mode.
1199          */
1200         public void updateEntry(ZipEntry e) {
1201             e.setMethod(ZipEntry.STORED);
1202             e.setSize(n);
1203             e.setCrc(crc.getValue());
1204         }
1205     }
1206 }