1 /*
   2  * Copyright (c) 1996, 2017, 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.lang.module.Configuration;
  30 import java.lang.module.InvalidModuleDescriptorException;
  31 import java.lang.module.ModuleDescriptor;
  32 import java.lang.module.ModuleDescriptor.Exports;
  33 import java.lang.module.ModuleDescriptor.Provides;
  34 import java.lang.module.ModuleDescriptor.Opens;
  35 import java.lang.module.ModuleDescriptor.Requires;
  36 import java.lang.module.ModuleDescriptor.Version;
  37 import java.lang.module.ModuleFinder;
  38 import java.lang.module.ModuleReader;
  39 import java.lang.module.ModuleReference;
  40 import java.lang.module.ResolutionException;
  41 import java.lang.module.ResolvedModule;
  42 import java.net.URI;
  43 import java.nio.ByteBuffer;
  44 import java.nio.file.Path;
  45 import java.nio.file.Files;
  46 import java.nio.file.Paths;
  47 import java.nio.file.StandardCopyOption;
  48 import java.util.*;
  49 import java.util.function.Consumer;
  50 import java.util.function.Supplier;
  51 import java.util.regex.Pattern;
  52 import java.util.stream.Collectors;
  53 import java.util.stream.Stream;
  54 import java.util.zip.*;
  55 import java.util.jar.*;
  56 import java.util.jar.Pack200.*;
  57 import java.util.jar.Manifest;
  58 import java.text.MessageFormat;
  59 
  60 import jdk.internal.module.Checks;
  61 import jdk.internal.module.ModuleHashes;
  62 import jdk.internal.module.ModuleHashesBuilder;
  63 import jdk.internal.module.ModuleInfo;
  64 import jdk.internal.module.ModuleInfoExtender;
  65 import jdk.internal.module.ModuleResolution;
  66 import jdk.internal.util.jar.JarIndex;
  67 
  68 import static jdk.internal.util.jar.JarIndex.INDEX_NAME;
  69 import static java.util.jar.JarFile.MANIFEST_NAME;
  70 import static java.util.stream.Collectors.joining;
  71 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
  72 
  73 /**
  74  * This class implements a simple utility for creating files in the JAR
  75  * (Java Archive) file format. The JAR format is based on the ZIP file
  76  * format, with optional meta-information stored in a MANIFEST entry.
  77  */
  78 public class Main {
  79     String program;
  80     PrintWriter out, err;
  81     String fname, mname, ename;
  82     String zname = "";
  83     String rootjar = null;
  84 
  85     private static final int BASE_VERSION = 0;
  86 
  87     private static class Entry {
  88         final String name;
  89         final File file;
  90         final boolean isDir;
  91 
  92         Entry(File file, String name, boolean isDir) {
  93             this.file = file;
  94             this.isDir = isDir;
  95             this.name = name;
  96         }
  97 
  98         @Override
  99         public boolean equals(Object o) {
 100             if (this == o) return true;
 101             if (!(o instanceof Entry)) return false;
 102             return this.file.equals(((Entry)o).file);
 103         }
 104 
 105         @Override
 106         public int hashCode() {
 107             return file.hashCode();
 108         }
 109     }
 110 
 111     // An entryName(path)->Entry map generated during "expand", it helps to
 112     // decide whether or not an existing entry in a jar file needs to be
 113     // replaced, during the "update" operation.
 114     Map<String, Entry> entryMap = new HashMap<>();
 115 
 116     // All entries need to be added/updated.
 117     Set<Entry> entries = new LinkedHashSet<>();
 118 
 119     // module-info.class entries need to be added/updated.
 120     Map<String,byte[]> moduleInfos = new HashMap<>();
 121 
 122     // A paths Set for each version, where each Set contains directories
 123     // specified by the "-C" operation.
 124     Map<Integer,Set<String>> pathsMap = new HashMap<>();
 125 
 126     // There's also a files array per version
 127     Map<Integer,String[]> filesMap = new HashMap<>();
 128 
 129     // Do we think this is a multi-release jar?  Set to true
 130     // if --release option found followed by at least file
 131     boolean isMultiRelease;
 132 
 133     /*
 134      * cflag: create
 135      * uflag: update
 136      * xflag: xtract
 137      * tflag: table
 138      * vflag: verbose
 139      * flag0: no zip compression (store only)
 140      * Mflag: DO NOT generate a manifest file (just ZIP)
 141      * iflag: generate jar index
 142      * nflag: Perform jar normalization at the end
 143      * pflag: preserve/don't strip leading slash and .. component from file name
 144      * dflag: print module descriptor
 145      */
 146     boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag, nflag, pflag, dflag;
 147 
 148     /* To support additional GNU Style informational options */
 149     Consumer<PrintWriter> info;
 150 
 151     /* Modular jar related options */
 152     Version moduleVersion;
 153     Pattern modulesToHash;
 154     ModuleResolution moduleResolution = ModuleResolution.empty();
 155     ModuleFinder moduleFinder = ModuleFinder.of();
 156 
 157     static final String MODULE_INFO = "module-info.class";
 158     static final String MANIFEST_DIR = "META-INF/";
 159     static final String VERSIONS_DIR = MANIFEST_DIR + "versions/";
 160     static final String VERSION = "1.0";
 161     static final int VERSIONS_DIR_LENGTH = VERSIONS_DIR.length();
 162     private static ResourceBundle rsrc;
 163 
 164     /**
 165      * If true, maintain compatibility with JDK releases prior to 6.0 by
 166      * timestamping extracted files with the time at which they are extracted.
 167      * Default is to use the time given in the archive.
 168      */
 169     private static final boolean useExtractionTime =
 170         Boolean.getBoolean("sun.tools.jar.useExtractionTime");
 171 
 172     /**
 173      * Initialize ResourceBundle
 174      */
 175     static {
 176         try {
 177             rsrc = ResourceBundle.getBundle("sun.tools.jar.resources.jar");
 178         } catch (MissingResourceException e) {
 179             throw new Error("Fatal: Resource for jar is missing");
 180         }
 181     }
 182 
 183     static String getMsg(String key) {
 184         try {
 185             return (rsrc.getString(key));
 186         } catch (MissingResourceException e) {
 187             throw new Error("Error in message file");
 188         }
 189     }
 190 
 191     static String formatMsg(String key, String arg) {
 192         String msg = getMsg(key);
 193         String[] args = new String[1];
 194         args[0] = arg;
 195         return MessageFormat.format(msg, (Object[]) args);
 196     }
 197 
 198     static String formatMsg2(String key, String arg, String arg1) {
 199         String msg = getMsg(key);
 200         String[] args = new String[2];
 201         args[0] = arg;
 202         args[1] = arg1;
 203         return MessageFormat.format(msg, (Object[]) args);
 204     }
 205 
 206     public Main(PrintStream out, PrintStream err, String program) {
 207         this.out = new PrintWriter(out, true);
 208         this.err = new PrintWriter(err, true);
 209         this.program = program;
 210     }
 211 
 212     public Main(PrintWriter out, PrintWriter err, String program) {
 213         this.out = out;
 214         this.err = err;
 215         this.program = program;
 216     }
 217 
 218     /**
 219      * Creates a new empty temporary file in the same directory as the
 220      * specified file.  A variant of File.createTempFile.
 221      */
 222     private static File createTempFileInSameDirectoryAs(File file)
 223         throws IOException {
 224         File dir = file.getParentFile();
 225         if (dir == null)
 226             dir = new File(".");
 227         return File.createTempFile("jartmp", null, dir);
 228     }
 229 
 230     private boolean ok;
 231 
 232     /**
 233      * Starts main program with the specified arguments.
 234      */
 235     public synchronized boolean run(String args[]) {
 236         ok = true;
 237         if (!parseArgs(args)) {
 238             return false;
 239         }
 240         File tmpFile = null;
 241         try {
 242             if (cflag || uflag) {
 243                 if (fname != null) {
 244                     // The name of the zip file as it would appear as its own
 245                     // zip file entry. We use this to make sure that we don't
 246                     // add the zip file to itself.
 247                     zname = fname.replace(File.separatorChar, '/');
 248                     if (zname.startsWith("./")) {
 249                         zname = zname.substring(2);
 250                     }
 251                 }
 252             }
 253             if (cflag) {
 254                 Manifest manifest = null;
 255                 if (!Mflag) {
 256                     if (mname != null) {
 257                         try (InputStream in = new FileInputStream(mname)) {
 258                             manifest = new Manifest(new BufferedInputStream(in));
 259                         }
 260                     } else {
 261                         manifest = new Manifest();
 262                     }
 263                     addVersion(manifest);
 264                     addCreatedBy(manifest);
 265                     if (isAmbiguousMainClass(manifest)) {
 266                         return false;
 267                     }
 268                     if (ename != null) {
 269                         addMainClass(manifest, ename);
 270                     }
 271                     if (isMultiRelease) {
 272                         addMultiRelease(manifest);
 273                     }
 274                 }
 275                 expand();
 276                 if (!moduleInfos.isEmpty()) {
 277                     // All actual file entries (excl manifest and module-info.class)
 278                     Set<String> jentries = new HashSet<>();
 279                     // all packages if it's a class or resource
 280                     Set<String> packages = new HashSet<>();
 281                     entries.stream()
 282                            .filter(e -> !e.isDir)
 283                            .forEach( e -> {
 284                                addPackageIfNamed(packages, e.name);
 285                                jentries.add(e.name);
 286                     });
 287                     addExtendedModuleAttributes(moduleInfos, packages);
 288 
 289                     // Basic consistency checks for modular jars.
 290                     if (!checkModuleInfo(moduleInfos.get(MODULE_INFO), jentries))
 291                         return false;
 292 
 293                 } else if (moduleVersion != null || modulesToHash != null) {
 294                     error(getMsg("error.module.options.without.info"));
 295                     return false;
 296                 }
 297                 if (vflag && fname == null) {
 298                     // Disable verbose output so that it does not appear
 299                     // on stdout along with file data
 300                     // error("Warning: -v option ignored");
 301                     vflag = false;
 302                 }
 303                 final String tmpbase = (fname == null)
 304                         ? "tmpjar"
 305                         : fname.substring(fname.indexOf(File.separatorChar) + 1);
 306 
 307                 tmpFile = createTemporaryFile(tmpbase, ".jar");
 308                 try (OutputStream out = new FileOutputStream(tmpFile)) {
 309                     create(new BufferedOutputStream(out, 4096), manifest);
 310                 }
 311                 if (nflag) {
 312                     File packFile = createTemporaryFile(tmpbase, ".pack");
 313                     try {
 314                         Packer packer = Pack200.newPacker();
 315                         Map<String, String> p = packer.properties();
 316                         p.put(Packer.EFFORT, "1"); // Minimal effort to conserve CPU
 317                         try (JarFile jarFile = new JarFile(tmpFile.getCanonicalPath());
 318                              OutputStream pack = new FileOutputStream(packFile))
 319                         {
 320                             packer.pack(jarFile, pack);
 321                         }
 322                         if (tmpFile.exists()) {
 323                             tmpFile.delete();
 324                         }
 325                         tmpFile = createTemporaryFile(tmpbase, ".jar");
 326                         try (OutputStream out = new FileOutputStream(tmpFile);
 327                              JarOutputStream jos = new JarOutputStream(out))
 328                         {
 329                             Unpacker unpacker = Pack200.newUnpacker();
 330                             unpacker.unpack(packFile, jos);
 331                         }
 332                     } finally {
 333                         Files.deleteIfExists(packFile.toPath());
 334                     }
 335                 }
 336                 validateAndClose(tmpFile);
 337             } else if (uflag) {
 338                 File inputFile = null;
 339                 if (fname != null) {
 340                     inputFile = new File(fname);
 341                     tmpFile = createTempFileInSameDirectoryAs(inputFile);
 342                 } else {
 343                     vflag = false;
 344                     tmpFile = createTemporaryFile("tmpjar", ".jar");
 345                 }
 346                 expand();
 347                 try (FileInputStream in = (fname != null) ? new FileInputStream(inputFile)
 348                         : new FileInputStream(FileDescriptor.in);
 349                      FileOutputStream out = new FileOutputStream(tmpFile);
 350                      InputStream manifest = (!Mflag && (mname != null)) ?
 351                             (new FileInputStream(mname)) : null;
 352                 ) {
 353                     boolean updateOk = update(in, new BufferedOutputStream(out),
 354                         manifest, moduleInfos, null);
 355                     if (ok) {
 356                         ok = updateOk;
 357                     }
 358                 }
 359                 validateAndClose(tmpFile);
 360             } else if (tflag) {
 361                 replaceFSC(filesMap);
 362                 // For the "list table contents" action, access using the
 363                 // ZipFile class is always most efficient since only a
 364                 // "one-finger" scan through the central directory is required.
 365                 String[] files = filesMapToFiles(filesMap);
 366                 if (fname != null) {
 367                     list(fname, files);
 368                 } else {
 369                     InputStream in = new FileInputStream(FileDescriptor.in);
 370                     try {
 371                         list(new BufferedInputStream(in), files);
 372                     } finally {
 373                         in.close();
 374                     }
 375                 }
 376             } else if (xflag) {
 377                 replaceFSC(filesMap);
 378                 // For the extract action, when extracting all the entries,
 379                 // access using the ZipInputStream class is most efficient,
 380                 // since only a single sequential scan through the zip file is
 381                 // required.  When using the ZipFile class, a "two-finger" scan
 382                 // is required, but this is likely to be more efficient when a
 383                 // partial extract is requested.  In case the zip file has
 384                 // "leading garbage", we fall back from the ZipInputStream
 385                 // implementation to the ZipFile implementation, since only the
 386                 // latter can handle it.
 387 
 388                 String[] files = filesMapToFiles(filesMap);
 389                 if (fname != null && files != null) {
 390                     extract(fname, files);
 391                 } else {
 392                     InputStream in = (fname == null)
 393                         ? new FileInputStream(FileDescriptor.in)
 394                         : new FileInputStream(fname);
 395                     try {
 396                         if (!extract(new BufferedInputStream(in), files) && fname != null) {
 397                             extract(fname, files);
 398                         }
 399                     } finally {
 400                         in.close();
 401                     }
 402                 }
 403             } else if (iflag) {
 404                 String[] files = filesMap.get(BASE_VERSION);  // base entries only, can be null
 405                 genIndex(rootjar, files);
 406             } else if (dflag) {
 407                 boolean found;
 408                 if (fname != null) {
 409                     try (ZipFile zf = new ZipFile(fname)) {
 410                         found = printModuleDescriptor(zf);
 411                     }
 412                 } else {
 413                     try (FileInputStream fin = new FileInputStream(FileDescriptor.in)) {
 414                         found = printModuleDescriptor(fin);
 415                     }
 416                 }
 417                 if (!found)
 418                     error(getMsg("error.module.descriptor.not.found"));
 419             }
 420         } catch (IOException e) {
 421             fatalError(e);
 422             ok = false;
 423         } catch (Error ee) {
 424             ee.printStackTrace();
 425             ok = false;
 426         } catch (Throwable t) {
 427             t.printStackTrace();
 428             ok = false;
 429         } finally {
 430             if (tmpFile != null && tmpFile.exists())
 431                 tmpFile.delete();
 432         }
 433         out.flush();
 434         err.flush();
 435         return ok;
 436     }
 437 
 438     private void validateAndClose(File tmpfile) throws IOException {
 439         if (ok && isMultiRelease) {
 440             try (JarFile jf = new JarFile(tmpfile)) {
 441                 ok = Validator.validate(this, jf);
 442                 if (!ok) {
 443                     error(formatMsg("error.validator.jarfile.invalid", fname));
 444                 }
 445             } catch (IOException e) {
 446                 error(formatMsg2("error.validator.jarfile.exception", fname, e.getMessage()));
 447             }
 448         }
 449         Path path = tmpfile.toPath();
 450         try {
 451             if (ok) {
 452                 if (fname != null) {
 453                     Files.move(path, Paths.get(fname), StandardCopyOption.REPLACE_EXISTING);
 454                 } else {
 455                     Files.copy(path, new FileOutputStream(FileDescriptor.out));
 456                 }
 457             }
 458         } finally {
 459             Files.deleteIfExists(path);
 460         }
 461     }
 462 
 463     private String[] filesMapToFiles(Map<Integer,String[]> filesMap) {
 464         if (filesMap.isEmpty()) return null;
 465         return filesMap.entrySet()
 466                 .stream()
 467                 .flatMap(this::filesToEntryNames)
 468                 .toArray(String[]::new);
 469     }
 470 
 471     Stream<String> filesToEntryNames(Map.Entry<Integer,String[]> fileEntries) {
 472         int version = fileEntries.getKey();
 473         Set<String> cpaths = pathsMap.get(version);
 474         return Stream.of(fileEntries.getValue())
 475             .map(f -> toVersionedName(toEntryName(f, cpaths, false), version));
 476     }
 477 
 478     /**
 479      * Parses command line arguments.
 480      */
 481     boolean parseArgs(String args[]) {
 482         /* Preprocess and expand @file arguments */
 483         try {
 484             args = CommandLine.parse(args);
 485         } catch (FileNotFoundException e) {
 486             fatalError(formatMsg("error.cant.open", e.getMessage()));
 487             return false;
 488         } catch (IOException e) {
 489             fatalError(e);
 490             return false;
 491         }
 492         /* parse flags */
 493         int count = 1;
 494         try {
 495             String flags = args[0];
 496 
 497             // Note: flags.length == 2 can be treated as the short version of
 498             // the GNU option since the there cannot be any other options,
 499             // excluding -C, as per the old way.
 500             if (flags.startsWith("--") ||
 501                 (flags.startsWith("-") && flags.length() == 2)) {
 502                 try {
 503                     count = GNUStyleOptions.parseOptions(this, args);
 504                 } catch (GNUStyleOptions.BadArgs x) {
 505                     if (info == null) {
 506                         if (x.showUsage) {
 507                             usageError(x.getMessage());
 508                         } else {
 509                             error(x.getMessage());
 510                         }
 511                         return false;
 512                     }
 513                 }
 514                 if (info != null) {
 515                     info.accept(out);
 516                     return true;
 517                 }
 518             } else {
 519                 // Legacy/compatibility options
 520                 if (flags.startsWith("-")) {
 521                     flags = flags.substring(1);
 522                 }
 523                 for (int i = 0; i < flags.length(); i++) {
 524                     switch (flags.charAt(i)) {
 525                         case 'c':
 526                             if (xflag || tflag || uflag || iflag) {
 527                                 usageError(getMsg("error.multiple.main.operations"));
 528                                 return false;
 529                             }
 530                             cflag = true;
 531                             break;
 532                         case 'u':
 533                             if (cflag || xflag || tflag || iflag) {
 534                                 usageError(getMsg("error.multiple.main.operations"));
 535                                 return false;
 536                             }
 537                             uflag = true;
 538                             break;
 539                         case 'x':
 540                             if (cflag || uflag || tflag || iflag) {
 541                                 usageError(getMsg("error.multiple.main.operations"));
 542                                 return false;
 543                             }
 544                             xflag = true;
 545                             break;
 546                         case 't':
 547                             if (cflag || uflag || xflag || iflag) {
 548                                 usageError(getMsg("error.multiple.main.operations"));
 549                                 return false;
 550                             }
 551                             tflag = true;
 552                             break;
 553                         case 'M':
 554                             Mflag = true;
 555                             break;
 556                         case 'v':
 557                             vflag = true;
 558                             break;
 559                         case 'f':
 560                             fname = args[count++];
 561                             break;
 562                         case 'm':
 563                             mname = args[count++];
 564                             break;
 565                         case '0':
 566                             flag0 = true;
 567                             break;
 568                         case 'i':
 569                             if (cflag || uflag || xflag || tflag) {
 570                                 usageError(getMsg("error.multiple.main.operations"));
 571                                 return false;
 572                             }
 573                             // do not increase the counter, files will contain rootjar
 574                             rootjar = args[count++];
 575                             iflag = true;
 576                             break;
 577                         case 'n':
 578                             nflag = true;
 579                             break;
 580                         case 'e':
 581                             ename = args[count++];
 582                             break;
 583                         case 'P':
 584                             pflag = true;
 585                             break;
 586                         default:
 587                             usageError(formatMsg("error.illegal.option",
 588                                        String.valueOf(flags.charAt(i))));
 589                             return false;
 590                     }
 591                 }
 592             }
 593         } catch (ArrayIndexOutOfBoundsException e) {
 594             usageError(getMsg("main.usage.summary"));
 595             return false;
 596         }
 597         if (!cflag && !tflag && !xflag && !uflag && !iflag && !dflag) {
 598             usageError(getMsg("error.bad.option"));
 599             return false;
 600         }
 601 
 602         /* parse file arguments */
 603         int n = args.length - count;
 604         if (n > 0) {
 605             if (dflag) {
 606                 // "--print-module-descriptor/-d" does not require file argument(s)
 607                 usageError(formatMsg("error.bad.dflag", args[count]));
 608                 return false;
 609             }
 610             int version = BASE_VERSION;
 611             int k = 0;
 612             String[] nameBuf = new String[n];
 613             pathsMap.put(version, new HashSet<>());
 614             try {
 615                 for (int i = count; i < args.length; i++) {
 616                     if (args[i].equals("-C")) {
 617                         /* change the directory */
 618                         String dir = args[++i];
 619                         dir = (dir.endsWith(File.separator) ?
 620                                dir : (dir + File.separator));
 621                         dir = dir.replace(File.separatorChar, '/');
 622                         while (dir.indexOf("//") > -1) {
 623                             dir = dir.replace("//", "/");
 624                         }
 625                         pathsMap.get(version).add(dir.replace(File.separatorChar, '/'));
 626                         nameBuf[k++] = dir + args[++i];
 627                     } else if (args[i].startsWith("--release")) {
 628                         int v = BASE_VERSION;
 629                         try {
 630                             v = Integer.valueOf(args[++i]);
 631                         } catch (NumberFormatException x) {
 632                             error(formatMsg("error.release.value.notnumber", args[i]));
 633                             // this will fall into the next error, thus returning false
 634                         }
 635                         if (v < 9) {
 636                             usageError(formatMsg("error.release.value.toosmall", String.valueOf(v)));
 637                             return false;
 638                         }
 639                         // associate the files, if any, with the previous version number
 640                         if (k > 0) {
 641                             String[] files = new String[k];
 642                             System.arraycopy(nameBuf, 0, files, 0, k);
 643                             filesMap.put(version, files);
 644                             isMultiRelease = version > BASE_VERSION;
 645                         }
 646                         // reset the counters and start with the new version number
 647                         k = 0;
 648                         nameBuf = new String[n];
 649                         version = v;
 650                         pathsMap.put(version, new HashSet<>());
 651                     } else {
 652                         nameBuf[k++] = args[i];
 653                     }
 654                 }
 655             } catch (ArrayIndexOutOfBoundsException e) {
 656                 usageError(getMsg("error.bad.file.arg"));
 657                 return false;
 658             }
 659             // associate remaining files, if any, with a version
 660             if (k > 0) {
 661                 String[] files = new String[k];
 662                 System.arraycopy(nameBuf, 0, files, 0, k);
 663                 filesMap.put(version, files);
 664                 isMultiRelease = version > BASE_VERSION;
 665             }
 666         } else if (cflag && (mname == null)) {
 667             usageError(getMsg("error.bad.cflag"));
 668             return false;
 669         } else if (uflag) {
 670             if ((mname != null) || (ename != null)) {
 671                 /* just want to update the manifest */
 672                 return true;
 673             } else {
 674                 usageError(getMsg("error.bad.uflag"));
 675                 return false;
 676             }
 677         }
 678         return true;
 679     }
 680 
 681     /*
 682      * Add the package of the given resource name if it's a .class
 683      * or a resource in a named package.
 684      */
 685     void addPackageIfNamed(Set<String> packages, String name) {
 686         if (name.startsWith(VERSIONS_DIR)) {
 687             // trim the version dir prefix
 688             int i0 = VERSIONS_DIR_LENGTH;
 689             int i = name.indexOf('/', i0);
 690             if (i <= 0) {
 691                 warn(formatMsg("warn.release.unexpected.versioned.entry", name));
 692                 return;
 693             }
 694             while (i0 < i) {
 695                 char c = name.charAt(i0);
 696                 if (c < '0' || c > '9') {
 697                     warn(formatMsg("warn.release.unexpected.versioned.entry", name));
 698                     return;
 699                 }
 700                 i0++;
 701             }
 702             name = name.substring(i + 1, name.length());
 703         }
 704         String pn = toPackageName(name);
 705         // add if this is a class or resource in a package
 706         if (Checks.isPackageName(pn)) {
 707             packages.add(pn);
 708         }
 709     }
 710 
 711     private String toEntryName(String name, Set<String> cpaths, boolean isDir) {
 712         name = name.replace(File.separatorChar, '/');
 713         if (isDir) {
 714             name = name.endsWith("/") ? name : name + "/";
 715         }
 716         String matchPath = "";
 717         for (String path : cpaths) {
 718             if (name.startsWith(path) && path.length() > matchPath.length()) {
 719                 matchPath = path;
 720             }
 721         }
 722         name = safeName(name.substring(matchPath.length()));
 723         // the old implementaton doesn't remove
 724         // "./" if it was led by "/" (?)
 725         if (name.startsWith("./")) {
 726             name = name.substring(2);
 727         }
 728         return name;
 729     }
 730 
 731     private static String toVersionedName(String name, int version) {
 732         return version > BASE_VERSION
 733                 ? VERSIONS_DIR + version + "/" + name : name;
 734     }
 735 
 736     private static String toPackageName(String path) {
 737         int index = path.lastIndexOf('/');
 738         if (index != -1) {
 739             return path.substring(0, index).replace('/', '.');
 740         } else {
 741             return "";
 742         }
 743     }
 744 
 745     private void expand() throws IOException {
 746         for (int version : filesMap.keySet()) {
 747             String[] files = filesMap.get(version);
 748             expand(null, files, pathsMap.get(version), version);
 749         }
 750     }
 751 
 752     /**
 753      * Expands list of files to process into full list of all files that
 754      * can be found by recursively descending directories.
 755      *
 756      * @param dir    parent directory
 757      * @param file s list of files to expand
 758      * @param cpaths set of directories specified by -C option for the files
 759      * @throws IOException if an I/O error occurs
 760      */
 761     private void expand(File dir, String[] files, Set<String> cpaths, int version)
 762         throws IOException
 763     {
 764         if (files == null)
 765             return;
 766 
 767         for (int i = 0; i < files.length; i++) {
 768             File f;
 769             if (dir == null)
 770                 f = new File(files[i]);
 771             else
 772                 f = new File(dir, files[i]);
 773 
 774             boolean isDir = f.isDirectory();
 775             String name = toEntryName(f.getPath(), cpaths, isDir);
 776 
 777             if (version != BASE_VERSION) {
 778                 if (name.startsWith(VERSIONS_DIR)) {
 779                     // the entry starts with VERSIONS_DIR and version != BASE_VERSION,
 780                     // which means the "[dirs|files]" in --release v [dirs|files]
 781                     // includes VERSIONS_DIR-ed entries --> warning and skip (?)
 782                     error(formatMsg2("error.release.unexpected.versioned.entry",
 783                                      name, String.valueOf(version)));
 784                     ok = false;
 785                     return;
 786                 }
 787                 name = toVersionedName(name, version);
 788             }
 789 
 790             if (f.isFile()) {
 791                 Entry e = new Entry(f, name, false);
 792                 if (isModuleInfoEntry(name)) {
 793                     moduleInfos.putIfAbsent(name, Files.readAllBytes(f.toPath()));
 794                     if (uflag)
 795                         entryMap.put(name, e);
 796                 } else if (entries.add(e)) {
 797                     if (uflag)
 798                         entryMap.put(name, e);
 799                 }
 800             } else if (isDir) {
 801                 Entry e = new Entry(f, name, true);
 802                 if (entries.add(e)) {
 803                     // utilize entryMap for the duplicate dir check even in
 804                     // case of cflag == true.
 805                     // dir name confilict/duplicate could happen with -C option.
 806                     // just remove the last "e" from the "entries" (zos will fail
 807                     // with "duplicated" entries), but continue expanding the
 808                     // sub tree
 809                     if (entryMap.containsKey(name)) {
 810                         entries.remove(e);
 811                     } else {
 812                         entryMap.put(name, e);
 813                     }
 814                     expand(f, f.list(), cpaths, version);
 815                 }
 816             } else {
 817                 error(formatMsg("error.nosuch.fileordir", String.valueOf(f)));
 818                 ok = false;
 819             }
 820         }
 821     }
 822 
 823     /**
 824      * Creates a new JAR file.
 825      */
 826     void create(OutputStream out, Manifest manifest) throws IOException
 827     {
 828         try (ZipOutputStream zos = new JarOutputStream(out)) {
 829             if (flag0) {
 830                 zos.setMethod(ZipOutputStream.STORED);
 831             }
 832             // TODO: check module-info attributes against manifest ??
 833             if (manifest != null) {
 834                 if (vflag) {
 835                     output(getMsg("out.added.manifest"));
 836                 }
 837                 ZipEntry e = new ZipEntry(MANIFEST_DIR);
 838                 e.setTime(System.currentTimeMillis());
 839                 e.setSize(0);
 840                 e.setCrc(0);
 841                 zos.putNextEntry(e);
 842                 e = new ZipEntry(MANIFEST_NAME);
 843                 e.setTime(System.currentTimeMillis());
 844                 if (flag0) {
 845                     crc32Manifest(e, manifest);
 846                 }
 847                 zos.putNextEntry(e);
 848                 manifest.write(zos);
 849                 zos.closeEntry();
 850             }
 851             updateModuleInfo(moduleInfos, zos);
 852             for (Entry entry : entries) {
 853                 addFile(zos, entry);
 854             }
 855         }
 856     }
 857 
 858     private char toUpperCaseASCII(char c) {
 859         return (c < 'a' || c > 'z') ? c : (char) (c + 'A' - 'a');
 860     }
 861 
 862     /**
 863      * Compares two strings for equality, ignoring case.  The second
 864      * argument must contain only upper-case ASCII characters.
 865      * We don't want case comparison to be locale-dependent (else we
 866      * have the notorious "turkish i bug").
 867      */
 868     private boolean equalsIgnoreCase(String s, String upper) {
 869         assert upper.toUpperCase(java.util.Locale.ENGLISH).equals(upper);
 870         int len;
 871         if ((len = s.length()) != upper.length())
 872             return false;
 873         for (int i = 0; i < len; i++) {
 874             char c1 = s.charAt(i);
 875             char c2 = upper.charAt(i);
 876             if (c1 != c2 && toUpperCaseASCII(c1) != c2)
 877                 return false;
 878         }
 879         return true;
 880     }
 881 
 882     /**
 883      * Updates an existing jar file.
 884      */
 885     boolean update(InputStream in, OutputStream out,
 886                    InputStream newManifest,
 887                    Map<String,byte[]> moduleInfos,
 888                    JarIndex jarIndex) throws IOException
 889     {
 890         ZipInputStream zis = new ZipInputStream(in);
 891         ZipOutputStream zos = new JarOutputStream(out);
 892         ZipEntry e = null;
 893         boolean foundManifest = false;
 894         boolean updateOk = true;
 895 
 896         // All actual entries added/updated/existing, in the jar file (excl manifest
 897         // and module-info.class ).
 898         Set<String> jentries = new HashSet<>();
 899 
 900         if (jarIndex != null) {
 901             addIndex(jarIndex, zos);
 902         }
 903 
 904         // put the old entries first, replace if necessary
 905         while ((e = zis.getNextEntry()) != null) {
 906             String name = e.getName();
 907 
 908             boolean isManifestEntry = equalsIgnoreCase(name, MANIFEST_NAME);
 909             boolean isModuleInfoEntry = isModuleInfoEntry(name);
 910 
 911             if ((jarIndex != null && equalsIgnoreCase(name, INDEX_NAME))
 912                 || (Mflag && isManifestEntry)) {
 913                 continue;
 914             } else if (isManifestEntry && ((newManifest != null) ||
 915                         (ename != null) || isMultiRelease)) {
 916                 foundManifest = true;
 917                 if (newManifest != null) {
 918                     // Don't read from the newManifest InputStream, as we
 919                     // might need it below, and we can't re-read the same data
 920                     // twice.
 921                     FileInputStream fis = new FileInputStream(mname);
 922                     boolean ambiguous = isAmbiguousMainClass(new Manifest(fis));
 923                     fis.close();
 924                     if (ambiguous) {
 925                         return false;
 926                     }
 927                 }
 928                 // Update the manifest.
 929                 Manifest old = new Manifest(zis);
 930                 if (newManifest != null) {
 931                     old.read(newManifest);
 932                 }
 933                 if (!updateManifest(old, zos)) {
 934                     return false;
 935                 }
 936             } else if (moduleInfos != null && isModuleInfoEntry) {
 937                 moduleInfos.putIfAbsent(name, zis.readAllBytes());
 938             } else {
 939                 boolean isDir = e.isDirectory();
 940                 if (!entryMap.containsKey(name)) { // copy the old stuff
 941                     // do our own compression
 942                     ZipEntry e2 = new ZipEntry(name);
 943                     e2.setMethod(e.getMethod());
 944                     e2.setTime(e.getTime());
 945                     e2.setComment(e.getComment());
 946                     e2.setExtra(e.getExtra());
 947                     if (e.getMethod() == ZipEntry.STORED) {
 948                         e2.setSize(e.getSize());
 949                         e2.setCrc(e.getCrc());
 950                     }
 951                     zos.putNextEntry(e2);
 952                     copy(zis, zos);
 953                 } else { // replace with the new files
 954                     Entry ent = entryMap.get(name);
 955                     addFile(zos, ent);
 956                     entryMap.remove(name);
 957                     entries.remove(ent);
 958                     isDir = ent.isDir;
 959                 }
 960                 if (!isDir) {
 961                     jentries.add(name);
 962                 }
 963             }
 964         }
 965 
 966         // add the remaining new files
 967         for (Entry entry : entries) {
 968             addFile(zos, entry);
 969             if (!entry.isDir) {
 970                 jentries.add(entry.name);
 971             }
 972         }
 973         if (!foundManifest) {
 974             if (newManifest != null) {
 975                 Manifest m = new Manifest(newManifest);
 976                 updateOk = !isAmbiguousMainClass(m);
 977                 if (updateOk) {
 978                     if (!updateManifest(m, zos)) {
 979                         updateOk = false;
 980                     }
 981                 }
 982             } else if (ename != null) {
 983                 if (!updateManifest(new Manifest(), zos)) {
 984                     updateOk = false;
 985                 }
 986             }
 987         }
 988         if (updateOk) {
 989             if (moduleInfos != null && !moduleInfos.isEmpty()) {
 990                 Set<String> pkgs = new HashSet<>();
 991                 jentries.forEach( je -> addPackageIfNamed(pkgs, je));
 992                 addExtendedModuleAttributes(moduleInfos, pkgs);
 993                 updateOk = checkModuleInfo(moduleInfos.get(MODULE_INFO), jentries);
 994                 updateModuleInfo(moduleInfos, zos);
 995                 // TODO: check manifest main classes, etc
 996             } else if (moduleVersion != null || modulesToHash != null) {
 997                 error(getMsg("error.module.options.without.info"));
 998                 updateOk = false;
 999             }
1000         }
1001         zis.close();
1002         zos.close();
1003         return updateOk;
1004     }
1005 
1006     private void addIndex(JarIndex index, ZipOutputStream zos)
1007         throws IOException
1008     {
1009         ZipEntry e = new ZipEntry(INDEX_NAME);
1010         e.setTime(System.currentTimeMillis());
1011         if (flag0) {
1012             CRC32OutputStream os = new CRC32OutputStream();
1013             index.write(os);
1014             os.updateEntry(e);
1015         }
1016         zos.putNextEntry(e);
1017         index.write(zos);
1018         zos.closeEntry();
1019     }
1020 
1021     private void updateModuleInfo(Map<String,byte[]> moduleInfos, ZipOutputStream zos)
1022         throws IOException
1023     {
1024         String fmt = uflag ? "out.update.module-info": "out.added.module-info";
1025         for (Map.Entry<String,byte[]> mi : moduleInfos.entrySet()) {
1026             String name = mi.getKey();
1027             byte[] bytes = mi.getValue();
1028             ZipEntry e = new ZipEntry(name);
1029             e.setTime(System.currentTimeMillis());
1030             if (flag0) {
1031                 crc32ModuleInfo(e, bytes);
1032             }
1033             zos.putNextEntry(e);
1034             zos.write(bytes);
1035             zos.closeEntry();
1036             if (vflag) {
1037                 output(formatMsg(fmt, name));
1038             }
1039         }
1040     }
1041 
1042     private boolean updateManifest(Manifest m, ZipOutputStream zos)
1043         throws IOException
1044     {
1045         addVersion(m);
1046         addCreatedBy(m);
1047         if (ename != null) {
1048             addMainClass(m, ename);
1049         }
1050         if (isMultiRelease) {
1051             addMultiRelease(m);
1052         }
1053         ZipEntry e = new ZipEntry(MANIFEST_NAME);
1054         e.setTime(System.currentTimeMillis());
1055         if (flag0) {
1056             crc32Manifest(e, m);
1057         }
1058         zos.putNextEntry(e);
1059         m.write(zos);
1060         if (vflag) {
1061             output(getMsg("out.update.manifest"));
1062         }
1063         return true;
1064     }
1065 
1066     private static final boolean isWinDriveLetter(char c) {
1067         return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'));
1068     }
1069 
1070     private String safeName(String name) {
1071         if (!pflag) {
1072             int len = name.length();
1073             int i = name.lastIndexOf("../");
1074             if (i == -1) {
1075                 i = 0;
1076             } else {
1077                 i += 3; // strip any dot-dot components
1078             }
1079             if (File.separatorChar == '\\') {
1080                 // the spec requests no drive letter. skip if
1081                 // the entry name has one.
1082                 while (i < len) {
1083                     int off = i;
1084                     if (i + 1 < len &&
1085                         name.charAt(i + 1) == ':' &&
1086                         isWinDriveLetter(name.charAt(i))) {
1087                         i += 2;
1088                     }
1089                     while (i < len && name.charAt(i) == '/') {
1090                         i++;
1091                     }
1092                     if (i == off) {
1093                         break;
1094                     }
1095                 }
1096             } else {
1097                 while (i < len && name.charAt(i) == '/') {
1098                     i++;
1099                 }
1100             }
1101             if (i != 0) {
1102                 name = name.substring(i);
1103             }
1104         }
1105         return name;
1106     }
1107 
1108     private void addVersion(Manifest m) {
1109         Attributes global = m.getMainAttributes();
1110         if (global.getValue(Attributes.Name.MANIFEST_VERSION) == null) {
1111             global.put(Attributes.Name.MANIFEST_VERSION, VERSION);
1112         }
1113     }
1114 
1115     private void addCreatedBy(Manifest m) {
1116         Attributes global = m.getMainAttributes();
1117         if (global.getValue(new Attributes.Name("Created-By")) == null) {
1118             String javaVendor = System.getProperty("java.vendor");
1119             String jdkVersion = System.getProperty("java.version");
1120             global.put(new Attributes.Name("Created-By"), jdkVersion + " (" +
1121                         javaVendor + ")");
1122         }
1123     }
1124 
1125     private void addMainClass(Manifest m, String mainApp) {
1126         Attributes global = m.getMainAttributes();
1127 
1128         // overrides any existing Main-Class attribute
1129         global.put(Attributes.Name.MAIN_CLASS, mainApp);
1130     }
1131 
1132     private void addMultiRelease(Manifest m) {
1133         Attributes global = m.getMainAttributes();
1134         global.put(Attributes.Name.MULTI_RELEASE, "true");
1135     }
1136 
1137     private boolean isAmbiguousMainClass(Manifest m) {
1138         if (ename != null) {
1139             Attributes global = m.getMainAttributes();
1140             if ((global.get(Attributes.Name.MAIN_CLASS) != null)) {
1141                 usageError(getMsg("error.bad.eflag"));
1142                 return true;
1143             }
1144         }
1145         return false;
1146     }
1147 
1148     /**
1149      * Adds a new file entry to the ZIP output stream.
1150      */
1151     void addFile(ZipOutputStream zos, Entry entry) throws IOException {
1152 
1153         File file = entry.file;
1154         String name = entry.name;
1155         boolean isDir = entry.isDir;
1156 
1157         if (name.equals("") || name.equals(".") || name.equals(zname)) {
1158             return;
1159         } else if ((name.equals(MANIFEST_DIR) || name.equals(MANIFEST_NAME))
1160                    && !Mflag) {
1161             if (vflag) {
1162                 output(formatMsg("out.ignore.entry", name));
1163             }
1164             return;
1165         } else if (name.equals(MODULE_INFO)) {
1166             throw new Error("Unexpected module info: " + name);
1167         }
1168 
1169         long size = isDir ? 0 : file.length();
1170 
1171         if (vflag) {
1172             out.print(formatMsg("out.adding", name));
1173         }
1174         ZipEntry e = new ZipEntry(name);
1175         e.setTime(file.lastModified());
1176         if (size == 0) {
1177             e.setMethod(ZipEntry.STORED);
1178             e.setSize(0);
1179             e.setCrc(0);
1180         } else if (flag0) {
1181             crc32File(e, file);
1182         }
1183         zos.putNextEntry(e);
1184         if (!isDir) {
1185             copy(file, zos);
1186         }
1187         zos.closeEntry();
1188         /* report how much compression occurred. */
1189         if (vflag) {
1190             size = e.getSize();
1191             long csize = e.getCompressedSize();
1192             out.print(formatMsg2("out.size", String.valueOf(size),
1193                         String.valueOf(csize)));
1194             if (e.getMethod() == ZipEntry.DEFLATED) {
1195                 long ratio = 0;
1196                 if (size != 0) {
1197                     ratio = ((size - csize) * 100) / size;
1198                 }
1199                 output(formatMsg("out.deflated", String.valueOf(ratio)));
1200             } else {
1201                 output(getMsg("out.stored"));
1202             }
1203         }
1204     }
1205 
1206     /**
1207      * A buffer for use only by copy(InputStream, OutputStream).
1208      * Not as clean as allocating a new buffer as needed by copy,
1209      * but significantly more efficient.
1210      */
1211     private byte[] copyBuf = new byte[8192];
1212 
1213     /**
1214      * Copies all bytes from the input stream to the output stream.
1215      * Does not close or flush either stream.
1216      *
1217      * @param from the input stream to read from
1218      * @param to the output stream to write to
1219      * @throws IOException if an I/O error occurs
1220      */
1221     private void copy(InputStream from, OutputStream to) throws IOException {
1222         int n;
1223         while ((n = from.read(copyBuf)) != -1)
1224             to.write(copyBuf, 0, n);
1225     }
1226 
1227     /**
1228      * Copies all bytes from the input file to the output stream.
1229      * Does not close or flush the output stream.
1230      *
1231      * @param from the input file to read from
1232      * @param to the output stream to write to
1233      * @throws IOException if an I/O error occurs
1234      */
1235     private void copy(File from, OutputStream to) throws IOException {
1236         try (InputStream in = new FileInputStream(from)) {
1237             copy(in, to);
1238         }
1239     }
1240 
1241     /**
1242      * Copies all bytes from the input stream to the output file.
1243      * Does not close the input stream.
1244      *
1245      * @param from the input stream to read from
1246      * @param to the output file to write to
1247      * @throws IOException if an I/O error occurs
1248      */
1249     private void copy(InputStream from, File to) throws IOException {
1250         try (OutputStream out = new FileOutputStream(to)) {
1251             copy(from, out);
1252         }
1253     }
1254 
1255     /**
1256      * Computes the crc32 of a module-info.class.  This is necessary when the
1257      * ZipOutputStream is in STORED mode.
1258      */
1259     private void crc32ModuleInfo(ZipEntry e, byte[] bytes) throws IOException {
1260         CRC32OutputStream os = new CRC32OutputStream();
1261         ByteArrayInputStream in = new ByteArrayInputStream(bytes);
1262         in.transferTo(os);
1263         os.updateEntry(e);
1264     }
1265 
1266     /**
1267      * Computes the crc32 of a Manifest.  This is necessary when the
1268      * ZipOutputStream is in STORED mode.
1269      */
1270     private void crc32Manifest(ZipEntry e, Manifest m) throws IOException {
1271         CRC32OutputStream os = new CRC32OutputStream();
1272         m.write(os);
1273         os.updateEntry(e);
1274     }
1275 
1276     /**
1277      * Computes the crc32 of a File.  This is necessary when the
1278      * ZipOutputStream is in STORED mode.
1279      */
1280     private void crc32File(ZipEntry e, File f) throws IOException {
1281         CRC32OutputStream os = new CRC32OutputStream();
1282         copy(f, os);
1283         if (os.n != f.length()) {
1284             throw new JarException(formatMsg(
1285                         "error.incorrect.length", f.getPath()));
1286         }
1287         os.updateEntry(e);
1288     }
1289 
1290     void replaceFSC(Map<Integer, String []> filesMap) {
1291         filesMap.keySet().forEach(version -> {
1292             String[] files = filesMap.get(version);
1293             if (files != null) {
1294                 for (int i = 0; i < files.length; i++) {
1295                     files[i] = files[i].replace(File.separatorChar, '/');
1296                 }
1297             }
1298         });
1299     }
1300 
1301     @SuppressWarnings("serial")
1302     Set<ZipEntry> newDirSet() {
1303         return new HashSet<ZipEntry>() {
1304             public boolean add(ZipEntry e) {
1305                 return ((e == null || useExtractionTime) ? false : super.add(e));
1306             }};
1307     }
1308 
1309     void updateLastModifiedTime(Set<ZipEntry> zes) throws IOException {
1310         for (ZipEntry ze : zes) {
1311             long lastModified = ze.getTime();
1312             if (lastModified != -1) {
1313                 String name = safeName(ze.getName().replace(File.separatorChar, '/'));
1314                 if (name.length() != 0) {
1315                     File f = new File(name.replace('/', File.separatorChar));
1316                     f.setLastModified(lastModified);
1317                 }
1318             }
1319         }
1320     }
1321 
1322     /**
1323      * Extracts specified entries from JAR file.
1324      *
1325      * @return whether entries were found and successfully extracted
1326      * (indicating this was a zip file without "leading garbage")
1327      */
1328     boolean extract(InputStream in, String files[]) throws IOException {
1329         ZipInputStream zis = new ZipInputStream(in);
1330         ZipEntry e;
1331         // Set of all directory entries specified in archive.  Disallows
1332         // null entries.  Disallows all entries if using pre-6.0 behavior.
1333         boolean entriesFound = false;
1334         Set<ZipEntry> dirs = newDirSet();
1335         while ((e = zis.getNextEntry()) != null) {
1336             entriesFound = true;
1337             if (files == null) {
1338                 dirs.add(extractFile(zis, e));
1339             } else {
1340                 String name = e.getName();
1341                 for (String file : files) {
1342                     if (name.startsWith(file)) {
1343                         dirs.add(extractFile(zis, e));
1344                         break;
1345                     }
1346                 }
1347             }
1348         }
1349 
1350         // Update timestamps of directories specified in archive with their
1351         // timestamps as given in the archive.  We do this after extraction,
1352         // instead of during, because creating a file in a directory changes
1353         // that directory's timestamp.
1354         updateLastModifiedTime(dirs);
1355 
1356         return entriesFound;
1357     }
1358 
1359     /**
1360      * Extracts specified entries from JAR file, via ZipFile.
1361      */
1362     void extract(String fname, String files[]) throws IOException {
1363         ZipFile zf = new ZipFile(fname);
1364         Set<ZipEntry> dirs = newDirSet();
1365         Enumeration<? extends ZipEntry> zes = zf.entries();
1366         while (zes.hasMoreElements()) {
1367             ZipEntry e = zes.nextElement();
1368             if (files == null) {
1369                 dirs.add(extractFile(zf.getInputStream(e), e));
1370             } else {
1371                 String name = e.getName();
1372                 for (String file : files) {
1373                     if (name.startsWith(file)) {
1374                         dirs.add(extractFile(zf.getInputStream(e), e));
1375                         break;
1376                     }
1377                 }
1378             }
1379         }
1380         zf.close();
1381         updateLastModifiedTime(dirs);
1382     }
1383 
1384     /**
1385      * Extracts next entry from JAR file, creating directories as needed.  If
1386      * the entry is for a directory which doesn't exist prior to this
1387      * invocation, returns that entry, otherwise returns null.
1388      */
1389     ZipEntry extractFile(InputStream is, ZipEntry e) throws IOException {
1390         ZipEntry rc = null;
1391         // The spec requres all slashes MUST be forward '/', it is possible
1392         // an offending zip/jar entry may uses the backwards slash in its
1393         // name. It might cause problem on Windows platform as it skips
1394         // our "safe" check for leading slahs and dot-dot. So replace them
1395         // with '/'.
1396         String name = safeName(e.getName().replace(File.separatorChar, '/'));
1397         if (name.length() == 0) {
1398             return rc;    // leading '/' or 'dot-dot' only path
1399         }
1400         File f = new File(name.replace('/', File.separatorChar));
1401         if (e.isDirectory()) {
1402             if (f.exists()) {
1403                 if (!f.isDirectory()) {
1404                     throw new IOException(formatMsg("error.create.dir",
1405                         f.getPath()));
1406                 }
1407             } else {
1408                 if (!f.mkdirs()) {
1409                     throw new IOException(formatMsg("error.create.dir",
1410                         f.getPath()));
1411                 } else {
1412                     rc = e;
1413                 }
1414             }
1415 
1416             if (vflag) {
1417                 output(formatMsg("out.create", name));
1418             }
1419         } else {
1420             if (f.getParent() != null) {
1421                 File d = new File(f.getParent());
1422                 if (!d.exists() && !d.mkdirs() || !d.isDirectory()) {
1423                     throw new IOException(formatMsg(
1424                         "error.create.dir", d.getPath()));
1425                 }
1426             }
1427             try {
1428                 copy(is, f);
1429             } finally {
1430                 if (is instanceof ZipInputStream)
1431                     ((ZipInputStream)is).closeEntry();
1432                 else
1433                     is.close();
1434             }
1435             if (vflag) {
1436                 if (e.getMethod() == ZipEntry.DEFLATED) {
1437                     output(formatMsg("out.inflated", name));
1438                 } else {
1439                     output(formatMsg("out.extracted", name));
1440                 }
1441             }
1442         }
1443         if (!useExtractionTime) {
1444             long lastModified = e.getTime();
1445             if (lastModified != -1) {
1446                 f.setLastModified(lastModified);
1447             }
1448         }
1449         return rc;
1450     }
1451 
1452     /**
1453      * Lists contents of JAR file.
1454      */
1455     void list(InputStream in, String files[]) throws IOException {
1456         ZipInputStream zis = new ZipInputStream(in);
1457         ZipEntry e;
1458         while ((e = zis.getNextEntry()) != null) {
1459             /*
1460              * In the case of a compressed (deflated) entry, the entry size
1461              * is stored immediately following the entry data and cannot be
1462              * determined until the entry is fully read. Therefore, we close
1463              * the entry first before printing out its attributes.
1464              */
1465             zis.closeEntry();
1466             printEntry(e, files);
1467         }
1468     }
1469 
1470     /**
1471      * Lists contents of JAR file, via ZipFile.
1472      */
1473     void list(String fname, String files[]) throws IOException {
1474         ZipFile zf = new ZipFile(fname);
1475         Enumeration<? extends ZipEntry> zes = zf.entries();
1476         while (zes.hasMoreElements()) {
1477             printEntry(zes.nextElement(), files);
1478         }
1479         zf.close();
1480     }
1481 
1482     /**
1483      * Outputs the class index table to the INDEX.LIST file of the
1484      * root jar file.
1485      */
1486     void dumpIndex(String rootjar, JarIndex index) throws IOException {
1487         File jarFile = new File(rootjar);
1488         Path jarPath = jarFile.toPath();
1489         Path tmpPath = createTempFileInSameDirectoryAs(jarFile).toPath();
1490         try {
1491             if (update(Files.newInputStream(jarPath),
1492                        Files.newOutputStream(tmpPath),
1493                        null, null, index)) {
1494                 try {
1495                     Files.move(tmpPath, jarPath, REPLACE_EXISTING);
1496                 } catch (IOException e) {
1497                     throw new IOException(getMsg("error.write.file"), e);
1498                 }
1499             }
1500         } finally {
1501             Files.deleteIfExists(tmpPath);
1502         }
1503     }
1504 
1505     private HashSet<String> jarPaths = new HashSet<String>();
1506 
1507     /**
1508      * Generates the transitive closure of the Class-Path attribute for
1509      * the specified jar file.
1510      */
1511     List<String> getJarPath(String jar) throws IOException {
1512         List<String> files = new ArrayList<String>();
1513         files.add(jar);
1514         jarPaths.add(jar);
1515 
1516         // take out the current path
1517         String path = jar.substring(0, Math.max(0, jar.lastIndexOf('/') + 1));
1518 
1519         // class path attribute will give us jar file name with
1520         // '/' as separators, so we need to change them to the
1521         // appropriate one before we open the jar file.
1522         JarFile rf = new JarFile(jar.replace('/', File.separatorChar));
1523 
1524         if (rf != null) {
1525             Manifest man = rf.getManifest();
1526             if (man != null) {
1527                 Attributes attr = man.getMainAttributes();
1528                 if (attr != null) {
1529                     String value = attr.getValue(Attributes.Name.CLASS_PATH);
1530                     if (value != null) {
1531                         StringTokenizer st = new StringTokenizer(value);
1532                         while (st.hasMoreTokens()) {
1533                             String ajar = st.nextToken();
1534                             if (!ajar.endsWith("/")) {  // it is a jar file
1535                                 ajar = path.concat(ajar);
1536                                 /* check on cyclic dependency */
1537                                 if (! jarPaths.contains(ajar)) {
1538                                     files.addAll(getJarPath(ajar));
1539                                 }
1540                             }
1541                         }
1542                     }
1543                 }
1544             }
1545         }
1546         rf.close();
1547         return files;
1548     }
1549 
1550     /**
1551      * Generates class index file for the specified root jar file.
1552      */
1553     void genIndex(String rootjar, String[] files) throws IOException {
1554         List<String> jars = getJarPath(rootjar);
1555         int njars = jars.size();
1556         String[] jarfiles;
1557 
1558         if (njars == 1 && files != null) {
1559             // no class-path attribute defined in rootjar, will
1560             // use command line specified list of jars
1561             for (int i = 0; i < files.length; i++) {
1562                 jars.addAll(getJarPath(files[i]));
1563             }
1564             njars = jars.size();
1565         }
1566         jarfiles = jars.toArray(new String[njars]);
1567         JarIndex index = new JarIndex(jarfiles);
1568         dumpIndex(rootjar, index);
1569     }
1570 
1571     /**
1572      * Prints entry information, if requested.
1573      */
1574     void printEntry(ZipEntry e, String[] files) throws IOException {
1575         if (files == null) {
1576             printEntry(e);
1577         } else {
1578             String name = e.getName();
1579             for (String file : files) {
1580                 if (name.startsWith(file)) {
1581                     printEntry(e);
1582                     return;
1583                 }
1584             }
1585         }
1586     }
1587 
1588     /**
1589      * Prints entry information.
1590      */
1591     void printEntry(ZipEntry e) throws IOException {
1592         if (vflag) {
1593             StringBuilder sb = new StringBuilder();
1594             String s = Long.toString(e.getSize());
1595             for (int i = 6 - s.length(); i > 0; --i) {
1596                 sb.append(' ');
1597             }
1598             sb.append(s).append(' ').append(new Date(e.getTime()).toString());
1599             sb.append(' ').append(e.getName());
1600             output(sb.toString());
1601         } else {
1602             output(e.getName());
1603         }
1604     }
1605 
1606     /**
1607      * Prints usage message.
1608      */
1609     void usageError(String s) {
1610         err.println(s);
1611         err.println(getMsg("main.usage.summary.try"));
1612     }
1613 
1614     /**
1615      * A fatal exception has been caught.  No recovery possible
1616      */
1617     void fatalError(Exception e) {
1618         e.printStackTrace();
1619     }
1620 
1621     /**
1622      * A fatal condition has been detected; message is "s".
1623      * No recovery possible
1624      */
1625     void fatalError(String s) {
1626         error(program + ": " + s);
1627     }
1628 
1629     /**
1630      * Print an output message; like verbose output and the like
1631      */
1632     protected void output(String s) {
1633         out.println(s);
1634     }
1635 
1636     /**
1637      * Print an error message; like something is broken
1638      */
1639     void error(String s) {
1640         err.println(s);
1641     }
1642 
1643     /**
1644      * Print a warning message
1645      */
1646     void warn(String s) {
1647         err.println(s);
1648     }
1649 
1650     /**
1651      * Main routine to start program.
1652      */
1653     public static void main(String args[]) {
1654         Main jartool = new Main(System.out, System.err, "jar");
1655         System.exit(jartool.run(args) ? 0 : 1);
1656     }
1657 
1658     /**
1659      * An OutputStream that doesn't send its output anywhere, (but could).
1660      * It's here to find the CRC32 of an input file, necessary for STORED
1661      * mode in ZIP.
1662      */
1663     private static class CRC32OutputStream extends java.io.OutputStream {
1664         final CRC32 crc = new CRC32();
1665         long n = 0;
1666 
1667         CRC32OutputStream() {}
1668 
1669         public void write(int r) throws IOException {
1670             crc.update(r);
1671             n++;
1672         }
1673 
1674         public void write(byte[] b, int off, int len) throws IOException {
1675             crc.update(b, off, len);
1676             n += len;
1677         }
1678 
1679         /**
1680          * Updates a ZipEntry which describes the data read by this
1681          * output stream, in STORED mode.
1682          */
1683         public void updateEntry(ZipEntry e) {
1684             e.setMethod(ZipEntry.STORED);
1685             e.setSize(n);
1686             e.setCrc(crc.getValue());
1687         }
1688     }
1689 
1690     /**
1691      * Attempt to create temporary file in the system-provided temporary folder, if failed attempts
1692      * to create it in the same folder as the file in parameter (if any)
1693      */
1694     private File createTemporaryFile(String tmpbase, String suffix) {
1695         File tmpfile = null;
1696 
1697         try {
1698             tmpfile = File.createTempFile(tmpbase, suffix);
1699         } catch (IOException | SecurityException e) {
1700             // Unable to create file due to permission violation or security exception
1701         }
1702         if (tmpfile == null) {
1703             // Were unable to create temporary file, fall back to temporary file in the same folder
1704             if (fname != null) {
1705                 try {
1706                     File tmpfolder = new File(fname).getAbsoluteFile().getParentFile();
1707                     tmpfile = File.createTempFile(fname, ".tmp" + suffix, tmpfolder);
1708                 } catch (IOException ioe) {
1709                     // Last option failed - fall gracefully
1710                     fatalError(ioe);
1711                 }
1712             } else {
1713                 // No options left - we can not compress to stdout without access to the temporary folder
1714                 fatalError(new IOException(getMsg("error.create.tempfile")));
1715             }
1716         }
1717         return tmpfile;
1718     }
1719 
1720     // Modular jar support
1721 
1722     static <T> String toString(Collection<T> c,
1723                                CharSequence prefix,
1724                                CharSequence suffix ) {
1725         if (c.isEmpty())
1726             return "";
1727         return c.stream().map(e -> e.toString())
1728                            .collect(joining(", ", prefix, suffix));
1729     }
1730 
1731     private boolean printModuleDescriptor(ZipFile zipFile)
1732         throws IOException
1733     {
1734         ZipEntry[] zes = zipFile.stream()
1735             .filter(e -> isModuleInfoEntry(e.getName()))
1736             .sorted(Validator.ENTRY_COMPARATOR)
1737             .toArray(ZipEntry[]::new);
1738         if (zes.length == 0)
1739             return false;
1740         for (ZipEntry ze : zes) {
1741             try (InputStream is = zipFile.getInputStream(ze)) {
1742                 printModuleDescriptor(is, ze.getName());
1743             }
1744         }
1745         return true;
1746     }
1747 
1748     private boolean printModuleDescriptor(FileInputStream fis)
1749         throws IOException
1750     {
1751         try (BufferedInputStream bis = new BufferedInputStream(fis);
1752              ZipInputStream zis = new ZipInputStream(bis)) {
1753             ZipEntry e;
1754             while ((e = zis.getNextEntry()) != null) {
1755                 String ename = e.getName();
1756                 if (isModuleInfoEntry(ename)){
1757                     moduleInfos.put(ename, zis.readAllBytes());
1758                 }
1759             }
1760         }
1761         if (moduleInfos.size() == 0)
1762             return false;
1763         String[] names = moduleInfos.keySet().stream()
1764             .sorted(Validator.ENTRYNAME_COMPARATOR)
1765             .toArray(String[]::new);
1766         for (String name : names) {
1767             printModuleDescriptor(new ByteArrayInputStream(moduleInfos.get(name)), name);
1768         }
1769         return true;
1770     }
1771 
1772     static <T> String toString(Collection<T> set) {
1773         if (set.isEmpty()) { return ""; }
1774         return set.stream().map(e -> e.toString().toLowerCase(Locale.ROOT))
1775                   .collect(joining(" "));
1776     }
1777 
1778     private void printModuleDescriptor(InputStream entryInputStream, String ename)
1779         throws IOException
1780     {
1781         ModuleInfo.Attributes attrs = ModuleInfo.read(entryInputStream, null);
1782         ModuleDescriptor md = attrs.descriptor();
1783         ModuleHashes hashes = attrs.recordedHashes();
1784 
1785         StringBuilder sb = new StringBuilder();
1786         sb.append("\nmodule ")
1787           .append(md.toNameAndVersion())
1788           .append(" (").append(ename).append(")");
1789 
1790         if (md.isOpen())
1791             sb.append("\n  open ");
1792 
1793         md.requires().stream()
1794             .sorted(Comparator.comparing(Requires::name))
1795             .forEach(r -> {
1796                 sb.append("\n  requires ");
1797                 if (!r.modifiers().isEmpty())
1798                     sb.append(toString(r.modifiers())).append(" ");
1799                 sb.append(r.name());
1800             });
1801 
1802         md.uses().stream().sorted()
1803             .forEach(p -> sb.append("\n  uses ").append(p));
1804 
1805         md.exports().stream()
1806             .sorted(Comparator.comparing(Exports::source))
1807             .forEach(p -> sb.append("\n  exports ").append(p));
1808 
1809         md.opens().stream()
1810             .sorted(Comparator.comparing(Opens::source))
1811             .forEach(p -> sb.append("\n  opens ").append(p));
1812 
1813         Set<String> concealed = new HashSet<>(md.packages());
1814         md.exports().stream().map(Exports::source).forEach(concealed::remove);
1815         md.opens().stream().map(Opens::source).forEach(concealed::remove);
1816         concealed.stream().sorted()
1817             .forEach(p -> sb.append("\n  contains ").append(p));
1818 
1819         md.provides().stream()
1820             .sorted(Comparator.comparing(Provides::service))
1821             .forEach(p -> sb.append("\n  provides ").append(p.service())
1822                             .append(" with ")
1823                             .append(toString(p.providers())));
1824 
1825         md.mainClass().ifPresent(v -> sb.append("\n  main-class " + v));
1826 
1827         md.osName().ifPresent(v -> sb.append("\n  operating-system-name " + v));
1828 
1829         md.osArch().ifPresent(v -> sb.append("\n  operating-system-architecture " + v));
1830 
1831         md.osVersion().ifPresent(v -> sb.append("\n  operating-system-version " + v));
1832 
1833        if (hashes != null) {
1834            hashes.names().stream().sorted().forEach(
1835                    mod -> sb.append("\n  hashes ").append(mod).append(" ")
1836                             .append(hashes.algorithm()).append(" ")
1837                             .append(toHex(hashes.hashFor(mod))));
1838         }
1839 
1840         output(sb.toString());
1841     }
1842 
1843     private static String toHex(byte[] ba) {
1844         StringBuilder sb = new StringBuilder(ba.length << 1);
1845         for (byte b: ba) {
1846             sb.append(String.format("%02x", b & 0xff));
1847         }
1848         return sb.toString();
1849     }
1850 
1851     static String toBinaryName(String classname) {
1852         return (classname.replace('.', '/')) + ".class";
1853     }
1854 
1855     private boolean checkModuleInfo(byte[] moduleInfoBytes, Set<String> entries)
1856         throws IOException
1857     {
1858         boolean ok = true;
1859         if (moduleInfoBytes != null) {  // no root module-info.class if null
1860             try {
1861                 // ModuleDescriptor.read() checks open/exported pkgs vs packages
1862                 ModuleDescriptor md = ModuleDescriptor.read(ByteBuffer.wrap(moduleInfoBytes));
1863                 // A module must have the implementation class of the services it 'provides'.
1864                 if (md.provides().stream().map(Provides::providers).flatMap(List::stream)
1865                       .filter(p -> !entries.contains(toBinaryName(p)))
1866                       .peek(p -> fatalError(formatMsg("error.missing.provider", p)))
1867                       .count() != 0) {
1868                     ok = false;
1869                 }
1870             } catch (InvalidModuleDescriptorException x) {
1871                 fatalError(x.getMessage());
1872                 ok = false;
1873             }
1874         }
1875         return ok;
1876     }
1877 
1878     /**
1879      * Adds extended modules attributes to the given module-info's.  The given
1880      * Map values are updated in-place. Returns false if an error occurs.
1881      */
1882     private void addExtendedModuleAttributes(Map<String,byte[]> moduleInfos,
1883                                                 Set<String> packages)
1884         throws IOException
1885     {
1886         for (Map.Entry<String,byte[]> e: moduleInfos.entrySet()) {
1887             ModuleDescriptor md = ModuleDescriptor.read(ByteBuffer.wrap(e.getValue()));
1888             e.setValue(extendedInfoBytes(md, e.getValue(), packages));
1889         }
1890     }
1891 
1892     static boolean isModuleInfoEntry(String name) {
1893         // root or versioned module-info.class
1894         if (name.endsWith(MODULE_INFO)) {
1895             int end = name.length() - MODULE_INFO.length();
1896             if (end == 0)
1897                 return true;
1898             if (name.startsWith(VERSIONS_DIR)) {
1899                 int off = VERSIONS_DIR_LENGTH;
1900                 if (off == end)      // meta-inf/versions/module-info.class
1901                     return false;
1902                 while (off < end - 1) {
1903                     char c = name.charAt(off++);
1904                     if (c < '0' || c > '9')
1905                         return false;
1906                 }
1907                 return name.charAt(off) == '/';
1908             }
1909         }
1910         return false;
1911     }
1912 
1913     /**
1914      * Returns a byte array containing the given module-info.class plus any
1915      * extended attributes.
1916      *
1917      * If --module-version, --main-class, or other options were provided
1918      * then the corresponding class file attributes are added to the
1919      * module-info here.
1920      */
1921     private byte[] extendedInfoBytes(ModuleDescriptor md,
1922                                      byte[] miBytes,
1923                                      Set<String> packages)
1924         throws IOException
1925     {
1926         ByteArrayOutputStream baos = new ByteArrayOutputStream();
1927         InputStream is = new ByteArrayInputStream(miBytes);
1928         ModuleInfoExtender extender = ModuleInfoExtender.newExtender(is);
1929 
1930         // Add (or replace) the Packages attribute
1931         extender.packages(packages);
1932 
1933         // --main-class
1934         if (ename != null)
1935             extender.mainClass(ename);
1936 
1937         // --module-version
1938         if (moduleVersion != null)
1939             extender.version(moduleVersion);
1940 
1941         // --hash-modules
1942         if (modulesToHash != null) {
1943             String mn = md.name();
1944             Hasher hasher = new Hasher(md, fname);
1945             ModuleHashes moduleHashes = hasher.computeHashes(mn);
1946             if (moduleHashes != null) {
1947                 extender.hashes(moduleHashes);
1948             } else {
1949                 warn("warning: no module is recorded in hash in " + mn);
1950             }
1951         }
1952 
1953         if (moduleResolution.value() != 0) {
1954             extender.moduleResolution(moduleResolution);
1955         }
1956 
1957         extender.write(baos);
1958         return baos.toByteArray();
1959     }
1960 
1961     /**
1962      * Compute and record hashes
1963      */
1964     private class Hasher {
1965         final ModuleHashesBuilder hashesBuilder;
1966         final ModuleFinder finder;
1967         final Set<String> modules;
1968         Hasher(ModuleDescriptor descriptor, String fname) throws IOException {
1969             // Create a module finder that finds the modular JAR
1970             // being created/updated
1971             URI uri = Paths.get(fname).toUri();
1972             ModuleReference mref = new ModuleReference(descriptor, uri) {
1973                 @Override
1974                 public ModuleReader open() {
1975                     throw new UnsupportedOperationException("should not reach here");
1976                 }
1977             };
1978 
1979             // Compose a module finder with the module path and
1980             // the modular JAR being created or updated
1981             this.finder = ModuleFinder.compose(moduleFinder,
1982                 new ModuleFinder() {
1983                     @Override
1984                     public Optional<ModuleReference> find(String name) {
1985                         if (descriptor.name().equals(name))
1986                             return Optional.of(mref);
1987                         else
1988                             return Optional.empty();
1989                     }
1990 
1991                     @Override
1992                     public Set<ModuleReference> findAll() {
1993                         return Collections.singleton(mref);
1994                     }
1995                 });
1996 
1997             // Determine the modules that matches the pattern {@code modulesToHash}
1998             Set<String> roots = finder.findAll().stream()
1999                 .map(ref -> ref.descriptor().name())
2000                 .filter(mn -> modulesToHash.matcher(mn).find())
2001                 .collect(Collectors.toSet());
2002 
2003             // use system module path unless it creates a modular JAR for
2004             // a module that is present in the system image e.g. upgradeable
2005             // module
2006             ModuleFinder system;
2007             String name = descriptor.name();
2008             if (name != null && ModuleFinder.ofSystem().find(name).isPresent()) {
2009                 system = ModuleFinder.of();
2010             } else {
2011                 system = ModuleFinder.ofSystem();
2012             }
2013             // get a resolved module graph
2014             Configuration config =
2015                 Configuration.empty().resolve(system, finder, roots);
2016 
2017             // filter modules resolved from the system module finder
2018             this.modules = config.modules().stream()
2019                 .map(ResolvedModule::name)
2020                 .filter(mn -> roots.contains(mn) && !system.find(mn).isPresent())
2021                 .collect(Collectors.toSet());
2022 
2023             this.hashesBuilder = new ModuleHashesBuilder(config, modules);
2024         }
2025 
2026         /**
2027          * Compute hashes of the specified module.
2028          *
2029          * It records the hashing modules that depend upon the specified
2030          * module directly or indirectly.
2031          */
2032         ModuleHashes computeHashes(String name) {
2033             if (hashesBuilder == null)
2034                 return null;
2035 
2036             return hashesBuilder.computeHashes(Set.of(name)).get(name);
2037         }
2038     }
2039 }