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