diff -r 024ed9c9ed13 -r 83c19f00452c langtools/src/jdk.compiler/share/classes/com/sun/tools/javac/file/ZipFileIndex.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/langtools/src/jdk.compiler/share/classes/com/sun/tools/javac/file/ZipFileIndex.java Sun Aug 17 15:52:32 2014 +0100 @@ -0,0 +1,1168 @@ +/* + * Copyright (c) 2007, 2012, 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 com.sun.tools.javac.file; + + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; +import java.util.zip.ZipException; + +import com.sun.tools.javac.file.RelativePath.RelativeDirectory; +import com.sun.tools.javac.file.RelativePath.RelativeFile; + +/** + * This class implements the building of index of a zip archive and access to + * its context. It also uses a prebuilt index if available. + * It supports invocations where it will serialize an optimized zip index file + * to disk. + * + * In order to use a secondary index file, set "usezipindex" in the Options + * object when JavacFileManager is invoked. (You can pass "-XDusezipindex" on + * the command line.) + * + * Location where to look for/generate optimized zip index files can be + * provided using "{@code -XDcachezipindexdir=}". If this flag is not + * provided, the default location is the value of the "java.io.tmpdir" system + * property. + * + * If "-XDwritezipindexfiles" is specified, there will be new optimized index + * file created for each archive, used by the compiler for compilation, at the + * location specified by the "cachezipindexdir" option. + * + * If system property nonBatchMode option is specified the compiler will use + * timestamp checking to reindex the zip files if it is needed. In batch mode + * the timestamps are not checked and the compiler uses the cached indexes. + * + *

This is NOT part of any supported API. + * If you write code that depends on this, you do so at your own risk. + * This code and its internal interfaces are subject to change or + * deletion without notice. + */ +public class ZipFileIndex { + private static final String MIN_CHAR = String.valueOf(Character.MIN_VALUE); + private static final String MAX_CHAR = String.valueOf(Character.MAX_VALUE); + + public final static long NOT_MODIFIED = Long.MIN_VALUE; + + + private static final boolean NON_BATCH_MODE = System.getProperty("nonBatchMode") != null;// TODO: Use -XD compiler switch for this. + + private Map directories = + Collections.emptyMap(); + private Set allDirs = + Collections.emptySet(); + + // ZipFileIndex data entries + final File zipFile; + private Reference absFileRef; + long zipFileLastModified = NOT_MODIFIED; + private RandomAccessFile zipRandomFile; + private Entry[] entries; + + private boolean readFromIndex = false; + private File zipIndexFile = null; + private boolean triedToReadIndex = false; + final RelativeDirectory symbolFilePrefix; + private final int symbolFilePrefixLength; + private boolean hasPopulatedData = false; + long lastReferenceTimeStamp = NOT_MODIFIED; + + private final boolean usePreindexedCache; + private final String preindexedCacheLocation; + + private boolean writeIndex = false; + + private Map> relativeDirectoryCache = new HashMap<>(); + + + public synchronized boolean isOpen() { + return (zipRandomFile != null); + } + + ZipFileIndex(File zipFile, RelativeDirectory symbolFilePrefix, boolean writeIndex, + boolean useCache, String cacheLocation) throws IOException { + this.zipFile = zipFile; + this.symbolFilePrefix = symbolFilePrefix; + this.symbolFilePrefixLength = (symbolFilePrefix == null ? 0 : + symbolFilePrefix.getPath().getBytes("UTF-8").length); + this.writeIndex = writeIndex; + this.usePreindexedCache = useCache; + this.preindexedCacheLocation = cacheLocation; + + if (zipFile != null) { + this.zipFileLastModified = zipFile.lastModified(); + } + + // Validate integrity of the zip file + checkIndex(); + } + + @Override + public String toString() { + return "ZipFileIndex[" + zipFile + "]"; + } + + // Just in case... + @Override + protected void finalize() throws Throwable { + closeFile(); + super.finalize(); + } + + private boolean isUpToDate() { + if (zipFile != null + && ((!NON_BATCH_MODE) || zipFileLastModified == zipFile.lastModified()) + && hasPopulatedData) { + return true; + } + + return false; + } + + /** + * Here we need to make sure that the ZipFileIndex is valid. Check the timestamp of the file and + * if its the same as the one at the time the index was build we don't need to reopen anything. + */ + private void checkIndex() throws IOException { + boolean isUpToDate = true; + if (!isUpToDate()) { + closeFile(); + isUpToDate = false; + } + + if (zipRandomFile != null || isUpToDate) { + lastReferenceTimeStamp = System.currentTimeMillis(); + return; + } + + hasPopulatedData = true; + + if (readIndex()) { + lastReferenceTimeStamp = System.currentTimeMillis(); + return; + } + + directories = Collections.emptyMap(); + allDirs = Collections.emptySet(); + + try { + openFile(); + long totalLength = zipRandomFile.length(); + ZipDirectory directory = new ZipDirectory(zipRandomFile, 0L, totalLength, this); + directory.buildIndex(); + } finally { + if (zipRandomFile != null) { + closeFile(); + } + } + + lastReferenceTimeStamp = System.currentTimeMillis(); + } + + private void openFile() throws FileNotFoundException { + if (zipRandomFile == null && zipFile != null) { + zipRandomFile = new RandomAccessFile(zipFile, "r"); + } + } + + private void cleanupState() { + // Make sure there is a valid but empty index if the file doesn't exist + entries = Entry.EMPTY_ARRAY; + directories = Collections.emptyMap(); + zipFileLastModified = NOT_MODIFIED; + allDirs = Collections.emptySet(); + } + + public synchronized void close() { + writeIndex(); + closeFile(); + } + + private void closeFile() { + if (zipRandomFile != null) { + try { + zipRandomFile.close(); + } catch (IOException ex) { + } + zipRandomFile = null; + } + } + + /** + * Returns the ZipFileIndexEntry for a path, if there is one. + */ + synchronized Entry getZipIndexEntry(RelativePath path) { + try { + checkIndex(); + DirectoryEntry de = directories.get(path.dirname()); + String lookFor = path.basename(); + return (de == null) ? null : de.getEntry(lookFor); + } + catch (IOException e) { + return null; + } + } + + /** + * Returns a javac List of filenames within a directory in the ZipFileIndex. + */ + public synchronized com.sun.tools.javac.util.List getFiles(RelativeDirectory path) { + try { + checkIndex(); + + DirectoryEntry de = directories.get(path); + com.sun.tools.javac.util.List ret = de == null ? null : de.getFiles(); + + if (ret == null) { + return com.sun.tools.javac.util.List.nil(); + } + return ret; + } + catch (IOException e) { + return com.sun.tools.javac.util.List.nil(); + } + } + + public synchronized List getDirectories(RelativeDirectory path) { + try { + checkIndex(); + + DirectoryEntry de = directories.get(path); + com.sun.tools.javac.util.List ret = de == null ? null : de.getDirectories(); + + if (ret == null) { + return com.sun.tools.javac.util.List.nil(); + } + + return ret; + } + catch (IOException e) { + return com.sun.tools.javac.util.List.nil(); + } + } + + public synchronized Set getAllDirectories() { + try { + checkIndex(); + if (allDirs == Collections.EMPTY_SET) { + allDirs = new java.util.LinkedHashSet<>(directories.keySet()); + } + + return allDirs; + } + catch (IOException e) { + return Collections.emptySet(); + } + } + + /** + * Tests if a specific path exists in the zip. This method will return true + * for file entries and directories. + * + * @param path A path within the zip. + * @return True if the path is a file or dir, false otherwise. + */ + public synchronized boolean contains(RelativePath path) { + try { + checkIndex(); + return getZipIndexEntry(path) != null; + } + catch (IOException e) { + return false; + } + } + + public synchronized boolean isDirectory(RelativePath path) throws IOException { + // The top level in a zip file is always a directory. + if (path.getPath().length() == 0) { + lastReferenceTimeStamp = System.currentTimeMillis(); + return true; + } + + checkIndex(); + return directories.get(path) != null; + } + + public synchronized long getLastModified(RelativeFile path) throws IOException { + Entry entry = getZipIndexEntry(path); + if (entry == null) + throw new FileNotFoundException(); + return entry.getLastModified(); + } + + public synchronized int length(RelativeFile path) throws IOException { + Entry entry = getZipIndexEntry(path); + if (entry == null) + throw new FileNotFoundException(); + + if (entry.isDir) { + return 0; + } + + byte[] header = getHeader(entry); + // entry is not compressed? + if (get2ByteLittleEndian(header, 8) == 0) { + return entry.compressedSize; + } else { + return entry.size; + } + } + + public synchronized byte[] read(RelativeFile path) throws IOException { + Entry entry = getZipIndexEntry(path); + if (entry == null) + throw new FileNotFoundException("Path not found in ZIP: " + path.path); + return read(entry); + } + + synchronized byte[] read(Entry entry) throws IOException { + openFile(); + byte[] result = readBytes(entry); + closeFile(); + return result; + } + + public synchronized int read(RelativeFile path, byte[] buffer) throws IOException { + Entry entry = getZipIndexEntry(path); + if (entry == null) + throw new FileNotFoundException(); + return read(entry, buffer); + } + + synchronized int read(Entry entry, byte[] buffer) + throws IOException { + int result = readBytes(entry, buffer); + return result; + } + + private byte[] readBytes(Entry entry) throws IOException { + byte[] header = getHeader(entry); + int csize = entry.compressedSize; + byte[] cbuf = new byte[csize]; + zipRandomFile.skipBytes(get2ByteLittleEndian(header, 26) + get2ByteLittleEndian(header, 28)); + zipRandomFile.readFully(cbuf, 0, csize); + + // is this compressed - offset 8 in the ZipEntry header + if (get2ByteLittleEndian(header, 8) == 0) + return cbuf; + + int size = entry.size; + byte[] buf = new byte[size]; + if (inflate(cbuf, buf) != size) + throw new ZipException("corrupted zip file"); + + return buf; + } + + /** + * + */ + private int readBytes(Entry entry, byte[] buffer) throws IOException { + byte[] header = getHeader(entry); + + // entry is not compressed? + if (get2ByteLittleEndian(header, 8) == 0) { + zipRandomFile.skipBytes(get2ByteLittleEndian(header, 26) + get2ByteLittleEndian(header, 28)); + int offset = 0; + int size = buffer.length; + while (offset < size) { + int count = zipRandomFile.read(buffer, offset, size - offset); + if (count == -1) + break; + offset += count; + } + return entry.size; + } + + int csize = entry.compressedSize; + byte[] cbuf = new byte[csize]; + zipRandomFile.skipBytes(get2ByteLittleEndian(header, 26) + get2ByteLittleEndian(header, 28)); + zipRandomFile.readFully(cbuf, 0, csize); + + int count = inflate(cbuf, buffer); + if (count == -1) + throw new ZipException("corrupted zip file"); + + return entry.size; + } + + //---------------------------------------------------------------------------- + // Zip utilities + //---------------------------------------------------------------------------- + + private byte[] getHeader(Entry entry) throws IOException { + zipRandomFile.seek(entry.offset); + byte[] header = new byte[30]; + zipRandomFile.readFully(header); + if (get4ByteLittleEndian(header, 0) != 0x04034b50) + throw new ZipException("corrupted zip file"); + if ((get2ByteLittleEndian(header, 6) & 1) != 0) + throw new ZipException("encrypted zip file"); // offset 6 in the header of the ZipFileEntry + return header; + } + + /* + * Inflate using the java.util.zip.Inflater class + */ + private SoftReference inflaterRef; + private int inflate(byte[] src, byte[] dest) { + Inflater inflater = (inflaterRef == null ? null : inflaterRef.get()); + + // construct the inflater object or reuse an existing one + if (inflater == null) + inflaterRef = new SoftReference<>(inflater = new Inflater(true)); + + inflater.reset(); + inflater.setInput(src); + try { + return inflater.inflate(dest); + } catch (DataFormatException ex) { + return -1; + } + } + + /** + * return the two bytes buf[pos], buf[pos+1] as an unsigned integer in little + * endian format. + */ + private static int get2ByteLittleEndian(byte[] buf, int pos) { + return (buf[pos] & 0xFF) + ((buf[pos+1] & 0xFF) << 8); + } + + /** + * return the 4 bytes buf[i..i+3] as an integer in little endian format. + */ + private static int get4ByteLittleEndian(byte[] buf, int pos) { + return (buf[pos] & 0xFF) + ((buf[pos + 1] & 0xFF) << 8) + + ((buf[pos + 2] & 0xFF) << 16) + ((buf[pos + 3] & 0xFF) << 24); + } + + /* ---------------------------------------------------------------------------- + * ZipDirectory + * ----------------------------------------------------------------------------*/ + + private class ZipDirectory { + private RelativeDirectory lastDir; + private int lastStart; + private int lastLen; + + byte[] zipDir; + RandomAccessFile zipRandomFile = null; + ZipFileIndex zipFileIndex = null; + + public ZipDirectory(RandomAccessFile zipRandomFile, long start, long end, ZipFileIndex index) throws IOException { + this.zipRandomFile = zipRandomFile; + this.zipFileIndex = index; + hasValidHeader(); + findCENRecord(start, end); + } + + /* + * the zip entry signature should be at offset 0, otherwise allow the + * calling logic to take evasive action by throwing ZipFormatException. + */ + private boolean hasValidHeader() throws IOException { + final long pos = zipRandomFile.getFilePointer(); + try { + if (zipRandomFile.read() == 'P') { + if (zipRandomFile.read() == 'K') { + if (zipRandomFile.read() == 0x03) { + if (zipRandomFile.read() == 0x04) { + return true; + } + } + } + } + } finally { + zipRandomFile.seek(pos); + } + throw new ZipFormatException("invalid zip magic"); + } + + /* + * Reads zip file central directory. + * For more details see readCEN in zip_util.c from the JDK sources. + * This is a Java port of that function. + */ + private void findCENRecord(long start, long end) throws IOException { + long totalLength = end - start; + int endbuflen = 1024; + byte[] endbuf = new byte[endbuflen]; + long endbufend = end - start; + + // There is a variable-length field after the dir offset record. We need to do consequential search. + while (endbufend >= 22) { + if (endbufend < endbuflen) + endbuflen = (int)endbufend; + long endbufpos = endbufend - endbuflen; + zipRandomFile.seek(start + endbufpos); + zipRandomFile.readFully(endbuf, 0, endbuflen); + int i = endbuflen - 22; + while (i >= 0 && + !(endbuf[i] == 0x50 && + endbuf[i + 1] == 0x4b && + endbuf[i + 2] == 0x05 && + endbuf[i + 3] == 0x06 && + endbufpos + i + 22 + + get2ByteLittleEndian(endbuf, i + 20) == totalLength)) { + i--; + } + + if (i >= 0) { + zipDir = new byte[get4ByteLittleEndian(endbuf, i + 12)]; + int sz = get4ByteLittleEndian(endbuf, i + 16); + // a negative offset or the entries field indicates a + // potential zip64 archive + if (sz < 0 || get2ByteLittleEndian(endbuf, i + 10) == 0xffff) { + throw new ZipFormatException("detected a zip64 archive"); + } + zipRandomFile.seek(start + sz); + zipRandomFile.readFully(zipDir, 0, zipDir.length); + return; + } else { + endbufend = endbufpos + 21; + } + } + throw new ZipException("cannot read zip file"); + } + + private void buildIndex() throws IOException { + int len = zipDir.length; + + // Add each of the files + if (len > 0) { + directories = new LinkedHashMap<>(); + ArrayList entryList = new ArrayList<>(); + for (int pos = 0; pos < len; ) { + pos = readEntry(pos, entryList, directories); + } + + // Add the accumulated dirs into the same list + for (RelativeDirectory d: directories.keySet()) { + // use shared RelativeDirectory objects for parent dirs + RelativeDirectory parent = getRelativeDirectory(d.dirname().getPath()); + String file = d.basename(); + Entry zipFileIndexEntry = new Entry(parent, file); + zipFileIndexEntry.isDir = true; + entryList.add(zipFileIndexEntry); + } + + entries = entryList.toArray(new Entry[entryList.size()]); + Arrays.sort(entries); + } else { + cleanupState(); + } + } + + private int readEntry(int pos, List entryList, + Map directories) throws IOException { + if (get4ByteLittleEndian(zipDir, pos) != 0x02014b50) { + throw new ZipException("cannot read zip file entry"); + } + + int dirStart = pos + 46; + int fileStart = dirStart; + int fileEnd = fileStart + get2ByteLittleEndian(zipDir, pos + 28); + + if (zipFileIndex.symbolFilePrefixLength != 0 && + ((fileEnd - fileStart) >= symbolFilePrefixLength)) { + dirStart += zipFileIndex.symbolFilePrefixLength; + fileStart += zipFileIndex.symbolFilePrefixLength; + } + // Force any '\' to '/'. Keep the position of the last separator. + for (int index = fileStart; index < fileEnd; index++) { + byte nextByte = zipDir[index]; + if (nextByte == (byte)'\\') { + zipDir[index] = (byte)'/'; + fileStart = index + 1; + } else if (nextByte == (byte)'/') { + fileStart = index + 1; + } + } + + RelativeDirectory directory = null; + if (fileStart == dirStart) + directory = getRelativeDirectory(""); + else if (lastDir != null && lastLen == fileStart - dirStart - 1) { + int index = lastLen - 1; + while (zipDir[lastStart + index] == zipDir[dirStart + index]) { + if (index == 0) { + directory = lastDir; + break; + } + index--; + } + } + + // Sub directories + if (directory == null) { + lastStart = dirStart; + lastLen = fileStart - dirStart - 1; + + directory = getRelativeDirectory(new String(zipDir, dirStart, lastLen, "UTF-8")); + lastDir = directory; + + // Enter also all the parent directories + RelativeDirectory tempDirectory = directory; + + while (directories.get(tempDirectory) == null) { + directories.put(tempDirectory, new DirectoryEntry(tempDirectory, zipFileIndex)); + if (tempDirectory.path.indexOf("/") == tempDirectory.path.length() - 1) + break; + else { + // use shared RelativeDirectory objects for parent dirs + tempDirectory = getRelativeDirectory(tempDirectory.dirname().getPath()); + } + } + } + else { + if (directories.get(directory) == null) { + directories.put(directory, new DirectoryEntry(directory, zipFileIndex)); + } + } + + // For each dir create also a file + if (fileStart != fileEnd) { + Entry entry = new Entry(directory, + new String(zipDir, fileStart, fileEnd - fileStart, "UTF-8")); + + entry.setNativeTime(get4ByteLittleEndian(zipDir, pos + 12)); + entry.compressedSize = get4ByteLittleEndian(zipDir, pos + 20); + entry.size = get4ByteLittleEndian(zipDir, pos + 24); + entry.offset = get4ByteLittleEndian(zipDir, pos + 42); + entryList.add(entry); + } + + return pos + 46 + + get2ByteLittleEndian(zipDir, pos + 28) + + get2ByteLittleEndian(zipDir, pos + 30) + + get2ByteLittleEndian(zipDir, pos + 32); + } + } + + /** + * Returns the last modified timestamp of a zip file. + * @return long + */ + public long getZipFileLastModified() throws IOException { + synchronized (this) { + checkIndex(); + return zipFileLastModified; + } + } + + /** ------------------------------------------------------------------------ + * DirectoryEntry class + * -------------------------------------------------------------------------*/ + + static class DirectoryEntry { + private boolean filesInited; + private boolean directoriesInited; + private boolean zipFileEntriesInited; + private boolean entriesInited; + + private long writtenOffsetOffset = 0; + + private RelativeDirectory dirName; + + private com.sun.tools.javac.util.List zipFileEntriesFiles = com.sun.tools.javac.util.List.nil(); + private com.sun.tools.javac.util.List zipFileEntriesDirectories = com.sun.tools.javac.util.List.nil(); + private com.sun.tools.javac.util.List zipFileEntries = com.sun.tools.javac.util.List.nil(); + + private List entries = new ArrayList<>(); + + private ZipFileIndex zipFileIndex; + + private int numEntries; + + DirectoryEntry(RelativeDirectory dirName, ZipFileIndex index) { + filesInited = false; + directoriesInited = false; + entriesInited = false; + + this.dirName = dirName; + this.zipFileIndex = index; + } + + private com.sun.tools.javac.util.List getFiles() { + if (!filesInited) { + initEntries(); + for (Entry e : entries) { + if (!e.isDir) { + zipFileEntriesFiles = zipFileEntriesFiles.append(e.name); + } + } + filesInited = true; + } + return zipFileEntriesFiles; + } + + private com.sun.tools.javac.util.List getDirectories() { + if (!directoriesInited) { + initEntries(); + for (Entry e : entries) { + if (e.isDir) { + zipFileEntriesDirectories = zipFileEntriesDirectories.append(e.name); + } + } + directoriesInited = true; + } + return zipFileEntriesDirectories; + } + + private com.sun.tools.javac.util.List getEntries() { + if (!zipFileEntriesInited) { + initEntries(); + zipFileEntries = com.sun.tools.javac.util.List.nil(); + for (Entry zfie : entries) { + zipFileEntries = zipFileEntries.append(zfie); + } + zipFileEntriesInited = true; + } + return zipFileEntries; + } + + private Entry getEntry(String rootName) { + initEntries(); + int index = Collections.binarySearch(entries, new Entry(dirName, rootName)); + if (index < 0) { + return null; + } + + return entries.get(index); + } + + private void initEntries() { + if (entriesInited) { + return; + } + + if (!zipFileIndex.readFromIndex) { + int from = -Arrays.binarySearch(zipFileIndex.entries, + new Entry(dirName, ZipFileIndex.MIN_CHAR)) - 1; + int to = -Arrays.binarySearch(zipFileIndex.entries, + new Entry(dirName, MAX_CHAR)) - 1; + + for (int i = from; i < to; i++) { + entries.add(zipFileIndex.entries[i]); + } + } else { + File indexFile = zipFileIndex.getIndexFile(); + if (indexFile != null) { + RandomAccessFile raf = null; + try { + raf = new RandomAccessFile(indexFile, "r"); + raf.seek(writtenOffsetOffset); + + for (int nFiles = 0; nFiles < numEntries; nFiles++) { + // Read the name bytes + int zfieNameBytesLen = raf.readInt(); + byte [] zfieNameBytes = new byte[zfieNameBytesLen]; + raf.read(zfieNameBytes); + String eName = new String(zfieNameBytes, "UTF-8"); + + // Read isDir + boolean eIsDir = raf.readByte() == (byte)0 ? false : true; + + // Read offset of bytes in the real Jar/Zip file + int eOffset = raf.readInt(); + + // Read size of the file in the real Jar/Zip file + int eSize = raf.readInt(); + + // Read compressed size of the file in the real Jar/Zip file + int eCsize = raf.readInt(); + + // Read java time stamp of the file in the real Jar/Zip file + long eJavaTimestamp = raf.readLong(); + + Entry rfie = new Entry(dirName, eName); + rfie.isDir = eIsDir; + rfie.offset = eOffset; + rfie.size = eSize; + rfie.compressedSize = eCsize; + rfie.javatime = eJavaTimestamp; + entries.add(rfie); + } + } catch (Throwable t) { + // Do nothing + } finally { + try { + if (raf != null) { + raf.close(); + } + } catch (Throwable t) { + // Do nothing + } + } + } + } + + entriesInited = true; + } + + List getEntriesAsCollection() { + initEntries(); + + return entries; + } + } + + private boolean readIndex() { + if (triedToReadIndex || !usePreindexedCache) { + return false; + } + + boolean ret = false; + synchronized (this) { + triedToReadIndex = true; + RandomAccessFile raf = null; + try { + File indexFileName = getIndexFile(); + raf = new RandomAccessFile(indexFileName, "r"); + + long fileStamp = raf.readLong(); + if (zipFile.lastModified() != fileStamp) { + ret = false; + } else { + directories = new LinkedHashMap<>(); + int numDirs = raf.readInt(); + for (int nDirs = 0; nDirs < numDirs; nDirs++) { + int dirNameBytesLen = raf.readInt(); + byte [] dirNameBytes = new byte[dirNameBytesLen]; + raf.read(dirNameBytes); + + RelativeDirectory dirNameStr = getRelativeDirectory(new String(dirNameBytes, "UTF-8")); + DirectoryEntry de = new DirectoryEntry(dirNameStr, this); + de.numEntries = raf.readInt(); + de.writtenOffsetOffset = raf.readLong(); + directories.put(dirNameStr, de); + } + ret = true; + zipFileLastModified = fileStamp; + } + } catch (Throwable t) { + // Do nothing + } finally { + if (raf != null) { + try { + raf.close(); + } catch (Throwable tt) { + // Do nothing + } + } + } + if (ret == true) { + readFromIndex = true; + } + } + + return ret; + } + + private boolean writeIndex() { + boolean ret = false; + if (readFromIndex || !usePreindexedCache) { + return true; + } + + if (!writeIndex) { + return true; + } + + File indexFile = getIndexFile(); + if (indexFile == null) { + return false; + } + + RandomAccessFile raf = null; + long writtenSoFar = 0; + try { + raf = new RandomAccessFile(indexFile, "rw"); + + raf.writeLong(zipFileLastModified); + writtenSoFar += 8; + + List directoriesToWrite = new ArrayList<>(); + Map offsets = new HashMap<>(); + raf.writeInt(directories.keySet().size()); + writtenSoFar += 4; + + for (RelativeDirectory dirName: directories.keySet()) { + DirectoryEntry dirEntry = directories.get(dirName); + + directoriesToWrite.add(dirEntry); + + // Write the dir name bytes + byte [] dirNameBytes = dirName.getPath().getBytes("UTF-8"); + int dirNameBytesLen = dirNameBytes.length; + raf.writeInt(dirNameBytesLen); + writtenSoFar += 4; + + raf.write(dirNameBytes); + writtenSoFar += dirNameBytesLen; + + // Write the number of files in the dir + List dirEntries = dirEntry.getEntriesAsCollection(); + raf.writeInt(dirEntries.size()); + writtenSoFar += 4; + + offsets.put(dirName, new Long(writtenSoFar)); + + // Write the offset of the file's data in the dir + dirEntry.writtenOffsetOffset = 0L; + raf.writeLong(0L); + writtenSoFar += 8; + } + + for (DirectoryEntry de : directoriesToWrite) { + // Fix up the offset in the directory table + long currFP = raf.getFilePointer(); + + long offsetOffset = offsets.get(de.dirName).longValue(); + raf.seek(offsetOffset); + raf.writeLong(writtenSoFar); + + raf.seek(currFP); + + // Now write each of the files in the DirectoryEntry + List list = de.getEntriesAsCollection(); + for (Entry zfie : list) { + // Write the name bytes + byte [] zfieNameBytes = zfie.name.getBytes("UTF-8"); + int zfieNameBytesLen = zfieNameBytes.length; + raf.writeInt(zfieNameBytesLen); + writtenSoFar += 4; + raf.write(zfieNameBytes); + writtenSoFar += zfieNameBytesLen; + + // Write isDir + raf.writeByte(zfie.isDir ? (byte)1 : (byte)0); + writtenSoFar += 1; + + // Write offset of bytes in the real Jar/Zip file + raf.writeInt(zfie.offset); + writtenSoFar += 4; + + // Write size of the file in the real Jar/Zip file + raf.writeInt(zfie.size); + writtenSoFar += 4; + + // Write compressed size of the file in the real Jar/Zip file + raf.writeInt(zfie.compressedSize); + writtenSoFar += 4; + + // Write java time stamp of the file in the real Jar/Zip file + raf.writeLong(zfie.getLastModified()); + writtenSoFar += 8; + } + } + } catch (Throwable t) { + // Do nothing + } finally { + try { + if (raf != null) { + raf.close(); + } + } catch(IOException ioe) { + // Do nothing + } + } + + return ret; + } + + public boolean writeZipIndex() { + synchronized (this) { + return writeIndex(); + } + } + + private File getIndexFile() { + if (zipIndexFile == null) { + if (zipFile == null) { + return null; + } + + zipIndexFile = new File((preindexedCacheLocation == null ? "" : preindexedCacheLocation) + + zipFile.getName() + ".index"); + } + + return zipIndexFile; + } + + public File getZipFile() { + return zipFile; + } + + File getAbsoluteFile() { + File absFile = (absFileRef == null ? null : absFileRef.get()); + if (absFile == null) { + absFile = zipFile.getAbsoluteFile(); + absFileRef = new SoftReference<>(absFile); + } + return absFile; + } + + private RelativeDirectory getRelativeDirectory(String path) { + RelativeDirectory rd; + SoftReference ref = relativeDirectoryCache.get(path); + if (ref != null) { + rd = ref.get(); + if (rd != null) + return rd; + } + rd = new RelativeDirectory(path); + relativeDirectoryCache.put(path, new SoftReference<>(rd)); + return rd; + } + + static class Entry implements Comparable { + public static final Entry[] EMPTY_ARRAY = {}; + + // Directory related + RelativeDirectory dir; + boolean isDir; + + // File related + String name; + + int offset; + int size; + int compressedSize; + long javatime; + + private int nativetime; + + public Entry(RelativePath path) { + this(path.dirname(), path.basename()); + } + + public Entry(RelativeDirectory directory, String name) { + this.dir = directory; + this.name = name; + } + + public String getName() { + return new RelativeFile(dir, name).getPath(); + } + + public String getFileName() { + return name; + } + + public long getLastModified() { + if (javatime == 0) { + javatime = dosToJavaTime(nativetime); + } + return javatime; + } + + // based on dosToJavaTime in java.util.Zip, but avoiding the + // use of deprecated Date constructor + private static long dosToJavaTime(int dtime) { + Calendar c = Calendar.getInstance(); + c.set(Calendar.YEAR, ((dtime >> 25) & 0x7f) + 1980); + c.set(Calendar.MONTH, ((dtime >> 21) & 0x0f) - 1); + c.set(Calendar.DATE, ((dtime >> 16) & 0x1f)); + c.set(Calendar.HOUR_OF_DAY, ((dtime >> 11) & 0x1f)); + c.set(Calendar.MINUTE, ((dtime >> 5) & 0x3f)); + c.set(Calendar.SECOND, ((dtime << 1) & 0x3e)); + c.set(Calendar.MILLISECOND, 0); + return c.getTimeInMillis(); + } + + void setNativeTime(int natTime) { + nativetime = natTime; + } + + public boolean isDirectory() { + return isDir; + } + + public int compareTo(Entry other) { + RelativeDirectory otherD = other.dir; + if (dir != otherD) { + int c = dir.compareTo(otherD); + if (c != 0) + return c; + } + return name.compareTo(other.name); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Entry)) + return false; + Entry other = (Entry) o; + return dir.equals(other.dir) && name.equals(other.name); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 97 * hash + (this.dir != null ? this.dir.hashCode() : 0); + hash = 97 * hash + (this.name != null ? this.name.hashCode() : 0); + return hash; + } + + @Override + public String toString() { + return isDir ? ("Dir:" + dir + " : " + name) : + (dir + ":" + name); + } + } + + /* + * Exception primarily used to implement a failover, used exclusively here. + */ + + static final class ZipFormatException extends IOException { + private static final long serialVersionUID = 8000196834066748623L; + protected ZipFormatException(String message) { + super(message); + } + + protected ZipFormatException(String message, Throwable cause) { + super(message, cause); + } + } +}