rev 50498 : 8199871: Deprecate pack200 and unpack200 tools
Reviewed-by:

   1 /*
   2  * Copyright (c) 1996, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package sun.tools.jar;
  27 
  28 import java.io.*;
  29 import java.lang.module.Configuration;
  30 import java.lang.module.FindException;
  31 import java.lang.module.InvalidModuleDescriptorException;
  32 import java.lang.module.ModuleDescriptor;
  33 import java.lang.module.ModuleDescriptor.Exports;

  34 import java.lang.module.ModuleDescriptor.Opens;
  35 import java.lang.module.ModuleDescriptor.Provides;
  36 import java.lang.module.ModuleDescriptor.Version;
  37 import java.lang.module.ModuleFinder;
  38 import java.lang.module.ModuleReader;
  39 import java.lang.module.ModuleReference;

  40 import java.lang.module.ResolvedModule;
  41 import java.net.URI;
  42 import java.nio.ByteBuffer;

  43 import java.nio.file.Files;
  44 import java.nio.file.Path;
  45 import java.nio.file.Paths;
  46 import java.nio.file.StandardCopyOption;
  47 import java.text.MessageFormat;
  48 import java.util.*;
  49 import java.util.function.Consumer;
  50 import java.util.jar.Attributes;
  51 import java.util.jar.JarFile;
  52 import java.util.jar.JarOutputStream;
  53 import java.util.jar.Manifest;
  54 import java.util.regex.Pattern;
  55 import java.util.stream.Collectors;
  56 import java.util.stream.Stream;
  57 import java.util.zip.CRC32;
  58 import java.util.zip.ZipEntry;
  59 import java.util.zip.ZipFile;
  60 import java.util.zip.ZipInputStream;
  61 import java.util.zip.ZipOutputStream;

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