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