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