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