< prev index next >
src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java
Print this page
rev 51534 : 8034802: (zipfs) newFileSystem throws UOE when the zip file is located in a custom file system
Reviewed-by: xiaofeya, clanger
@@ -28,10 +28,11 @@
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
+import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
@@ -68,37 +69,38 @@
class ZipFileSystem extends FileSystem {
private final ZipFileSystemProvider provider;
private final Path zfpath;
final ZipCoder zc;
- private final boolean noExtt; // see readExtra()
private final ZipPath rootdir;
+ private boolean readOnly = false; // readonly file system
+
// configurable by env map
+ private final boolean noExtt; // see readExtra()
private final boolean useTempFile; // use a temp file for newOS, default
// is to use BAOS for better performance
- private boolean readOnly = false; // readonly file system
private static final boolean isWindows = AccessController.doPrivileged(
(PrivilegedAction<Boolean>) () -> System.getProperty("os.name")
.startsWith("Windows"));
private final boolean forceEnd64;
+ private final int defaultMethod; // METHOD_STORED if "noCompression=true"
+ // METHOD_DEFLATED otherwise
ZipFileSystem(ZipFileSystemProvider provider,
Path zfpath,
Map<String, ?> env) throws IOException
{
- // create a new zip if not exists
- boolean createNew = "true".equals(env.get("create"));
// default encoding for name/comment
String nameEncoding = env.containsKey("encoding") ?
(String)env.get("encoding") : "UTF-8";
this.noExtt = "false".equals(env.get("zipinfo-time"));
- this.useTempFile = TRUE.equals(env.get("useTempFile"));
- this.forceEnd64 = "true".equals(env.get("forceZIP64End"));
- this.provider = provider;
- this.zfpath = zfpath;
+ this.useTempFile = isTrue(env, "useTempFile");
+ this.forceEnd64 = isTrue(env, "forceZIP64End");
+ this.defaultMethod = isTrue(env, "noCompression") ? METHOD_STORED: METHOD_DEFLATED;
if (Files.notExists(zfpath)) {
- if (createNew) {
+ // create a new zip if not exists
+ if (isTrue(env, "create")) {
try (OutputStream os = Files.newOutputStream(zfpath, CREATE_NEW, WRITE)) {
new END().write(os, 0, forceEnd64);
}
} else {
throw new FileSystemNotFoundException(zfpath.toString());
@@ -120,10 +122,17 @@
} catch (IOException xx) {
x.addSuppressed(xx);
}
throw x;
}
+ this.provider = provider;
+ this.zfpath = zfpath;
+ }
+
+ // returns true if there is a name=true/"true" setting in env
+ private static boolean isTrue(Map<String, ?> env, String name) {
+ return "true".equals(env.get(name)) || TRUE.equals(env.get(name));
}
@Override
public FileSystemProvider provider() {
return provider;
@@ -267,11 +276,12 @@
try {
AccessController.doPrivileged((PrivilegedExceptionAction<Void>) () -> {
sync(); return null;
});
ch.close(); // close the ch just in case no update
- } catch (PrivilegedActionException e) { // and sync dose not close the ch
+ // and sync didn't close the ch
+ } catch (PrivilegedActionException e) {
throw (IOException)e.getException();
} finally {
endWrite();
}
@@ -314,12 +324,12 @@
e = getEntry(path);
if (e == null) {
IndexNode inode = getInode(path);
if (inode == null)
return null;
- e = new Entry(inode.name, inode.isdir); // pseudo directory
- e.method = METHOD_STORED; // STORED for dir
+ // pseudo directory, uses METHOD_STORED
+ e = new Entry(inode.name, inode.isdir, METHOD_STORED);
e.mtime = e.atime = e.ctime = zfsDefaultTimeStamp;
}
} finally {
endRead();
}
@@ -423,12 +433,11 @@
try {
ensureOpen();
if (dir.length == 0 || exists(dir)) // root dir, or exiting dir
throw new FileAlreadyExistsException(getString(dir));
checkParents(dir);
- Entry e = new Entry(dir, Entry.NEW, true);
- e.method = METHOD_STORED; // STORED for dir
+ Entry e = new Entry(dir, Entry.NEW, true, METHOD_STORED);
update(e);
} finally {
endWrite();
}
}
@@ -525,20 +534,20 @@
if (e.isDir() || hasCreateNew)
throw new FileAlreadyExistsException(getString(path));
if (hasAppend) {
InputStream is = getInputStream(e);
OutputStream os = getOutputStream(new Entry(e, Entry.NEW));
- copyStream(is, os);
+ is.transferTo(os);
is.close();
return os;
}
return getOutputStream(new Entry(e, Entry.NEW));
} else {
if (!hasCreate && !hasCreateNew)
throw new NoSuchFileException(getString(path));
checkParents(path);
- return getOutputStream(new Entry(path, Entry.NEW, false));
+ return getOutputStream(new Entry(path, Entry.NEW, false, defaultMethod));
}
} finally {
endRead();
}
}
@@ -570,10 +579,41 @@
}
if (options.contains(APPEND) && options.contains(TRUNCATE_EXISTING))
throw new IllegalArgumentException("APPEND + TRUNCATE_EXISTING not allowed");
}
+
+ // Returns an output SeekableByteChannel for either
+ // (1) writing the contents of a new entry, if the entry doesn't exit, or
+ // (2) updating/replacing the contents of an existing entry.
+ // Note: The content is not compressed.
+ private class EntryOutputChannel extends ByteArrayChannel {
+ Entry e;
+
+ EntryOutputChannel(Entry e) throws IOException {
+ super(e.size > 0? (int)e.size : 8192, false);
+ this.e = e;
+ if (e.mtime == -1)
+ e.mtime = System.currentTimeMillis();
+ if (e.method == -1)
+ e.method = defaultMethod;
+ // store size, compressed size, and crc-32 in datadescriptor
+ e.flag = FLAG_DATADESCR;
+ if (zc.isUTF8())
+ e.flag |= FLAG_USE_UTF8;
+ }
+
+ @Override
+ public void close() throws IOException {
+ e.bytes = toByteArray();
+ e.size = e.bytes.length;
+ e.crc = -1;
+ super.close();
+ update(e);
+ }
+ }
+
// Returns a Writable/ReadByteChannel for now. Might consdier to use
// newFileChannel() instead, which dump the entry data into a regular
// file on the default file system and create a FileChannel on top of
// it.
SeekableByteChannel newByteChannel(byte[] path,
@@ -583,116 +623,54 @@
{
checkOptions(options);
if (options.contains(StandardOpenOption.WRITE) ||
options.contains(StandardOpenOption.APPEND)) {
checkWritable();
- beginRead();
+ beginRead(); // only need a readlock, the "update()" will obtain
+ // thewritelock when the channel is closed
try {
- final WritableByteChannel wbc = Channels.newChannel(
- newOutputStream(path, options.toArray(new OpenOption[0])));
- long leftover = 0;
- if (options.contains(StandardOpenOption.APPEND)) {
+ ensureOpen();
Entry e = getEntry(path);
- if (e != null && e.size >= 0)
- leftover = e.size;
- }
- final long offset = leftover;
- return new SeekableByteChannel() {
- long written = offset;
- public boolean isOpen() {
- return wbc.isOpen();
- }
-
- public long position() throws IOException {
- return written;
- }
-
- public SeekableByteChannel position(long pos)
- throws IOException
- {
- throw new UnsupportedOperationException();
- }
-
- public int read(ByteBuffer dst) throws IOException {
- throw new UnsupportedOperationException();
+ if (e != null) {
+ if (e.isDir() || options.contains(CREATE_NEW))
+ throw new FileAlreadyExistsException(getString(path));
+ SeekableByteChannel sbc =
+ new EntryOutputChannel(new Entry(e, Entry.NEW));
+ if (options.contains(APPEND)) {
+ try (InputStream is = getInputStream(e)) { // copyover
+ byte[] buf = new byte[8192];
+ ByteBuffer bb = ByteBuffer.wrap(buf);
+ int n;
+ while ((n = is.read(buf)) != -1) {
+ bb.position(0);
+ bb.limit(n);
+ sbc.write(bb);
}
-
- public SeekableByteChannel truncate(long size)
- throws IOException
- {
- throw new UnsupportedOperationException();
}
-
- public int write(ByteBuffer src) throws IOException {
- int n = wbc.write(src);
- written += n;
- return n;
}
-
- public long size() throws IOException {
- return written;
+ return sbc;
}
+ if (!options.contains(CREATE) && !options.contains(CREATE_NEW))
+ throw new NoSuchFileException(getString(path));
+ checkParents(path);
+ return new EntryOutputChannel(
+ new Entry(path, Entry.NEW, false, defaultMethod));
- public void close() throws IOException {
- wbc.close();
- }
- };
} finally {
endRead();
}
} else {
beginRead();
try {
ensureOpen();
Entry e = getEntry(path);
if (e == null || e.isDir())
throw new NoSuchFileException(getString(path));
- final ReadableByteChannel rbc =
- Channels.newChannel(getInputStream(e));
- final long size = e.size;
- return new SeekableByteChannel() {
- long read = 0;
- public boolean isOpen() {
- return rbc.isOpen();
+ try (InputStream is = getInputStream(e)) {
+ // TBD: if (e.size < NNNNN);
+ return new ByteArrayChannel(is.readAllBytes(), true);
}
-
- public long position() throws IOException {
- return read;
- }
-
- public SeekableByteChannel position(long pos)
- throws IOException
- {
- throw new UnsupportedOperationException();
- }
-
- public int read(ByteBuffer dst) throws IOException {
- int n = rbc.read(dst);
- if (n > 0) {
- read += n;
- }
- return n;
- }
-
- public SeekableByteChannel truncate(long size)
- throws IOException
- {
- throw new NonWritableChannelException();
- }
-
- public int write (ByteBuffer src) throws IOException {
- throw new NonWritableChannelException();
- }
-
- public long size() throws IOException {
- return size;
- }
-
- public void close() throws IOException {
- rbc.close();
- }
- };
} finally {
endRead();
}
}
}
@@ -844,14 +822,10 @@
// the outstanding input streams that need to be closed
private Set<InputStream> streams =
Collections.synchronizedSet(new HashSet<InputStream>());
- // the ex-channel and ex-path that need to close when their outstanding
- // input streams are all closed by the obtainers.
- private Set<ExChannelCloser> exChClosers = new HashSet<>();
-
private Set<Path> tmppaths = Collections.synchronizedSet(new HashSet<Path>());
private Path getTempPathForEntry(byte[] path) throws IOException {
Path tmpPath = createTempFileInSameDirectoryAs(zfpath);
if (path != null) {
Entry e = getEntry(path);
@@ -1085,11 +1059,11 @@
zerror("invalid CEN header (unsupported compression method: " + method + ")");
}
if (pos + CENHDR + nlen > limit) {
zerror("invalid CEN header (bad header size)");
}
- IndexNode inode = new IndexNode(cen, pos + CENHDR, nlen, pos);
+ IndexNode inode = new IndexNode(cen, pos + CENHDR, pos, nlen);
inodes.put(inode, inode);
// skip ext and comment
pos += (CENHDR + nlen + elen + clen);
}
@@ -1198,23 +1172,41 @@
locoff += n;
}
return written;
}
- // sync the zip file system, if there is any udpate
- private void sync() throws IOException {
- // System.out.printf("->sync(%s) starting....!%n", toString());
- // check ex-closer
- if (!exChClosers.isEmpty()) {
- for (ExChannelCloser ecc : exChClosers) {
- if (ecc.streams.isEmpty()) {
- ecc.ch.close();
- Files.delete(ecc.path);
- exChClosers.remove(ecc);
+ private long writeEntry(Entry e, OutputStream os, byte[] buf)
+ throws IOException {
+
+ if (e.bytes == null && e.file == null) // dir, 0-length data
+ return 0;
+
+ long written = 0;
+ try (OutputStream os2 = e.method == METHOD_STORED ?
+ new EntryOutputStreamCRC32(e, os) : new EntryOutputStreamDef(e, os)) {
+ if (e.bytes != null) { // in-memory
+ os2.write(e.bytes, 0, e.bytes.length);
+ } else if (e.file != null) { // tmp file
+ if (e.type == Entry.NEW || e.type == Entry.FILECH) {
+ try (InputStream is = Files.newInputStream(e.file)) {
+ is.transferTo(os2);
+ }
+ }
+ Files.delete(e.file);
+ tmppaths.remove(e.file);
}
}
+ written += e.csize;
+ if ((e.flag & FLAG_DATADESCR) != 0) {
+ written += e.writeEXT(os);
+ }
+ return written;
}
+
+ // sync the zip file system, if there is any udpate
+ private void sync() throws IOException {
+
if (!hasUpdate)
return;
Path tmpFile = createTempFileInSameDirectoryAs(zfpath);
try (OutputStream os = new BufferedOutputStream(Files.newOutputStream(tmpFile, WRITE)))
{
@@ -1236,38 +1228,11 @@
// file LOC entry.
written += copyLOCEntry(e, true, os, written, buf);
} else { // NEW, FILECH or CEN
e.locoff = written;
written += e.writeLOC(os); // write loc header
- if (e.bytes != null) { // in-memory, deflated
- os.write(e.bytes); // already
- written += e.bytes.length;
- } else if (e.file != null) { // tmp file
- try (InputStream is = Files.newInputStream(e.file)) {
- int n;
- if (e.type == Entry.NEW) { // deflated already
- while ((n = is.read(buf)) != -1) {
- os.write(buf, 0, n);
- written += n;
- }
- } else if (e.type == Entry.FILECH) {
- // the data are not deflated, use ZEOS
- try (OutputStream os2 = new EntryOutputStream(e, os)) {
- while ((n = is.read(buf)) != -1) {
- os2.write(buf, 0, n);
- }
- }
- written += e.csize;
- if ((e.flag & FLAG_DATADESCR) != 0)
- written += e.writeEXT(os);
- }
- }
- Files.delete(e.file);
- tmppaths.remove(e.file);
- } else {
- // dir, 0-length data
- }
+ written += writeEntry(e, os, buf);
}
elist.add(e);
} catch (IOException x) {
x.printStackTrace(); // skip any in-accurate entry
}
@@ -1292,31 +1257,13 @@
}
end.centot = elist.size();
end.cenlen = written - end.cenoff;
end.write(os, written, forceEnd64);
}
- if (!streams.isEmpty()) {
- //
- // TBD: ExChannelCloser should not be necessary if we only
- // sync when being closed, all streams should have been
- // closed already. Keep the logic here for now.
- //
- // There are outstanding input streams open on existing "ch",
- // so, don't close the "cha" and delete the "file for now, let
- // the "ex-channel-closer" to handle them
- ExChannelCloser ecc = new ExChannelCloser(
- createTempFileInSameDirectoryAs(zfpath),
- ch,
- streams);
- Files.move(zfpath, ecc.path, REPLACE_EXISTING);
- exChClosers.add(ecc);
- streams = Collections.synchronizedSet(new HashSet<InputStream>());
- } else {
+
ch.close();
Files.delete(zfpath);
- }
-
Files.move(tmpFile, zfpath, REPLACE_EXISTING);
hasUpdate = false; // clear
}
IndexNode getInode(byte[] path) {
@@ -1350,31 +1297,21 @@
throw new DirectoryNotEmptyException(getString(path));
updateDelete(inode);
}
}
- private static void copyStream(InputStream is, OutputStream os)
- throws IOException
- {
- byte[] copyBuf = new byte[8192];
- int n;
- while ((n = is.read(copyBuf)) != -1) {
- os.write(copyBuf, 0, n);
- }
- }
-
// Returns an out stream for either
// (1) writing the contents of a new entry, if the entry exits, or
// (2) updating/replacing the contents of the specified existing entry.
private OutputStream getOutputStream(Entry e) throws IOException {
if (e.mtime == -1)
e.mtime = System.currentTimeMillis();
if (e.method == -1)
- e.method = METHOD_DEFLATED; // TBD: use default method
- // store size, compressed size, and crc-32 in LOC header
- e.flag = 0;
+ e.method = defaultMethod;
+ // store size, compressed size, and crc-32 in datadescr
+ e.flag = FLAG_DATADESCR;
if (zc.isUTF8())
e.flag |= FLAG_USE_UTF8;
OutputStream os;
if (useTempFile) {
e.file = getTempPathForEntry(null);
@@ -1383,20 +1320,134 @@
os = new ByteArrayOutputStream((e.size > 0)? (int)e.size : 8192);
}
return new EntryOutputStream(e, os);
}
+ private class EntryOutputStream extends FilterOutputStream {
+ private Entry e;
+ private long written;
+ private boolean isClosed;
+
+ EntryOutputStream(Entry e, OutputStream os) throws IOException {
+ super(os);
+ this.e = Objects.requireNonNull(e, "Zip entry is null");
+ // this.written = 0;
+ }
+
+ @Override
+ public synchronized void write(int b) throws IOException {
+ out.write(b);
+ written += 1;
+ }
+
+ @Override
+ public synchronized void write(byte b[], int off, int len)
+ throws IOException {
+ out.write(b, off, len);
+ written += len;
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ if (isClosed) {
+ return;
+ }
+ isClosed = true;
+ e.size = written;
+ if (out instanceof ByteArrayOutputStream)
+ e.bytes = ((ByteArrayOutputStream)out).toByteArray();
+ super.close();
+ update(e);
+ }
+ }
+
+ // Wrapper output stream class to write out a "stored" entry.
+ // (1) this class does not close the underlying out stream when
+ // being closed.
+ // (2) no need to be "synchronized", only used by sync()
+ private class EntryOutputStreamCRC32 extends FilterOutputStream {
+ private Entry e;
+ private CRC32 crc;
+ private long written;
+ private boolean isClosed;
+
+ EntryOutputStreamCRC32(Entry e, OutputStream os) throws IOException {
+ super(os);
+ this.e = Objects.requireNonNull(e, "Zip entry is null");
+ this.crc = new CRC32();
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ out.write(b);
+ crc.update(b);
+ written += 1;
+ }
+
+ @Override
+ public void write(byte b[], int off, int len)
+ throws IOException {
+ out.write(b, off, len);
+ crc.update(b, off, len);
+ written += len;
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (isClosed)
+ return;
+ isClosed = true;
+ e.size = e.csize = written;
+ e.size = crc.getValue();
+ }
+ }
+
+ // Wrapper output stream class to write out a "deflated" entry.
+ // (1) this class does not close the underlying out stream when
+ // being closed.
+ // (2) no need to be "synchronized", only used by sync()
+ private class EntryOutputStreamDef extends DeflaterOutputStream {
+ private CRC32 crc;
+ private Entry e;
+ private boolean isClosed;
+
+ EntryOutputStreamDef(Entry e, OutputStream os) throws IOException {
+ super(os, getDeflater());
+ this.e = Objects.requireNonNull(e, "Zip entry is null");
+ this.crc = new CRC32();
+ }
+
+ @Override
+ public void write(byte b[], int off, int len)
+ throws IOException {
+ super.write(b, off, len);
+ crc.update(b, off, len);
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (isClosed)
+ return;
+ isClosed = true;
+ finish();
+ e.size = def.getBytesRead();
+ e.csize = def.getBytesWritten();
+ e.crc = crc.getValue();
+ }
+ }
+
private InputStream getInputStream(Entry e)
throws IOException
{
InputStream eis = null;
if (e.type == Entry.NEW) {
+ // now bytes & file is uncompressed.
if (e.bytes != null)
- eis = new ByteArrayInputStream(e.bytes);
+ return new ByteArrayInputStream(e.bytes);
else if (e.file != null)
- eis = Files.newInputStream(e.file);
+ return Files.newInputStream(e.file);
else
throw new ZipException("update entry data is missing");
} else if (e.type == Entry.FILECH) {
// FILECH result is un-compressed.
eis = Files.newInputStream(e.file);
@@ -1558,91 +1609,10 @@
pos += LOCHDR + LOCNAM(buf) + LOCEXT(buf);
}
}
}
- class EntryOutputStream extends DeflaterOutputStream
- {
- private CRC32 crc;
- private Entry e;
- private long written;
- private boolean isClosed = false;
-
- EntryOutputStream(Entry e, OutputStream os)
- throws IOException
- {
- super(os, getDeflater());
- if (e == null)
- throw new NullPointerException("Zip entry is null");
- this.e = e;
- crc = new CRC32();
- }
-
- @Override
- public synchronized void write(byte b[], int off, int len)
- throws IOException
- {
- if (e.type != Entry.FILECH) // only from sync
- ensureOpen();
- if (isClosed) {
- throw new IOException("Stream closed");
- }
- if (off < 0 || len < 0 || off > b.length - len) {
- throw new IndexOutOfBoundsException();
- } else if (len == 0) {
- return;
- }
- switch (e.method) {
- case METHOD_DEFLATED:
- super.write(b, off, len);
- break;
- case METHOD_STORED:
- written += len;
- out.write(b, off, len);
- break;
- default:
- throw new ZipException("invalid compression method");
- }
- crc.update(b, off, len);
- }
-
- @Override
- public synchronized void close() throws IOException {
- if (isClosed) {
- return;
- }
- isClosed = true;
- // TBD ensureOpen();
- switch (e.method) {
- case METHOD_DEFLATED:
- finish();
- e.size = def.getBytesRead();
- e.csize = def.getBytesWritten();
- e.crc = crc.getValue();
- break;
- case METHOD_STORED:
- // we already know that both e.size and e.csize are the same
- e.size = e.csize = written;
- e.crc = crc.getValue();
- break;
- default:
- throw new ZipException("invalid compression method");
- }
- //crc.reset();
- if (out instanceof ByteArrayOutputStream)
- e.bytes = ((ByteArrayOutputStream)out).toByteArray();
-
- if (e.type == Entry.FILECH) {
- releaseDeflater(def);
- return;
- }
- super.close();
- releaseDeflater(def);
- update(e);
- }
- }
-
static void zerror(String msg) throws ZipException {
throw new ZipException(msg);
}
// Maxmum number of de/inflater we cache
@@ -1795,11 +1765,11 @@
name(name);
this.pos = pos;
}
// constructor for cenInit()
- IndexNode(byte[] cen, int noff, int nlen, int pos) {
+ IndexNode(byte[] cen, int noff, int pos, int nlen) {
if (cen[noff + nlen - 1] == '/') {
isdir = true;
nlen--;
}
name = new byte[nlen + 1];
@@ -1886,22 +1856,22 @@
long locoff;
byte[] comment;
Entry() {}
- Entry(byte[] name, boolean isdir) {
+ Entry(byte[] name, boolean isdir, int method) {
name(name);
this.isdir = isdir;
this.mtime = this.ctime = this.atime = System.currentTimeMillis();
this.crc = 0;
this.size = 0;
this.csize = 0;
- this.method = METHOD_DEFLATED;
+ this.method = method;
}
- Entry(byte[] name, int type, boolean isdir) {
- this(name, isdir);
+ Entry(byte[] name, int type, boolean isdir, int method) {
+ this(name, isdir, method);
this.type = type;
}
Entry (Entry e, int type) {
name(e.name);
@@ -1925,13 +1895,12 @@
this.comment = e.comment;
this.type = type;
}
Entry (byte[] name, Path file, int type) {
- this(name, type, false);
+ this(name, type, false, METHOD_STORED);
this.file = file;
- this.method = METHOD_STORED;
}
int version() throws ZipException {
if (method == METHOD_DEFLATED)
return 20;
@@ -2406,10 +2375,11 @@
}
public String toString() {
StringBuilder sb = new StringBuilder(1024);
Formatter fm = new Formatter(sb);
+ fm.format(" name : %s%n", new String(name));
fm.format(" creationTime : %tc%n", creationTime().toMillis());
fm.format(" lastAccessTime : %tc%n", lastAccessTime().toMillis());
fm.format(" lastModifiedTime: %tc%n", lastModifiedTime().toMillis());
fm.format(" isRegularFile : %b%n", isRegularFile());
fm.format(" isDirectory : %b%n", isDirectory());
@@ -2423,24 +2393,10 @@
fm.close();
return sb.toString();
}
}
- private static class ExChannelCloser {
- Path path;
- SeekableByteChannel ch;
- Set<InputStream> streams;
- ExChannelCloser(Path path,
- SeekableByteChannel ch,
- Set<InputStream> streams)
- {
- this.path = path;
- this.ch = ch;
- this.streams = streams;
- }
- }
-
// ZIP directory has two issues:
// (1) ZIP spec does not require the ZIP file to include
// directory entry
// (2) all entries are not stored/organized in a "tree"
// structure.
< prev index next >