/* * Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package jdk.nio.zipfs; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.*; import java.nio.file.*; import java.nio.file.attribute.*; import java.nio.file.spi.*; import java.security.AccessController; import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.*; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.regex.Pattern; import java.util.zip.CRC32; import java.util.zip.Inflater; import java.util.zip.Deflater; import java.util.zip.InflaterInputStream; import java.util.zip.DeflaterOutputStream; import java.util.zip.ZipException; import static java.lang.Boolean.*; import static jdk.nio.zipfs.ZipConstants.*; import static jdk.nio.zipfs.ZipUtils.*; import static java.nio.file.StandardOpenOption.*; import static java.nio.file.StandardCopyOption.*; /** * A FileSystem built on a zip file * * @author Xueming Shen */ class ZipFileSystem extends FileSystem { private final ZipFileSystemProvider provider; private final ZipPath defaultdir; private boolean readOnly = false; private final Path zfpath; private final ZipCoder zc; // configurable by env map private final String defaultDir; // default dir for the file system private final String nameEncoding; // default encoding for name/comment private final boolean useTempFile; // use a temp file for newOS, default // is to use BAOS for better performance private final boolean createNew; // create a new zip if not exists private static final boolean isWindows = AccessController.doPrivileged( (PrivilegedAction) () -> System.getProperty("os.name") .startsWith("Windows")); ZipFileSystem(ZipFileSystemProvider provider, Path zfpath, Map env) throws IOException { // configurable env setup this.createNew = "true".equals(env.get("create")); this.nameEncoding = env.containsKey("encoding") ? (String)env.get("encoding") : "UTF-8"; this.useTempFile = TRUE.equals(env.get("useTempFile")); this.defaultDir = env.containsKey("default.dir") ? (String)env.get("default.dir") : "/"; if (this.defaultDir.charAt(0) != '/') throw new IllegalArgumentException("default dir should be absolute"); this.provider = provider; this.zfpath = zfpath; if (Files.notExists(zfpath)) { if (createNew) { try (OutputStream os = Files.newOutputStream(zfpath, CREATE_NEW, WRITE)) { new END().write(os, 0); } } else { throw new FileSystemNotFoundException(zfpath.toString()); } } // sm and existence check zfpath.getFileSystem().provider().checkAccess(zfpath, AccessMode.READ); boolean writeable = AccessController.doPrivileged( (PrivilegedAction) () -> Files.isWritable(zfpath)); if (!writeable) this.readOnly = true; this.zc = ZipCoder.get(nameEncoding); this.defaultdir = new ZipPath(this, getBytes(defaultDir)); this.ch = Files.newByteChannel(zfpath, READ); try { this.cen = initCEN(); } catch (IOException x) { try { this.ch.close(); } catch (IOException xx) { x.addSuppressed(xx); } throw x; } } @Override public FileSystemProvider provider() { return provider; } @Override public String getSeparator() { return "/"; } @Override public boolean isOpen() { return isOpen; } @Override public boolean isReadOnly() { return readOnly; } private void checkWritable() throws IOException { if (readOnly) throw new ReadOnlyFileSystemException(); } @Override public Iterable getRootDirectories() { ArrayList pathArr = new ArrayList<>(); pathArr.add(new ZipPath(this, new byte[]{'/'})); return pathArr; } ZipPath getDefaultDir() { // package private return defaultdir; } @Override public ZipPath getPath(String first, String... more) { String path; if (more.length == 0) { path = first; } else { StringBuilder sb = new StringBuilder(); sb.append(first); for (String segment: more) { if (segment.length() > 0) { if (sb.length() > 0) sb.append('/'); sb.append(segment); } } path = sb.toString(); } return new ZipPath(this, getBytes(path)); } @Override public UserPrincipalLookupService getUserPrincipalLookupService() { throw new UnsupportedOperationException(); } @Override public WatchService newWatchService() { throw new UnsupportedOperationException(); } FileStore getFileStore(ZipPath path) { return new ZipFileStore(path); } @Override public Iterable getFileStores() { ArrayList list = new ArrayList<>(1); list.add(new ZipFileStore(new ZipPath(this, new byte[]{'/'}))); return list; } private static final Set supportedFileAttributeViews = Collections.unmodifiableSet( new HashSet(Arrays.asList("basic", "zip"))); @Override public Set supportedFileAttributeViews() { return supportedFileAttributeViews; } @Override public String toString() { return zfpath.toString(); } Path getZipFile() { return zfpath; } private static final String GLOB_SYNTAX = "glob"; private static final String REGEX_SYNTAX = "regex"; @Override public PathMatcher getPathMatcher(String syntaxAndInput) { int pos = syntaxAndInput.indexOf(':'); if (pos <= 0 || pos == syntaxAndInput.length()) { throw new IllegalArgumentException(); } String syntax = syntaxAndInput.substring(0, pos); String input = syntaxAndInput.substring(pos + 1); String expr; if (syntax.equalsIgnoreCase(GLOB_SYNTAX)) { expr = toRegexPattern(input); } else { if (syntax.equalsIgnoreCase(REGEX_SYNTAX)) { expr = input; } else { throw new UnsupportedOperationException("Syntax '" + syntax + "' not recognized"); } } // return matcher final Pattern pattern = Pattern.compile(expr); return new PathMatcher() { @Override public boolean matches(Path path) { return pattern.matcher(path.toString()).matches(); } }; } @Override public void close() throws IOException { beginWrite(); try { if (!isOpen) return; isOpen = false; // set closed } finally { endWrite(); } if (!streams.isEmpty()) { // unlock and close all remaining streams Set copy = new HashSet<>(streams); for (InputStream is: copy) is.close(); } beginWrite(); // lock and sync try { AccessController.doPrivileged((PrivilegedExceptionAction) () -> { sync(); return null; }); ch.close(); // close the ch just in case no update } catch (PrivilegedActionException e) { // and sync dose not close the ch throw (IOException)e.getException(); } finally { endWrite(); } synchronized (inflaters) { for (Inflater inf : inflaters) inf.end(); } synchronized (deflaters) { for (Deflater def : deflaters) def.end(); } IOException ioe = null; synchronized (tmppaths) { for (Path p: tmppaths) { try { AccessController.doPrivileged( (PrivilegedExceptionAction)() -> Files.deleteIfExists(p)); } catch (PrivilegedActionException e) { IOException x = (IOException)e.getException(); if (ioe == null) ioe = x; else ioe.addSuppressed(x); } } } provider.removeFileSystem(zfpath, this); if (ioe != null) throw ioe; } ZipFileAttributes getFileAttributes(byte[] path) throws IOException { Entry e; beginRead(); try { ensureOpen(); e = getEntry0(path); if (e == null) { IndexNode inode = getInode(path); if (inode == null) return null; e = new Entry(inode.name); // pseudo directory e.method = METHOD_STORED; // STORED for dir e.mtime = e.atime = e.ctime = -1;// -1 for all times } } finally { endRead(); } return new ZipFileAttributes(e); } void setTimes(byte[] path, FileTime mtime, FileTime atime, FileTime ctime) throws IOException { checkWritable(); beginWrite(); try { ensureOpen(); Entry e = getEntry0(path); // ensureOpen checked if (e == null) throw new NoSuchFileException(getString(path)); if (e.type == Entry.CEN) e.type = Entry.COPY; // copy e if (mtime != null) e.mtime = mtime.toMillis(); if (atime != null) e.atime = atime.toMillis(); if (ctime != null) e.ctime = ctime.toMillis(); update(e); } finally { endWrite(); } } boolean exists(byte[] path) throws IOException { beginRead(); try { ensureOpen(); return getInode(path) != null; } finally { endRead(); } } boolean isDirectory(byte[] path) throws IOException { beginRead(); try { IndexNode n = getInode(path); return n != null && n.isDir(); } finally { endRead(); } } private ZipPath toZipPath(byte[] path) { // make it absolute byte[] p = new byte[path.length + 1]; p[0] = '/'; System.arraycopy(path, 0, p, 1, path.length); return new ZipPath(this, p); } // returns the list of child paths of "path" Iterator iteratorOf(byte[] path, DirectoryStream.Filter filter) throws IOException { beginWrite(); // iteration of inodes needs exclusive lock try { ensureOpen(); IndexNode inode = getInode(path); if (inode == null) throw new NotDirectoryException(getString(path)); List list = new ArrayList<>(); IndexNode child = inode.child; while (child != null) { ZipPath zp = toZipPath(child.name); if (filter == null || filter.accept(zp)) list.add(zp); child = child.sibling; } return list.iterator(); } finally { endWrite(); } } void createDirectory(byte[] dir, FileAttribute... attrs) throws IOException { checkWritable(); dir = toDirectoryPath(dir); beginWrite(); 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); e.method = METHOD_STORED; // STORED for dir update(e); } finally { endWrite(); } } void copyFile(boolean deletesrc, byte[]src, byte[] dst, CopyOption... options) throws IOException { checkWritable(); if (Arrays.equals(src, dst)) return; // do nothing, src and dst are the same beginWrite(); try { ensureOpen(); Entry eSrc = getEntry0(src); // ensureOpen checked if (eSrc == null) throw new NoSuchFileException(getString(src)); if (eSrc.isDir()) { // spec says to create dst dir createDirectory(dst); return; } boolean hasReplace = false; boolean hasCopyAttrs = false; for (CopyOption opt : options) { if (opt == REPLACE_EXISTING) hasReplace = true; else if (opt == COPY_ATTRIBUTES) hasCopyAttrs = true; } Entry eDst = getEntry0(dst); if (eDst != null) { if (!hasReplace) throw new FileAlreadyExistsException(getString(dst)); } else { checkParents(dst); } Entry u = new Entry(eSrc, Entry.COPY); // copy eSrc entry u.name(dst); // change name if (eSrc.type == Entry.NEW || eSrc.type == Entry.FILECH) { u.type = eSrc.type; // make it the same type if (deletesrc) { // if it's a "rename", take the data u.bytes = eSrc.bytes; u.file = eSrc.file; } else { // if it's not "rename", copy the data if (eSrc.bytes != null) u.bytes = Arrays.copyOf(eSrc.bytes, eSrc.bytes.length); else if (eSrc.file != null) { u.file = getTempPathForEntry(null); Files.copy(eSrc.file, u.file, REPLACE_EXISTING); } } } if (!hasCopyAttrs) u.mtime = u.atime= u.ctime = System.currentTimeMillis(); update(u); if (deletesrc) updateDelete(eSrc); } finally { endWrite(); } } // Returns an output stream for writing the contents into the specified // entry. OutputStream newOutputStream(byte[] path, OpenOption... options) throws IOException { checkWritable(); boolean hasCreateNew = false; boolean hasCreate = false; boolean hasAppend = false; for (OpenOption opt: options) { if (opt == READ) throw new IllegalArgumentException("READ not allowed"); if (opt == CREATE_NEW) hasCreateNew = true; if (opt == CREATE) hasCreate = true; if (opt == APPEND) hasAppend = true; } beginRead(); // only need a readlock, the "update()" will try { // try to obtain a writelock when the os is ensureOpen(); // being closed. Entry e = getEntry0(path); if (e != null) { 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.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)); } } finally { endRead(); } } // Returns an input stream for reading the contents of the specified // file entry. InputStream newInputStream(byte[] path) throws IOException { beginRead(); try { ensureOpen(); Entry e = getEntry0(path); if (e == null) throw new NoSuchFileException(getString(path)); if (e.isDir()) throw new FileSystemException(getString(path), "is a directory", null); return getInputStream(e); } finally { endRead(); } } private void checkOptions(Set options) { // check for options of null type and option is an intance of StandardOpenOption for (OpenOption option : options) { if (option == null) throw new NullPointerException(); if (!(option instanceof StandardOpenOption)) throw new IllegalArgumentException(); } } // 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, Set options, FileAttribute... attrs) throws IOException { checkOptions(options); if (options.contains(StandardOpenOption.WRITE) || options.contains(StandardOpenOption.APPEND)) { checkWritable(); beginRead(); try { final WritableByteChannel wbc = Channels.newChannel( newOutputStream(path, options.toArray(new OpenOption[0]))); long leftover = 0; if (options.contains(StandardOpenOption.APPEND)) { Entry e = getEntry0(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(); } 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; } public void close() throws IOException { wbc.close(); } }; } finally { endRead(); } } else { beginRead(); try { ensureOpen(); Entry e = getEntry0(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(); } 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(); } } } // Returns a FileChannel of the specified entry. // // This implementation creates a temporary file on the default file system, // copy the entry data into it if the entry exists, and then create a // FileChannel on top of it. FileChannel newFileChannel(byte[] path, Set options, FileAttribute... attrs) throws IOException { checkOptions(options); final boolean forWrite = (options.contains(StandardOpenOption.WRITE) || options.contains(StandardOpenOption.APPEND)); beginRead(); try { ensureOpen(); Entry e = getEntry0(path); if (forWrite) { checkWritable(); if (e == null) { if (!options.contains(StandardOpenOption.CREATE_NEW)) throw new NoSuchFileException(getString(path)); } else { if (options.contains(StandardOpenOption.CREATE_NEW)) throw new FileAlreadyExistsException(getString(path)); if (e.isDir()) throw new FileAlreadyExistsException("directory <" + getString(path) + "> exists"); } options.remove(StandardOpenOption.CREATE_NEW); // for tmpfile } else if (e == null || e.isDir()) { throw new NoSuchFileException(getString(path)); } final boolean isFCH = (e != null && e.type == Entry.FILECH); final Path tmpfile = isFCH ? e.file : getTempPathForEntry(path); final FileChannel fch = tmpfile.getFileSystem() .provider() .newFileChannel(tmpfile, options, attrs); final Entry u = isFCH ? e : new Entry(path, tmpfile, Entry.FILECH); if (forWrite) { u.flag = FLAG_DATADESCR; u.method = METHOD_DEFLATED; } // is there a better way to hook into the FileChannel's close method? return new FileChannel() { public int write(ByteBuffer src) throws IOException { return fch.write(src); } public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { return fch.write(srcs, offset, length); } public long position() throws IOException { return fch.position(); } public FileChannel position(long newPosition) throws IOException { fch.position(newPosition); return this; } public long size() throws IOException { return fch.size(); } public FileChannel truncate(long size) throws IOException { fch.truncate(size); return this; } public void force(boolean metaData) throws IOException { fch.force(metaData); } public long transferTo(long position, long count, WritableByteChannel target) throws IOException { return fch.transferTo(position, count, target); } public long transferFrom(ReadableByteChannel src, long position, long count) throws IOException { return fch.transferFrom(src, position, count); } public int read(ByteBuffer dst) throws IOException { return fch.read(dst); } public int read(ByteBuffer dst, long position) throws IOException { return fch.read(dst, position); } public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { return fch.read(dsts, offset, length); } public int write(ByteBuffer src, long position) throws IOException { return fch.write(src, position); } public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException { throw new UnsupportedOperationException(); } public FileLock lock(long position, long size, boolean shared) throws IOException { return fch.lock(position, size, shared); } public FileLock tryLock(long position, long size, boolean shared) throws IOException { return fch.tryLock(position, size, shared); } protected void implCloseChannel() throws IOException { fch.close(); if (forWrite) { u.mtime = System.currentTimeMillis(); u.size = Files.size(u.file); update(u); } else { if (!isFCH) // if this is a new fch for reading removeTempPathForEntry(tmpfile); } } }; } finally { endRead(); } } // the outstanding input streams that need to be closed private Set streams = Collections.synchronizedSet(new HashSet()); // the ex-channel and ex-path that need to close when their outstanding // input streams are all closed by the obtainers. private Set exChClosers = new HashSet<>(); private Set tmppaths = Collections.synchronizedSet(new HashSet()); private Path getTempPathForEntry(byte[] path) throws IOException { Path tmpPath = createTempFileInSameDirectoryAs(zfpath); if (path != null) { Entry e = getEntry0(path); if (e != null) { try (InputStream is = newInputStream(path)) { Files.copy(is, tmpPath, REPLACE_EXISTING); } } } return tmpPath; } private void removeTempPathForEntry(Path path) throws IOException { Files.delete(path); tmppaths.remove(path); } // check if all parents really exit. ZIP spec does not require // the existence of any "parent directory". private void checkParents(byte[] path) throws IOException { beginRead(); try { while ((path = getParent(path)) != null && path.length != 0) { if (!inodes.containsKey(IndexNode.keyOf(path))) { throw new NoSuchFileException(getString(path)); } } } finally { endRead(); } } private static byte[] ROOTPATH = new byte[0]; private static byte[] getParent(byte[] path) { int off = path.length - 1; if (off > 0 && path[off] == '/') // isDirectory off--; while (off > 0 && path[off] != '/') { off--; } if (off <= 0) return ROOTPATH; return Arrays.copyOf(path, off + 1); } private final void beginWrite() { rwlock.writeLock().lock(); } private final void endWrite() { rwlock.writeLock().unlock(); } private final void beginRead() { rwlock.readLock().lock(); } private final void endRead() { rwlock.readLock().unlock(); } /////////////////////////////////////////////////////////////////// private volatile boolean isOpen = true; private final SeekableByteChannel ch; // channel to the zipfile final byte[] cen; // CEN & ENDHDR private END end; private long locpos; // position of first LOC header (usually 0) private final ReadWriteLock rwlock = new ReentrantReadWriteLock(); // name -> pos (in cen), IndexNode itself can be used as a "key" private LinkedHashMap inodes; final byte[] getBytes(String name) { return zc.getBytes(name); } final String getString(byte[] name) { return zc.toString(name); } protected void finalize() throws IOException { close(); } private long getDataPos(Entry e) throws IOException { if (e.locoff == -1) { Entry e2 = getEntry0(e.name); if (e2 == null) throw new ZipException("invalid loc for entry <" + e.name + ">"); e.locoff = e2.locoff; } byte[] buf = new byte[LOCHDR]; if (readFullyAt(buf, 0, buf.length, e.locoff) != buf.length) throw new ZipException("invalid loc for entry <" + e.name + ">"); return locpos + e.locoff + LOCHDR + LOCNAM(buf) + LOCEXT(buf); } // Reads len bytes of data from the specified offset into buf. // Returns the total number of bytes read. // Each/every byte read from here (except the cen, which is mapped). final long readFullyAt(byte[] buf, int off, long len, long pos) throws IOException { ByteBuffer bb = ByteBuffer.wrap(buf); bb.position(off); bb.limit((int)(off + len)); return readFullyAt(bb, pos); } private final long readFullyAt(ByteBuffer bb, long pos) throws IOException { synchronized(ch) { return ch.position(pos).read(bb); } } // Searches for end of central directory (END) header. The contents of // the END header will be read and placed in endbuf. Returns the file // position of the END header, otherwise returns -1 if the END header // was not found or an error occurred. private END findEND() throws IOException { byte[] buf = new byte[READBLOCKSZ]; long ziplen = ch.size(); long minHDR = (ziplen - END_MAXLEN) > 0 ? ziplen - END_MAXLEN : 0; long minPos = minHDR - (buf.length - ENDHDR); for (long pos = ziplen - buf.length; pos >= minPos; pos -= (buf.length - ENDHDR)) { int off = 0; if (pos < 0) { // Pretend there are some NUL bytes before start of file off = (int)-pos; Arrays.fill(buf, 0, off, (byte)0); } int len = buf.length - off; if (readFullyAt(buf, off, len, pos + off) != len) zerror("zip END header not found"); // Now scan the block backwards for END header signature for (int i = buf.length - ENDHDR; i >= 0; i--) { if (buf[i+0] == (byte)'P' && buf[i+1] == (byte)'K' && buf[i+2] == (byte)'\005' && buf[i+3] == (byte)'\006' && (pos + i + ENDHDR + ENDCOM(buf, i) == ziplen)) { // Found END header buf = Arrays.copyOfRange(buf, i, i + ENDHDR); END end = new END(); end.endsub = ENDSUB(buf); end.centot = ENDTOT(buf); end.cenlen = ENDSIZ(buf); end.cenoff = ENDOFF(buf); end.comlen = ENDCOM(buf); end.endpos = pos + i; if (end.cenlen == ZIP64_MINVAL || end.cenoff == ZIP64_MINVAL || end.centot == ZIP64_MINVAL32) { // need to find the zip64 end; byte[] loc64 = new byte[ZIP64_LOCHDR]; if (readFullyAt(loc64, 0, loc64.length, end.endpos - ZIP64_LOCHDR) != loc64.length) { return end; } long end64pos = ZIP64_LOCOFF(loc64); byte[] end64buf = new byte[ZIP64_ENDHDR]; if (readFullyAt(end64buf, 0, end64buf.length, end64pos) != end64buf.length) { return end; } // end64 found, re-calcualte everything. end.cenlen = ZIP64_ENDSIZ(end64buf); end.cenoff = ZIP64_ENDOFF(end64buf); end.centot = (int)ZIP64_ENDTOT(end64buf); // assume total < 2g end.endpos = end64pos; } return end; } } } zerror("zip END header not found"); return null; //make compiler happy } // Reads zip file central directory. Returns the file position of first // CEN header, otherwise returns -1 if an error occurred. If zip->msg != NULL // then the error was a zip format error and zip->msg has the error text. // Always pass in -1 for knownTotal; it's used for a recursive call. private byte[] initCEN() throws IOException { end = findEND(); if (end.endpos == 0) { inodes = new LinkedHashMap<>(10); locpos = 0; buildNodeTree(); return null; // only END header present } if (end.cenlen > end.endpos) zerror("invalid END header (bad central directory size)"); long cenpos = end.endpos - end.cenlen; // position of CEN table // Get position of first local file (LOC) header, taking into // account that there may be a stub prefixed to the zip file. locpos = cenpos - end.cenoff; if (locpos < 0) zerror("invalid END header (bad central directory offset)"); // read in the CEN and END byte[] cen = new byte[(int)(end.cenlen + ENDHDR)]; if (readFullyAt(cen, 0, cen.length, cenpos) != end.cenlen + ENDHDR) { zerror("read CEN tables failed"); } // Iterate through the entries in the central directory inodes = new LinkedHashMap<>(end.centot + 1); int pos = 0; int limit = cen.length - ENDHDR; while (pos < limit) { if (CENSIG(cen, pos) != CENSIG) zerror("invalid CEN header (bad signature)"); int method = CENHOW(cen, pos); int nlen = CENNAM(cen, pos); int elen = CENEXT(cen, pos); int clen = CENCOM(cen, pos); if ((CENFLG(cen, pos) & 1) != 0) { zerror("invalid CEN header (encrypted entry)"); } if (method != METHOD_STORED && method != METHOD_DEFLATED) { zerror("invalid CEN header (unsupported compression method: " + method + ")"); } if (pos + CENHDR + nlen > limit) { zerror("invalid CEN header (bad header size)"); } byte[] name = Arrays.copyOfRange(cen, pos + CENHDR, pos + CENHDR + nlen); IndexNode inode = new IndexNode(name, pos); inodes.put(inode, inode); // skip ext and comment pos += (CENHDR + nlen + elen + clen); } if (pos + ENDHDR != cen.length) { zerror("invalid CEN header (bad header size)"); } buildNodeTree(); return cen; } private void ensureOpen() throws IOException { if (!isOpen) throw new ClosedFileSystemException(); } // Creates a new empty temporary file in the same directory as the // specified file. A variant of Files.createTempFile. private Path createTempFileInSameDirectoryAs(Path path) throws IOException { Path parent = path.toAbsolutePath().getParent(); Path dir = (parent == null) ? path.getFileSystem().getPath(".") : parent; Path tmpPath = Files.createTempFile(dir, "zipfstmp", null); tmppaths.add(tmpPath); return tmpPath; } ////////////////////update & sync ////////////////////////////////////// private boolean hasUpdate = false; // shared key. consumer guarantees the "writeLock" before use it. private final IndexNode LOOKUPKEY = IndexNode.keyOf(null); private void updateDelete(IndexNode inode) { beginWrite(); try { removeFromTree(inode); inodes.remove(inode); hasUpdate = true; } finally { endWrite(); } } private void update(Entry e) { beginWrite(); try { IndexNode old = inodes.put(e, e); if (old != null) { removeFromTree(old); } if (e.type == Entry.NEW || e.type == Entry.FILECH || e.type == Entry.COPY) { IndexNode parent = inodes.get(LOOKUPKEY.as(getParent(e.name))); e.sibling = parent.child; parent.child = e; } hasUpdate = true; } finally { endWrite(); } } // copy over the whole LOC entry (header if necessary, data and ext) from // old zip to the new one. private long copyLOCEntry(Entry e, boolean updateHeader, OutputStream os, long written, byte[] buf) throws IOException { long locoff = e.locoff; // where to read e.locoff = written; // update the e.locoff with new value // calculate the size need to write out long size = 0; // if there is A ext if ((e.flag & FLAG_DATADESCR) != 0) { if (e.size >= ZIP64_MINVAL || e.csize >= ZIP64_MINVAL) size = 24; else size = 16; } // read loc, use the original loc.elen/nlen if (readFullyAt(buf, 0, LOCHDR , locoff) != LOCHDR) throw new ZipException("loc: reading failed"); if (updateHeader) { locoff += LOCHDR + LOCNAM(buf) + LOCEXT(buf); // skip header size += e.csize; written = e.writeLOC(os) + size; } else { os.write(buf, 0, LOCHDR); // write out the loc header locoff += LOCHDR; // use e.csize, LOCSIZ(buf) is zero if FLAG_DATADESCR is on // size += LOCNAM(buf) + LOCEXT(buf) + LOCSIZ(buf); size += LOCNAM(buf) + LOCEXT(buf) + e.csize; written = LOCHDR + size; } int n; while (size > 0 && (n = (int)readFullyAt(buf, 0, buf.length, locoff)) != -1) { if (size < n) n = (int)size; os.write(buf, 0, n); size -= n; 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); } } } if (!hasUpdate) return; Path tmpFile = createTempFileInSameDirectoryAs(zfpath); try (OutputStream os = new BufferedOutputStream(Files.newOutputStream(tmpFile, WRITE))) { ArrayList elist = new ArrayList<>(inodes.size()); long written = 0; byte[] buf = new byte[8192]; Entry e = null; // write loc for (IndexNode inode : inodes.values()) { if (inode instanceof Entry) { // an updated inode e = (Entry)inode; try { if (e.type == Entry.COPY) { // entry copy: the only thing changed is the "name" // and "nlen" in LOC header, so we udpate/rewrite the // LOC in new file and simply copy the rest (data and // ext) without enflating/deflating from the old zip // 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 } } elist.add(e); } catch (IOException x) { x.printStackTrace(); // skip any in-accurate entry } } else { // unchanged inode if (inode.pos == -1) { continue; // pseudo directory node } e = Entry.readCEN(this, inode.pos); try { written += copyLOCEntry(e, false, os, written, buf); elist.add(e); } catch (IOException x) { x.printStackTrace(); // skip any wrong entry } } } // now write back the cen and end table end.cenoff = written; for (Entry entry : elist) { written += entry.writeCEN(os); } end.centot = elist.size(); end.cenlen = written - end.cenoff; end.write(os, written); } 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()); } else { ch.close(); Files.delete(zfpath); } Files.move(tmpFile, zfpath, REPLACE_EXISTING); hasUpdate = false; // clear /* if (isOpen) { ch = zfpath.newByteChannel(READ); // re-fresh "ch" and "cen" cen = initCEN(); } */ //System.out.printf("->sync(%s) done!%n", toString()); } private IndexNode getInode(byte[] path) { if (path == null) throw new NullPointerException("path"); IndexNode key = IndexNode.keyOf(path); IndexNode inode = inodes.get(key); if (inode == null && (path.length == 0 || path[path.length -1] != '/')) { // if does not ends with a slash path = Arrays.copyOf(path, path.length + 1); path[path.length - 1] = '/'; inode = inodes.get(key.as(path)); } return inode; } private Entry getEntry0(byte[] path) throws IOException { IndexNode inode = getInode(path); if (inode instanceof Entry) return (Entry)inode; if (inode == null || inode.pos == -1) return null; return Entry.readCEN(this, inode.pos); } public void deleteFile(byte[] path, boolean failIfNotExists) throws IOException { checkWritable(); IndexNode inode = getInode(path); if (inode == null) { if (path != null && path.length == 0) throw new ZipException("root directory can't not be delete"); if (failIfNotExists) throw new NoSuchFileException(getString(path)); } else { if (inode.isDir() && inode.child != null) 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; if (zc.isUTF8()) e.flag |= FLAG_EFS; OutputStream os; if (useTempFile) { e.file = getTempPathForEntry(null); os = Files.newOutputStream(e.file, WRITE); } else { os = new ByteArrayOutputStream((e.size > 0)? (int)e.size : 8192); } return new EntryOutputStream(e, os); } private InputStream getInputStream(Entry e) throws IOException { InputStream eis = null; if (e.type == Entry.NEW) { if (e.bytes != null) eis = new ByteArrayInputStream(e.bytes); else if (e.file != null) eis = 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); // TBD: wrap to hook close() // streams.add(eis); return eis; } else { // untouced CEN or COPY eis = new EntryInputStream(e, ch); } if (e.method == METHOD_DEFLATED) { // MORE: Compute good size for inflater stream: long bufSize = e.size + 2; // Inflater likes a bit of slack if (bufSize > 65536) bufSize = 8192; final long size = e.size; eis = new InflaterInputStream(eis, getInflater(), (int)bufSize) { private boolean isClosed = false; public void close() throws IOException { if (!isClosed) { releaseInflater(inf); this.in.close(); isClosed = true; streams.remove(this); } } // Override fill() method to provide an extra "dummy" byte // at the end of the input stream. This is required when // using the "nowrap" Inflater option. (it appears the new // zlib in 7 does not need it, but keep it for now) protected void fill() throws IOException { if (eof) { throw new EOFException( "Unexpected end of ZLIB input stream"); } len = this.in.read(buf, 0, buf.length); if (len == -1) { buf[0] = 0; len = 1; eof = true; } inf.setInput(buf, 0, len); } private boolean eof; public int available() throws IOException { if (isClosed) return 0; long avail = size - inf.getBytesWritten(); return avail > (long) Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) avail; } }; } else if (e.method == METHOD_STORED) { // TBD: wrap/ it does not seem necessary } else { throw new ZipException("invalid compression method"); } streams.add(eis); return eis; } // Inner class implementing the input stream used to read // a (possibly compressed) zip file entry. private class EntryInputStream extends InputStream { private final SeekableByteChannel zfch; // local ref to zipfs's "ch". zipfs.ch might // point to a new channel after sync() private long pos; // current position within entry data protected long rem; // number of remaining bytes within entry protected final long size; // uncompressed size of this entry EntryInputStream(Entry e, SeekableByteChannel zfch) throws IOException { this.zfch = zfch; rem = e.csize; size = e.size; pos = getDataPos(e); } public int read(byte b[], int off, int len) throws IOException { ensureOpen(); if (rem == 0) { return -1; } if (len <= 0) { return 0; } if (len > rem) { len = (int) rem; } // readFullyAt() long n = 0; ByteBuffer bb = ByteBuffer.wrap(b); bb.position(off); bb.limit(off + len); synchronized(zfch) { n = zfch.position(pos).read(bb); } if (n > 0) { pos += n; rem -= n; } if (rem == 0) { close(); } return (int)n; } public int read() throws IOException { byte[] b = new byte[1]; if (read(b, 0, 1) == 1) { return b[0] & 0xff; } else { return -1; } } public long skip(long n) throws IOException { ensureOpen(); if (n > rem) n = rem; pos += n; rem -= n; if (rem == 0) { close(); } return n; } public int available() { return rem > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) rem; } public long size() { return size; } public void close() { rem = 0; streams.remove(this); } } 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 private final int MAX_FLATER = 20; // List of available Inflater objects for decompression private final List inflaters = new ArrayList<>(); // Gets an inflater from the list of available inflaters or allocates // a new one. private Inflater getInflater() { synchronized (inflaters) { int size = inflaters.size(); if (size > 0) { Inflater inf = inflaters.remove(size - 1); return inf; } else { return new Inflater(true); } } } // Releases the specified inflater to the list of available inflaters. private void releaseInflater(Inflater inf) { synchronized (inflaters) { if (inflaters.size() < MAX_FLATER) { inf.reset(); inflaters.add(inf); } else { inf.end(); } } } // List of available Deflater objects for compression private final List deflaters = new ArrayList<>(); // Gets an deflater from the list of available deflaters or allocates // a new one. private Deflater getDeflater() { synchronized (deflaters) { int size = deflaters.size(); if (size > 0) { Deflater def = deflaters.remove(size - 1); return def; } else { return new Deflater(Deflater.DEFAULT_COMPRESSION, true); } } } // Releases the specified inflater to the list of available inflaters. private void releaseDeflater(Deflater def) { synchronized (deflaters) { if (inflaters.size() < MAX_FLATER) { def.reset(); deflaters.add(def); } else { def.end(); } } } // End of central directory record static class END { int disknum; int sdisknum; int endsub; // endsub int centot; // 4 bytes long cenlen; // 4 bytes long cenoff; // 4 bytes int comlen; // comment length byte[] comment; /* members of Zip64 end of central directory locator */ int diskNum; long endpos; int disktot; void write(OutputStream os, long offset) throws IOException { boolean hasZip64 = false; long xlen = cenlen; long xoff = cenoff; if (xlen >= ZIP64_MINVAL) { xlen = ZIP64_MINVAL; hasZip64 = true; } if (xoff >= ZIP64_MINVAL) { xoff = ZIP64_MINVAL; hasZip64 = true; } int count = centot; if (count >= ZIP64_MINVAL32) { count = ZIP64_MINVAL32; hasZip64 = true; } if (hasZip64) { long off64 = offset; //zip64 end of central directory record writeInt(os, ZIP64_ENDSIG); // zip64 END record signature writeLong(os, ZIP64_ENDHDR - 12); // size of zip64 end writeShort(os, 45); // version made by writeShort(os, 45); // version needed to extract writeInt(os, 0); // number of this disk writeInt(os, 0); // central directory start disk writeLong(os, centot); // number of directory entires on disk writeLong(os, centot); // number of directory entires writeLong(os, cenlen); // length of central directory writeLong(os, cenoff); // offset of central directory //zip64 end of central directory locator writeInt(os, ZIP64_LOCSIG); // zip64 END locator signature writeInt(os, 0); // zip64 END start disk writeLong(os, off64); // offset of zip64 END writeInt(os, 1); // total number of disks (?) } writeInt(os, ENDSIG); // END record signature writeShort(os, 0); // number of this disk writeShort(os, 0); // central directory start disk writeShort(os, count); // number of directory entries on disk writeShort(os, count); // total number of directory entries writeInt(os, xlen); // length of central directory writeInt(os, xoff); // offset of central directory if (comment != null) { // zip file comment writeShort(os, comment.length); writeBytes(os, comment); } else { writeShort(os, 0); } } } // Internal node that links a "name" to its pos in cen table. // The node itself can be used as a "key" to lookup itself in // the HashMap inodes. static class IndexNode { byte[] name; int hashcode; // node is hashable/hashed by its name int pos = -1; // position in cen table, -1 menas the // entry does not exists in zip file IndexNode(byte[] name, int pos) { name(name); this.pos = pos; } final static IndexNode keyOf(byte[] name) { // get a lookup key; return new IndexNode(name, -1); } final void name(byte[] name) { this.name = name; this.hashcode = Arrays.hashCode(name); } final IndexNode as(byte[] name) { // reuse the node, mostly name(name); // as a lookup "key" return this; } boolean isDir() { return name != null && (name.length == 0 || name[name.length - 1] == '/'); } public boolean equals(Object other) { if (!(other instanceof IndexNode)) { return false; } return Arrays.equals(name, ((IndexNode)other).name); } public int hashCode() { return hashcode; } IndexNode() {} IndexNode sibling; IndexNode child; // 1st child } static class Entry extends IndexNode { static final int CEN = 1; // entry read from cen static final int NEW = 2; // updated contents in bytes or file static final int FILECH = 3; // fch update in "file" static final int COPY = 4; // copy of a CEN entry byte[] bytes; // updated content bytes Path file; // use tmp file to store bytes; int type = CEN; // default is the entry read from cen // entry attributes int version; int flag; int method = -1; // compression method long mtime = -1; // last modification time (in DOS time) long atime = -1; // last access time long ctime = -1; // create time long crc = -1; // crc-32 of entry data long csize = -1; // compressed size of entry data long size = -1; // uncompressed size of entry data byte[] extra; // cen int versionMade; int disk; int attrs; long attrsEx; long locoff; byte[] comment; Entry() {} Entry(byte[] name) { name(name); this.mtime = this.ctime = this.atime = System.currentTimeMillis(); this.crc = 0; this.size = 0; this.csize = 0; this.method = METHOD_DEFLATED; } Entry(byte[] name, int type) { this(name); this.type = type; } Entry (Entry e, int type) { name(e.name); this.version = e.version; this.ctime = e.ctime; this.atime = e.atime; this.mtime = e.mtime; this.crc = e.crc; this.size = e.size; this.csize = e.csize; this.method = e.method; this.extra = e.extra; this.versionMade = e.versionMade; this.disk = e.disk; this.attrs = e.attrs; this.attrsEx = e.attrsEx; this.locoff = e.locoff; this.comment = e.comment; this.type = type; } Entry (byte[] name, Path file, int type) { this(name, type); this.file = file; this.method = METHOD_STORED; } int version() throws ZipException { if (method == METHOD_DEFLATED) return 20; else if (method == METHOD_STORED) return 10; throw new ZipException("unsupported compression method"); } ///////////////////// CEN ////////////////////// static Entry readCEN(ZipFileSystem zipfs, int pos) throws IOException { return new Entry().cen(zipfs, pos); } private Entry cen(ZipFileSystem zipfs, int pos) throws IOException { byte[] cen = zipfs.cen; if (CENSIG(cen, pos) != CENSIG) zerror("invalid CEN header (bad signature)"); versionMade = CENVEM(cen, pos); version = CENVER(cen, pos); flag = CENFLG(cen, pos); method = CENHOW(cen, pos); mtime = dosToJavaTime(CENTIM(cen, pos)); crc = CENCRC(cen, pos); csize = CENSIZ(cen, pos); size = CENLEN(cen, pos); int nlen = CENNAM(cen, pos); int elen = CENEXT(cen, pos); int clen = CENCOM(cen, pos); disk = CENDSK(cen, pos); attrs = CENATT(cen, pos); attrsEx = CENATX(cen, pos); locoff = CENOFF(cen, pos); pos += CENHDR; name(Arrays.copyOfRange(cen, pos, pos + nlen)); pos += nlen; if (elen > 0) { extra = Arrays.copyOfRange(cen, pos, pos + elen); pos += elen; readExtra(zipfs); } if (clen > 0) { comment = Arrays.copyOfRange(cen, pos, pos + clen); } return this; } int writeCEN(OutputStream os) throws IOException { int written = CENHDR; int version0 = version(); long csize0 = csize; long size0 = size; long locoff0 = locoff; int elen64 = 0; // extra for ZIP64 int elenNTFS = 0; // extra for NTFS (a/c/mtime) int elenEXTT = 0; // extra for Extended Timestamp boolean foundExtraTime = false; // if time stamp NTFS, EXTT present // confirm size/length int nlen = (name != null) ? name.length : 0; int elen = (extra != null) ? extra.length : 0; int eoff = 0; int clen = (comment != null) ? comment.length : 0; if (csize >= ZIP64_MINVAL) { csize0 = ZIP64_MINVAL; elen64 += 8; // csize(8) } if (size >= ZIP64_MINVAL) { size0 = ZIP64_MINVAL; // size(8) elen64 += 8; } if (locoff >= ZIP64_MINVAL) { locoff0 = ZIP64_MINVAL; elen64 += 8; // offset(8) } if (elen64 != 0) { elen64 += 4; // header and data sz 4 bytes } while (eoff + 4 < elen) { int tag = SH(extra, eoff); int sz = SH(extra, eoff + 2); if (tag == EXTID_EXTT || tag == EXTID_NTFS) { foundExtraTime = true; } eoff += (4 + sz); } if (!foundExtraTime) { if (isWindows) { // use NTFS elenNTFS = 36; // total 36 bytes } else { // Extended Timestamp otherwise elenEXTT = 9; // only mtime in cen } } writeInt(os, CENSIG); // CEN header signature if (elen64 != 0) { writeShort(os, 45); // ver 4.5 for zip64 writeShort(os, 45); } else { writeShort(os, version0); // version made by writeShort(os, version0); // version needed to extract } writeShort(os, flag); // general purpose bit flag writeShort(os, method); // compression method // last modification time writeInt(os, (int)javaToDosTime(mtime)); writeInt(os, crc); // crc-32 writeInt(os, csize0); // compressed size writeInt(os, size0); // uncompressed size writeShort(os, name.length); writeShort(os, elen + elen64 + elenNTFS + elenEXTT); if (comment != null) { writeShort(os, Math.min(clen, 0xffff)); } else { writeShort(os, 0); } writeShort(os, 0); // starting disk number writeShort(os, 0); // internal file attributes (unused) writeInt(os, 0); // external file attributes (unused) writeInt(os, locoff0); // relative offset of local header writeBytes(os, name); if (elen64 != 0) { writeShort(os, EXTID_ZIP64);// Zip64 extra writeShort(os, elen64 - 4); // size of "this" extra block if (size0 == ZIP64_MINVAL) writeLong(os, size); if (csize0 == ZIP64_MINVAL) writeLong(os, csize); if (locoff0 == ZIP64_MINVAL) writeLong(os, locoff); } if (elenNTFS != 0) { writeShort(os, EXTID_NTFS); writeShort(os, elenNTFS - 4); writeInt(os, 0); // reserved writeShort(os, 0x0001); // NTFS attr tag writeShort(os, 24); writeLong(os, javaToWinTime(mtime)); writeLong(os, javaToWinTime(atime)); writeLong(os, javaToWinTime(ctime)); } if (elenEXTT != 0) { writeShort(os, EXTID_EXTT); writeShort(os, elenEXTT - 4); if (ctime == -1) os.write(0x3); // mtime and atime else os.write(0x7); // mtime, atime and ctime writeInt(os, javaToUnixTime(mtime)); } if (extra != null) // whatever not recognized writeBytes(os, extra); if (comment != null) //TBD: 0, Math.min(commentBytes.length, 0xffff)); writeBytes(os, comment); return CENHDR + nlen + elen + clen + elen64 + elenNTFS + elenEXTT; } ///////////////////// LOC ////////////////////// static Entry readLOC(ZipFileSystem zipfs, long pos) throws IOException { return readLOC(zipfs, pos, new byte[1024]); } static Entry readLOC(ZipFileSystem zipfs, long pos, byte[] buf) throws IOException { return new Entry().loc(zipfs, pos, buf); } Entry loc(ZipFileSystem zipfs, long pos, byte[] buf) throws IOException { assert (buf.length >= LOCHDR); if (zipfs.readFullyAt(buf, 0, LOCHDR , pos) != LOCHDR) throw new ZipException("loc: reading failed"); if (LOCSIG(buf) != LOCSIG) throw new ZipException("loc: wrong sig ->" + Long.toString(LOCSIG(buf), 16)); //startPos = pos; version = LOCVER(buf); flag = LOCFLG(buf); method = LOCHOW(buf); mtime = dosToJavaTime(LOCTIM(buf)); crc = LOCCRC(buf); csize = LOCSIZ(buf); size = LOCLEN(buf); int nlen = LOCNAM(buf); int elen = LOCEXT(buf); name = new byte[nlen]; if (zipfs.readFullyAt(name, 0, nlen, pos + LOCHDR) != nlen) { throw new ZipException("loc: name reading failed"); } if (elen > 0) { extra = new byte[elen]; if (zipfs.readFullyAt(extra, 0, elen, pos + LOCHDR + nlen) != elen) { throw new ZipException("loc: ext reading failed"); } } pos += (LOCHDR + nlen + elen); if ((flag & FLAG_DATADESCR) != 0) { // Data Descriptor Entry e = zipfs.getEntry0(name); // get the size/csize from cen if (e == null) throw new ZipException("loc: name not found in cen"); size = e.size; csize = e.csize; pos += (method == METHOD_STORED ? size : csize); if (size >= ZIP64_MINVAL || csize >= ZIP64_MINVAL) pos += 24; else pos += 16; } else { if (extra != null && (size == ZIP64_MINVAL || csize == ZIP64_MINVAL)) { // zip64 ext: must include both size and csize int off = 0; while (off + 20 < elen) { // HeaderID+DataSize+Data int sz = SH(extra, off + 2); if (SH(extra, off) == EXTID_ZIP64 && sz == 16) { size = LL(extra, off + 4); csize = LL(extra, off + 12); break; } off += (sz + 4); } } pos += (method == METHOD_STORED ? size : csize); } return this; } int writeLOC(OutputStream os) throws IOException { writeInt(os, LOCSIG); // LOC header signature int version = version(); int nlen = (name != null) ? name.length : 0; int elen = (extra != null) ? extra.length : 0; boolean foundExtraTime = false; // if extra timestamp present int eoff = 0; int elen64 = 0; int elenEXTT = 0; int elenNTFS = 0; if ((flag & FLAG_DATADESCR) != 0) { writeShort(os, version()); // version needed to extract writeShort(os, flag); // general purpose bit flag writeShort(os, method); // compression method // last modification time writeInt(os, (int)javaToDosTime(mtime)); // store size, uncompressed size, and crc-32 in data descriptor // immediately following compressed entry data writeInt(os, 0); writeInt(os, 0); writeInt(os, 0); } else { if (csize >= ZIP64_MINVAL || size >= ZIP64_MINVAL) { elen64 = 20; //headid(2) + size(2) + size(8) + csize(8) writeShort(os, 45); // ver 4.5 for zip64 } else { writeShort(os, version()); // version needed to extract } writeShort(os, flag); // general purpose bit flag writeShort(os, method); // compression method // last modification time writeInt(os, (int)javaToDosTime(mtime)); writeInt(os, crc); // crc-32 if (elen64 != 0) { writeInt(os, ZIP64_MINVAL); writeInt(os, ZIP64_MINVAL); } else { writeInt(os, csize); // compressed size writeInt(os, size); // uncompressed size } } while (eoff + 4 < elen) { int tag = SH(extra, eoff); int sz = SH(extra, eoff + 2); if (tag == EXTID_EXTT || tag == EXTID_NTFS) { foundExtraTime = true; } eoff += (4 + sz); } if (!foundExtraTime) { if (isWindows) { elenNTFS = 36; // NTFS, total 36 bytes } else { // on unix use "ext time" elenEXTT = 9; if (atime != -1) elenEXTT += 4; if (ctime != -1) elenEXTT += 4; } } writeShort(os, name.length); writeShort(os, elen + elen64 + elenNTFS + elenEXTT); writeBytes(os, name); if (elen64 != 0) { writeShort(os, EXTID_ZIP64); writeShort(os, 16); writeLong(os, size); writeLong(os, csize); } if (elenNTFS != 0) { writeShort(os, EXTID_NTFS); writeShort(os, elenNTFS - 4); writeInt(os, 0); // reserved writeShort(os, 0x0001); // NTFS attr tag writeShort(os, 24); writeLong(os, javaToWinTime(mtime)); writeLong(os, javaToWinTime(atime)); writeLong(os, javaToWinTime(ctime)); } if (elenEXTT != 0) { writeShort(os, EXTID_EXTT); writeShort(os, elenEXTT - 4);// size for the folowing data block int fbyte = 0x1; if (atime != -1) // mtime and atime fbyte |= 0x2; if (ctime != -1) // mtime, atime and ctime fbyte |= 0x4; os.write(fbyte); // flags byte writeInt(os, javaToUnixTime(mtime)); if (atime != -1) writeInt(os, javaToUnixTime(atime)); if (ctime != -1) writeInt(os, javaToUnixTime(ctime)); } if (extra != null) { writeBytes(os, extra); } return LOCHDR + name.length + elen + elen64 + elenNTFS + elenEXTT; } // Data Descriptior int writeEXT(OutputStream os) throws IOException { writeInt(os, EXTSIG); // EXT header signature writeInt(os, crc); // crc-32 if (csize >= ZIP64_MINVAL || size >= ZIP64_MINVAL) { writeLong(os, csize); writeLong(os, size); return 24; } else { writeInt(os, csize); // compressed size writeInt(os, size); // uncompressed size return 16; } } // read NTFS, UNIX and ZIP64 data from cen.extra void readExtra(ZipFileSystem zipfs) throws IOException { if (extra == null) return; int elen = extra.length; int off = 0; int newOff = 0; while (off + 4 < elen) { // extra spec: HeaderID+DataSize+Data int pos = off; int tag = SH(extra, pos); int sz = SH(extra, pos + 2); pos += 4; if (pos + sz > elen) // invalid data break; switch (tag) { case EXTID_ZIP64 : if (size == ZIP64_MINVAL) { if (pos + 8 > elen) // invalid zip64 extra break; // fields, just skip size = LL(extra, pos); pos += 8; } if (csize == ZIP64_MINVAL) { if (pos + 8 > elen) break; csize = LL(extra, pos); pos += 8; } if (locoff == ZIP64_MINVAL) { if (pos + 8 > elen) break; locoff = LL(extra, pos); pos += 8; } break; case EXTID_NTFS: pos += 4; // reserved 4 bytes if (SH(extra, pos) != 0x0001) break; if (SH(extra, pos + 2) != 24) break; // override the loc field, datatime here is // more "accurate" mtime = winToJavaTime(LL(extra, pos + 4)); atime = winToJavaTime(LL(extra, pos + 12)); ctime = winToJavaTime(LL(extra, pos + 20)); break; case EXTID_EXTT: // spec says the Extened timestamp in cen only has mtime // need to read the loc to get the extra a/ctime byte[] buf = new byte[LOCHDR]; if (zipfs.readFullyAt(buf, 0, buf.length , locoff) != buf.length) throw new ZipException("loc: reading failed"); if (LOCSIG(buf) != LOCSIG) throw new ZipException("loc: wrong sig ->" + Long.toString(LOCSIG(buf), 16)); int locElen = LOCEXT(buf); if (locElen < 9) // EXTT is at lease 9 bytes break; int locNlen = LOCNAM(buf); buf = new byte[locElen]; if (zipfs.readFullyAt(buf, 0, buf.length , locoff + LOCHDR + locNlen) != buf.length) throw new ZipException("loc extra: reading failed"); int locPos = 0; while (locPos + 4 < buf.length) { int locTag = SH(buf, locPos); int locSZ = SH(buf, locPos + 2); locPos += 4; if (locTag != EXTID_EXTT) { locPos += locSZ; continue; } int flag = CH(buf, locPos++); if ((flag & 0x1) != 0) { mtime = unixToJavaTime(LG(buf, locPos)); locPos += 4; } if ((flag & 0x2) != 0) { atime = unixToJavaTime(LG(buf, locPos)); locPos += 4; } if ((flag & 0x4) != 0) { ctime = unixToJavaTime(LG(buf, locPos)); locPos += 4; } break; } break; default: // unknown tag System.arraycopy(extra, off, extra, newOff, sz + 4); newOff += (sz + 4); } off += (sz + 4); } if (newOff != 0 && newOff != extra.length) extra = Arrays.copyOf(extra, newOff); else extra = null; } } private static class ExChannelCloser { Path path; SeekableByteChannel ch; Set streams; ExChannelCloser(Path path, SeekableByteChannel ch, Set 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. // A possible solution is to build the node tree ourself as // implemented below. private IndexNode root; private void addToTree(IndexNode inode, HashSet dirs) { if (dirs.contains(inode)) { return; } IndexNode parent; byte[] name = inode.name; byte[] pname = getParent(name); if (inodes.containsKey(LOOKUPKEY.as(pname))) { parent = inodes.get(LOOKUPKEY); } else { // pseudo directory entry parent = new IndexNode(pname, -1); inodes.put(parent, parent); } addToTree(parent, dirs); inode.sibling = parent.child; parent.child = inode; if (name[name.length -1] == '/') dirs.add(inode); } private void removeFromTree(IndexNode inode) { IndexNode parent = inodes.get(LOOKUPKEY.as(getParent(inode.name))); IndexNode child = parent.child; if (child.equals(inode)) { parent.child = child.sibling; } else { IndexNode last = child; while ((child = child.sibling) != null) { if (child.equals(inode)) { last.sibling = child.sibling; break; } else { last = child; } } } } private void buildNodeTree() throws IOException { beginWrite(); try { HashSet dirs = new HashSet<>(); IndexNode root = new IndexNode(ROOTPATH, -1); inodes.put(root, root); dirs.add(root); for (IndexNode node : inodes.keySet().toArray(new IndexNode[0])) { addToTree(node, dirs); } } finally { endWrite(); } } }