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