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 toString(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 1921 private void describeModule(InputStream entryInputStream, String uriString) 1922 throws IOException 1923 { 1924 ModuleInfo.Attributes attrs = ModuleInfo.read(entryInputStream, null); 1925 ModuleDescriptor md = attrs.descriptor(); 1926 ModuleTarget target = attrs.target(); 1927 ModuleHashes hashes = attrs.recordedHashes(); 1928 1929 describeModule(md, target, hashes, uriString); 1930 } 1931 1932 private void describeModule(ModuleDescriptor md, 1933 ModuleTarget target, 1934 ModuleHashes hashes, 1935 String uriString) 1936 throws IOException 1937 { 1938 StringBuilder sb = new StringBuilder(); 1939 1940 sb.append(md.toNameAndVersion()); 1941 1942 if (!uriString.equals("")) 1943 sb.append(" ").append(uriString); 1944 if (md.isOpen()) 1945 sb.append(" open"); 1946 if (md.isAutomatic()) 1947 sb.append(" automatic"); 1948 sb.append("\n"); 1949 1950 // unqualified exports (sorted by package) 1951 md.exports().stream() 1952 .sorted(Comparator.comparing(Exports::source)) 1953 .filter(e -> !e.isQualified()) 1954 .forEach(e -> sb.append("exports ").append(e.source()) 1955 .append(toString(e.modifiers())).append("\n")); 1956 1957 // dependences 1958 md.requires().stream().sorted() 1959 .forEach(r -> sb.append("requires ").append(r.name()) 1960 .append(toString(r.modifiers())).append("\n")); 1961 1962 // service use and provides 1963 md.uses().stream().sorted() 1964 .forEach(s -> sb.append("uses ").append(s).append("\n")); 1965 1966 md.provides().stream() 1967 .sorted(Comparator.comparing(Provides::service)) 1968 .forEach(p -> sb.append("provides ").append(p.service()) 1969 .append(" with") 1970 .append(toString(p.providers())) 1971 .append("\n")); 1972 1973 // qualified exports 1974 md.exports().stream() 1975 .sorted(Comparator.comparing(Exports::source)) 1976 .filter(Exports::isQualified) 1977 .forEach(e -> sb.append("qualified exports ").append(e.source()) 1978 .append(" to").append(toString(e.targets())) 1979 .append("\n")); 1980 1981 // open packages 1982 md.opens().stream() 1983 .sorted(Comparator.comparing(Opens::source)) 1984 .filter(o -> !o.isQualified()) 1985 .forEach(o -> sb.append("opens ").append(o.source()) 1986 .append(toString(o.modifiers())) 1987 .append("\n")); 1988 1989 md.opens().stream() 1990 .sorted(Comparator.comparing(Opens::source)) 1991 .filter(Opens::isQualified) 1992 .forEach(o -> sb.append("qualified opens ").append(o.source()) 1993 .append(toString(o.modifiers())) 1994 .append(" to").append(toString(o.targets())) 1995 .append("\n")); 1996 1997 // non-exported/non-open packages 1998 Set<String> concealed = new TreeSet<>(md.packages()); 1999 md.exports().stream().map(Exports::source).forEach(concealed::remove); 2000 md.opens().stream().map(Opens::source).forEach(concealed::remove); 2001 concealed.forEach(p -> sb.append("contains ").append(p).append("\n")); 2002 2003 md.mainClass().ifPresent(v -> sb.append("main-class ").append(v).append("\n")); 2004 2005 if (target != null) { 2006 String targetPlatform = target.targetPlatform(); 2007 if (!targetPlatform.isEmpty()) 2008 sb.append("platform ").append(targetPlatform).append("\n"); 2009 } 2010 2011 if (hashes != null) { 2012 hashes.names().stream().sorted().forEach( 2013 mod -> sb.append("hashes ").append(mod).append(" ") 2014 .append(hashes.algorithm()).append(" ") 2015 .append(toHex(hashes.hashFor(mod))) 2016 .append("\n")); 2017 } 2018 2019 output(sb.toString()); 2020 } 2021 2022 private static String toHex(byte[] ba) { 2023 StringBuilder sb = new StringBuilder(ba.length << 1); 2024 for (byte b: ba) { 2025 sb.append(String.format("%02x", b & 0xff)); 2026 } 2027 return sb.toString(); 2028 } 2029 2030 static String toBinaryName(String classname) { 2031 return (classname.replace('.', '/')) + ".class"; 2032 } 2033 2034 private boolean checkModuleInfo(byte[] moduleInfoBytes, Set<String> entries) 2035 throws IOException 2036 { 2037 boolean ok = true; 2038 if (moduleInfoBytes != null) { // no root module-info.class if null 2039 try { 2040 // ModuleDescriptor.read() checks open/exported pkgs vs packages 2041 ModuleDescriptor md = ModuleDescriptor.read(ByteBuffer.wrap(moduleInfoBytes)); 2042 // A module must have the implementation class of the services it 'provides'. 2043 if (md.provides().stream().map(Provides::providers).flatMap(List::stream) 2044 .filter(p -> !entries.contains(toBinaryName(p))) 2045 .peek(p -> fatalError(formatMsg("error.missing.provider", p))) 2046 .count() != 0) { 2047 ok = false; 2048 } 2049 } catch (InvalidModuleDescriptorException x) { 2050 fatalError(x.getMessage()); 2051 ok = false; 2052 } 2053 } 2054 return ok; 2055 } 2056 2057 /** 2058 * Adds extended modules attributes to the given module-info's. The given 2059 * Map values are updated in-place. Returns false if an error occurs. 2060 */ 2061 private void addExtendedModuleAttributes(Map<String,byte[]> moduleInfos, 2062 Set<String> packages) 2063 throws IOException 2064 { 2065 for (Map.Entry<String,byte[]> e: moduleInfos.entrySet()) { 2066 ModuleDescriptor md = ModuleDescriptor.read(ByteBuffer.wrap(e.getValue())); 2067 e.setValue(extendedInfoBytes(md, e.getValue(), packages)); 2068 } 2069 } 2070 2071 static boolean isModuleInfoEntry(String name) { 2072 // root or versioned module-info.class 2073 if (name.endsWith(MODULE_INFO)) { 2074 int end = name.length() - MODULE_INFO.length(); 2075 if (end == 0) 2076 return true; 2077 if (name.startsWith(VERSIONS_DIR)) { 2078 int off = VERSIONS_DIR_LENGTH; 2079 if (off == end) // meta-inf/versions/module-info.class 2080 return false; 2081 while (off < end - 1) { 2082 char c = name.charAt(off++); 2083 if (c < '0' || c > '9') 2084 return false; 2085 } 2086 return name.charAt(off) == '/'; 2087 } 2088 } 2089 return false; 2090 } 2091 2092 /** 2093 * Returns a byte array containing the given module-info.class plus any 2094 * extended attributes. 2095 * 2096 * If --module-version, --main-class, or other options were provided 2097 * then the corresponding class file attributes are added to the 2098 * module-info here. 2099 */ 2100 private byte[] extendedInfoBytes(ModuleDescriptor md, 2101 byte[] miBytes, 2102 Set<String> packages) 2103 throws IOException 2104 { 2105 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 2106 InputStream is = new ByteArrayInputStream(miBytes); 2107 ModuleInfoExtender extender = ModuleInfoExtender.newExtender(is); 2108 2109 // Add (or replace) the Packages attribute 2110 extender.packages(packages); 2111 2112 // --main-class 2113 if (ename != null) 2114 extender.mainClass(ename); 2115 2116 // --module-version 2117 if (moduleVersion != null) 2118 extender.version(moduleVersion); 2119 2120 // --hash-modules 2121 if (modulesToHash != null) { 2122 String mn = md.name(); 2123 Hasher hasher = new Hasher(md, fname); 2124 ModuleHashes moduleHashes = hasher.computeHashes(mn); 2125 if (moduleHashes != null) { 2126 extender.hashes(moduleHashes); 2127 } else { 2128 warn("warning: no module is recorded in hash in " + mn); 2129 } 2130 } 2131 2132 if (moduleResolution.value() != 0) { 2133 extender.moduleResolution(moduleResolution); 2134 } 2135 2136 extender.write(baos); 2137 return baos.toByteArray(); 2138 } 2139 2140 /** 2141 * Compute and record hashes 2142 */ 2143 private class Hasher { 2144 final ModuleHashesBuilder hashesBuilder; 2145 final ModuleFinder finder; 2146 final Set<String> modules; 2147 Hasher(ModuleDescriptor descriptor, String fname) throws IOException { 2148 // Create a module finder that finds the modular JAR 2149 // being created/updated 2150 URI uri = Paths.get(fname).toUri(); 2151 ModuleReference mref = new ModuleReference(descriptor, uri) { 2152 @Override 2153 public ModuleReader open() { 2154 throw new UnsupportedOperationException("should not reach here"); 2155 } 2156 }; 2157 2158 // Compose a module finder with the module path and 2159 // the modular JAR being created or updated 2160 this.finder = ModuleFinder.compose(moduleFinder, 2161 new ModuleFinder() { 2162 @Override 2163 public Optional<ModuleReference> find(String name) { 2164 if (descriptor.name().equals(name)) 2165 return Optional.of(mref); 2166 else 2167 return Optional.empty(); 2168 } 2169 2170 @Override 2171 public Set<ModuleReference> findAll() { 2172 return Collections.singleton(mref); 2173 } 2174 }); 2175 2176 // Determine the modules that matches the pattern {@code modulesToHash} 2177 Set<String> roots = finder.findAll().stream() 2178 .map(ref -> ref.descriptor().name()) 2179 .filter(mn -> modulesToHash.matcher(mn).find()) 2180 .collect(Collectors.toSet()); 2181 2182 // use system module path unless it creates a modular JAR for 2183 // a module that is present in the system image e.g. upgradeable 2184 // module 2185 ModuleFinder system; 2186 String name = descriptor.name(); 2187 if (name != null && ModuleFinder.ofSystem().find(name).isPresent()) { 2188 system = ModuleFinder.of(); 2189 } else { 2190 system = ModuleFinder.ofSystem(); 2191 } 2192 // get a resolved module graph 2193 Configuration config = 2194 Configuration.empty().resolve(system, finder, roots); 2195 2196 // filter modules resolved from the system module finder 2197 this.modules = config.modules().stream() 2198 .map(ResolvedModule::name) 2199 .filter(mn -> roots.contains(mn) && !system.find(mn).isPresent()) 2200 .collect(Collectors.toSet()); 2201 2202 this.hashesBuilder = new ModuleHashesBuilder(config, modules); 2203 } 2204 2205 /** 2206 * Compute hashes of the specified module. 2207 * 2208 * It records the hashing modules that depend upon the specified 2209 * module directly or indirectly. 2210 */ 2211 ModuleHashes computeHashes(String name) { 2212 if (hashesBuilder == null) 2213 return null; 2214 2215 return hashesBuilder.computeHashes(Set.of(name)).get(name); 2216 } 2217 } 2218 2219 // sort base entries before versioned entries, and sort entry classes with 2220 // nested classes so that the outter class appears before the associated 2221 // nested class 2222 static Comparator<String> ENTRYNAME_COMPARATOR = (s1, s2) -> { 2223 2224 if (s1.equals(s2)) return 0; 2225 boolean b1 = s1.startsWith(VERSIONS_DIR); 2226 boolean b2 = s2.startsWith(VERSIONS_DIR); 2227 if (b1 && !b2) return 1; 2228 if (!b1 && b2) return -1; 2229 int n = 0; // starting char for String compare 2230 if (b1 && b2) { 2231 // normally strings would be sorted so "10" goes before "9", but 2232 // version number strings need to be sorted numerically 2233 n = VERSIONS_DIR.length(); // skip the common prefix 2234 int i1 = s1.indexOf('/', n); 2235 int i2 = s2.indexOf('/', n); 2236 if (i1 == -1) throw new Validator.InvalidJarException(s1); 2237 if (i2 == -1) throw new Validator.InvalidJarException(s2); 2238 // shorter version numbers go first 2239 if (i1 != i2) return i1 - i2; 2240 // otherwise, handle equal length numbers below 2241 } 2242 int l1 = s1.length(); 2243 int l2 = s2.length(); 2244 int lim = Math.min(l1, l2); 2245 for (int k = n; k < lim; k++) { 2246 char c1 = s1.charAt(k); 2247 char c2 = s2.charAt(k); 2248 if (c1 != c2) { 2249 // change natural ordering so '.' comes before '$' 2250 // i.e. outer classes come before nested classes 2251 if (c1 == '$' && c2 == '.') return 1; 2252 if (c1 == '.' && c2 == '$') return -1; 2253 return c1 - c2; 2254 } 2255 } 2256 return l1 - l2; 2257 }; 2258 2259 static Comparator<ZipEntry> ENTRY_COMPARATOR = 2260 Comparator.comparing(ZipEntry::getName, ENTRYNAME_COMPARATOR); 2261 2262 }