1 /*
   2  * Copyright (c) 1996, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package sun.tools.jar;
  27 
  28 import java.io.*;
  29 import java.lang.module.Configuration;
  30 import java.lang.module.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                         if (dir.startsWith("//") && (File.separatorChar == '\\') ) {
 638                             while (dir.indexOf("//") > -1) { 
 639                                 dir = dir.replace("//", "/");
 640                             }
 641                             // restore the leading / to fix the Win UNC path
 642                             dir = "/" + dir;
 643                         } else {
 644                             while (dir.indexOf("//") > -1) {
 645                                 dir = dir.replace("//", "/");
 646                             }
 647                         }
 648                         pathsMap.get(version).add(dir.replace(File.separatorChar, '/'));
 649                         nameBuf[k++] = dir + args[++i];
 650                     } else if (args[i].startsWith("--release")) {
 651                         int v = BASE_VERSION;
 652                         try {
 653                             v = Integer.valueOf(args[++i]);
 654                         } catch (NumberFormatException x) {
 655                             error(formatMsg("error.release.value.notnumber", args[i]));
 656                             // this will fall into the next error, thus returning false
 657                         }
 658                         if (v < 9) {
 659                             usageError(formatMsg("error.release.value.toosmall", String.valueOf(v)));
 660                             return false;
 661                         }
 662                         // associate the files, if any, with the previous version number
 663                         if (k > 0) {
 664                             String[] files = new String[k];
 665                             System.arraycopy(nameBuf, 0, files, 0, k);
 666                             filesMap.put(version, files);
 667                             isMultiRelease = version > BASE_VERSION;
 668                         }
 669                         // reset the counters and start with the new version number
 670                         k = 0;
 671                         nameBuf = new String[n];
 672                         version = v;
 673                         releaseValue = version;
 674                         pathsMap.put(version, new HashSet<>());
 675                     } else {
 676                         if (dflag) {
 677                             // "--describe-module/-d" does not require file argument(s),
 678                             // but does accept --release
 679                             usageError(getMsg("error.bad.dflag"));
 680                             return false;
 681                         }
 682                         nameBuf[k++] = args[i];
 683                     }
 684                 }
 685             } catch (ArrayIndexOutOfBoundsException e) {
 686                 usageError(getMsg("error.bad.file.arg"));
 687                 return false;
 688             }
 689             // associate remaining files, if any, with a version
 690             if (k > 0) {
 691                 String[] files = new String[k];
 692                 System.arraycopy(nameBuf, 0, files, 0, k);
 693                 filesMap.put(version, files);
 694                 isMultiRelease = version > BASE_VERSION;
 695             }
 696         } else if (cflag && (mname == null)) {
 697             usageError(getMsg("error.bad.cflag"));
 698             return false;
 699         } else if (uflag) {
 700             if ((mname != null) || (ename != null)) {
 701                 /* just want to update the manifest */
 702                 return true;
 703             } else {
 704                 usageError(getMsg("error.bad.uflag"));
 705                 return false;
 706             }
 707         }
 708         return true;
 709     }
 710 
 711     /*
 712      * Add the package of the given resource name if it's a .class
 713      * or a resource in a named package.
 714      */
 715     void addPackageIfNamed(Set<String> packages, String name) {
 716         if (name.startsWith(VERSIONS_DIR)) {
 717             // trim the version dir prefix
 718             int i0 = VERSIONS_DIR_LENGTH;
 719             int i = name.indexOf('/', i0);
 720             if (i <= 0) {
 721                 warn(formatMsg("warn.release.unexpected.versioned.entry", name));
 722                 return;
 723             }
 724             while (i0 < i) {
 725                 char c = name.charAt(i0);
 726                 if (c < '0' || c > '9') {
 727                     warn(formatMsg("warn.release.unexpected.versioned.entry", name));
 728                     return;
 729                 }
 730                 i0++;
 731             }
 732             name = name.substring(i + 1, name.length());
 733         }
 734         String pn = toPackageName(name);
 735         // add if this is a class or resource in a package
 736         if (Checks.isPackageName(pn)) {
 737             packages.add(pn);
 738         }
 739     }
 740 
 741     private String toEntryName(String name, Set<String> cpaths, boolean isDir) {
 742         name = name.replace(File.separatorChar, '/');
 743         if (isDir) {
 744             name = name.endsWith("/") ? name : name + "/";
 745         }
 746         String matchPath = "";
 747         for (String path : cpaths) {
 748             if (name.startsWith(path) && path.length() > matchPath.length()) {
 749                 matchPath = path;
 750             }
 751         }
 752         name = safeName(name.substring(matchPath.length()));
 753         // the old implementaton doesn't remove
 754         // "./" if it was led by "/" (?)
 755         if (name.startsWith("./")) {
 756             name = name.substring(2);
 757         }
 758         return name;
 759     }
 760 
 761     private static String toVersionedName(String name, int version) {
 762         return version > BASE_VERSION
 763                 ? VERSIONS_DIR + version + "/" + name : name;
 764     }
 765 
 766     private static String toPackageName(String path) {
 767         int index = path.lastIndexOf('/');
 768         if (index != -1) {
 769             return path.substring(0, index).replace('/', '.');
 770         } else {
 771             return "";
 772         }
 773     }
 774 
 775     private void expand() throws IOException {
 776         for (int version : filesMap.keySet()) {
 777             String[] files = filesMap.get(version);
 778             expand(null, files, pathsMap.get(version), version);
 779         }
 780     }
 781 
 782     /**
 783      * Expands list of files to process into full list of all files that
 784      * can be found by recursively descending directories.
 785      *
 786      * @param dir    parent directory
 787      * @param files  list of files to expand
 788      * @param cpaths set of directories specified by -C option for the files
 789      * @throws IOException if an I/O error occurs
 790      */
 791     private void expand(File dir, String[] files, Set<String> cpaths, int version)
 792         throws IOException
 793     {
 794         if (files == null)
 795             return;
 796 
 797         for (int i = 0; i < files.length; i++) {
 798             File f;
 799             if (dir == null)
 800                 f = new File(files[i]);
 801             else
 802                 f = new File(dir, files[i]);
 803 
 804             boolean isDir = f.isDirectory();
 805             String name = toEntryName(f.getPath(), cpaths, isDir);
 806 
 807             if (version != BASE_VERSION) {
 808                 if (name.startsWith(VERSIONS_DIR)) {
 809                     // the entry starts with VERSIONS_DIR and version != BASE_VERSION,
 810                     // which means the "[dirs|files]" in --release v [dirs|files]
 811                     // includes VERSIONS_DIR-ed entries --> warning and skip (?)
 812                     error(formatMsg2("error.release.unexpected.versioned.entry",
 813                                      name, String.valueOf(version)));
 814                     ok = false;
 815                     return;
 816                 }
 817                 name = toVersionedName(name, version);
 818             }
 819 
 820             if (f.isFile()) {
 821                 Entry e = new Entry(f, name, false);
 822                 if (isModuleInfoEntry(name)) {
 823                     moduleInfos.putIfAbsent(name, Files.readAllBytes(f.toPath()));
 824                     if (uflag)
 825                         entryMap.put(name, e);
 826                 } else if (entries.add(e)) {
 827                     if (uflag)
 828                         entryMap.put(name, e);
 829                 }
 830             } else if (isDir) {
 831                 Entry e = new Entry(f, name, true);
 832                 if (entries.add(e)) {
 833                     // utilize entryMap for the duplicate dir check even in
 834                     // case of cflag == true.
 835                     // dir name confilict/duplicate could happen with -C option.
 836                     // just remove the last "e" from the "entries" (zos will fail
 837                     // with "duplicated" entries), but continue expanding the
 838                     // sub tree
 839                     if (entryMap.containsKey(name)) {
 840                         entries.remove(e);
 841                     } else {
 842                         entryMap.put(name, e);
 843                     }
 844                     expand(f, f.list(), cpaths, version);
 845                 }
 846             } else {
 847                 error(formatMsg("error.nosuch.fileordir", String.valueOf(f)));
 848                 ok = false;
 849             }
 850         }
 851     }
 852 
 853     /**
 854      * Creates a new JAR file.
 855      */
 856     void create(OutputStream out, Manifest manifest) throws IOException
 857     {
 858         try (ZipOutputStream zos = new JarOutputStream(out)) {
 859             if (flag0) {
 860                 zos.setMethod(ZipOutputStream.STORED);
 861             }
 862             // TODO: check module-info attributes against manifest ??
 863             if (manifest != null) {
 864                 if (vflag) {
 865                     output(getMsg("out.added.manifest"));
 866                 }
 867                 ZipEntry e = new ZipEntry(MANIFEST_DIR);
 868                 e.setTime(System.currentTimeMillis());
 869                 e.setSize(0);
 870                 e.setCrc(0);
 871                 zos.putNextEntry(e);
 872                 e = new ZipEntry(MANIFEST_NAME);
 873                 e.setTime(System.currentTimeMillis());
 874                 if (flag0) {
 875                     crc32Manifest(e, manifest);
 876                 }
 877                 zos.putNextEntry(e);
 878                 manifest.write(zos);
 879                 zos.closeEntry();
 880             }
 881             updateModuleInfo(moduleInfos, zos);
 882             for (Entry entry : entries) {
 883                 addFile(zos, entry);
 884             }
 885         }
 886     }
 887 
 888     private char toUpperCaseASCII(char c) {
 889         return (c < 'a' || c > 'z') ? c : (char) (c + 'A' - 'a');
 890     }
 891 
 892     /**
 893      * Compares two strings for equality, ignoring case.  The second
 894      * argument must contain only upper-case ASCII characters.
 895      * We don't want case comparison to be locale-dependent (else we
 896      * have the notorious "turkish i bug").
 897      */
 898     private boolean equalsIgnoreCase(String s, String upper) {
 899         assert upper.toUpperCase(java.util.Locale.ENGLISH).equals(upper);
 900         int len;
 901         if ((len = s.length()) != upper.length())
 902             return false;
 903         for (int i = 0; i < len; i++) {
 904             char c1 = s.charAt(i);
 905             char c2 = upper.charAt(i);
 906             if (c1 != c2 && toUpperCaseASCII(c1) != c2)
 907                 return false;
 908         }
 909         return true;
 910     }
 911 
 912     /**
 913      * Updates an existing jar file.
 914      */
 915     boolean update(InputStream in, OutputStream out,
 916                    InputStream newManifest,
 917                    Map<String,byte[]> moduleInfos,
 918                    JarIndex jarIndex) throws IOException
 919     {
 920         ZipInputStream zis = new ZipInputStream(in);
 921         ZipOutputStream zos = new JarOutputStream(out);
 922         ZipEntry e = null;
 923         boolean foundManifest = false;
 924         boolean updateOk = true;
 925 
 926         // All actual entries added/updated/existing, in the jar file (excl manifest
 927         // and module-info.class ).
 928         Set<String> jentries = new HashSet<>();
 929 
 930         if (jarIndex != null) {
 931             addIndex(jarIndex, zos);
 932         }
 933 
 934         // put the old entries first, replace if necessary
 935         while ((e = zis.getNextEntry()) != null) {
 936             String name = e.getName();
 937 
 938             boolean isManifestEntry = equalsIgnoreCase(name, MANIFEST_NAME);
 939             boolean isModuleInfoEntry = isModuleInfoEntry(name);
 940 
 941             if ((jarIndex != null && equalsIgnoreCase(name, INDEX_NAME))
 942                 || (Mflag && isManifestEntry)) {
 943                 continue;
 944             } else if (isManifestEntry && ((newManifest != null) ||
 945                         (ename != null) || isMultiRelease)) {
 946                 foundManifest = true;
 947                 if (newManifest != null) {
 948                     // Don't read from the newManifest InputStream, as we
 949                     // might need it below, and we can't re-read the same data
 950                     // twice.
 951                     FileInputStream fis = new FileInputStream(mname);
 952                     boolean ambiguous = isAmbiguousMainClass(new Manifest(fis));
 953                     fis.close();
 954                     if (ambiguous) {
 955                         return false;
 956                     }
 957                 }
 958                 // Update the manifest.
 959                 Manifest old = new Manifest(zis);
 960                 if (newManifest != null) {
 961                     old.read(newManifest);
 962                 }
 963                 if (!updateManifest(old, zos)) {
 964                     return false;
 965                 }
 966             } else if (moduleInfos != null && isModuleInfoEntry) {
 967                 moduleInfos.putIfAbsent(name, zis.readAllBytes());
 968             } else {
 969                 boolean isDir = e.isDirectory();
 970                 if (!entryMap.containsKey(name)) { // copy the old stuff
 971                     // do our own compression
 972                     ZipEntry e2 = new ZipEntry(name);
 973                     e2.setMethod(e.getMethod());
 974                     e2.setTime(e.getTime());
 975                     e2.setComment(e.getComment());
 976                     e2.setExtra(e.getExtra());
 977                     if (e.getMethod() == ZipEntry.STORED) {
 978                         e2.setSize(e.getSize());
 979                         e2.setCrc(e.getCrc());
 980                     }
 981                     zos.putNextEntry(e2);
 982                     copy(zis, zos);
 983                 } else { // replace with the new files
 984                     Entry ent = entryMap.get(name);
 985                     addFile(zos, ent);
 986                     entryMap.remove(name);
 987                     entries.remove(ent);
 988                     isDir = ent.isDir;
 989                 }
 990                 if (!isDir) {
 991                     jentries.add(name);
 992                 }
 993             }
 994         }
 995 
 996         // add the remaining new files
 997         for (Entry entry : entries) {
 998             addFile(zos, entry);
 999             if (!entry.isDir) {
1000                 jentries.add(entry.name);
1001             }
1002         }
1003         if (!foundManifest) {
1004             if (newManifest != null) {
1005                 Manifest m = new Manifest(newManifest);
1006                 updateOk = !isAmbiguousMainClass(m);
1007                 if (updateOk) {
1008                     if (!updateManifest(m, zos)) {
1009                         updateOk = false;
1010                     }
1011                 }
1012             } else if (ename != null) {
1013                 if (!updateManifest(new Manifest(), zos)) {
1014                     updateOk = false;
1015                 }
1016             }
1017         }
1018         if (updateOk) {
1019             if (moduleInfos != null && !moduleInfos.isEmpty()) {
1020                 Set<String> pkgs = new HashSet<>();
1021                 jentries.forEach( je -> addPackageIfNamed(pkgs, je));
1022                 addExtendedModuleAttributes(moduleInfos, pkgs);
1023                 updateOk = checkModuleInfo(moduleInfos.get(MODULE_INFO), jentries);
1024                 updateModuleInfo(moduleInfos, zos);
1025                 // TODO: check manifest main classes, etc
1026             } else if (moduleVersion != null || modulesToHash != null) {
1027                 error(getMsg("error.module.options.without.info"));
1028                 updateOk = false;
1029             }
1030         }
1031         zis.close();
1032         zos.close();
1033         return updateOk;
1034     }
1035 
1036     private void addIndex(JarIndex index, ZipOutputStream zos)
1037         throws IOException
1038     {
1039         ZipEntry e = new ZipEntry(INDEX_NAME);
1040         e.setTime(System.currentTimeMillis());
1041         if (flag0) {
1042             CRC32OutputStream os = new CRC32OutputStream();
1043             index.write(os);
1044             os.updateEntry(e);
1045         }
1046         zos.putNextEntry(e);
1047         index.write(zos);
1048         zos.closeEntry();
1049     }
1050 
1051     private void updateModuleInfo(Map<String,byte[]> moduleInfos, ZipOutputStream zos)
1052         throws IOException
1053     {
1054         String fmt = uflag ? "out.update.module-info": "out.added.module-info";
1055         for (Map.Entry<String,byte[]> mi : moduleInfos.entrySet()) {
1056             String name = mi.getKey();
1057             byte[] bytes = mi.getValue();
1058             ZipEntry e = new ZipEntry(name);
1059             e.setTime(System.currentTimeMillis());
1060             if (flag0) {
1061                 crc32ModuleInfo(e, bytes);
1062             }
1063             zos.putNextEntry(e);
1064             zos.write(bytes);
1065             zos.closeEntry();
1066             if (vflag) {
1067                 output(formatMsg(fmt, name));
1068             }
1069         }
1070     }
1071 
1072     private boolean updateManifest(Manifest m, ZipOutputStream zos)
1073         throws IOException
1074     {
1075         addVersion(m);
1076         addCreatedBy(m);
1077         if (ename != null) {
1078             addMainClass(m, ename);
1079         }
1080         if (isMultiRelease) {
1081             addMultiRelease(m);
1082         }
1083         ZipEntry e = new ZipEntry(MANIFEST_NAME);
1084         e.setTime(System.currentTimeMillis());
1085         if (flag0) {
1086             crc32Manifest(e, m);
1087         }
1088         zos.putNextEntry(e);
1089         m.write(zos);
1090         if (vflag) {
1091             output(getMsg("out.update.manifest"));
1092         }
1093         return true;
1094     }
1095 
1096     private static final boolean isWinDriveLetter(char c) {
1097         return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'));
1098     }
1099 
1100     private String safeName(String name) {
1101         if (!pflag) {
1102             int len = name.length();
1103             int i = name.lastIndexOf("../");
1104             if (i == -1) {
1105                 i = 0;
1106             } else {
1107                 i += 3; // strip any dot-dot components
1108             }
1109             if (File.separatorChar == '\\') {
1110                 // the spec requests no drive letter. skip if
1111                 // the entry name has one.
1112                 while (i < len) {
1113                     int off = i;
1114                     if (i + 1 < len &&
1115                         name.charAt(i + 1) == ':' &&
1116                         isWinDriveLetter(name.charAt(i))) {
1117                         i += 2;
1118                     }
1119                     while (i < len && name.charAt(i) == '/') {
1120                         i++;
1121                     }
1122                     if (i == off) {
1123                         break;
1124                     }
1125                 }
1126             } else {
1127                 while (i < len && name.charAt(i) == '/') {
1128                     i++;
1129                 }
1130             }
1131             if (i != 0) {
1132                 name = name.substring(i);
1133             }
1134         }
1135         return name;
1136     }
1137 
1138     private void addVersion(Manifest m) {
1139         Attributes global = m.getMainAttributes();
1140         if (global.getValue(Attributes.Name.MANIFEST_VERSION) == null) {
1141             global.put(Attributes.Name.MANIFEST_VERSION, VERSION);
1142         }
1143     }
1144 
1145     private void addCreatedBy(Manifest m) {
1146         Attributes global = m.getMainAttributes();
1147         if (global.getValue(new Attributes.Name("Created-By")) == null) {
1148             String javaVendor = System.getProperty("java.vendor");
1149             String jdkVersion = System.getProperty("java.version");
1150             global.put(new Attributes.Name("Created-By"), jdkVersion + " (" +
1151                         javaVendor + ")");
1152         }
1153     }
1154 
1155     private void addMainClass(Manifest m, String mainApp) {
1156         Attributes global = m.getMainAttributes();
1157 
1158         // overrides any existing Main-Class attribute
1159         global.put(Attributes.Name.MAIN_CLASS, mainApp);
1160     }
1161 
1162     private void addMultiRelease(Manifest m) {
1163         Attributes global = m.getMainAttributes();
1164         global.put(Attributes.Name.MULTI_RELEASE, "true");
1165     }
1166 
1167     private boolean isAmbiguousMainClass(Manifest m) {
1168         if (ename != null) {
1169             Attributes global = m.getMainAttributes();
1170             if ((global.get(Attributes.Name.MAIN_CLASS) != null)) {
1171                 usageError(getMsg("error.bad.eflag"));
1172                 return true;
1173             }
1174         }
1175         return false;
1176     }
1177 
1178     /**
1179      * Adds a new file entry to the ZIP output stream.
1180      */
1181     void addFile(ZipOutputStream zos, Entry entry) throws IOException {
1182 
1183         File file = entry.file;
1184         String name = entry.name;
1185         boolean isDir = entry.isDir;
1186 
1187         if (name.equals("") || name.equals(".") || name.equals(zname)) {
1188             return;
1189         } else if ((name.equals(MANIFEST_DIR) || name.equals(MANIFEST_NAME))
1190                    && !Mflag) {
1191             if (vflag) {
1192                 output(formatMsg("out.ignore.entry", name));
1193             }
1194             return;
1195         } else if (name.equals(MODULE_INFO)) {
1196             throw new Error("Unexpected module info: " + name);
1197         }
1198 
1199         long size = isDir ? 0 : file.length();
1200 
1201         if (vflag) {
1202             out.print(formatMsg("out.adding", name));
1203         }
1204         ZipEntry e = new ZipEntry(name);
1205         e.setTime(file.lastModified());
1206         if (size == 0) {
1207             e.setMethod(ZipEntry.STORED);
1208             e.setSize(0);
1209             e.setCrc(0);
1210         } else if (flag0) {
1211             crc32File(e, file);
1212         }
1213         zos.putNextEntry(e);
1214         if (!isDir) {
1215             copy(file, zos);
1216         }
1217         zos.closeEntry();
1218         /* report how much compression occurred. */
1219         if (vflag) {
1220             size = e.getSize();
1221             long csize = e.getCompressedSize();
1222             out.print(formatMsg2("out.size", String.valueOf(size),
1223                         String.valueOf(csize)));
1224             if (e.getMethod() == ZipEntry.DEFLATED) {
1225                 long ratio = 0;
1226                 if (size != 0) {
1227                     ratio = ((size - csize) * 100) / size;
1228                 }
1229                 output(formatMsg("out.deflated", String.valueOf(ratio)));
1230             } else {
1231                 output(getMsg("out.stored"));
1232             }
1233         }
1234     }
1235 
1236     /**
1237      * A buffer for use only by copy(InputStream, OutputStream).
1238      * Not as clean as allocating a new buffer as needed by copy,
1239      * but significantly more efficient.
1240      */
1241     private byte[] copyBuf = new byte[8192];
1242 
1243     /**
1244      * Copies all bytes from the input stream to the output stream.
1245      * Does not close or flush either stream.
1246      *
1247      * @param from the input stream to read from
1248      * @param to the output stream to write to
1249      * @throws IOException if an I/O error occurs
1250      */
1251     private void copy(InputStream from, OutputStream to) throws IOException {
1252         int n;
1253         while ((n = from.read(copyBuf)) != -1)
1254             to.write(copyBuf, 0, n);
1255     }
1256 
1257     /**
1258      * Copies all bytes from the input file to the output stream.
1259      * Does not close or flush the output stream.
1260      *
1261      * @param from the input file to read from
1262      * @param to the output stream to write to
1263      * @throws IOException if an I/O error occurs
1264      */
1265     private void copy(File from, OutputStream to) throws IOException {
1266         try (InputStream in = new FileInputStream(from)) {
1267             copy(in, to);
1268         }
1269     }
1270 
1271     /**
1272      * Copies all bytes from the input stream to the output file.
1273      * Does not close the input stream.
1274      *
1275      * @param from the input stream to read from
1276      * @param to the output file to write to
1277      * @throws IOException if an I/O error occurs
1278      */
1279     private void copy(InputStream from, File to) throws IOException {
1280         try (OutputStream out = new FileOutputStream(to)) {
1281             copy(from, out);
1282         }
1283     }
1284 
1285     /**
1286      * Computes the crc32 of a module-info.class.  This is necessary when the
1287      * ZipOutputStream is in STORED mode.
1288      */
1289     private void crc32ModuleInfo(ZipEntry e, byte[] bytes) throws IOException {
1290         CRC32OutputStream os = new CRC32OutputStream();
1291         ByteArrayInputStream in = new ByteArrayInputStream(bytes);
1292         in.transferTo(os);
1293         os.updateEntry(e);
1294     }
1295 
1296     /**
1297      * Computes the crc32 of a Manifest.  This is necessary when the
1298      * ZipOutputStream is in STORED mode.
1299      */
1300     private void crc32Manifest(ZipEntry e, Manifest m) throws IOException {
1301         CRC32OutputStream os = new CRC32OutputStream();
1302         m.write(os);
1303         os.updateEntry(e);
1304     }
1305 
1306     /**
1307      * Computes the crc32 of a File.  This is necessary when the
1308      * ZipOutputStream is in STORED mode.
1309      */
1310     private void crc32File(ZipEntry e, File f) throws IOException {
1311         CRC32OutputStream os = new CRC32OutputStream();
1312         copy(f, os);
1313         if (os.n != f.length()) {
1314             throw new JarException(formatMsg(
1315                         "error.incorrect.length", f.getPath()));
1316         }
1317         os.updateEntry(e);
1318     }
1319 
1320     void replaceFSC(Map<Integer, String []> filesMap) {
1321         filesMap.keySet().forEach(version -> {
1322             String[] files = filesMap.get(version);
1323             if (files != null) {
1324                 for (int i = 0; i < files.length; i++) {
1325                     files[i] = files[i].replace(File.separatorChar, '/');
1326                 }
1327             }
1328         });
1329     }
1330 
1331     @SuppressWarnings("serial")
1332     Set<ZipEntry> newDirSet() {
1333         return new HashSet<ZipEntry>() {
1334             public boolean add(ZipEntry e) {
1335                 return ((e == null || useExtractionTime) ? false : super.add(e));
1336             }};
1337     }
1338 
1339     void updateLastModifiedTime(Set<ZipEntry> zes) throws IOException {
1340         for (ZipEntry ze : zes) {
1341             long lastModified = ze.getTime();
1342             if (lastModified != -1) {
1343                 String name = safeName(ze.getName().replace(File.separatorChar, '/'));
1344                 if (name.length() != 0) {
1345                     File f = new File(name.replace('/', File.separatorChar));
1346                     f.setLastModified(lastModified);
1347                 }
1348             }
1349         }
1350     }
1351 
1352     /**
1353      * Extracts specified entries from JAR file.
1354      *
1355      * @return whether entries were found and successfully extracted
1356      * (indicating this was a zip file without "leading garbage")
1357      */
1358     boolean extract(InputStream in, String files[]) throws IOException {
1359         ZipInputStream zis = new ZipInputStream(in);
1360         ZipEntry e;
1361         // Set of all directory entries specified in archive.  Disallows
1362         // null entries.  Disallows all entries if using pre-6.0 behavior.
1363         boolean entriesFound = false;
1364         Set<ZipEntry> dirs = newDirSet();
1365         while ((e = zis.getNextEntry()) != null) {
1366             entriesFound = true;
1367             if (files == null) {
1368                 dirs.add(extractFile(zis, e));
1369             } else {
1370                 String name = e.getName();
1371                 for (String file : files) {
1372                     if (name.startsWith(file)) {
1373                         dirs.add(extractFile(zis, e));
1374                         break;
1375                     }
1376                 }
1377             }
1378         }
1379 
1380         // Update timestamps of directories specified in archive with their
1381         // timestamps as given in the archive.  We do this after extraction,
1382         // instead of during, because creating a file in a directory changes
1383         // that directory's timestamp.
1384         updateLastModifiedTime(dirs);
1385 
1386         return entriesFound;
1387     }
1388 
1389     /**
1390      * Extracts specified entries from JAR file, via ZipFile.
1391      */
1392     void extract(String fname, String files[]) throws IOException {
1393         ZipFile zf = new ZipFile(fname);
1394         Set<ZipEntry> dirs = newDirSet();
1395         Enumeration<? extends ZipEntry> zes = zf.entries();
1396         while (zes.hasMoreElements()) {
1397             ZipEntry e = zes.nextElement();
1398             if (files == null) {
1399                 dirs.add(extractFile(zf.getInputStream(e), e));
1400             } else {
1401                 String name = e.getName();
1402                 for (String file : files) {
1403                     if (name.startsWith(file)) {
1404                         dirs.add(extractFile(zf.getInputStream(e), e));
1405                         break;
1406                     }
1407                 }
1408             }
1409         }
1410         zf.close();
1411         updateLastModifiedTime(dirs);
1412     }
1413 
1414     /**
1415      * Extracts next entry from JAR file, creating directories as needed.  If
1416      * the entry is for a directory which doesn't exist prior to this
1417      * invocation, returns that entry, otherwise returns null.
1418      */
1419     ZipEntry extractFile(InputStream is, ZipEntry e) throws IOException {
1420         ZipEntry rc = null;
1421         // The spec requres all slashes MUST be forward '/', it is possible
1422         // an offending zip/jar entry may uses the backwards slash in its
1423         // name. It might cause problem on Windows platform as it skips
1424         // our "safe" check for leading slahs and dot-dot. So replace them
1425         // with '/'.
1426         String name = safeName(e.getName().replace(File.separatorChar, '/'));
1427         if (name.length() == 0) {
1428             return rc;    // leading '/' or 'dot-dot' only path
1429         }
1430         File f = new File(name.replace('/', File.separatorChar));
1431         if (e.isDirectory()) {
1432             if (f.exists()) {
1433                 if (!f.isDirectory()) {
1434                     throw new IOException(formatMsg("error.create.dir",
1435                         f.getPath()));
1436                 }
1437             } else {
1438                 if (!f.mkdirs()) {
1439                     throw new IOException(formatMsg("error.create.dir",
1440                         f.getPath()));
1441                 } else {
1442                     rc = e;
1443                 }
1444             }
1445 
1446             if (vflag) {
1447                 output(formatMsg("out.create", name));
1448             }
1449         } else {
1450             if (f.getParent() != null) {
1451                 File d = new File(f.getParent());
1452                 if (!d.exists() && !d.mkdirs() || !d.isDirectory()) {
1453                     throw new IOException(formatMsg(
1454                         "error.create.dir", d.getPath()));
1455                 }
1456             }
1457             try {
1458                 copy(is, f);
1459             } finally {
1460                 if (is instanceof ZipInputStream)
1461                     ((ZipInputStream)is).closeEntry();
1462                 else
1463                     is.close();
1464             }
1465             if (vflag) {
1466                 if (e.getMethod() == ZipEntry.DEFLATED) {
1467                     output(formatMsg("out.inflated", name));
1468                 } else {
1469                     output(formatMsg("out.extracted", name));
1470                 }
1471             }
1472         }
1473         if (!useExtractionTime) {
1474             long lastModified = e.getTime();
1475             if (lastModified != -1) {
1476                 f.setLastModified(lastModified);
1477             }
1478         }
1479         return rc;
1480     }
1481 
1482     /**
1483      * Lists contents of JAR file.
1484      */
1485     void list(InputStream in, String files[]) throws IOException {
1486         ZipInputStream zis = new ZipInputStream(in);
1487         ZipEntry e;
1488         while ((e = zis.getNextEntry()) != null) {
1489             /*
1490              * In the case of a compressed (deflated) entry, the entry size
1491              * is stored immediately following the entry data and cannot be
1492              * determined until the entry is fully read. Therefore, we close
1493              * the entry first before printing out its attributes.
1494              */
1495             zis.closeEntry();
1496             printEntry(e, files);
1497         }
1498     }
1499 
1500     /**
1501      * Lists contents of JAR file, via ZipFile.
1502      */
1503     void list(String fname, String files[]) throws IOException {
1504         ZipFile zf = new ZipFile(fname);
1505         Enumeration<? extends ZipEntry> zes = zf.entries();
1506         while (zes.hasMoreElements()) {
1507             printEntry(zes.nextElement(), files);
1508         }
1509         zf.close();
1510     }
1511 
1512     /**
1513      * Outputs the class index table to the INDEX.LIST file of the
1514      * root jar file.
1515      */
1516     void dumpIndex(String rootjar, JarIndex index) throws IOException {
1517         File jarFile = new File(rootjar);
1518         Path jarPath = jarFile.toPath();
1519         Path tmpPath = createTempFileInSameDirectoryAs(jarFile).toPath();
1520         try {
1521             if (update(Files.newInputStream(jarPath),
1522                        Files.newOutputStream(tmpPath),
1523                        null, null, index)) {
1524                 try {
1525                     Files.move(tmpPath, jarPath, REPLACE_EXISTING);
1526                 } catch (IOException e) {
1527                     throw new IOException(getMsg("error.write.file"), e);
1528                 }
1529             }
1530         } finally {
1531             Files.deleteIfExists(tmpPath);
1532         }
1533     }
1534 
1535     private HashSet<String> jarPaths = new HashSet<String>();
1536 
1537     /**
1538      * Generates the transitive closure of the Class-Path attribute for
1539      * the specified jar file.
1540      */
1541     List<String> getJarPath(String jar) throws IOException {
1542         List<String> files = new ArrayList<String>();
1543         files.add(jar);
1544         jarPaths.add(jar);
1545 
1546         // take out the current path
1547         String path = jar.substring(0, Math.max(0, jar.lastIndexOf('/') + 1));
1548 
1549         // class path attribute will give us jar file name with
1550         // '/' as separators, so we need to change them to the
1551         // appropriate one before we open the jar file.
1552         JarFile rf = new JarFile(jar.replace('/', File.separatorChar));
1553 
1554         if (rf != null) {
1555             Manifest man = rf.getManifest();
1556             if (man != null) {
1557                 Attributes attr = man.getMainAttributes();
1558                 if (attr != null) {
1559                     String value = attr.getValue(Attributes.Name.CLASS_PATH);
1560                     if (value != null) {
1561                         StringTokenizer st = new StringTokenizer(value);
1562                         while (st.hasMoreTokens()) {
1563                             String ajar = st.nextToken();
1564                             if (!ajar.endsWith("/")) {  // it is a jar file
1565                                 ajar = path.concat(ajar);
1566                                 /* check on cyclic dependency */
1567                                 if (! jarPaths.contains(ajar)) {
1568                                     files.addAll(getJarPath(ajar));
1569                                 }
1570                             }
1571                         }
1572                     }
1573                 }
1574             }
1575         }
1576         rf.close();
1577         return files;
1578     }
1579 
1580     /**
1581      * Generates class index file for the specified root jar file.
1582      */
1583     void genIndex(String rootjar, String[] files) throws IOException {
1584         List<String> jars = getJarPath(rootjar);
1585         int njars = jars.size();
1586         String[] jarfiles;
1587 
1588         if (njars == 1 && files != null) {
1589             // no class-path attribute defined in rootjar, will
1590             // use command line specified list of jars
1591             for (int i = 0; i < files.length; i++) {
1592                 jars.addAll(getJarPath(files[i]));
1593             }
1594             njars = jars.size();
1595         }
1596         jarfiles = jars.toArray(new String[njars]);
1597         JarIndex index = new JarIndex(jarfiles);
1598         dumpIndex(rootjar, index);
1599     }
1600 
1601     /**
1602      * Prints entry information, if requested.
1603      */
1604     void printEntry(ZipEntry e, String[] files) throws IOException {
1605         if (files == null) {
1606             printEntry(e);
1607         } else {
1608             String name = e.getName();
1609             for (String file : files) {
1610                 if (name.startsWith(file)) {
1611                     printEntry(e);
1612                     return;
1613                 }
1614             }
1615         }
1616     }
1617 
1618     /**
1619      * Prints entry information.
1620      */
1621     void printEntry(ZipEntry e) throws IOException {
1622         if (vflag) {
1623             StringBuilder sb = new StringBuilder();
1624             String s = Long.toString(e.getSize());
1625             for (int i = 6 - s.length(); i > 0; --i) {
1626                 sb.append(' ');
1627             }
1628             sb.append(s).append(' ').append(new Date(e.getTime()).toString());
1629             sb.append(' ').append(e.getName());
1630             output(sb.toString());
1631         } else {
1632             output(e.getName());
1633         }
1634     }
1635 
1636     /**
1637      * Prints usage message.
1638      */
1639     void usageError(String s) {
1640         err.println(s);
1641         err.println(getMsg("main.usage.summary.try"));
1642     }
1643 
1644     /**
1645      * A fatal exception has been caught.  No recovery possible
1646      */
1647     void fatalError(Exception e) {
1648         e.printStackTrace();
1649     }
1650 
1651     /**
1652      * A fatal condition has been detected; message is "s".
1653      * No recovery possible
1654      */
1655     void fatalError(String s) {
1656         error(program + ": " + s);
1657     }
1658 
1659     /**
1660      * Print an output message; like verbose output and the like
1661      */
1662     protected void output(String s) {
1663         out.println(s);
1664     }
1665 
1666     /**
1667      * Print an error message; like something is broken
1668      */
1669     void error(String s) {
1670         err.println(s);
1671     }
1672 
1673     /**
1674      * Print a warning message
1675      */
1676     void warn(String s) {
1677         err.println(s);
1678     }
1679 
1680     /**
1681      * Main routine to start program.
1682      */
1683     public static void main(String args[]) {
1684         Main jartool = new Main(System.out, System.err, "jar");
1685         System.exit(jartool.run(args) ? 0 : 1);
1686     }
1687 
1688     /**
1689      * An OutputStream that doesn't send its output anywhere, (but could).
1690      * It's here to find the CRC32 of an input file, necessary for STORED
1691      * mode in ZIP.
1692      */
1693     private static class CRC32OutputStream extends java.io.OutputStream {
1694         final CRC32 crc = new CRC32();
1695         long n = 0;
1696 
1697         CRC32OutputStream() {}
1698 
1699         public void write(int r) throws IOException {
1700             crc.update(r);
1701             n++;
1702         }
1703 
1704         public void write(byte[] b, int off, int len) throws IOException {
1705             crc.update(b, off, len);
1706             n += len;
1707         }
1708 
1709         /**
1710          * Updates a ZipEntry which describes the data read by this
1711          * output stream, in STORED mode.
1712          */
1713         public void updateEntry(ZipEntry e) {
1714             e.setMethod(ZipEntry.STORED);
1715             e.setSize(n);
1716             e.setCrc(crc.getValue());
1717         }
1718     }
1719 
1720     /**
1721      * Attempt to create temporary file in the system-provided temporary folder, if failed attempts
1722      * to create it in the same folder as the file in parameter (if any)
1723      */
1724     private File createTemporaryFile(String tmpbase, String suffix) {
1725         File tmpfile = null;
1726 
1727         try {
1728             tmpfile = File.createTempFile(tmpbase, suffix);
1729         } catch (IOException | SecurityException e) {
1730             // Unable to create file due to permission violation or security exception
1731         }
1732         if (tmpfile == null) {
1733             // Were unable to create temporary file, fall back to temporary file in the same folder
1734             if (fname != null) {
1735                 try {
1736                     File tmpfolder = new File(fname).getAbsoluteFile().getParentFile();
1737                     tmpfile = File.createTempFile(fname, ".tmp" + suffix, tmpfolder);
1738                 } catch (IOException ioe) {
1739                     // Last option failed - fall gracefully
1740                     fatalError(ioe);
1741                 }
1742             } else {
1743                 // No options left - we can not compress to stdout without access to the temporary folder
1744                 fatalError(new IOException(getMsg("error.create.tempfile")));
1745             }
1746         }
1747         return tmpfile;
1748     }
1749 
1750     // Modular jar support
1751 
1752     /**
1753      * Associates a module descriptor's zip entry name along with its
1754      * bytes and an optional URI. Used when describing modules.
1755      */
1756     interface ModuleInfoEntry {
1757        String name();
1758        Optional<String> uriString();
1759        InputStream bytes() throws IOException;
1760     }
1761 
1762     static class ZipFileModuleInfoEntry implements ModuleInfoEntry {
1763         private final ZipFile zipFile;
1764         private final ZipEntry entry;
1765         ZipFileModuleInfoEntry(ZipFile zipFile, ZipEntry entry) {
1766             this.zipFile = zipFile;
1767             this.entry = entry;
1768         }
1769         @Override public String name() { return entry.getName(); }
1770         @Override public InputStream bytes() throws IOException {
1771             return zipFile.getInputStream(entry);
1772         }
1773         /** Returns an optional containing the effective URI. */
1774         @Override public Optional<String> uriString() {
1775             String uri = (Paths.get(zipFile.getName())).toUri().toString();
1776             uri = "jar:" + uri + "/!" + entry.getName();
1777             return Optional.of(uri);
1778         }
1779     }
1780 
1781     static class StreamedModuleInfoEntry implements ModuleInfoEntry {
1782         private final String name;
1783         private final byte[] bytes;
1784         StreamedModuleInfoEntry(String name, byte[] bytes) {
1785             this.name = name;
1786             this.bytes = bytes;
1787         }
1788         @Override public String name() { return name; }
1789         @Override public InputStream bytes() throws IOException {
1790             return new ByteArrayInputStream(bytes);
1791         }
1792         /** Returns an empty optional. */
1793         @Override public Optional<String> uriString() {
1794             return Optional.empty();  // no URI can be derived
1795         }
1796     }
1797 
1798     /** Describes a module from a given zip file. */
1799     private boolean describeModule(ZipFile zipFile) throws IOException {
1800         ZipFileModuleInfoEntry[] infos = zipFile.stream()
1801                 .filter(e -> isModuleInfoEntry(e.getName()))
1802                 .sorted(ENTRY_COMPARATOR)
1803                 .map(e -> new ZipFileModuleInfoEntry(zipFile, e))
1804                 .toArray(ZipFileModuleInfoEntry[]::new);
1805 
1806         if (infos.length == 0) {
1807             // No module descriptor found, derive and describe the automatic module
1808             String fn = zipFile.getName();
1809             ModuleFinder mf = ModuleFinder.of(Paths.get(fn));
1810             try {
1811                 Set<ModuleReference> mref = mf.findAll();
1812                 if (mref.isEmpty()) {
1813                     output(formatMsg("error.unable.derive.automodule", fn));
1814                     return true;
1815                 }
1816                 ModuleDescriptor md = mref.iterator().next().descriptor();
1817                 output(getMsg("out.automodule") + "\n");
1818                 describeModule(md, null, null, "");
1819             } catch (FindException e) {
1820                 String msg = formatMsg("error.unable.derive.automodule", fn);
1821                 Throwable t = e.getCause();
1822                 if (t != null)
1823                     msg = msg + "\n" + t.getMessage();
1824                 output(msg);
1825             }
1826         } else {
1827             return describeModuleFromEntries(infos);
1828         }
1829         return true;
1830     }
1831 
1832     private boolean describeModuleFromStream(FileInputStream fis)
1833         throws IOException
1834     {
1835         List<ModuleInfoEntry> infos = new LinkedList<>();
1836 
1837         try (BufferedInputStream bis = new BufferedInputStream(fis);
1838              ZipInputStream zis = new ZipInputStream(bis)) {
1839             ZipEntry e;
1840             while ((e = zis.getNextEntry()) != null) {
1841                 String ename = e.getName();
1842                 if (isModuleInfoEntry(ename)) {
1843                     infos.add(new StreamedModuleInfoEntry(ename, zis.readAllBytes()));
1844                 }
1845             }
1846         }
1847 
1848         if (infos.size() == 0)
1849             return false;
1850 
1851         ModuleInfoEntry[] sorted = infos.stream()
1852                 .sorted(Comparator.comparing(ModuleInfoEntry::name, ENTRYNAME_COMPARATOR))
1853                 .toArray(ModuleInfoEntry[]::new);
1854 
1855         return describeModuleFromEntries(sorted);
1856     }
1857 
1858     private boolean lessThanEqualReleaseValue(ModuleInfoEntry entry) {
1859         return intVersionFromEntry(entry) <= releaseValue ? true : false;
1860     }
1861 
1862     private static String versionFromEntryName(String name) {
1863         String s = name.substring(VERSIONS_DIR_LENGTH);
1864         return s.substring(0, s.indexOf("/"));
1865     }
1866 
1867     private static int intVersionFromEntry(ModuleInfoEntry entry) {
1868         String name = entry.name();
1869         if (!name.startsWith(VERSIONS_DIR))
1870             return BASE_VERSION;
1871 
1872         String s = name.substring(VERSIONS_DIR_LENGTH);
1873         s = s.substring(0, s.indexOf('/'));
1874         return Integer.valueOf(s);
1875     }
1876 
1877     /**
1878      * Describes a single module descriptor, determined by the specified
1879      * --release, if any, from the given ordered entries.
1880      * The given infos must be ordered as per ENTRY_COMPARATOR.
1881      */
1882     private boolean describeModuleFromEntries(ModuleInfoEntry[] infos)
1883         throws IOException
1884     {
1885         assert infos.length > 0;
1886 
1887         // Informative: output all non-root descriptors, if any
1888         String releases = Arrays.stream(infos)
1889                 .filter(e -> !e.name().equals(MODULE_INFO))
1890                 .map(ModuleInfoEntry::name)
1891                 .map(Main::versionFromEntryName)
1892                 .collect(joining(" "));
1893         if (!releases.equals(""))
1894             output("releases: " + releases + "\n");
1895 
1896         // Describe the operative descriptor for the specified --release, if any
1897         if (releaseValue != -1) {
1898             ModuleInfoEntry entry = null;
1899             int i = 0;
1900             while (i < infos.length && lessThanEqualReleaseValue(infos[i])) {
1901                 entry = infos[i];
1902                 i++;
1903             }
1904 
1905             if (entry == null) {
1906                 output(formatMsg("error.no.operative.descriptor",
1907                                  String.valueOf(releaseValue)));
1908                 return false;
1909             }
1910 
1911             String uriString = entry.uriString().orElse("");
1912             try (InputStream is = entry.bytes()) {
1913                 describeModule(is, uriString);
1914             }
1915         } else {
1916             // no specific --release specified, output the root, if any
1917             if (infos[0].name().equals(MODULE_INFO)) {
1918                 String uriString = infos[0].uriString().orElse("");
1919                 try (InputStream is = infos[0].bytes()) {
1920                     describeModule(is, uriString);
1921                 }
1922             } else {
1923                 // no root, output message to specify --release
1924                 output(getMsg("error.no.root.descriptor"));
1925             }
1926         }
1927         return true;
1928     }
1929 
1930     static <T> String toLowerCaseString(Collection<T> set) {
1931         if (set.isEmpty()) { return ""; }
1932         return " " + set.stream().map(e -> e.toString().toLowerCase(Locale.ROOT))
1933                   .sorted().collect(joining(" "));
1934     }
1935 
1936     static <T> String toString(Collection<T> set) {
1937         if (set.isEmpty()) { return ""; }
1938         return " " + set.stream().map(e -> e.toString()).sorted().collect(joining(" "));
1939     }
1940 
1941     private void describeModule(InputStream entryInputStream, String uriString)
1942         throws IOException
1943     {
1944         ModuleInfo.Attributes attrs = ModuleInfo.read(entryInputStream, null);
1945         ModuleDescriptor md = attrs.descriptor();
1946         ModuleTarget target = attrs.target();
1947         ModuleHashes hashes = attrs.recordedHashes();
1948 
1949         describeModule(md, target, hashes, uriString);
1950     }
1951 
1952     private void describeModule(ModuleDescriptor md,
1953                                 ModuleTarget target,
1954                                 ModuleHashes hashes,
1955                                 String uriString)
1956         throws IOException
1957     {
1958         StringBuilder sb = new StringBuilder();
1959 
1960         sb.append(md.toNameAndVersion());
1961 
1962         if (!uriString.equals(""))
1963             sb.append(" ").append(uriString);
1964         if (md.isOpen())
1965             sb.append(" open");
1966         if (md.isAutomatic())
1967             sb.append(" automatic");
1968         sb.append("\n");
1969 
1970         // unqualified exports (sorted by package)
1971         md.exports().stream()
1972                 .sorted(Comparator.comparing(Exports::source))
1973                 .filter(e -> !e.isQualified())
1974                 .forEach(e -> sb.append("exports ").append(e.source())
1975                                 .append(toLowerCaseString(e.modifiers()))
1976                                 .append("\n"));
1977 
1978         // dependences
1979         md.requires().stream().sorted()
1980                 .forEach(r -> sb.append("requires ").append(r.name())
1981                                 .append(toLowerCaseString(r.modifiers()))
1982                                 .append("\n"));
1983 
1984         // service use and provides
1985         md.uses().stream().sorted()
1986                 .forEach(s -> sb.append("uses ").append(s).append("\n"));
1987 
1988         md.provides().stream()
1989                 .sorted(Comparator.comparing(Provides::service))
1990                 .forEach(p -> sb.append("provides ").append(p.service())
1991                                 .append(" with")
1992                                 .append(toString(p.providers()))
1993                                 .append("\n"));
1994 
1995         // qualified exports
1996         md.exports().stream()
1997                 .sorted(Comparator.comparing(Exports::source))
1998                 .filter(Exports::isQualified)
1999                 .forEach(e -> sb.append("qualified exports ").append(e.source())
2000                                 .append(" to").append(toLowerCaseString(e.targets()))
2001                                 .append("\n"));
2002 
2003         // open packages
2004         md.opens().stream()
2005                 .sorted(Comparator.comparing(Opens::source))
2006                 .filter(o -> !o.isQualified())
2007                 .forEach(o -> sb.append("opens ").append(o.source())
2008                                  .append(toLowerCaseString(o.modifiers()))
2009                                  .append("\n"));
2010 
2011         md.opens().stream()
2012                 .sorted(Comparator.comparing(Opens::source))
2013                 .filter(Opens::isQualified)
2014                 .forEach(o -> sb.append("qualified opens ").append(o.source())
2015                                  .append(toLowerCaseString(o.modifiers()))
2016                                  .append(" to").append(toLowerCaseString(o.targets()))
2017                                  .append("\n"));
2018 
2019         // non-exported/non-open packages
2020         Set<String> concealed = new TreeSet<>(md.packages());
2021         md.exports().stream().map(Exports::source).forEach(concealed::remove);
2022         md.opens().stream().map(Opens::source).forEach(concealed::remove);
2023         concealed.forEach(p -> sb.append("contains ").append(p).append("\n"));
2024 
2025         md.mainClass().ifPresent(v -> sb.append("main-class ").append(v).append("\n"));
2026 
2027         if (target != null) {
2028             String targetPlatform = target.targetPlatform();
2029             if (!targetPlatform.isEmpty())
2030                 sb.append("platform ").append(targetPlatform).append("\n");
2031        }
2032 
2033        if (hashes != null) {
2034            hashes.names().stream().sorted().forEach(
2035                    mod -> sb.append("hashes ").append(mod).append(" ")
2036                             .append(hashes.algorithm()).append(" ")
2037                             .append(toHex(hashes.hashFor(mod)))
2038                             .append("\n"));
2039         }
2040 
2041         output(sb.toString());
2042     }
2043 
2044     private static String toHex(byte[] ba) {
2045         StringBuilder sb = new StringBuilder(ba.length << 1);
2046         for (byte b: ba) {
2047             sb.append(String.format("%02x", b & 0xff));
2048         }
2049         return sb.toString();
2050     }
2051 
2052     static String toBinaryName(String classname) {
2053         return (classname.replace('.', '/')) + ".class";
2054     }
2055 
2056     private boolean checkModuleInfo(byte[] moduleInfoBytes, Set<String> entries)
2057         throws IOException
2058     {
2059         boolean ok = true;
2060         if (moduleInfoBytes != null) {  // no root module-info.class if null
2061             try {
2062                 // ModuleDescriptor.read() checks open/exported pkgs vs packages
2063                 ModuleDescriptor md = ModuleDescriptor.read(ByteBuffer.wrap(moduleInfoBytes));
2064                 // A module must have the implementation class of the services it 'provides'.
2065                 if (md.provides().stream().map(Provides::providers).flatMap(List::stream)
2066                       .filter(p -> !entries.contains(toBinaryName(p)))
2067                       .peek(p -> fatalError(formatMsg("error.missing.provider", p)))
2068                       .count() != 0) {
2069                     ok = false;
2070                 }
2071             } catch (InvalidModuleDescriptorException x) {
2072                 fatalError(x.getMessage());
2073                 ok = false;
2074             }
2075         }
2076         return ok;
2077     }
2078 
2079     /**
2080      * Adds extended modules attributes to the given module-info's.  The given
2081      * Map values are updated in-place. Returns false if an error occurs.
2082      */
2083     private void addExtendedModuleAttributes(Map<String,byte[]> moduleInfos,
2084                                                 Set<String> packages)
2085         throws IOException
2086     {
2087         for (Map.Entry<String,byte[]> e: moduleInfos.entrySet()) {
2088             ModuleDescriptor md = ModuleDescriptor.read(ByteBuffer.wrap(e.getValue()));
2089             e.setValue(extendedInfoBytes(md, e.getValue(), packages));
2090         }
2091     }
2092 
2093     static boolean isModuleInfoEntry(String name) {
2094         // root or versioned module-info.class
2095         if (name.endsWith(MODULE_INFO)) {
2096             int end = name.length() - MODULE_INFO.length();
2097             if (end == 0)
2098                 return true;
2099             if (name.startsWith(VERSIONS_DIR)) {
2100                 int off = VERSIONS_DIR_LENGTH;
2101                 if (off == end)      // meta-inf/versions/module-info.class
2102                     return false;
2103                 while (off < end - 1) {
2104                     char c = name.charAt(off++);
2105                     if (c < '0' || c > '9')
2106                         return false;
2107                 }
2108                 return name.charAt(off) == '/';
2109             }
2110         }
2111         return false;
2112     }
2113 
2114     /**
2115      * Returns a byte array containing the given module-info.class plus any
2116      * extended attributes.
2117      *
2118      * If --module-version, --main-class, or other options were provided
2119      * then the corresponding class file attributes are added to the
2120      * module-info here.
2121      */
2122     private byte[] extendedInfoBytes(ModuleDescriptor md,
2123                                      byte[] miBytes,
2124                                      Set<String> packages)
2125         throws IOException
2126     {
2127         ByteArrayOutputStream baos = new ByteArrayOutputStream();
2128         InputStream is = new ByteArrayInputStream(miBytes);
2129         ModuleInfoExtender extender = ModuleInfoExtender.newExtender(is);
2130 
2131         // Add (or replace) the Packages attribute
2132         extender.packages(packages);
2133 
2134         // --main-class
2135         if (ename != null)
2136             extender.mainClass(ename);
2137 
2138         // --module-version
2139         if (moduleVersion != null)
2140             extender.version(moduleVersion);
2141 
2142         // --hash-modules
2143         if (modulesToHash != null) {
2144             String mn = md.name();
2145             Hasher hasher = new Hasher(md, fname);
2146             ModuleHashes moduleHashes = hasher.computeHashes(mn);
2147             if (moduleHashes != null) {
2148                 extender.hashes(moduleHashes);
2149             } else {
2150                 warn("warning: no module is recorded in hash in " + mn);
2151             }
2152         }
2153 
2154         if (moduleResolution.value() != 0) {
2155             extender.moduleResolution(moduleResolution);
2156         }
2157 
2158         extender.write(baos);
2159         return baos.toByteArray();
2160     }
2161 
2162     /**
2163      * Compute and record hashes
2164      */
2165     private class Hasher {
2166         final ModuleHashesBuilder hashesBuilder;
2167         final ModuleFinder finder;
2168         final Set<String> modules;
2169         Hasher(ModuleDescriptor descriptor, String fname) throws IOException {
2170             // Create a module finder that finds the modular JAR
2171             // being created/updated
2172             URI uri = Paths.get(fname).toUri();
2173             ModuleReference mref = new ModuleReference(descriptor, uri) {
2174                 @Override
2175                 public ModuleReader open() {
2176                     throw new UnsupportedOperationException("should not reach here");
2177                 }
2178             };
2179 
2180             // Compose a module finder with the module path and
2181             // the modular JAR being created or updated
2182             this.finder = ModuleFinder.compose(moduleFinder,
2183                 new ModuleFinder() {
2184                     @Override
2185                     public Optional<ModuleReference> find(String name) {
2186                         if (descriptor.name().equals(name))
2187                             return Optional.of(mref);
2188                         else
2189                             return Optional.empty();
2190                     }
2191 
2192                     @Override
2193                     public Set<ModuleReference> findAll() {
2194                         return Collections.singleton(mref);
2195                     }
2196                 });
2197 
2198             // Determine the modules that matches the pattern {@code modulesToHash}
2199             Set<String> roots = finder.findAll().stream()
2200                 .map(ref -> ref.descriptor().name())
2201                 .filter(mn -> modulesToHash.matcher(mn).find())
2202                 .collect(Collectors.toSet());
2203 
2204             // use system module path unless it creates a modular JAR for
2205             // a module that is present in the system image e.g. upgradeable
2206             // module
2207             ModuleFinder system;
2208             String name = descriptor.name();
2209             if (name != null && ModuleFinder.ofSystem().find(name).isPresent()) {
2210                 system = ModuleFinder.of();
2211             } else {
2212                 system = ModuleFinder.ofSystem();
2213             }
2214             // get a resolved module graph
2215             Configuration config =
2216                 Configuration.empty().resolve(system, finder, roots);
2217 
2218             // filter modules resolved from the system module finder
2219             this.modules = config.modules().stream()
2220                 .map(ResolvedModule::name)
2221                 .filter(mn -> roots.contains(mn) && !system.find(mn).isPresent())
2222                 .collect(Collectors.toSet());
2223 
2224             this.hashesBuilder = new ModuleHashesBuilder(config, modules);
2225         }
2226 
2227         /**
2228          * Compute hashes of the specified module.
2229          *
2230          * It records the hashing modules that depend upon the specified
2231          * module directly or indirectly.
2232          */
2233         ModuleHashes computeHashes(String name) {
2234             if (hashesBuilder == null)
2235                 return null;
2236 
2237             return hashesBuilder.computeHashes(Set.of(name)).get(name);
2238         }
2239     }
2240 
2241     // sort base entries before versioned entries, and sort entry classes with
2242     // nested classes so that the outter class appears before the associated
2243     // nested class
2244     static Comparator<String> ENTRYNAME_COMPARATOR = (s1, s2) ->  {
2245 
2246         if (s1.equals(s2)) return 0;
2247         boolean b1 = s1.startsWith(VERSIONS_DIR);
2248         boolean b2 = s2.startsWith(VERSIONS_DIR);
2249         if (b1 && !b2) return 1;
2250         if (!b1 && b2) return -1;
2251         int n = 0; // starting char for String compare
2252         if (b1 && b2) {
2253             // normally strings would be sorted so "10" goes before "9", but
2254             // version number strings need to be sorted numerically
2255             n = VERSIONS_DIR.length();   // skip the common prefix
2256             int i1 = s1.indexOf('/', n);
2257             int i2 = s2.indexOf('/', n);
2258             if (i1 == -1) throw new Validator.InvalidJarException(s1);
2259             if (i2 == -1) throw new Validator.InvalidJarException(s2);
2260             // shorter version numbers go first
2261             if (i1 != i2) return i1 - i2;
2262             // otherwise, handle equal length numbers below
2263         }
2264         int l1 = s1.length();
2265         int l2 = s2.length();
2266         int lim = Math.min(l1, l2);
2267         for (int k = n; k < lim; k++) {
2268             char c1 = s1.charAt(k);
2269             char c2 = s2.charAt(k);
2270             if (c1 != c2) {
2271                 // change natural ordering so '.' comes before '$'
2272                 // i.e. outer classes come before nested classes
2273                 if (c1 == '$' && c2 == '.') return 1;
2274                 if (c1 == '.' && c2 == '$') return -1;
2275                 return c1 - c2;
2276             }
2277         }
2278         return l1 - l2;
2279     };
2280 
2281     static Comparator<ZipEntry> ENTRY_COMPARATOR =
2282         Comparator.comparing(ZipEntry::getName, ENTRYNAME_COMPARATOR);
2283 
2284 }