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