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