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