1 /* 2 * Copyright (c) 2003, 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 com.sun.java.util.jar.pack; 27 28 import com.sun.java.util.jar.pack.Attribute.Layout; 29 import java.beans.PropertyChangeListener; 30 import java.io.BufferedInputStream; 31 import java.io.ByteArrayInputStream; 32 import java.io.ByteArrayOutputStream; 33 import java.io.File; 34 import java.io.FileInputStream; 35 import java.io.IOException; 36 import java.io.InputStream; 37 import java.io.OutputStream; 38 import java.util.ArrayList; 39 import java.util.Collections; 40 import java.util.HashMap; 41 import java.util.List; 42 import java.util.ListIterator; 43 import java.util.Map; 44 import java.util.SortedMap; 45 import java.util.TimeZone; 46 import java.util.jar.JarEntry; 47 import java.util.jar.JarFile; 48 import java.util.jar.JarInputStream; 49 import java.util.jar.Pack200; 50 51 52 /* 53 * Implementation of the Pack provider. 54 * </pre></blockquote> 55 * @author John Rose 56 * @author Kumar Srinivasan 57 */ 58 59 60 public class PackerImpl extends TLGlobals implements Pack200.Packer { 61 62 /** 63 * Constructs a Packer object and sets the initial state of 64 * the packer engines. 65 */ 66 public PackerImpl() {} 67 68 /** 69 * Get the set of options for the pack and unpack engines. 70 * @return A sorted association of option key strings to option values. 71 */ 72 @SuppressWarnings("unchecked") 73 public SortedMap properties() { 74 return props; 75 } 76 77 //Driver routines 78 79 /** 80 * Takes a JarFile and converts into a pack-stream. 81 * <p> 82 * Closes its input but not its output. (Pack200 archives are appendable.) 83 * @param in a JarFile 84 * @param out an OutputStream 85 * @exception IOException if an error is encountered. 86 */ 87 public void pack(JarFile in, OutputStream out) throws IOException { 88 assert(Utils.currentInstance.get() == null); 89 TimeZone tz = (props.getBoolean(Utils.PACK_DEFAULT_TIMEZONE)) 90 ? null 91 : TimeZone.getDefault(); 92 try { 93 Utils.currentInstance.set(this); 94 if (tz != null) TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 95 96 if ("0".equals(props.getProperty(Pack200.Packer.EFFORT))) { 97 Utils.copyJarFile(in, out); 98 } else { 99 (new DoPack()).run(in, out); 100 } 101 } finally { 102 Utils.currentInstance.set(null); 103 if (tz != null) TimeZone.setDefault(tz); 104 in.close(); 105 } 106 } 107 108 /** 109 * Takes a JarInputStream and converts into a pack-stream. 110 * <p> 111 * Closes its input but not its output. (Pack200 archives are appendable.) 112 * <p> 113 * The modification time and deflation hint attributes are not available, 114 * for the jar-manifest file and the directory containing the file. 115 * 116 * @see #MODIFICATION_TIME 117 * @see #DEFLATION_HINT 118 * @param in a JarInputStream 119 * @param out an OutputStream 120 * @exception IOException if an error is encountered. 121 */ 122 public void pack(JarInputStream in, OutputStream out) throws IOException { 123 assert(Utils.currentInstance.get() == null); 124 TimeZone tz = (props.getBoolean(Utils.PACK_DEFAULT_TIMEZONE)) ? null : 125 TimeZone.getDefault(); 126 try { 127 Utils.currentInstance.set(this); 128 if (tz != null) TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 129 if ("0".equals(props.getProperty(Pack200.Packer.EFFORT))) { 130 Utils.copyJarFile(in, out); 131 } else { 132 (new DoPack()).run(in, out); 133 } 134 } finally { 135 Utils.currentInstance.set(null); 136 if (tz != null) TimeZone.setDefault(tz); 137 in.close(); 138 } 139 } 140 /** 141 * Register a listener for changes to options. 142 * @param listener An object to be invoked when a property is changed. 143 */ 144 public void addPropertyChangeListener(PropertyChangeListener listener) { 145 props.addListener(listener); 146 } 147 148 /** 149 * Remove a listener for the PropertyChange event. 150 * @param listener The PropertyChange listener to be removed. 151 */ 152 public void removePropertyChangeListener(PropertyChangeListener listener) { 153 props.removeListener(listener); 154 } 155 156 157 158 // All the worker bees..... 159 // The packer worker. 160 @SuppressWarnings("unchecked") 161 private class DoPack { 162 final int verbose = props.getInteger(Utils.DEBUG_VERBOSE); 163 164 { 165 props.setInteger(Pack200.Packer.PROGRESS, 0); 166 if (verbose > 0) Utils.log.info(props.toString()); 167 } 168 169 // Here's where the bits are collected before getting packed: 170 final Package pkg = new Package(); 171 172 final String unknownAttrCommand; 173 { 174 String uaMode = props.getProperty(Pack200.Packer.UNKNOWN_ATTRIBUTE, Pack200.Packer.PASS); 175 if (!(Pack200.Packer.STRIP.equals(uaMode) || 176 Pack200.Packer.PASS.equals(uaMode) || 177 Pack200.Packer.ERROR.equals(uaMode))) { 178 throw new RuntimeException("Bad option: " + Pack200.Packer.UNKNOWN_ATTRIBUTE + " = " + uaMode); 179 } 180 unknownAttrCommand = uaMode.intern(); 181 } 182 183 final Map<Attribute.Layout, Attribute> attrDefs; 184 final Map<Attribute.Layout, String> attrCommands; 185 { 186 Map<Attribute.Layout, Attribute> lattrDefs = new HashMap<>(); 187 Map<Attribute.Layout, String> lattrCommands = new HashMap<>(); 188 String[] keys = { 189 Pack200.Packer.CLASS_ATTRIBUTE_PFX, 190 Pack200.Packer.FIELD_ATTRIBUTE_PFX, 191 Pack200.Packer.METHOD_ATTRIBUTE_PFX, 192 Pack200.Packer.CODE_ATTRIBUTE_PFX 193 }; 194 int[] ctypes = { 195 Constants.ATTR_CONTEXT_CLASS, 196 Constants.ATTR_CONTEXT_FIELD, 197 Constants.ATTR_CONTEXT_METHOD, 198 Constants.ATTR_CONTEXT_CODE 199 }; 200 for (int i = 0; i < ctypes.length; i++) { 201 String pfx = keys[i]; 202 Map<Object, Object> map = props.prefixMap(pfx); 203 for (Object k : map.keySet()) { 204 String key = (String)k; 205 assert(key.startsWith(pfx)); 206 String name = key.substring(pfx.length()); 207 String layout = props.getProperty(key); 208 Layout lkey = Attribute.keyForLookup(ctypes[i], name); 209 if (Pack200.Packer.STRIP.equals(layout) || 210 Pack200.Packer.PASS.equals(layout) || 211 Pack200.Packer.ERROR.equals(layout)) { 212 lattrCommands.put(lkey, layout.intern()); 213 } else { 214 Attribute.define(lattrDefs, ctypes[i], name, layout); 215 if (verbose > 1) { 216 Utils.log.fine("Added layout for "+Constants.ATTR_CONTEXT_NAME[i]+" attribute "+name+" = "+layout); 217 } 218 assert(lattrDefs.containsKey(lkey)); 219 } 220 } 221 } 222 this.attrDefs = (lattrDefs.isEmpty()) ? null : lattrDefs; 223 this.attrCommands = (lattrCommands.isEmpty()) ? null : lattrCommands; 224 } 225 226 final boolean keepFileOrder 227 = props.getBoolean(Pack200.Packer.KEEP_FILE_ORDER); 228 final boolean keepClassOrder 229 = props.getBoolean(Utils.PACK_KEEP_CLASS_ORDER); 230 231 final boolean keepModtime 232 = Pack200.Packer.KEEP.equals(props.getProperty(Pack200.Packer.MODIFICATION_TIME)); 233 final boolean latestModtime 234 = Pack200.Packer.LATEST.equals(props.getProperty(Pack200.Packer.MODIFICATION_TIME)); 235 final boolean keepDeflateHint 236 = Pack200.Packer.KEEP.equals(props.getProperty(Pack200.Packer.DEFLATE_HINT)); 237 { 238 if (!keepModtime && !latestModtime) { 239 int modtime = props.getTime(Pack200.Packer.MODIFICATION_TIME); 240 if (modtime != Constants.NO_MODTIME) { 241 pkg.default_modtime = modtime; 242 } 243 } 244 if (!keepDeflateHint) { 245 boolean deflate_hint = props.getBoolean(Pack200.Packer.DEFLATE_HINT); 246 if (deflate_hint) { 247 pkg.default_options |= Constants.AO_DEFLATE_HINT; 248 } 249 } 250 } 251 252 long totalOutputSize = 0; 253 int segmentCount = 0; 254 long segmentTotalSize = 0; 255 long segmentSize = 0; // running counter 256 final long segmentLimit; 257 { 258 long limit; 259 if (props.getProperty(Pack200.Packer.SEGMENT_LIMIT, "").equals("")) 260 limit = -1; 261 else 262 limit = props.getLong(Pack200.Packer.SEGMENT_LIMIT); 263 limit = Math.min(Integer.MAX_VALUE, limit); 264 limit = Math.max(-1, limit); 265 if (limit == -1) 266 limit = Long.MAX_VALUE; 267 segmentLimit = limit; 268 } 269 270 final List<String> passFiles; // parsed pack.pass.file options 271 { 272 // Which class files will be passed through? 273 passFiles = props.getProperties(Pack200.Packer.PASS_FILE_PFX); 274 for (ListIterator<String> i = passFiles.listIterator(); i.hasNext(); ) { 275 String file = i.next(); 276 if (file == null) { i.remove(); continue; } 277 file = Utils.getJarEntryName(file); // normalize '\\' to '/' 278 if (file.endsWith("/")) 279 file = file.substring(0, file.length()-1); 280 i.set(file); 281 } 282 if (verbose > 0) Utils.log.info("passFiles = " + passFiles); 283 } 284 285 { 286 // Fill in permitted range of major/minor version numbers. 287 int ver; 288 if ((ver = props.getInteger(Utils.COM_PREFIX+"min.class.majver")) != 0) 289 pkg.min_class_majver = (short) ver; 290 if ((ver = props.getInteger(Utils.COM_PREFIX+"min.class.minver")) != 0) 291 pkg.min_class_minver = (short) ver; 292 if ((ver = props.getInteger(Utils.COM_PREFIX+"max.class.majver")) != 0) 293 pkg.max_class_majver = (short) ver; 294 if ((ver = props.getInteger(Utils.COM_PREFIX+"max.class.minver")) != 0) 295 pkg.max_class_minver = (short) ver; 296 if ((ver = props.getInteger(Utils.COM_PREFIX+"package.minver")) != 0) 297 pkg.package_minver = (short) ver; 298 if ((ver = props.getInteger(Utils.COM_PREFIX+"package.majver")) != 0) 299 pkg.package_majver = (short) ver; 300 } 301 302 { 303 // Hook for testing: Forces use of special archive modes. 304 int opt = props.getInteger(Utils.COM_PREFIX+"archive.options"); 305 if (opt != 0) 306 pkg.default_options |= opt; 307 } 308 309 // (Done collecting options from props.) 310 311 boolean isClassFile(String name) { 312 if (!name.endsWith(".class")) return false; 313 for (String prefix = name; ; ) { 314 if (passFiles.contains(prefix)) return false; 315 int chop = prefix.lastIndexOf('/'); 316 if (chop < 0) break; 317 prefix = prefix.substring(0, chop); 318 } 319 return true; 320 } 321 322 boolean isMetaInfFile(String name) { 323 return name.startsWith("/" + Utils.METAINF) || 324 name.startsWith(Utils.METAINF); 325 } 326 327 // Get a new package, based on the old one. 328 private void makeNextPackage() { 329 pkg.reset(); 330 } 331 332 final class InFile { 333 final String name; 334 final JarFile jf; 335 final JarEntry je; 336 final File f; 337 int modtime = Constants.NO_MODTIME; 338 int options; 339 InFile(String name) { 340 this.name = Utils.getJarEntryName(name); 341 this.f = new File(name); 342 this.jf = null; 343 this.je = null; 344 int timeSecs = getModtime(f.lastModified()); 345 if (keepModtime && timeSecs != Constants.NO_MODTIME) { 346 this.modtime = timeSecs; 347 } else if (latestModtime && timeSecs > pkg.default_modtime) { 348 pkg.default_modtime = timeSecs; 349 } 350 } 351 InFile(JarFile jf, JarEntry je) { 352 this.name = Utils.getJarEntryName(je.getName()); 353 this.f = null; 354 this.jf = jf; 355 this.je = je; 356 int timeSecs = getModtime(je.getTime()); 357 if (keepModtime && timeSecs != Constants.NO_MODTIME) { 358 this.modtime = timeSecs; 359 } else if (latestModtime && timeSecs > pkg.default_modtime) { 360 pkg.default_modtime = timeSecs; 361 } 362 if (keepDeflateHint && je.getMethod() == JarEntry.DEFLATED) { 363 options |= Constants.FO_DEFLATE_HINT; 364 } 365 } 366 InFile(JarEntry je) { 367 this(null, je); 368 } 369 long getInputLength() { 370 long len = (je != null)? je.getSize(): f.length(); 371 assert(len >= 0) : this+".len="+len; 372 // Bump size by pathname length and modtime/def-hint bytes. 373 return Math.max(0, len) + name.length() + 5; 374 } 375 int getModtime(long timeMillis) { 376 // Convert milliseconds to seconds. 377 long seconds = (timeMillis+500) / 1000; 378 if ((int)seconds == seconds) { 379 return (int)seconds; 380 } else { 381 Utils.log.warning("overflow in modtime for "+f); 382 return Constants.NO_MODTIME; 383 } 384 } 385 void copyTo(Package.File file) { 386 if (modtime != Constants.NO_MODTIME) 387 file.modtime = modtime; 388 file.options |= options; 389 } 390 InputStream getInputStream() throws IOException { 391 if (jf != null) 392 return jf.getInputStream(je); 393 else 394 return new FileInputStream(f); 395 } 396 397 public String toString() { 398 return name; 399 } 400 } 401 402 private int nread = 0; // used only if (verbose > 0) 403 private void noteRead(InFile f) { 404 nread++; 405 if (verbose > 2) 406 Utils.log.fine("...read "+f.name); 407 if (verbose > 0 && (nread % 1000) == 0) 408 Utils.log.info("Have read "+nread+" files..."); 409 } 410 411 void run(JarInputStream in, OutputStream out) throws IOException { 412 // First thing we do is get the manifest, as JIS does 413 // not provide the Manifest as an entry. 414 if (in.getManifest() != null) { 415 ByteArrayOutputStream tmp = new ByteArrayOutputStream(); 416 in.getManifest().write(tmp); 417 InputStream tmpIn = new ByteArrayInputStream(tmp.toByteArray()); 418 pkg.addFile(readFile(JarFile.MANIFEST_NAME, tmpIn)); 419 } 420 for (JarEntry je; (je = in.getNextJarEntry()) != null; ) { 421 InFile inFile = new InFile(je); 422 423 String name = inFile.name; 424 Package.File bits = readFile(name, in); 425 Package.File file = null; 426 // (5078608) : discount the resource files in META-INF 427 // from segment computation. 428 long inflen = (isMetaInfFile(name)) 429 ? 0L 430 : inFile.getInputLength(); 431 432 if ((segmentSize += inflen) > segmentLimit) { 433 segmentSize -= inflen; 434 int nextCount = -1; // don't know; it's a stream 435 flushPartial(out, nextCount); 436 } 437 if (verbose > 1) { 438 Utils.log.fine("Reading " + name); 439 } 440 441 assert(je.isDirectory() == name.endsWith("/")); 442 443 if (isClassFile(name)) { 444 file = readClass(name, bits.getInputStream()); 445 } 446 if (file == null) { 447 file = bits; 448 pkg.addFile(file); 449 } 450 inFile.copyTo(file); 451 noteRead(inFile); 452 } 453 flushAll(out); 454 } 455 456 void run(JarFile in, OutputStream out) throws IOException { 457 List<InFile> inFiles = scanJar(in); 458 459 if (verbose > 0) 460 Utils.log.info("Reading " + inFiles.size() + " files..."); 461 462 int numDone = 0; 463 for (InFile inFile : inFiles) { 464 String name = inFile.name; 465 // (5078608) : discount the resource files completely from segmenting 466 long inflen = (isMetaInfFile(name)) 467 ? 0L 468 : inFile.getInputLength() ; 469 if ((segmentSize += inflen) > segmentLimit) { 470 segmentSize -= inflen; 471 // Estimate number of remaining segments: 472 float filesDone = numDone+1; 473 float segsDone = segmentCount+1; 474 float filesToDo = inFiles.size() - filesDone; 475 float segsToDo = filesToDo * (segsDone/filesDone); 476 if (verbose > 1) 477 Utils.log.fine("Estimated segments to do: "+segsToDo); 478 flushPartial(out, (int) Math.ceil(segsToDo)); 479 } 480 InputStream strm = inFile.getInputStream(); 481 if (verbose > 1) 482 Utils.log.fine("Reading " + name); 483 Package.File file = null; 484 if (isClassFile(name)) { 485 file = readClass(name, strm); 486 if (file == null) { 487 strm.close(); 488 strm = inFile.getInputStream(); 489 } 490 } 491 if (file == null) { 492 file = readFile(name, strm); 493 pkg.addFile(file); 494 } 495 inFile.copyTo(file); 496 strm.close(); // tidy up 497 noteRead(inFile); 498 numDone += 1; 499 } 500 flushAll(out); 501 } 502 503 Package.File readClass(String fname, InputStream in) throws IOException { 504 Package.Class cls = pkg.new Class(fname); 505 in = new BufferedInputStream(in); 506 ClassReader reader = new ClassReader(cls, in); 507 reader.setAttrDefs(attrDefs); 508 reader.setAttrCommands(attrCommands); 509 reader.unknownAttrCommand = unknownAttrCommand; 510 try { 511 reader.read(); 512 } catch (IOException ioe) { 513 String message = "Passing class file uncompressed due to"; 514 if (ioe instanceof Attribute.FormatException) { 515 Attribute.FormatException ee = (Attribute.FormatException) ioe; 516 // He passed up the category to us in layout. 517 if (ee.layout.equals(Pack200.Packer.PASS)) { 518 Utils.log.info(ee.toString()); 519 Utils.log.warning(message + " unrecognized attribute: " + 520 fname); 521 return null; 522 } 523 } else if (ioe instanceof ClassReader.ClassFormatException) { 524 ClassReader.ClassFormatException ce = (ClassReader.ClassFormatException) ioe; 525 // %% TODO: Do we invent a new property for this or reuse %% 526 if (unknownAttrCommand.equals(Pack200.Packer.PASS)) { 527 Utils.log.info(ce.toString()); 528 Utils.log.warning(message + " unknown class format: " + 529 fname); 530 return null; 531 } 532 } 533 // Otherwise, it must be an error. 534 throw ioe; 535 } 536 pkg.addClass(cls); 537 return cls.file; 538 } 539 540 // Read raw data. 541 Package.File readFile(String fname, InputStream in) throws IOException { 542 543 Package.File file = pkg.new File(fname); 544 file.readFrom(in); 545 if (file.isDirectory() && file.getFileLength() != 0) 546 throw new IllegalArgumentException("Non-empty directory: "+file.getFileName()); 547 return file; 548 } 549 550 void flushPartial(OutputStream out, int nextCount) throws IOException { 551 if (pkg.files.isEmpty() && pkg.classes.isEmpty()) { 552 return; // do not flush an empty segment 553 } 554 flushPackage(out, Math.max(1, nextCount)); 555 props.setInteger(Pack200.Packer.PROGRESS, 25); 556 // In case there will be another segment: 557 makeNextPackage(); 558 segmentCount += 1; 559 segmentTotalSize += segmentSize; 560 segmentSize = 0; 561 } 562 563 void flushAll(OutputStream out) throws IOException { 564 props.setInteger(Pack200.Packer.PROGRESS, 50); 565 flushPackage(out, 0); 566 out.flush(); 567 props.setInteger(Pack200.Packer.PROGRESS, 100); 568 segmentCount += 1; 569 segmentTotalSize += segmentSize; 570 segmentSize = 0; 571 if (verbose > 0 && segmentCount > 1) { 572 Utils.log.info("Transmitted " 573 +segmentTotalSize+" input bytes in " 574 +segmentCount+" segments totaling " 575 +totalOutputSize+" bytes"); 576 } 577 } 578 579 580 /** Write all information in the current package segment 581 * to the output stream. 582 */ 583 void flushPackage(OutputStream out, int nextCount) throws IOException { 584 int nfiles = pkg.files.size(); 585 if (!keepFileOrder) { 586 // Keeping the order of classes costs about 1% 587 // Keeping the order of all files costs something more. 588 if (verbose > 1) Utils.log.fine("Reordering files."); 589 boolean stripDirectories = true; 590 pkg.reorderFiles(keepClassOrder, stripDirectories); 591 } else { 592 // Package builder must have created a stub for each class. 593 assert(pkg.files.containsAll(pkg.getClassStubs())); 594 // Order of stubs in file list must agree with classes. 595 List<Package.File> res = pkg.files; 596 assert((res = new ArrayList<>(pkg.files)) 597 .retainAll(pkg.getClassStubs()) || true); 598 assert(res.equals(pkg.getClassStubs())); 599 } 600 pkg.trimStubs(); 601 602 // Do some stripping, maybe. 603 if (props.getBoolean(Utils.COM_PREFIX+"strip.debug")) pkg.stripAttributeKind("Debug"); 604 if (props.getBoolean(Utils.COM_PREFIX+"strip.compile")) pkg.stripAttributeKind("Compile"); 605 if (props.getBoolean(Utils.COM_PREFIX+"strip.constants")) pkg.stripAttributeKind("Constant"); 606 if (props.getBoolean(Utils.COM_PREFIX+"strip.exceptions")) pkg.stripAttributeKind("Exceptions"); 607 if (props.getBoolean(Utils.COM_PREFIX+"strip.innerclasses")) pkg.stripAttributeKind("InnerClasses"); 608 609 // Must choose an archive version; PackageWriter does not. 610 if (pkg.package_majver <= 0) pkg.choosePackageVersion(); 611 612 PackageWriter pw = new PackageWriter(pkg, out); 613 pw.archiveNextCount = nextCount; 614 pw.write(); 615 out.flush(); 616 if (verbose > 0) { 617 long outSize = pw.archiveSize0+pw.archiveSize1; 618 totalOutputSize += outSize; 619 long inSize = segmentSize; 620 Utils.log.info("Transmitted " 621 +nfiles+" files of " 622 +inSize+" input bytes in a segment of " 623 +outSize+" bytes"); 624 } 625 } 626 627 List<InFile> scanJar(JarFile jf) throws IOException { 628 // Collect jar entries, preserving order. 629 List<InFile> inFiles = new ArrayList<>(); 630 try { 631 for (JarEntry je : Collections.list(jf.entries())) { 632 InFile inFile = new InFile(jf, je); 633 assert(je.isDirectory() == inFile.name.endsWith("/")); 634 inFiles.add(inFile); 635 } 636 } catch (IllegalStateException ise) { 637 throw new IOException(ise.getLocalizedMessage(), ise); 638 } 639 return inFiles; 640 } 641 } 642 }