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