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