1 /* 2 * Copyright (c) 1996, 2012, 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.nio.file.Path; 30 import java.nio.file.Files; 31 import java.util.*; 32 import java.util.zip.*; 33 import java.util.jar.*; 34 import java.util.jar.Manifest; 35 import java.text.MessageFormat; 36 import sun.misc.JarIndex; 37 import static sun.misc.JarIndex.INDEX_NAME; 38 import static java.util.jar.JarFile.MANIFEST_NAME; 39 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; 40 41 /** 42 * This class implements a simple utility for creating files in the JAR 43 * (Java Archive) file format. The JAR format is based on the ZIP file 44 * format, with optional meta-information stored in a MANIFEST entry. 45 */ 46 public 47 class Main { 48 String program; 49 PrintStream out, err; 50 String fname, mname, ename, pname; 51 String zname = ""; 52 String[] files; 53 String rootjar = null; 54 55 // An entryName(path)->File map generated during "expand", it helps to 56 // decide whether or not an existing entry in a jar file needs to be 57 // replaced, during the "update" operation. 58 Map<String, File> entryMap = new HashMap<String, File>(); 59 60 // All files need to be added/updated. 61 Set<File> entries = new LinkedHashSet<File>(); 62 63 // Directories specified by "-C" operation. 64 Set<String> paths = new HashSet<String>(); 65 66 /* 67 * cflag: create 68 * uflag: update 69 * xflag: xtract 70 * tflag: table 71 * vflag: verbose 72 * flag0: no zip compression (store only) 73 * Mflag: DO NOT generate a manifest file (just ZIP) 74 * iflag: generate jar index 75 */ 76 boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag; 77 78 static final String MANIFEST_DIR = "META-INF/"; 79 static final String VERSION = "1.0"; 80 81 // valid values for Profile attribute 82 private static final String[] PROFILES = { "compact1", "compact2", "compact3" }; 83 84 private static ResourceBundle rsrc; 85 86 /** 87 * If true, maintain compatibility with JDK releases prior to 6.0 by 88 * timestamping extracted files with the time at which they are extracted. 89 * Default is to use the time given in the archive. 90 */ 91 private static final boolean useExtractionTime = 92 Boolean.getBoolean("sun.tools.jar.useExtractionTime"); 93 94 /** 95 * Initialize ResourceBundle 96 */ 97 static { 98 try { 99 rsrc = ResourceBundle.getBundle("sun.tools.jar.resources.jar"); 100 } catch (MissingResourceException e) { 101 throw new Error("Fatal: Resource for jar is missing"); 102 } 103 } 104 105 private String getMsg(String key) { 106 try { 107 return (rsrc.getString(key)); 108 } catch (MissingResourceException e) { 109 throw new Error("Error in message file"); 110 } 111 } 112 113 private String formatMsg(String key, String arg) { 114 String msg = getMsg(key); 115 String[] args = new String[1]; 116 args[0] = arg; 117 return MessageFormat.format(msg, (Object[]) args); 118 } 119 120 private String formatMsg2(String key, String arg, String arg1) { 121 String msg = getMsg(key); 122 String[] args = new String[2]; 123 args[0] = arg; 124 args[1] = arg1; 125 return MessageFormat.format(msg, (Object[]) args); 126 } 127 128 public Main(PrintStream out, PrintStream err, String program) { 129 this.out = out; 130 this.err = err; 131 this.program = program; 132 } 133 134 /** 135 * Creates a new empty temporary file in the same directory as the 136 * specified file. A variant of File.createTempFile. 137 */ 138 private static File createTempFileInSameDirectoryAs(File file) 139 throws IOException { 140 File dir = file.getParentFile(); 141 if (dir == null) 142 dir = new File("."); 143 return File.createTempFile("jartmp", null, dir); 144 } 145 146 private boolean ok; 147 148 /** 149 * Starts main program with the specified arguments. 150 */ 151 public synchronized boolean run(String args[]) { 152 ok = true; 153 if (!parseArgs(args)) { 154 return false; 155 } 156 try { 157 if (cflag || uflag) { 158 if (fname != null) { 159 // The name of the zip file as it would appear as its own 160 // zip file entry. We use this to make sure that we don't 161 // add the zip file to itself. 162 zname = fname.replace(File.separatorChar, '/'); 163 if (zname.startsWith("./")) { 164 zname = zname.substring(2); 165 } 166 } 167 } 168 if (cflag) { 169 Manifest manifest = null; 170 InputStream in = null; 171 172 if (!Mflag) { 173 if (mname != null) { 174 in = new FileInputStream(mname); 175 manifest = new Manifest(new BufferedInputStream(in)); 176 } else { 177 manifest = new Manifest(); 178 } 179 addVersion(manifest); 180 addCreatedBy(manifest); 181 if (isAmbiguousMainClass(manifest)) { 182 if (in != null) { 183 in.close(); 184 } 185 return false; 186 } 187 if (ename != null) { 188 addMainClass(manifest, ename); 189 } 190 if (pname != null) { 191 if (!addProfileName(manifest, pname)) { 192 if (in != null) { 193 in.close(); 194 } 195 return false; 196 } 197 } 198 } 199 OutputStream out; 200 if (fname != null) { 201 out = new FileOutputStream(fname); 202 } else { 203 out = new FileOutputStream(FileDescriptor.out); 204 if (vflag) { 205 // Disable verbose output so that it does not appear 206 // on stdout along with file data 207 // error("Warning: -v option ignored"); 208 vflag = false; 209 } 210 } 211 expand(null, files, false); 212 create(new BufferedOutputStream(out, 4096), manifest); 213 if (in != null) { 214 in.close(); 215 } 216 out.close(); 217 } else if (uflag) { 218 File inputFile = null, tmpFile = null; 219 FileInputStream in; 220 FileOutputStream out; 221 if (fname != null) { 222 inputFile = new File(fname); 223 tmpFile = createTempFileInSameDirectoryAs(inputFile); 224 in = new FileInputStream(inputFile); 225 out = new FileOutputStream(tmpFile); 226 } else { 227 in = new FileInputStream(FileDescriptor.in); 228 out = new FileOutputStream(FileDescriptor.out); 229 vflag = false; 230 } 231 InputStream manifest = (!Mflag && (mname != null)) ? 232 (new FileInputStream(mname)) : null; 233 expand(null, files, true); 234 boolean updateOk = update(in, new BufferedOutputStream(out), 235 manifest, null); 236 if (ok) { 237 ok = updateOk; 238 } 239 in.close(); 240 out.close(); 241 if (manifest != null) { 242 manifest.close(); 243 } 244 if (ok && fname != null) { 245 // on Win32, we need this delete 246 inputFile.delete(); 247 if (!tmpFile.renameTo(inputFile)) { 248 tmpFile.delete(); 249 throw new IOException(getMsg("error.write.file")); 250 } 251 tmpFile.delete(); 252 } 253 } else if (tflag) { 254 replaceFSC(files); 255 if (fname != null) { 256 list(fname, files); 257 } else { 258 InputStream in = new FileInputStream(FileDescriptor.in); 259 try{ 260 list(new BufferedInputStream(in), files); 261 } finally { 262 in.close(); 263 } 264 } 265 } else if (xflag) { 266 replaceFSC(files); 267 if (fname != null && files != null) { 268 extract(fname, files); 269 } else { 270 InputStream in = (fname == null) 271 ? new FileInputStream(FileDescriptor.in) 272 : new FileInputStream(fname); 273 try { 274 extract(new BufferedInputStream(in), files); 275 } finally { 276 in.close(); 277 } 278 } 279 } else if (iflag) { 280 genIndex(rootjar, files); 281 } 282 } catch (IOException e) { 283 fatalError(e); 284 ok = false; 285 } catch (Error ee) { 286 ee.printStackTrace(); 287 ok = false; 288 } catch (Throwable t) { 289 t.printStackTrace(); 290 ok = false; 291 } 292 out.flush(); 293 err.flush(); 294 return ok; 295 } 296 297 /** 298 * Parses command line arguments. 299 */ 300 boolean parseArgs(String args[]) { 301 /* Preprocess and expand @file arguments */ 302 try { 303 args = CommandLine.parse(args); 304 } catch (FileNotFoundException e) { 305 fatalError(formatMsg("error.cant.open", e.getMessage())); 306 return false; 307 } catch (IOException e) { 308 fatalError(e); 309 return false; 310 } 311 /* parse flags */ 312 int count = 1; 313 try { 314 String flags = args[0]; 315 if (flags.startsWith("-")) { 316 flags = flags.substring(1); 317 } 318 for (int i = 0; i < flags.length(); i++) { 319 switch (flags.charAt(i)) { 320 case 'c': 321 if (xflag || tflag || uflag || iflag) { 322 usageError(); 323 return false; 324 } 325 cflag = true; 326 break; 327 case 'u': 328 if (cflag || xflag || tflag || iflag) { 329 usageError(); 330 return false; 331 } 332 uflag = true; 333 break; 334 case 'x': 335 if (cflag || uflag || tflag || iflag) { 336 usageError(); 337 return false; 338 } 339 xflag = true; 340 break; 341 case 't': 342 if (cflag || uflag || xflag || iflag) { 343 usageError(); 344 return false; 345 } 346 tflag = true; 347 break; 348 case 'M': 349 Mflag = true; 350 break; 351 case 'v': 352 vflag = true; 353 break; 354 case 'f': 355 fname = args[count++]; 356 break; 357 case 'm': 358 mname = args[count++]; 359 break; 360 case '0': 361 flag0 = true; 362 break; 363 case 'i': 364 if (cflag || uflag || xflag || tflag) { 365 usageError(); 366 return false; 367 } 368 // do not increase the counter, files will contain rootjar 369 rootjar = args[count++]; 370 iflag = true; 371 break; 372 case 'e': 373 ename = args[count++]; 374 break; 375 case 'p': 376 pname = args[count++]; 377 break; 378 default: 379 error(formatMsg("error.illegal.option", 380 String.valueOf(flags.charAt(i)))); 381 usageError(); 382 return false; 383 } 384 } 385 } catch (ArrayIndexOutOfBoundsException e) { 386 usageError(); 387 return false; 388 } 389 if (!cflag && !tflag && !xflag && !uflag && !iflag) { 390 error(getMsg("error.bad.option")); 391 usageError(); 392 return false; 393 } 394 /* parse file arguments */ 395 int n = args.length - count; 396 if (n > 0) { 397 int k = 0; 398 String[] nameBuf = new String[n]; 399 try { 400 for (int i = count; i < args.length; i++) { 401 if (args[i].equals("-C")) { 402 /* change the directory */ 403 String dir = args[++i]; 404 dir = (dir.endsWith(File.separator) ? 405 dir : (dir + File.separator)); 406 dir = dir.replace(File.separatorChar, '/'); 407 while (dir.indexOf("//") > -1) { 408 dir = dir.replace("//", "/"); 409 } 410 paths.add(dir.replace(File.separatorChar, '/')); 411 nameBuf[k++] = dir + args[++i]; 412 } else { 413 nameBuf[k++] = args[i]; 414 } 415 } 416 } catch (ArrayIndexOutOfBoundsException e) { 417 usageError(); 418 return false; 419 } 420 files = new String[k]; 421 System.arraycopy(nameBuf, 0, files, 0, k); 422 } else if (cflag && (mname == null)) { 423 error(getMsg("error.bad.cflag")); 424 usageError(); 425 return false; 426 } else if (uflag) { 427 if ((mname != null) || (ename != null) || (pname != null)) { 428 /* just want to update the manifest */ 429 return true; 430 } else { 431 error(getMsg("error.bad.uflag")); 432 usageError(); 433 return false; 434 } 435 } 436 return true; 437 } 438 439 /** 440 * Expands list of files to process into full list of all files that 441 * can be found by recursively descending directories. 442 */ 443 void expand(File dir, String[] files, boolean isUpdate) { 444 if (files == null) { 445 return; 446 } 447 for (int i = 0; i < files.length; i++) { 448 File f; 449 if (dir == null) { 450 f = new File(files[i]); 451 } else { 452 f = new File(dir, files[i]); 453 } 454 if (f.isFile()) { 455 if (entries.add(f)) { 456 if (isUpdate) 457 entryMap.put(entryName(f.getPath()), f); 458 } 459 } else if (f.isDirectory()) { 460 if (entries.add(f)) { 461 if (isUpdate) { 462 String dirPath = f.getPath(); 463 dirPath = (dirPath.endsWith(File.separator)) ? dirPath : 464 (dirPath + File.separator); 465 entryMap.put(entryName(dirPath), f); 466 } 467 expand(f, f.list(), isUpdate); 468 } 469 } else { 470 error(formatMsg("error.nosuch.fileordir", String.valueOf(f))); 471 ok = false; 472 } 473 } 474 } 475 476 /** 477 * Creates a new JAR file. 478 */ 479 void create(OutputStream out, Manifest manifest) 480 throws IOException 481 { 482 ZipOutputStream zos = new JarOutputStream(out); 483 if (flag0) { 484 zos.setMethod(ZipOutputStream.STORED); 485 } 486 if (manifest != null) { 487 if (vflag) { 488 output(getMsg("out.added.manifest")); 489 } 490 ZipEntry e = new ZipEntry(MANIFEST_DIR); 491 e.setTime(System.currentTimeMillis()); 492 e.setSize(0); 493 e.setCrc(0); 494 zos.putNextEntry(e); 495 e = new ZipEntry(MANIFEST_NAME); 496 e.setTime(System.currentTimeMillis()); 497 if (flag0) { 498 crc32Manifest(e, manifest); 499 } 500 zos.putNextEntry(e); 501 manifest.write(zos); 502 zos.closeEntry(); 503 } 504 for (File file: entries) { 505 addFile(zos, file); 506 } 507 zos.close(); 508 } 509 510 private char toUpperCaseASCII(char c) { 511 return (c < 'a' || c > 'z') ? c : (char) (c + 'A' - 'a'); 512 } 513 514 /** 515 * Compares two strings for equality, ignoring case. The second 516 * argument must contain only upper-case ASCII characters. 517 * We don't want case comparison to be locale-dependent (else we 518 * have the notorious "turkish i bug"). 519 */ 520 private boolean equalsIgnoreCase(String s, String upper) { 521 assert upper.toUpperCase(java.util.Locale.ENGLISH).equals(upper); 522 int len; 523 if ((len = s.length()) != upper.length()) 524 return false; 525 for (int i = 0; i < len; i++) { 526 char c1 = s.charAt(i); 527 char c2 = upper.charAt(i); 528 if (c1 != c2 && toUpperCaseASCII(c1) != c2) 529 return false; 530 } 531 return true; 532 } 533 534 /** 535 * Updates an existing jar file. 536 */ 537 boolean update(InputStream in, OutputStream out, 538 InputStream newManifest, 539 JarIndex jarIndex) throws IOException 540 { 541 ZipInputStream zis = new ZipInputStream(in); 542 ZipOutputStream zos = new JarOutputStream(out); 543 ZipEntry e = null; 544 boolean foundManifest = false; 545 boolean updateOk = true; 546 547 if (jarIndex != null) { 548 addIndex(jarIndex, zos); 549 } 550 551 // put the old entries first, replace if necessary 552 while ((e = zis.getNextEntry()) != null) { 553 String name = e.getName(); 554 555 boolean isManifestEntry = equalsIgnoreCase(name, MANIFEST_NAME); 556 557 if ((jarIndex != null && equalsIgnoreCase(name, INDEX_NAME)) 558 || (Mflag && isManifestEntry)) { 559 continue; 560 } else if (isManifestEntry && ((newManifest != null) || 561 (ename != null) || (pname != null))) { 562 foundManifest = true; 563 if (newManifest != null) { 564 // Don't read from the newManifest InputStream, as we 565 // might need it below, and we can't re-read the same data 566 // twice. 567 FileInputStream fis = new FileInputStream(mname); 568 boolean ambiguous = isAmbiguousMainClass(new Manifest(fis)); 569 fis.close(); 570 if (ambiguous) { 571 return false; 572 } 573 } 574 575 // Update the manifest. 576 Manifest old = new Manifest(zis); 577 if (newManifest != null) { 578 old.read(newManifest); 579 } 580 if (!updateManifest(old, zos)) { 581 return false; 582 } 583 } else { 584 if (!entryMap.containsKey(name)) { // copy the old stuff 585 // do our own compression 586 ZipEntry e2 = new ZipEntry(name); 587 e2.setMethod(e.getMethod()); 588 e2.setTime(e.getTime()); 589 e2.setComment(e.getComment()); 590 e2.setExtra(e.getExtra()); 591 if (e.getMethod() == ZipEntry.STORED) { 592 e2.setSize(e.getSize()); 593 e2.setCrc(e.getCrc()); 594 } 595 zos.putNextEntry(e2); 596 copy(zis, zos); 597 } else { // replace with the new files 598 File f = entryMap.get(name); 599 addFile(zos, f); 600 entryMap.remove(name); 601 entries.remove(f); 602 } 603 } 604 } 605 606 // add the remaining new files 607 for (File f: entries) { 608 addFile(zos, f); 609 } 610 if (!foundManifest) { 611 if (newManifest != null) { 612 Manifest m = new Manifest(newManifest); 613 updateOk = !isAmbiguousMainClass(m); 614 if (updateOk) { 615 if (!updateManifest(m, zos)) { 616 updateOk = false; 617 } 618 } 619 } else if (ename != null || pname != null) { 620 if (!updateManifest(new Manifest(), zos)) { 621 updateOk = false; 622 } 623 } 624 } 625 zis.close(); 626 zos.close(); 627 return updateOk; 628 } 629 630 631 private void addIndex(JarIndex index, ZipOutputStream zos) 632 throws IOException 633 { 634 ZipEntry e = new ZipEntry(INDEX_NAME); 635 e.setTime(System.currentTimeMillis()); 636 if (flag0) { 637 CRC32OutputStream os = new CRC32OutputStream(); 638 index.write(os); 639 os.updateEntry(e); 640 } 641 zos.putNextEntry(e); 642 index.write(zos); 643 zos.closeEntry(); 644 } 645 646 private boolean updateManifest(Manifest m, ZipOutputStream zos) 647 throws IOException 648 { 649 addVersion(m); 650 addCreatedBy(m); 651 if (ename != null) { 652 addMainClass(m, ename); 653 } 654 if (pname != null) { 655 if (!addProfileName(m, pname)) { 656 return false; 657 } 658 } 659 ZipEntry e = new ZipEntry(MANIFEST_NAME); 660 e.setTime(System.currentTimeMillis()); 661 if (flag0) { 662 crc32Manifest(e, m); 663 } 664 zos.putNextEntry(e); 665 m.write(zos); 666 if (vflag) { 667 output(getMsg("out.update.manifest")); 668 } 669 return true; 670 } 671 672 673 private String entryName(String name) { 674 name = name.replace(File.separatorChar, '/'); 675 String matchPath = ""; 676 for (String path : paths) { 677 if (name.startsWith(path) 678 && (path.length() > matchPath.length())) { 679 matchPath = path; 680 } 681 } 682 name = name.substring(matchPath.length()); 683 684 if (name.startsWith("/")) { 685 name = name.substring(1); 686 } else if (name.startsWith("./")) { 687 name = name.substring(2); 688 } 689 return name; 690 } 691 692 private void addVersion(Manifest m) { 693 Attributes global = m.getMainAttributes(); 694 if (global.getValue(Attributes.Name.MANIFEST_VERSION) == null) { 695 global.put(Attributes.Name.MANIFEST_VERSION, VERSION); 696 } 697 } 698 699 private void addCreatedBy(Manifest m) { 700 Attributes global = m.getMainAttributes(); 701 if (global.getValue(new Attributes.Name("Created-By")) == null) { 702 String javaVendor = System.getProperty("java.vendor"); 703 String jdkVersion = System.getProperty("java.version"); 704 global.put(new Attributes.Name("Created-By"), jdkVersion + " (" + 705 javaVendor + ")"); 706 } 707 } 708 709 private void addMainClass(Manifest m, String mainApp) { 710 Attributes global = m.getMainAttributes(); 711 712 // overrides any existing Main-Class attribute 713 global.put(Attributes.Name.MAIN_CLASS, mainApp); 714 } 715 716 private boolean addProfileName(Manifest m, String profile) { 717 // check profile name 718 boolean found = false; 719 int i = 0; 720 while (i < PROFILES.length) { 721 if (profile.equals(PROFILES[i])) { 722 found = true; 723 break; 724 } 725 i++; 726 } 727 if (!found) { 728 error(formatMsg("error.bad.pvalue", profile)); 729 return false; 730 } 731 732 // overrides any existing Profile attribute 733 Attributes global = m.getMainAttributes(); 734 global.put(Attributes.Name.PROFILE, profile); 735 return true; 736 } 737 738 private boolean isAmbiguousMainClass(Manifest m) { 739 if (ename != null) { 740 Attributes global = m.getMainAttributes(); 741 if ((global.get(Attributes.Name.MAIN_CLASS) != null)) { 742 error(getMsg("error.bad.eflag")); 743 usageError(); 744 return true; 745 } 746 } 747 return false; 748 } 749 750 /** 751 * Adds a new file entry to the ZIP output stream. 752 */ 753 void addFile(ZipOutputStream zos, File file) throws IOException { 754 String name = file.getPath(); 755 boolean isDir = file.isDirectory(); 756 if (isDir) { 757 name = name.endsWith(File.separator) ? name : 758 (name + File.separator); 759 } 760 name = entryName(name); 761 762 if (name.equals("") || name.equals(".") || name.equals(zname)) { 763 return; 764 } else if ((name.equals(MANIFEST_DIR) || name.equals(MANIFEST_NAME)) 765 && !Mflag) { 766 if (vflag) { 767 output(formatMsg("out.ignore.entry", name)); 768 } 769 return; 770 } 771 772 long size = isDir ? 0 : file.length(); 773 774 if (vflag) { 775 out.print(formatMsg("out.adding", name)); 776 } 777 ZipEntry e = new ZipEntry(name); 778 e.setTime(file.lastModified()); 779 if (size == 0) { 780 e.setMethod(ZipEntry.STORED); 781 e.setSize(0); 782 e.setCrc(0); 783 } else if (flag0) { 784 crc32File(e, file); 785 } 786 zos.putNextEntry(e); 787 if (!isDir) { 788 copy(file, zos); 789 } 790 zos.closeEntry(); 791 /* report how much compression occurred. */ 792 if (vflag) { 793 size = e.getSize(); 794 long csize = e.getCompressedSize(); 795 out.print(formatMsg2("out.size", String.valueOf(size), 796 String.valueOf(csize))); 797 if (e.getMethod() == ZipEntry.DEFLATED) { 798 long ratio = 0; 799 if (size != 0) { 800 ratio = ((size - csize) * 100) / size; 801 } 802 output(formatMsg("out.deflated", String.valueOf(ratio))); 803 } else { 804 output(getMsg("out.stored")); 805 } 806 } 807 } 808 809 /** 810 * A buffer for use only by copy(InputStream, OutputStream). 811 * Not as clean as allocating a new buffer as needed by copy, 812 * but significantly more efficient. 813 */ 814 private byte[] copyBuf = new byte[8192]; 815 816 /** 817 * Copies all bytes from the input stream to the output stream. 818 * Does not close or flush either stream. 819 * 820 * @param from the input stream to read from 821 * @param to the output stream to write to 822 * @throws IOException if an I/O error occurs 823 */ 824 private void copy(InputStream from, OutputStream to) throws IOException { 825 int n; 826 while ((n = from.read(copyBuf)) != -1) 827 to.write(copyBuf, 0, n); 828 } 829 830 /** 831 * Copies all bytes from the input file to the output stream. 832 * Does not close or flush the output stream. 833 * 834 * @param from the input file to read from 835 * @param to the output stream to write to 836 * @throws IOException if an I/O error occurs 837 */ 838 private void copy(File from, OutputStream to) throws IOException { 839 InputStream in = new FileInputStream(from); 840 try { 841 copy(in, to); 842 } finally { 843 in.close(); 844 } 845 } 846 847 /** 848 * Copies all bytes from the input stream to the output file. 849 * Does not close the input stream. 850 * 851 * @param from the input stream to read from 852 * @param to the output file to write to 853 * @throws IOException if an I/O error occurs 854 */ 855 private void copy(InputStream from, File to) throws IOException { 856 OutputStream out = new FileOutputStream(to); 857 try { 858 copy(from, out); 859 } finally { 860 out.close(); 861 } 862 } 863 864 /** 865 * Computes the crc32 of a Manifest. This is necessary when the 866 * ZipOutputStream is in STORED mode. 867 */ 868 private void crc32Manifest(ZipEntry e, Manifest m) throws IOException { 869 CRC32OutputStream os = new CRC32OutputStream(); 870 m.write(os); 871 os.updateEntry(e); 872 } 873 874 /** 875 * Computes the crc32 of a File. This is necessary when the 876 * ZipOutputStream is in STORED mode. 877 */ 878 private void crc32File(ZipEntry e, File f) throws IOException { 879 CRC32OutputStream os = new CRC32OutputStream(); 880 copy(f, os); 881 if (os.n != f.length()) { 882 throw new JarException(formatMsg( 883 "error.incorrect.length", f.getPath())); 884 } 885 os.updateEntry(e); 886 } 887 888 void replaceFSC(String files[]) { 889 if (files != null) { 890 for (int i = 0; i < files.length; i++) { 891 files[i] = files[i].replace(File.separatorChar, '/'); 892 } 893 } 894 } 895 896 @SuppressWarnings("serial") 897 Set<ZipEntry> newDirSet() { 898 return new HashSet<ZipEntry>() { 899 public boolean add(ZipEntry e) { 900 return ((e == null || useExtractionTime) ? false : super.add(e)); 901 }}; 902 } 903 904 void updateLastModifiedTime(Set<ZipEntry> zes) throws IOException { 905 for (ZipEntry ze : zes) { 906 long lastModified = ze.getTime(); 907 if (lastModified != -1) { 908 File f = new File(ze.getName().replace('/', File.separatorChar)); 909 f.setLastModified(lastModified); 910 } 911 } 912 } 913 914 /** 915 * Extracts specified entries from JAR file. 916 */ 917 void extract(InputStream in, String files[]) throws IOException { 918 ZipInputStream zis = new ZipInputStream(in); 919 ZipEntry e; 920 // Set of all directory entries specified in archive. Disallows 921 // null entries. Disallows all entries if using pre-6.0 behavior. 922 Set<ZipEntry> dirs = newDirSet(); 923 while ((e = zis.getNextEntry()) != null) { 924 if (files == null) { 925 dirs.add(extractFile(zis, e)); 926 } else { 927 String name = e.getName(); 928 for (String file : files) { 929 if (name.startsWith(file)) { 930 dirs.add(extractFile(zis, e)); 931 break; 932 } 933 } 934 } 935 } 936 937 // Update timestamps of directories specified in archive with their 938 // timestamps as given in the archive. We do this after extraction, 939 // instead of during, because creating a file in a directory changes 940 // that directory's timestamp. 941 updateLastModifiedTime(dirs); 942 } 943 944 /** 945 * Extracts specified entries from JAR file, via ZipFile. 946 */ 947 void extract(String fname, String files[]) throws IOException { 948 ZipFile zf = new ZipFile(fname); 949 Set<ZipEntry> dirs = newDirSet(); 950 Enumeration<? extends ZipEntry> zes = zf.entries(); 951 while (zes.hasMoreElements()) { 952 ZipEntry e = zes.nextElement(); 953 InputStream is; 954 if (files == null) { 955 dirs.add(extractFile(zf.getInputStream(e), e)); 956 } else { 957 String name = e.getName(); 958 for (String file : files) { 959 if (name.startsWith(file)) { 960 dirs.add(extractFile(zf.getInputStream(e), e)); 961 break; 962 } 963 } 964 } 965 } 966 zf.close(); 967 updateLastModifiedTime(dirs); 968 } 969 970 /** 971 * Extracts next entry from JAR file, creating directories as needed. If 972 * the entry is for a directory which doesn't exist prior to this 973 * invocation, returns that entry, otherwise returns null. 974 */ 975 ZipEntry extractFile(InputStream is, ZipEntry e) throws IOException { 976 ZipEntry rc = null; 977 String name = e.getName(); 978 File f = new File(e.getName().replace('/', File.separatorChar)); 979 if (e.isDirectory()) { 980 if (f.exists()) { 981 if (!f.isDirectory()) { 982 throw new IOException(formatMsg("error.create.dir", 983 f.getPath())); 984 } 985 } else { 986 if (!f.mkdirs()) { 987 throw new IOException(formatMsg("error.create.dir", 988 f.getPath())); 989 } else { 990 rc = e; 991 } 992 } 993 994 if (vflag) { 995 output(formatMsg("out.create", name)); 996 } 997 } else { 998 if (f.getParent() != null) { 999 File d = new File(f.getParent()); 1000 if (!d.exists() && !d.mkdirs() || !d.isDirectory()) { 1001 throw new IOException(formatMsg( 1002 "error.create.dir", d.getPath())); 1003 } 1004 } 1005 try { 1006 copy(is, f); 1007 } finally { 1008 if (is instanceof ZipInputStream) 1009 ((ZipInputStream)is).closeEntry(); 1010 else 1011 is.close(); 1012 } 1013 if (vflag) { 1014 if (e.getMethod() == ZipEntry.DEFLATED) { 1015 output(formatMsg("out.inflated", name)); 1016 } else { 1017 output(formatMsg("out.extracted", name)); 1018 } 1019 } 1020 } 1021 if (!useExtractionTime) { 1022 long lastModified = e.getTime(); 1023 if (lastModified != -1) { 1024 f.setLastModified(lastModified); 1025 } 1026 } 1027 return rc; 1028 } 1029 1030 /** 1031 * Lists contents of JAR file. 1032 */ 1033 void list(InputStream in, String files[]) throws IOException { 1034 ZipInputStream zis = new ZipInputStream(in); 1035 ZipEntry e; 1036 while ((e = zis.getNextEntry()) != null) { 1037 /* 1038 * In the case of a compressed (deflated) entry, the entry size 1039 * is stored immediately following the entry data and cannot be 1040 * determined until the entry is fully read. Therefore, we close 1041 * the entry first before printing out its attributes. 1042 */ 1043 zis.closeEntry(); 1044 printEntry(e, files); 1045 } 1046 } 1047 1048 /** 1049 * Lists contents of JAR file, via ZipFile. 1050 */ 1051 void list(String fname, String files[]) throws IOException { 1052 ZipFile zf = new ZipFile(fname); 1053 Enumeration<? extends ZipEntry> zes = zf.entries(); 1054 while (zes.hasMoreElements()) { 1055 printEntry(zes.nextElement(), files); 1056 } 1057 zf.close(); 1058 } 1059 1060 /** 1061 * Outputs the class index table to the INDEX.LIST file of the 1062 * root jar file. 1063 */ 1064 void dumpIndex(String rootjar, JarIndex index) throws IOException { 1065 File jarFile = new File(rootjar); 1066 Path jarPath = jarFile.toPath(); 1067 Path tmpPath = createTempFileInSameDirectoryAs(jarFile).toPath(); 1068 try { 1069 if (update(Files.newInputStream(jarPath), 1070 Files.newOutputStream(tmpPath), 1071 null, index)) { 1072 try { 1073 Files.move(tmpPath, jarPath, REPLACE_EXISTING); 1074 } catch (IOException e) { 1075 throw new IOException(getMsg("error.write.file"), e); 1076 } 1077 } 1078 } finally { 1079 Files.deleteIfExists(tmpPath); 1080 } 1081 } 1082 1083 private HashSet<String> jarPaths = new HashSet<String>(); 1084 1085 /** 1086 * Generates the transitive closure of the Class-Path attribute for 1087 * the specified jar file. 1088 */ 1089 List<String> getJarPath(String jar) throws IOException { 1090 List<String> files = new ArrayList<String>(); 1091 files.add(jar); 1092 jarPaths.add(jar); 1093 1094 // take out the current path 1095 String path = jar.substring(0, Math.max(0, jar.lastIndexOf('/') + 1)); 1096 1097 // class path attribute will give us jar file name with 1098 // '/' as separators, so we need to change them to the 1099 // appropriate one before we open the jar file. 1100 JarFile rf = new JarFile(jar.replace('/', File.separatorChar)); 1101 1102 if (rf != null) { 1103 Manifest man = rf.getManifest(); 1104 if (man != null) { 1105 Attributes attr = man.getMainAttributes(); 1106 if (attr != null) { 1107 String value = attr.getValue(Attributes.Name.CLASS_PATH); 1108 if (value != null) { 1109 StringTokenizer st = new StringTokenizer(value); 1110 while (st.hasMoreTokens()) { 1111 String ajar = st.nextToken(); 1112 if (!ajar.endsWith("/")) { // it is a jar file 1113 ajar = path.concat(ajar); 1114 /* check on cyclic dependency */ 1115 if (! jarPaths.contains(ajar)) { 1116 files.addAll(getJarPath(ajar)); 1117 } 1118 } 1119 } 1120 } 1121 } 1122 } 1123 } 1124 rf.close(); 1125 return files; 1126 } 1127 1128 /** 1129 * Generates class index file for the specified root jar file. 1130 */ 1131 void genIndex(String rootjar, String[] files) throws IOException { 1132 List<String> jars = getJarPath(rootjar); 1133 int njars = jars.size(); 1134 String[] jarfiles; 1135 1136 if (njars == 1 && files != null) { 1137 // no class-path attribute defined in rootjar, will 1138 // use command line specified list of jars 1139 for (int i = 0; i < files.length; i++) { 1140 jars.addAll(getJarPath(files[i])); 1141 } 1142 njars = jars.size(); 1143 } 1144 jarfiles = jars.toArray(new String[njars]); 1145 JarIndex index = new JarIndex(jarfiles); 1146 dumpIndex(rootjar, index); 1147 } 1148 1149 /** 1150 * Prints entry information, if requested. 1151 */ 1152 void printEntry(ZipEntry e, String[] files) throws IOException { 1153 if (files == null) { 1154 printEntry(e); 1155 } else { 1156 String name = e.getName(); 1157 for (String file : files) { 1158 if (name.startsWith(file)) { 1159 printEntry(e); 1160 return; 1161 } 1162 } 1163 } 1164 } 1165 1166 /** 1167 * Prints entry information. 1168 */ 1169 void printEntry(ZipEntry e) throws IOException { 1170 if (vflag) { 1171 StringBuilder sb = new StringBuilder(); 1172 String s = Long.toString(e.getSize()); 1173 for (int i = 6 - s.length(); i > 0; --i) { 1174 sb.append(' '); 1175 } 1176 sb.append(s).append(' ').append(new Date(e.getTime()).toString()); 1177 sb.append(' ').append(e.getName()); 1178 output(sb.toString()); 1179 } else { 1180 output(e.getName()); 1181 } 1182 } 1183 1184 /** 1185 * Prints usage message. 1186 */ 1187 void usageError() { 1188 error(getMsg("usage")); 1189 } 1190 1191 /** 1192 * A fatal exception has been caught. No recovery possible 1193 */ 1194 void fatalError(Exception e) { 1195 e.printStackTrace(); 1196 } 1197 1198 /** 1199 * A fatal condition has been detected; message is "s". 1200 * No recovery possible 1201 */ 1202 void fatalError(String s) { 1203 error(program + ": " + s); 1204 } 1205 1206 /** 1207 * Print an output message; like verbose output and the like 1208 */ 1209 protected void output(String s) { 1210 out.println(s); 1211 } 1212 1213 /** 1214 * Print an error mesage; like something is broken 1215 */ 1216 protected void error(String s) { 1217 err.println(s); 1218 } 1219 1220 /** 1221 * Main routine to start program. 1222 */ 1223 public static void main(String args[]) { 1224 Main jartool = new Main(System.out, System.err, "jar"); 1225 System.exit(jartool.run(args) ? 0 : 1); 1226 } 1227 1228 /** 1229 * An OutputStream that doesn't send its output anywhere, (but could). 1230 * It's here to find the CRC32 of an input file, necessary for STORED 1231 * mode in ZIP. 1232 */ 1233 private static class CRC32OutputStream extends java.io.OutputStream { 1234 final CRC32 crc = new CRC32(); 1235 long n = 0; 1236 1237 CRC32OutputStream() {} 1238 1239 public void write(int r) throws IOException { 1240 crc.update(r); 1241 n++; 1242 } 1243 1244 public void write(byte[] b, int off, int len) throws IOException { 1245 crc.update(b, off, len); 1246 n += len; 1247 } 1248 1249 /** 1250 * Updates a ZipEntry which describes the data read by this 1251 * output stream, in STORED mode. 1252 */ 1253 public void updateEntry(ZipEntry e) { 1254 e.setMethod(ZipEntry.STORED); 1255 e.setSize(n); 1256 e.setCrc(crc.getValue()); 1257 } 1258 } 1259 }