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