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