1 /*
   2  * Copyright (c) 2003, 2013, 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     public SortedMap<String, String> properties() {
  73         return props;
  74     }
  75 
  76     //Driver routines
  77 
  78     /**
  79      * Takes a JarFile and converts into a pack-stream.
  80      * <p>
  81      * Closes its input but not its output.  (Pack200 archives are appendable.)
  82      *
  83      * @param in  a JarFile
  84      * @param out an OutputStream
  85      * @exception IOException if an error is encountered.
  86      */
  87     public synchronized void pack(JarFile in, OutputStream out) throws IOException {
  88         assert (Utils.currentInstance.get() == null);
  89 
  90         boolean needUTC = !props.getBoolean(Utils.PACK_DEFAULT_TIMEZONE);
  91         try {
  92             Utils.currentInstance.set(this);
  93             if (needUTC) {
  94                 Utils.changeDefaultTimeZoneToUtc();
  95             }
  96 
  97             if ("0".equals(props.getProperty(Pack200.Packer.EFFORT))) {
  98                 Utils.copyJarFile(in, out);
  99             } else {
 100                 (new DoPack()).run(in, out);
 101             }
 102         } finally {
 103             Utils.currentInstance.set(null);
 104             if (needUTC) {
 105                 Utils.restoreDefaultTimeZone();
 106             }
 107             in.close();
 108         }
 109     }
 110 
 111     /**
 112      * Takes a JarInputStream and converts into a pack-stream.
 113      * <p>
 114      * Closes its input but not its output.  (Pack200 archives are appendable.)
 115      * <p>
 116      * The modification time and deflation hint attributes are not available,
 117      * for the jar-manifest file and the directory containing the file.
 118      *
 119      * @see #MODIFICATION_TIME
 120      * @see #DEFLATION_HINT
 121      * @param in a JarInputStream
 122      * @param out an OutputStream
 123      * @exception IOException if an error is encountered.
 124      */
 125     public synchronized void pack(JarInputStream in, OutputStream out) throws IOException {
 126         assert (Utils.currentInstance.get() == null);
 127         boolean needUTC = !props.getBoolean(Utils.PACK_DEFAULT_TIMEZONE);
 128         try {
 129             Utils.currentInstance.set(this);
 130             if (needUTC) {
 131                 Utils.changeDefaultTimeZoneToUtc();
 132             }
 133             if ("0".equals(props.getProperty(Pack200.Packer.EFFORT))) {
 134                 Utils.copyJarFile(in, out);
 135             } else {
 136                 (new DoPack()).run(in, out);
 137             }
 138         } finally {
 139             Utils.currentInstance.set(null);
 140             if (needUTC) {
 141                 Utils.restoreDefaultTimeZone();
 142             }
 143             in.close();
 144         }
 145     }
 146 
 147     /**
 148      * Register a listener for changes to options.
 149      * @param listener  An object to be invoked when a property is changed.
 150      */
 151     public void addPropertyChangeListener(PropertyChangeListener listener) {
 152         props.addListener(listener);
 153     }
 154 
 155     /**
 156      * Remove a listener for the PropertyChange event.
 157      * @param listener  The PropertyChange listener to be removed.
 158      */
 159     public void removePropertyChangeListener(PropertyChangeListener listener) {
 160         props.removeListener(listener);
 161     }
 162 
 163 
 164 
 165     // All the worker bees.....
 166     // The packer worker.
 167     private class DoPack {
 168         final int verbose = props.getInteger(Utils.DEBUG_VERBOSE);
 169 
 170         {
 171             props.setInteger(Pack200.Packer.PROGRESS, 0);
 172             if (verbose > 0) Utils.log.info(props.toString());
 173         }
 174 
 175         // Here's where the bits are collected before getting packed, we also
 176         // initialize the version numbers now.
 177         final Package pkg = new Package(Package.Version.makeVersion(props, "min.class"),
 178                                         Package.Version.makeVersion(props, "max.class"),
 179                                         Package.Version.makeVersion(props, "package"));
 180 
 181         final String unknownAttrCommand;
 182         {
 183             String uaMode = props.getProperty(Pack200.Packer.UNKNOWN_ATTRIBUTE, Pack200.Packer.PASS);
 184             if (!(Pack200.Packer.STRIP.equals(uaMode) ||
 185                   Pack200.Packer.PASS.equals(uaMode) ||
 186                   Pack200.Packer.ERROR.equals(uaMode))) {
 187                 throw new RuntimeException("Bad option: " + Pack200.Packer.UNKNOWN_ATTRIBUTE + " = " + uaMode);
 188             }
 189             unknownAttrCommand = uaMode.intern();
 190         }
 191         final String classFormatCommand;
 192         {
 193             String fmtMode = props.getProperty(Utils.CLASS_FORMAT_ERROR, Pack200.Packer.PASS);
 194             if (!(Pack200.Packer.PASS.equals(fmtMode) ||
 195                   Pack200.Packer.ERROR.equals(fmtMode))) {
 196                 throw new RuntimeException("Bad option: " + Utils.CLASS_FORMAT_ERROR + " = " + fmtMode);
 197             }
 198             classFormatCommand = fmtMode.intern();
 199         }
 200 
 201         final Map<Attribute.Layout, Attribute> attrDefs;
 202         final Map<Attribute.Layout, String> attrCommands;
 203         {
 204             Map<Attribute.Layout, Attribute> lattrDefs   = new HashMap<>();
 205             Map<Attribute.Layout, String>  lattrCommands = new HashMap<>();
 206             String[] keys = {
 207                 Pack200.Packer.CLASS_ATTRIBUTE_PFX,
 208                 Pack200.Packer.FIELD_ATTRIBUTE_PFX,
 209                 Pack200.Packer.METHOD_ATTRIBUTE_PFX,
 210                 Pack200.Packer.CODE_ATTRIBUTE_PFX
 211             };
 212             int[] ctypes = {
 213                 Constants.ATTR_CONTEXT_CLASS,
 214                 Constants.ATTR_CONTEXT_FIELD,
 215                 Constants.ATTR_CONTEXT_METHOD,
 216                 Constants.ATTR_CONTEXT_CODE
 217             };
 218             for (int i = 0; i < ctypes.length; i++) {
 219                 String pfx = keys[i];
 220                 Map<String, String> map = props.prefixMap(pfx);
 221                 for (String key : map.keySet()) {
 222                     assert(key.startsWith(pfx));
 223                     String name = key.substring(pfx.length());
 224                     String layout = props.getProperty(key);
 225                     Layout lkey = Attribute.keyForLookup(ctypes[i], name);
 226                     if (Pack200.Packer.STRIP.equals(layout) ||
 227                         Pack200.Packer.PASS.equals(layout) ||
 228                         Pack200.Packer.ERROR.equals(layout)) {
 229                         lattrCommands.put(lkey, layout.intern());
 230                     } else {
 231                         Attribute.define(lattrDefs, ctypes[i], name, layout);
 232                         if (verbose > 1) {
 233                             Utils.log.fine("Added layout for "+Constants.ATTR_CONTEXT_NAME[i]+" attribute "+name+" = "+layout);
 234                         }
 235                         assert(lattrDefs.containsKey(lkey));
 236                     }
 237                 }
 238             }
 239             this.attrDefs = (lattrDefs.isEmpty()) ? null : lattrDefs;
 240             this.attrCommands = (lattrCommands.isEmpty()) ? null : lattrCommands;
 241         }
 242 
 243         final boolean keepFileOrder
 244             = props.getBoolean(Pack200.Packer.KEEP_FILE_ORDER);
 245         final boolean keepClassOrder
 246             = props.getBoolean(Utils.PACK_KEEP_CLASS_ORDER);
 247 
 248         final boolean keepModtime
 249             = Pack200.Packer.KEEP.equals(props.getProperty(Pack200.Packer.MODIFICATION_TIME));
 250         final boolean latestModtime
 251             = Pack200.Packer.LATEST.equals(props.getProperty(Pack200.Packer.MODIFICATION_TIME));
 252         final boolean keepDeflateHint
 253             = Pack200.Packer.KEEP.equals(props.getProperty(Pack200.Packer.DEFLATE_HINT));
 254         {
 255             if (!keepModtime && !latestModtime) {
 256                 int modtime = props.getTime(Pack200.Packer.MODIFICATION_TIME);
 257                 if (modtime != Constants.NO_MODTIME) {
 258                     pkg.default_modtime = modtime;
 259                 }
 260             }
 261             if (!keepDeflateHint) {
 262                 boolean deflate_hint = props.getBoolean(Pack200.Packer.DEFLATE_HINT);
 263                 if (deflate_hint) {
 264                     pkg.default_options |= Constants.AO_DEFLATE_HINT;
 265                 }
 266             }
 267         }
 268 
 269         long totalOutputSize = 0;
 270         int  segmentCount = 0;
 271         long segmentTotalSize = 0;
 272         long segmentSize = 0;  // running counter
 273         final long segmentLimit;
 274         {
 275             long limit;
 276             if (props.getProperty(Pack200.Packer.SEGMENT_LIMIT, "").equals(""))
 277                 limit = -1;
 278             else
 279                 limit = props.getLong(Pack200.Packer.SEGMENT_LIMIT);
 280             limit = Math.min(Integer.MAX_VALUE, limit);
 281             limit = Math.max(-1, limit);
 282             if (limit == -1)
 283                 limit = Long.MAX_VALUE;
 284             segmentLimit = limit;
 285         }
 286 
 287         final List<String> passFiles;  // parsed pack.pass.file options
 288         {
 289             // Which class files will be passed through?
 290             passFiles = props.getProperties(Pack200.Packer.PASS_FILE_PFX);
 291             for (ListIterator<String> i = passFiles.listIterator(); i.hasNext(); ) {
 292                 String file = i.next();
 293                 if (file == null) { i.remove(); continue; }
 294                 file = Utils.getJarEntryName(file);  // normalize '\\' to '/'
 295                 if (file.endsWith("/"))
 296                     file = file.substring(0, file.length()-1);
 297                 i.set(file);
 298             }
 299             if (verbose > 0) Utils.log.info("passFiles = " + passFiles);
 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                     if (classFormatCommand.equals(Pack200.Packer.PASS)) {
 526                         Utils.log.info(ce.toString());
 527                         Utils.log.warning(message + " unknown class format: " +
 528                                 fname);
 529                         return null;
 530                     }
 531                 }
 532                 // Otherwise, it must be an error.
 533                 throw ioe;
 534             }
 535             pkg.addClass(cls);
 536             return cls.file;
 537         }
 538 
 539         // Read raw data.
 540         Package.File readFile(String fname, InputStream in) throws IOException {
 541 
 542             Package.File file = pkg.new File(fname);
 543             file.readFrom(in);
 544             if (file.isDirectory() && file.getFileLength() != 0)
 545                 throw new IllegalArgumentException("Non-empty directory: "+file.getFileName());
 546             return file;
 547         }
 548 
 549         void flushPartial(OutputStream out, int nextCount) throws IOException {
 550             if (pkg.files.isEmpty() && pkg.classes.isEmpty()) {
 551                 return;  // do not flush an empty segment
 552             }
 553             flushPackage(out, Math.max(1, nextCount));
 554             props.setInteger(Pack200.Packer.PROGRESS, 25);
 555             // In case there will be another segment:
 556             makeNextPackage();
 557             segmentCount += 1;
 558             segmentTotalSize += segmentSize;
 559             segmentSize = 0;
 560         }
 561 
 562         void flushAll(OutputStream out) throws IOException {
 563             props.setInteger(Pack200.Packer.PROGRESS, 50);
 564             flushPackage(out, 0);
 565             out.flush();
 566             props.setInteger(Pack200.Packer.PROGRESS, 100);
 567             segmentCount += 1;
 568             segmentTotalSize += segmentSize;
 569             segmentSize = 0;
 570             if (verbose > 0 && segmentCount > 1) {
 571                 Utils.log.info("Transmitted "
 572                                  +segmentTotalSize+" input bytes in "
 573                                  +segmentCount+" segments totaling "
 574                                  +totalOutputSize+" bytes");
 575             }
 576         }
 577 
 578 
 579         /** Write all information in the current package segment
 580          *  to the output stream.
 581          */
 582         void flushPackage(OutputStream out, int nextCount) throws IOException {
 583             int nfiles = pkg.files.size();
 584             if (!keepFileOrder) {
 585                 // Keeping the order of classes costs about 1%
 586                 // Keeping the order of all files costs something more.
 587                 if (verbose > 1)  Utils.log.fine("Reordering files.");
 588                 boolean stripDirectories = true;
 589                 pkg.reorderFiles(keepClassOrder, stripDirectories);
 590             } else {
 591                 // Package builder must have created a stub for each class.
 592                 assert(pkg.files.containsAll(pkg.getClassStubs()));
 593                 // Order of stubs in file list must agree with classes.
 594                 List<Package.File> res = pkg.files;
 595                 assert((res = new ArrayList<>(pkg.files))
 596                        .retainAll(pkg.getClassStubs()) || true);
 597                 assert(res.equals(pkg.getClassStubs()));
 598             }
 599             pkg.trimStubs();
 600 
 601             // Do some stripping, maybe.
 602             if (props.getBoolean(Utils.COM_PREFIX+"strip.debug"))        pkg.stripAttributeKind("Debug");
 603             if (props.getBoolean(Utils.COM_PREFIX+"strip.compile"))      pkg.stripAttributeKind("Compile");
 604             if (props.getBoolean(Utils.COM_PREFIX+"strip.constants"))    pkg.stripAttributeKind("Constant");
 605             if (props.getBoolean(Utils.COM_PREFIX+"strip.exceptions"))   pkg.stripAttributeKind("Exceptions");
 606             if (props.getBoolean(Utils.COM_PREFIX+"strip.innerclasses")) pkg.stripAttributeKind("InnerClasses");
 607 
 608             PackageWriter pw = new PackageWriter(pkg, out);
 609             pw.archiveNextCount = nextCount;
 610             pw.write();
 611             out.flush();
 612             if (verbose > 0) {
 613                 long outSize = pw.archiveSize0+pw.archiveSize1;
 614                 totalOutputSize += outSize;
 615                 long inSize = segmentSize;
 616                 Utils.log.info("Transmitted "
 617                                  +nfiles+" files of "
 618                                  +inSize+" input bytes in a segment of "
 619                                  +outSize+" bytes");
 620             }
 621         }
 622 
 623         List<InFile> scanJar(JarFile jf) throws IOException {
 624             // Collect jar entries, preserving order.
 625             List<InFile> inFiles = new ArrayList<>();
 626             try {
 627                 for (JarEntry je : Collections.list(jf.entries())) {
 628                     InFile inFile = new InFile(jf, je);
 629                     assert(je.isDirectory() == inFile.name.endsWith("/"));
 630                     inFiles.add(inFile);
 631                 }
 632             } catch (IllegalStateException ise) {
 633                 throw new IOException(ise.getLocalizedMessage(), ise);
 634             }
 635             return inFiles;
 636         }
 637     }
 638 }