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