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