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