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.Provides;
  35 import java.lang.module.ModuleDescriptor.Opens;
  36 import java.lang.module.ModuleDescriptor.Requires;
  37 import java.lang.module.ModuleDescriptor.Version;
  38 import java.lang.module.ModuleFinder;
  39 import java.lang.module.ModuleReader;
  40 import java.lang.module.ModuleReference;
  41 import java.lang.module.ResolutionException;
  42 import java.lang.module.ResolvedModule;
  43 import java.net.URI;
  44 import java.nio.ByteBuffer;
  45 import java.nio.file.Path;
  46 import java.nio.file.Files;

  47 import java.nio.file.Paths;
  48 import java.nio.file.StandardCopyOption;

  49 import java.util.*;
  50 import java.util.function.Consumer;
  51 import java.util.function.Supplier;



  52 import java.util.regex.Pattern;
  53 import java.util.stream.Collectors;
  54 import java.util.stream.Stream;
  55 import java.util.zip.*;
  56 import java.util.jar.*;
  57 import java.util.jar.Pack200.*;
  58 import java.util.jar.Manifest;
  59 import java.text.MessageFormat;
  60 
  61 import jdk.internal.module.Checks;
  62 import jdk.internal.module.ModuleHashes;
  63 import jdk.internal.module.ModuleHashesBuilder;
  64 import jdk.internal.module.ModuleInfo;
  65 import jdk.internal.module.ModuleInfoExtender;
  66 import jdk.internal.module.ModuleResolution;
  67 import jdk.internal.module.ModuleTarget;
  68 import jdk.internal.util.jar.JarIndex;
  69 
  70 import static jdk.internal.util.jar.JarIndex.INDEX_NAME;
  71 import static java.util.jar.JarFile.MANIFEST_NAME;
  72 import static java.util.stream.Collectors.joining;
  73 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
  74 
  75 /**
  76  * This class implements a simple utility for creating files in the JAR
  77  * (Java Archive) file format. The JAR format is based on the ZIP file
  78  * format, with optional meta-information stored in a MANIFEST entry.
  79  */
  80 public class Main {
  81     String program;
  82     PrintWriter out, err;
  83     String fname, mname, ename;
  84     String zname = "";
  85     String rootjar = null;
  86 
  87     private static final int BASE_VERSION = 0;
  88 
  89     private static class Entry {
  90         final String name;
  91         final File file;
  92         final boolean isDir;
  93 
  94         Entry(File file, String name, boolean isDir) {
  95             this.file = file;
  96             this.isDir = isDir;
  97             this.name = name;
  98         }
  99 
 100         @Override
 101         public boolean equals(Object o) {
 102             if (this == o) return true;
 103             if (!(o instanceof Entry)) return false;
 104             return this.file.equals(((Entry)o).file);
 105         }
 106 
 107         @Override
 108         public int hashCode() {
 109             return file.hashCode();
 110         }
 111     }
 112 
 113     // An entryName(path)->Entry map generated during "expand", it helps to
 114     // decide whether or not an existing entry in a jar file needs to be
 115     // replaced, during the "update" operation.
 116     Map<String, Entry> entryMap = new HashMap<>();
 117 
 118     // All entries need to be added/updated.
 119     Set<Entry> entries = new LinkedHashSet<>();
 120 
 121     // module-info.class entries need to be added/updated.
 122     Map<String,byte[]> moduleInfos = new HashMap<>();
 123 
 124     // A paths Set for each version, where each Set contains directories
 125     // specified by the "-C" operation.
 126     Map<Integer,Set<String>> pathsMap = new HashMap<>();
 127 
 128     // There's also a files array per version
 129     Map<Integer,String[]> filesMap = new HashMap<>();
 130 
 131     // Do we think this is a multi-release jar?  Set to true
 132     // if --release option found followed by at least file
 133     boolean isMultiRelease;
 134 
 135     // The last parsed --release value, if any. Used in conjunction with
 136     // "-d,--describe-module" to select the operative module descriptor.
 137     int releaseValue = -1;
 138 
 139     /*
 140      * cflag: create
 141      * uflag: update
 142      * xflag: xtract
 143      * tflag: table
 144      * vflag: verbose
 145      * flag0: no zip compression (store only)
 146      * Mflag: DO NOT generate a manifest file (just ZIP)
 147      * iflag: generate jar index
 148      * nflag: Perform jar normalization at the end
 149      * pflag: preserve/don't strip leading slash and .. component from file name
 150      * dflag: print module descriptor
 151      */
 152     boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag, nflag, pflag, dflag;
 153 


 154     /* To support additional GNU Style informational options */
 155     Consumer<PrintWriter> info;
 156 
 157     /* Modular jar related options */
 158     Version moduleVersion;
 159     Pattern modulesToHash;
 160     ModuleResolution moduleResolution = ModuleResolution.empty();
 161     ModuleFinder moduleFinder = ModuleFinder.of();
 162 
 163     static final String MODULE_INFO = "module-info.class";
 164     static final String MANIFEST_DIR = "META-INF/";
 165     static final String VERSIONS_DIR = MANIFEST_DIR + "versions/";
 166     static final String VERSION = "1.0";
 167     static final int VERSIONS_DIR_LENGTH = VERSIONS_DIR.length();
 168     private static ResourceBundle rsrc;
 169 
 170     /**
 171      * If true, maintain compatibility with JDK releases prior to 6.0 by
 172      * timestamping extracted files with the time at which they are extracted.
 173      * Default is to use the time given in the archive.
 174      */
 175     private static final boolean useExtractionTime =
 176         Boolean.getBoolean("sun.tools.jar.useExtractionTime");
 177 
 178     /**
 179      * Initialize ResourceBundle
 180      */
 181     static {
 182         try {
 183             rsrc = ResourceBundle.getBundle("sun.tools.jar.resources.jar");
 184         } catch (MissingResourceException e) {
 185             throw new Error("Fatal: Resource for jar is missing");
 186         }
 187     }
 188 
 189     static String getMsg(String key) {
 190         try {
 191             return (rsrc.getString(key));
 192         } catch (MissingResourceException e) {
 193             throw new Error("Error in message file");
 194         }
 195     }
 196 
 197     static String formatMsg(String key, String arg) {
 198         String msg = getMsg(key);
 199         String[] args = new String[1];
 200         args[0] = arg;
 201         return MessageFormat.format(msg, (Object[]) args);
 202     }
 203 
 204     static String formatMsg2(String key, String arg, String arg1) {
 205         String msg = getMsg(key);
 206         String[] args = new String[2];
 207         args[0] = arg;
 208         args[1] = arg1;
 209         return MessageFormat.format(msg, (Object[]) args);
 210     }
 211 
 212     public Main(PrintStream out, PrintStream err, String program) {
 213         this.out = new PrintWriter(out, true);
 214         this.err = new PrintWriter(err, true);
 215         this.program = program;
 216     }
 217 
 218     public Main(PrintWriter out, PrintWriter err, String program) {
 219         this.out = out;
 220         this.err = err;
 221         this.program = program;
 222     }
 223 
 224     /**
 225      * Creates a new empty temporary file in the same directory as the
 226      * specified file.  A variant of File.createTempFile.
 227      */
 228     private static File createTempFileInSameDirectoryAs(File file)
 229         throws IOException {
 230         File dir = file.getParentFile();
 231         if (dir == null)
 232             dir = new File(".");
 233         return File.createTempFile("jartmp", null, dir);
 234     }
 235 
 236     private boolean ok;
 237 
 238     /**
 239      * Starts main program with the specified arguments.
 240      */

 241     public synchronized boolean run(String args[]) {
 242         ok = true;
 243         if (!parseArgs(args)) {
 244             return false;
 245         }
 246         File tmpFile = null;
 247         try {
 248             if (cflag || uflag) {
 249                 if (fname != null) {
 250                     // The name of the zip file as it would appear as its own
 251                     // zip file entry. We use this to make sure that we don't
 252                     // add the zip file to itself.
 253                     zname = fname.replace(File.separatorChar, '/');
 254                     if (zname.startsWith("./")) {
 255                         zname = zname.substring(2);
 256                     }
 257                 }
 258             }
 259             if (cflag) {
 260                 Manifest manifest = null;
 261                 if (!Mflag) {
 262                     if (mname != null) {
 263                         try (InputStream in = new FileInputStream(mname)) {
 264                             manifest = new Manifest(new BufferedInputStream(in));
 265                         }
 266                     } else {
 267                         manifest = new Manifest();
 268                     }
 269                     addVersion(manifest);
 270                     addCreatedBy(manifest);
 271                     if (isAmbiguousMainClass(manifest)) {
 272                         return false;
 273                     }
 274                     if (ename != null) {
 275                         addMainClass(manifest, ename);
 276                     }
 277                     if (isMultiRelease) {
 278                         addMultiRelease(manifest);
 279                     }
 280                 }
 281                 expand();
 282                 if (!moduleInfos.isEmpty()) {
 283                     // All actual file entries (excl manifest and module-info.class)
 284                     Set<String> jentries = new HashSet<>();
 285                     // all packages if it's a class or resource
 286                     Set<String> packages = new HashSet<>();
 287                     entries.stream()
 288                            .filter(e -> !e.isDir)
 289                            .forEach( e -> {
 290                                addPackageIfNamed(packages, e.name);
 291                                jentries.add(e.name);
 292                     });
 293                     addExtendedModuleAttributes(moduleInfos, packages);
 294 
 295                     // Basic consistency checks for modular jars.
 296                     if (!checkModuleInfo(moduleInfos.get(MODULE_INFO), jentries))
 297                         return false;
 298 
 299                 } else if (moduleVersion != null || modulesToHash != null) {
 300                     error(getMsg("error.module.options.without.info"));
 301                     return false;
 302                 }
 303                 if (vflag && fname == null) {
 304                     // Disable verbose output so that it does not appear
 305                     // on stdout along with file data
 306                     // error("Warning: -v option ignored");
 307                     vflag = false;
 308                 }
 309                 final String tmpbase = (fname == null)
 310                         ? "tmpjar"
 311                         : fname.substring(fname.indexOf(File.separatorChar) + 1);
 312 
 313                 tmpFile = createTemporaryFile(tmpbase, ".jar");
 314                 try (OutputStream out = new FileOutputStream(tmpFile)) {
 315                     create(new BufferedOutputStream(out, 4096), manifest);
 316                 }
 317                 if (nflag) {



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