de.schlichtherle.util.zip.BasicZipFile Maven / Gradle / Ivy
Show all versions of truezip Show documentation
/*
* Copyright (C) 2006-2010 Schlichtherle IT Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.schlichtherle.util.zip;
import de.schlichtherle.io.rof.BufferedReadOnlyFile;
import de.schlichtherle.io.rof.ReadOnlyFile;
import de.schlichtherle.io.rof.SimpleReadOnlyFile;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import java.util.zip.ZipException;
/**
* This class is not intended for public use!
* The methods in this class are unsynchronized and
* {@link #entries}/{@link #getEntry} enumerate/return {@link ZipEntry}
* instances which are shared with this class rather than clones
* of them.
* This class is used within other parts of the TrueZIP API in order to benefit
* from the slightly better performance.
*
* Where the constructors of this class accept a {@code charset}
* parameter, this is used to decode comments and entry names in the ZIP file.
* However, if an entry has bit 11 set in its General Purpose Bit Flag,
* then this parameter is ignored and "UTF-8" is used for this entry.
* This is in accordance to Appendix D of PKWARE's ZIP File Format
* Specification, version 6.3.0 and later.
*
* This class is able to skip a preamble like the one found in self extracting
* archives.
*
* Note that the entries returned by this class are instances of
* {@code de.schlichtherle.util.zip.ZipEntry} instead of
* {@code java.util.zip.ZipEntry}.
*
* @author Christian Schlichtherle
* @version $Id$
* @since TrueZIP 6.4
*/
public class BasicZipFile {
private static final long LONG_MSB = 0x8000000000000000L;
private static final int LFH_FILE_NAME_LENGTH_OFF =
/* local file header signature */ 4 +
/* version needed to extract */ 2 +
/* general purpose bit flag */ 2 +
/* compression method */ 2 +
/* last mod file time */ 2 +
/* last mod file date */ 2 +
/* crc-32 */ 4 +
/* compressed size */ 4 +
/* uncompressed size */ 4;
/**
* The default character set used for entry names and comments in ZIP
* compatible files.
* This is {@value} for compatibility with Sun's JDK implementation.
* Note that you should use "IBM437" for ordinary ZIP files
* instead.
*/
public static final String DEFAULT_CHARSET = ZIP.DEFAULT_CHARSET;
/** The charset to use for entry names and comments. */
private String charset;
/** The comment of this ZIP compatible file. */
private String comment;
/** Maps entry names to zip entries. */
private final Map entries = new LinkedHashMap();
/** The actual data source. */
private ReadOnlyFile archive;
/** The number of open streams reading from this ZIP compatible file. */
private int openStreams;
/** The number of bytes in the preamble of this ZIP compatible file. */
private long preamble;
/** The number of bytes in the postamble of this ZIP compatible file. */
private long postamble;
/** Maps offsets specified in the ZIP file to real offsets in the file. */
private OffsetMapper mapper;
/**
* Equivalent to {@link #BasicZipFile(String, String, boolean, boolean)
* BasicZipFile(name, DEFAULT_CHARSET, true, false)}
*
* @deprecated Use {@code new}
* {@link #BasicZipFile(ReadOnlyFile, String, boolean, boolean)}
* instead.
*/
public BasicZipFile(String name)
throws NullPointerException,
FileNotFoundException,
ZipException,
IOException {
this(name, DEFAULT_CHARSET, true, false);
}
/**
* Equivalent to {@link #BasicZipFile(String, String, boolean, boolean)
* BasicZipFile(name, charset, true, false)}
*
* @deprecated Use {@code new}
* {@link #BasicZipFile(ReadOnlyFile, String, boolean, boolean)}
* instead.
*/
public BasicZipFile(String name, String charset)
throws NullPointerException,
UnsupportedEncodingException,
FileNotFoundException,
ZipException,
IOException {
this(name, charset, true, false);
}
/**
* Opens the ZIP file identified by the given path name for reading its
* entries.
*
* @param name The path name of the file.
* @param charset The charset to use for decoding entry names and ZIP file
* comment.
* @param preambled If this is {@code true}, then the ZIP file may have a
* preamble.
* Otherwise, the ZIP file must start with either a Local File
* Header (LFH) signature or an End Of Central Directory (EOCD)
* Header, causing this constructor to fail if the file is actually
* a false positive ZIP file, i.e. not compatible to the ZIP File
* Format Specification.
* This may be useful to read Self Extracting ZIP files (SFX), which
* usually contain the application code required for extraction in
* the preamble.
* @param postambled If this is {@code true}, then the ZIP file may have a
* postamble of arbitrary length.
* Otherwise, the ZIP file must not have a postamble which exceeds
* 64KB size, including the End Of Central Directory record
* (i.e. including the ZIP file comment), causing this constructor
* to fail if the file is actually a false positive ZIP file, i.e.
* not compatible to the ZIP File Format Specification.
* This may be useful to read Self Extracting ZIP files (SFX) with
* large postambles.
* @throws NullPointerException If {@code name} or {@code charset} is
* {@code null}.
* @throws UnsupportedEncodingException If charset is not supported by
* this JVM.
* @throws FileNotFoundException If the file cannot get opened for reading.
* @throws ZipException If the file is not compatible with the ZIP File
* Format Specification.
* @throws IOException On any other I/O related issue.
* @deprecated Use {@code new}
* {@link #BasicZipFile(ReadOnlyFile, String, boolean, boolean)}
* instead.
*/
public BasicZipFile(
String name,
String charset,
boolean preambled,
boolean postambled)
throws NullPointerException,
UnsupportedEncodingException,
FileNotFoundException,
ZipException,
IOException {
this(null, new File(name), charset, preambled, postambled);
}
/**
* Equivalent to {@link #BasicZipFile(File, String, boolean, boolean)
* BasicZipFile(file, DEFAULT_CHARSET, true, false)}
*
* @deprecated Use {@code new}
* {@link #BasicZipFile(ReadOnlyFile, String, boolean, boolean)}
* instead.
*/
public BasicZipFile(File file)
throws NullPointerException,
FileNotFoundException,
ZipException,
IOException {
this(file, DEFAULT_CHARSET, true, false);
}
/**
* Equivalent to {@link #BasicZipFile(File, String, boolean, boolean)
* BasicZipFile(file, charset, true, false)}
*
* @deprecated Use {@code new}
* {@link #BasicZipFile(ReadOnlyFile, String, boolean, boolean)}
* instead.
*/
public BasicZipFile(File file, String charset)
throws NullPointerException,
UnsupportedEncodingException,
FileNotFoundException,
ZipException,
IOException {
this(file, charset, true, false);
}
/**
* Opens the given {@link File} for reading its entries.
*
* @param file The file.
* @param charset The charset to use for decoding entry names and ZIP file
* comment.
* @param preambled If this is {@code true}, then the ZIP file may have a
* preamble.
* Otherwise, the ZIP file must start with either a Local File
* Header (LFH) signature or an End Of Central Directory (EOCD)
* Header, causing this constructor to fail if the file is actually
* a false positive ZIP file, i.e. not compatible to the ZIP File
* Format Specification.
* This may be useful to read Self Extracting ZIP files (SFX), which
* usually contain the application code required for extraction in
* the preamble.
* @param postambled If this is {@code true}, then the ZIP file may have a
* postamble of arbitrary length.
* Otherwise, the ZIP file must not have a postamble which exceeds
* 64KB size, including the End Of Central Directory record
* (i.e. including the ZIP file comment), causing this constructor
* to fail if the file is actually a false positive ZIP file, i.e.
* not compatible to the ZIP File Format Specification.
* This may be useful to read Self Extracting ZIP files (SFX) with
* large postambles.
* @throws NullPointerException If {@code file} or {@code charset} is
* {@code null}.
* @throws UnsupportedEncodingException If charset is not supported by
* this JVM.
* @throws FileNotFoundException If the file cannot get opened for reading.
* @throws ZipException If the file is not compatible with the ZIP File
* Format Specification.
* @throws IOException On any other I/O related issue.
* @deprecated Use {@code new}
* {@link #BasicZipFile(ReadOnlyFile, String, boolean, boolean)}
* instead.
*/
public BasicZipFile(
File file,
String charset,
boolean preambled,
boolean postambled)
throws NullPointerException,
UnsupportedEncodingException,
FileNotFoundException,
ZipException,
IOException {
this(null, file, charset, preambled, postambled);
}
/**
* Equivalent to {@link #BasicZipFile(ReadOnlyFile, String, boolean, boolean)
* BasicZipFile(rof, DEFAULT_CHARSET, true, false)}
*
* @deprecated Use {@code new}
* {@link #BasicZipFile(ReadOnlyFile, String, boolean, boolean)}
* instead.
*/
public BasicZipFile(ReadOnlyFile rof)
throws NullPointerException,
FileNotFoundException,
ZipException,
IOException {
this(rof, DEFAULT_CHARSET, true, false);
}
/**
* Equivalent to {@link #BasicZipFile(ReadOnlyFile, String, boolean, boolean)
* BasicZipFile(rof, charset, true, false)}
*
* @deprecated Use {@code new}
* {@link #BasicZipFile(ReadOnlyFile, String, boolean, boolean)}
* instead.
*/
public BasicZipFile(ReadOnlyFile rof, String charset)
throws NullPointerException,
UnsupportedEncodingException,
FileNotFoundException,
ZipException,
IOException {
this(rof, charset, true, false);
}
/**
* Opens the given {@link ReadOnlyFile} for reading its entries.
*
* @param rof The random access read only file.
* @param charset The charset to use for decoding entry names and ZIP file
* comment.
* @param preambled If this is {@code true}, then the ZIP file may have a
* preamble.
* Otherwise, the ZIP file must start with either a Local File
* Header (LFH) signature or an End Of Central Directory (EOCD)
* Header, causing this constructor to fail if the file is actually
* a false positive ZIP file, i.e. not compatible to the ZIP File
* Format Specification.
* This may be useful to read Self Extracting ZIP files (SFX), which
* usually contain the application code required for extraction in
* the preamble.
* @param postambled If this is {@code true}, then the ZIP file may have a
* postamble of arbitrary length.
* Otherwise, the ZIP file must not have a postamble which exceeds
* 64KB size, including the End Of Central Directory record
* (i.e. including the ZIP file comment), causing this constructor
* to fail if the file is actually a false positive ZIP file, i.e.
* not compatible to the ZIP File Format Specification.
* This may be useful to read Self Extracting ZIP files (SFX) with
* large postambles.
* @throws NullPointerException If {@code rof} or {@code charset} is
* {@code null}.
* @throws UnsupportedEncodingException If charset is not supported by
* this JVM.
* @throws FileNotFoundException If the file cannot get opened for reading.
* @throws ZipException If the file is not compatible with the ZIP File
* Format Specification.
* @throws IOException On any other I/O related issue.
*/
public BasicZipFile(
ReadOnlyFile rof,
String charset,
boolean preambled,
boolean postambled)
throws NullPointerException,
UnsupportedEncodingException,
FileNotFoundException,
ZipException,
IOException {
this(rof, null, charset, preambled, postambled);
}
private BasicZipFile(
ReadOnlyFile rof,
final File file,
final String charset,
final boolean preambled,
final boolean postambled)
throws NullPointerException,
UnsupportedEncodingException,
FileNotFoundException,
ZipException,
IOException {
// Check parameters (fail fast).
if (charset == null)
throw new NullPointerException("charset");
new String(new byte[0], charset); // may throw UnsupportedEncodingException!
this.charset = charset;
if (rof == null) {
if (file == null)
throw new NullPointerException();
rof = createReadOnlyFile(file);
} else { // rof != null
assert file == null;
}
archive = rof;
try {
final BufferedReadOnlyFile brof;
if (archive instanceof BufferedReadOnlyFile)
brof = (BufferedReadOnlyFile) archive;
else
brof = new BufferedReadOnlyFile(archive);
mountCentralDirectory(brof, preambled, postambled);
// Do NOT close brof - would close rof as well!
} catch (IOException failure) {
if (file != null)
rof.close();
throw failure;
}
assert mapper != null;
}
/**
* A factory method called by the constructor to get a read only file
* to access the contents of the ZIP file.
* This method is only used if the constructor isn't called with a read
* only file as its parameter.
*
* @throws FileNotFoundException If the file cannot get opened for reading.
* @throws IOException On any other I/O related issue.
* @deprecated This method is unsafe because it's called by some constructors.
* Use a constructor with a {@code ReadOnlyFile} parameter
* instead.
*/
protected ReadOnlyFile createReadOnlyFile(File file)
throws FileNotFoundException, IOException {
return new SimpleReadOnlyFile(file);
}
/**
* Reads the central directory of the given file and populates
* the internal tables with ZipEntry instances.
*
* The ZipEntrys will know all data that can be obtained from
* the central directory alone, but not the data that requires the
* local file header or additional data to be read.
*
* @throws ZipException If the file is not ZIP compatible.
* @throws IOException On any other I/O related issue.
*/
private void mountCentralDirectory(
final ReadOnlyFile rof,
final boolean preambled,
final boolean postambled)
throws ZipException, IOException {
int numEntries = findCentralDirectory(rof, preambled, postambled);
assert mapper != null;
preamble = Long.MAX_VALUE;
final byte[] sig = new byte[4];
final byte[] cfh = new byte[ZIP.CFH_MIN_LEN - sig.length];
for (; ; numEntries--) {
rof.readFully(sig);
if (LittleEndian.readUInt(sig, 0) != ZIP.CFH_SIG)
break;
rof.readFully(cfh);
final int general = LittleEndian.readUShort(cfh, 4);
final int nameLen = LittleEndian.readUShort(cfh, 24);
final byte[] name = new byte[nameLen];
rof.readFully(name);
// See appendix D of PKWARE's ZIP File Format Specification.
final boolean utf8 = (general & (1 << 11)) != 0;
final String charset = utf8 ? ZIP.UTF8 : this.charset;
final ZipEntry entry = createZipEntry(new String(name, charset));
try {
int off = 0;
final int versionMadeBy = LittleEndian.readUShort(cfh, off);
off += 2;
entry.setPlatform((short) (versionMadeBy >> 8));
off += 2; // version needed to extract
entry.setGeneral(general);
off += 2; // general purpose bit flag
assert entry.getGeneralBit(11) == utf8;
final int method = LittleEndian.readUShort(cfh, off);
off += 2;
if (method != ZIP.STORED && method != ZIP.DEFLATED)
throw new ZipException(entry.getName()
+ ": unsupported compression method: " + method);
entry.setMethod(method);
entry.setDosTime(LittleEndian.readUInt(cfh, off));
off += 4;
entry.setCrc(LittleEndian.readUInt(cfh, off));
off += 4;
entry.setCompressedSize32(LittleEndian.readUInt(cfh, off));
off += 4;
entry.setSize32(LittleEndian.readUInt(cfh, off));
off += 4;
off += 2; // file name length
final int extraLen = LittleEndian.readUShort(cfh, off);
off += 2;
final int commentLen = LittleEndian.readUShort(cfh, off);
off += 2;
off += 2; // disk number
//ze.setInternalAttributes(readUShort(cfh, off));
off += 2;
//ze.setExternalAttributes(readUInt(cfh, off));
off += 4;
// Relative Offset Of Local File Header.
long lfhOff = LittleEndian.readUInt(cfh, off);
//off += 4;
entry.setOffset32(lfhOff); // must be unmapped!
if (extraLen > 0) {
final byte[] extra = new byte[extraLen];
rof.readFully(extra);
entry.setExtra(extra);
}
if (commentLen > 0) {
final byte[] comment = new byte[commentLen];
rof.readFully(comment);
entry.setComment(new String(comment, charset));
}
// Re-read virtual offset after ZIP64 Extended Information
// Extra Field may have been parsed, map it to the real
// offset and conditionally update the preamble size from it.
lfhOff = mapper.location(entry.getOffset());
if (lfhOff < preamble)
preamble = lfhOff;
} catch (RuntimeException incompatibleZipFile) {
final ZipException exc = new ZipException(entry.getName());
exc.initCause(incompatibleZipFile);
throw exc;
}
// Map the entry using the name that has been determined
// by createZipEntry().
// Note that this name may differ from what has been found
// in the ZIP file!
entries.put(entry.getName(), entry);
}
// Check if the number of entries found matches the number of entries
// declared in the (ZIP64) End Of Central Directory header.
// Sometimes, legacy ZIP32 archives (those without ZIP64 extensions)
// contain more than the maximum number of entries specified in the
// ZIP File Format Specification, which is 65535 (= 0xffff, a two byte
// unsigned integer).
// In this case, the declared number of entries usually overflows and
// may get negative (Java does not support unsigned integers).
// Although beyond the spec, we silently tolerate this in the test.
// Thanks to Jean-Francois Thamie for this hint!
if (numEntries % 0x10000 != 0)
throw new ZipException(
"Expected " +
Math.abs(numEntries) +
(numEntries > 0 ? " more" : " less") +
" entries in the Central Directory!");
if (preamble == ULong.MAX_VALUE)
preamble = 0;
}
/**
* Positions the file pointer at the first Central File Header.
* Performs some means to check that this is really a ZIP compatible
* file.
*
* As a side effect, both {@code mapper} and }postamble}
* will be set.
*
* @throws ZipException If the file is not ZIP compatible.
* @throws IOException On any other I/O related issue.
*/
private int findCentralDirectory(
final ReadOnlyFile rof,
boolean preambled,
final boolean postambled)
throws ZipException, IOException {
final byte[] sig = new byte[4];
if (!preambled) {
rof.seek(0);
rof.readFully(sig);
final long signature = LittleEndian.readUInt(sig, 0);
// Constraint: A ZIP file must start with a Local File Header
// or a (ZIP64) End Of Central Directory Record iff it's emtpy.
preambled = signature == ZIP.LFH_SIG
|| signature == ZIP.ZIP64_EOCDR_SIG
|| signature == ZIP.EOCDR_SIG;
}
if (preambled) {
final long length = rof.length();
final long max = length - ZIP.EOCDR_MIN_LEN;
final long min = !postambled && max >= 0xffff ? max - 0xffff : 0;
for (long eocdrOffset = max; eocdrOffset >= min; eocdrOffset--) {
rof.seek(eocdrOffset);
rof.readFully(sig);
if (LittleEndian.readUInt(sig, 0) != ZIP.EOCDR_SIG)
continue;
long diskNo; // number of this disk
long cdDiskNo; // number of the disk with the start of the central directory
long cdEntriesDisk; // total number of entries in the central directory on this disk
long cdEntries; // total number of entries in the central directory
long cdSize; // size of the central directory
long cdOffset; // offset of start of central directory with respect to the starting disk number
int commentLen; // .ZIP file comment length
int off = 0;
// Process EOCDR.
final byte[] eocdr = new byte[ZIP.EOCDR_MIN_LEN - sig.length];
rof.readFully(eocdr);
diskNo = LittleEndian.readUShort(eocdr, off);
off += 2;
cdDiskNo = LittleEndian.readUShort(eocdr, off);
off += 2;
cdEntriesDisk = LittleEndian.readUShort(eocdr, off);
off += 2;
cdEntries = LittleEndian.readUShort(eocdr, off);
off += 2;
if (diskNo != 0 || cdDiskNo != 0 || cdEntriesDisk != cdEntries)
throw new ZipException(
"ZIP file spanning/splitting is not supported!");
cdSize = LittleEndian.readUInt(eocdr, off);
off += 4;
cdOffset = LittleEndian.readUInt(eocdr, off);
off += 4;
commentLen = LittleEndian.readUShort(eocdr, off);
//off += 2;
if (commentLen > 0) {
final byte[] comment = new byte[commentLen];
rof.readFully(comment);
setComment(new String(comment, charset));
}
postamble = length - rof.getFilePointer();
// Check for ZIP64 End Of Central Directory Locator.
try {
// Read Zip64 End Of Central Directory Locator.
final byte[] zip64eocdl = new byte[ZIP.ZIP64_EOCDL_LEN];
rof.seek(eocdrOffset - ZIP.ZIP64_EOCDL_LEN);
rof.readFully(zip64eocdl);
off = 0; // reuse
final long zip64eocdlSig = LittleEndian.readUInt(zip64eocdl, off);
off += 4;
if (zip64eocdlSig != ZIP.ZIP64_EOCDL_SIG)
throw new IOException( // MUST be IOException, not ZipException - see catch clauses!
"Expected ZIP64 End Of Central Directory Locator signature!");
final long zip64eocdrDisk; // number of the disk with the start of the zip64 end of central directory record
final long zip64eocdrOffset; // relative offset of the zip64 end of central directory record
final long totalDisks; // total number of disks
zip64eocdrDisk = LittleEndian.readUInt(zip64eocdl, off);
off += 4;
zip64eocdrOffset = LittleEndian.readLong(zip64eocdl, off);
off += 8;
totalDisks = LittleEndian.readUInt(zip64eocdl, off);
//off += 4;
if (zip64eocdrDisk != 0 || totalDisks != 1)
throw new ZipException( // MUST be ZipException, not IOException - see catch clauses!
"ZIP file spanning/splitting is not supported!");
// Read Zip64 End Of Central Directory Record.
final byte[] zip64eocdr = new byte[ZIP.ZIP64_EOCDR_MIN_LEN];
rof.seek(zip64eocdrOffset);
rof.readFully(zip64eocdr);
off = 0; // reuse
final long zip64eocdrSig = LittleEndian.readUInt(zip64eocdr, off);
off += 4;
if (zip64eocdrSig != ZIP.ZIP64_EOCDR_SIG)
throw new ZipException( // MUST be ZipException, not IOException - see catch clauses!
"Expected ZIP64 End Of Central Directory Record signature!");
//final long zip64eocdrSize; // size of zip64 end of central directory record
//final int madeBy; // version made by
//final int needed2extract; // version needed to extract
//zip64eocdrSize = LittleEndian.readLong(zip64eocdr, off);
off += 8;
//madeBy = LittleEndian.readUShort(zip64eocdr, off);
off += 2;
//needed2extract = LittleEndian.readUShort(zip64eocdr, off);
off += 2;
diskNo = LittleEndian.readUInt(zip64eocdr, off);
off += 4;
cdDiskNo = LittleEndian.readUInt(zip64eocdr, off);
off += 4;
cdEntriesDisk = LittleEndian.readLong(zip64eocdr, off);
off += 8;
cdEntries = LittleEndian.readLong(zip64eocdr, off);
off += 8;
if (diskNo != 0 || cdDiskNo != 0 || cdEntriesDisk != cdEntries)
throw new ZipException( // MUST be ZipException, not IOException - see catch clauses!
"ZIP file spanning/splitting is not supported!");
if (cdEntries < 0 || Integer.MAX_VALUE < cdEntries)
throw new ZipException( // MUST be ZipException, not IOException - see catch clauses!
"Total Number Of Entries In The Central Directory out of range!");
cdSize = LittleEndian.readLong(zip64eocdr, off);
off += 8;
cdOffset = LittleEndian.readLong(zip64eocdr, off);
//off += 8;
rof.seek(cdOffset);
mapper = new OffsetMapper();
} catch (ZipException ze) {
throw ze;
} catch (IOException ioe) {
// Seek and check first CFH, probably using an offset mapper.
long start = eocdrOffset - cdSize;
rof.seek(start);
start -= cdOffset;
if (start != 0) {
mapper = new IrregularOffsetMapper(start);
} else {
mapper = new OffsetMapper();
}
}
return (int) cdEntries;
}
}
throw new ZipException(
"Expected End Of Central Directory Record signature!");
}
/**
* A factory method returning a newly created ZipEntry for the given name.
*
* @deprecated This method is called from a constructor!
* It will be replaced by a factory interface in TrueZIP 7.
*/
protected ZipEntry createZipEntry(String name) {
return new ZipEntry(name);
}
/**
* Returns the comment of this ZIP compatible file or {@code null}
* if no comment exists.
*/
public String getComment() {
return comment;
}
private void setComment(String comment) {
this.comment = comment;
}
/**
* Returns {@code true} if and only if some input streams are open to
* read from this ZIP compatible file.
*/
public boolean busy() {
return openStreams > 0;
}
/** Returns the charset to use for entry names and comments. */
public String getCharset() {
return charset;
}
/**
* Returns an enumeration of the ZIP entries in this ZIP file.
* Note that the enumerated entries are shared with this class.
* It is illegal to change their state!
*/
public Enumeration entries() {
return Collections.enumeration(entries.values());
}
/**
* Returns the {@link ZipEntry} for the given name or
* {@code null} if no entry with that name exists.
* Note that the returned entry is shared with this class.
* It is illegal to change its state!
*
* @param name Name of the ZIP entry.
*/
public ZipEntry getEntry(String name) {
return (ZipEntry) entries.get(name);
}
/**
* Returns the number of entries in this ZIP compatible file.
*/
public int size() {
return entries.size();
}
/**
* Returns the file length of this ZIP compatible file in bytes.
*/
public long length() throws IOException {
ensureOpen();
return archive.length();
}
/**
* Returns the length of the preamble of this ZIP compatible file in bytes.
*
* @return A positive value or zero to indicate that this ZIP compatible
* file does not have a preamble.
*
* @since TrueZIP 5.1
*/
public long getPreambleLength() {
return preamble;
}
/**
* Returns an {@link InputStream} to read the preamble of this ZIP
* compatible file.
*
* Note that the returned stream is a lightweight stream,
* i.e. there is no external resource such as a {@link ReadOnlyFile}
* allocated for it. Instead, all streams returned by this method share
* the underlying {@code ReadOnlyFile} of this {@code ZipFile}.
* This allows to close this object (and hence the underlying
* {@code ReadOnlyFile}) without cooperation of the returned
* streams, which is important if the client application wants to work on
* the underlying file again (e.g. update or delete it).
*
* @since TrueZIP 5.1
* @throws ZipException If this ZIP file has been closed.
*/
public InputStream getPreambleInputStream() throws IOException {
ensureOpen();
return new IntervalInputStream(0, preamble);
}
/**
* Returns the length of the postamble of this ZIP compatible file in bytes.
*
* @return A positive value or zero to indicate that this ZIP compatible
* file does not have an postamble.
*
* @since TrueZIP 5.1
*/
public long getPostambleLength() {
return postamble;
}
/**
* Returns an {@link InputStream} to read the postamble of this ZIP
* compatible file.
*
* Note that the returned stream is a lightweight stream,
* i.e. there is no external resource such as a {@link ReadOnlyFile}
* allocated for it. Instead, all streams returned by this method share
* the underlying {@code ReadOnlyFile} of this {@code ZipFile}.
* This allows to close this object (and hence the underlying
* {@code ReadOnlyFile}) without cooperation of the returned
* streams, which is important if the client application wants to work on
* the underlying file again (e.g. update or delete it).
*
* @since TrueZIP 5.1
* @throws ZipException If this ZIP file has been closed.
*/
public InputStream getPostambleInputStream() throws IOException {
ensureOpen();
return new IntervalInputStream(archive.length() - postamble, postamble);
}
/**
* Returns {@code true} if and only if the offsets in this ZIP file
* are relative to the start of the file, rather than the first Local
* File Header.
*
* This method is intended for very special purposes only.
*/
public boolean offsetsConsiderPreamble() {
assert mapper != null;
return mapper.location(0) == 0;
}
/**
* Equivalent to {@link #getInputStream(String, boolean, boolean)
* getInputStream(name, false, true)}.
*/
public final InputStream getInputStream(String name)
throws IOException {
return getInputStream(name, false, true);
}
/**
* Equivalent to {@link #getInputStream(String, boolean, boolean)
* getInputStream(entry.getName(), false, true)} instead.
*/
public final InputStream getInputStream(ZipEntry entry)
throws IOException {
return getInputStream(entry.getName(), false, true);
}
/**
* Equivalent to {@link #getInputStream(String, boolean, boolean)
* getInputStream(name, true, true)}.
*/
public final InputStream getCheckedInputStream(String name)
throws IOException {
return getInputStream(name, true, true);
}
/**
* Equivalent to {@link #getInputStream(String, boolean, boolean)
* getInputStream(entry.getName(), true, true)} instead.
*/
public final InputStream getCheckedInputStream(ZipEntry entry)
throws IOException {
return getInputStream(entry.getName(), true, true);
}
/** @deprecated */
public InputStream getInputStream(String name, boolean inflate)
throws IOException {
return getInputStream(name, false, inflate);
}
/** @deprecated */
public final InputStream getInputStream(ZipEntry entry, boolean inflate)
throws IOException {
return getInputStream(entry.getName(), false, inflate);
}
/**
* Returns an {@code InputStream} for reading the inflated or
* deflated data of the given entry.
*
* If the {@link #close} method is called on this instance, all input
* streams returned by this method are closed, too.
*
* @param name The name of the entry to get the stream for
* - may not be {@code null}!
* @param check Whether or not the entry's CRC-32 value is checked.
* If and only if this parameter is true, two additional checks are
* performed for the ZIP entry:
*
* - All entry headers are checked to have consistent declarations
* of the CRC-32 value for the inflated entry data.
*
- When calling {@link InputStream#close} on the returned entry
* stream, the CRC-32 value computed from the inflated entry
* data is checked against the declared CRC-32 values.
* This is independent from the {@code inflate} parameter.
*
* If any of these checks fail, a {@link CRC32Exception} is thrown.
*
* This parameter should be {@code false} for most
* applications, and is the default for the sibling of this class
* in {@link java.util.zip.ZipFile java.util.zip.ZipFile}.
* @param inflate Whether or not the entry data should be inflated.
* If {@code false}, the entry data is not inflated,
* even if the entry data is deflated.
* This parameter should be {@code true} for most applications.
* @return A stream to read the entry data from or {@code null} if the
* entry does not exist.
* @throws NullPointerException If {@code name} is {@code null}.
* @throws CRC32Exception If the declared CRC-32 values of the inflated
* entry data are inconsistent across the entry headers.
* @throws ZipException If this file is not compatible to the ZIP File
* Format Specification.
* @throws IOException If the entry cannot get read from this ZipFile.
* @since TrueZIP 6.4
*/
protected InputStream getInputStream(
final String name,
final boolean check,
final boolean inflate)
throws IOException {
ensureOpen();
if (name == null)
throw new NullPointerException();
final ZipEntry entry = (ZipEntry) entries.get(name);
if (entry == null)
return null;
long offset = entry.getOffset();
assert offset != ZipEntry.UNKNOWN;
// This offset has been set by mountCentralDirectory()
// and needs to be resolved first.
offset = mapper.location(offset);
archive.seek(offset);
final byte[] lfh = new byte[ZIP.LFH_MIN_LEN];
archive.readFully(lfh);
final long lfhSig = LittleEndian.readUInt(lfh, 0);
if (lfhSig != ZIP.LFH_SIG)
throw new ZipException(name
+ ": Expected Local File Header Signature!");
offset += ZIP.LFH_MIN_LEN
+ LittleEndian.readUShort(lfh, LFH_FILE_NAME_LENGTH_OFF) // file name length
+ LittleEndian.readUShort(lfh, LFH_FILE_NAME_LENGTH_OFF + 2); // extra field length
if (check) {
// Check CRC-32 in the Local File Header or Data Descriptor.
final long localCrc;
if (entry.getGeneralBit(3)) {
// The CRC-32 is in the Data Descriptor after the compressed
// size.
// Note the Data Descriptor's Signature is optional:
// All newer apps should write it (and so does TrueZIP),
// but older apps might not.
final byte[] dd = new byte[8];
archive.seek(offset + entry.getCompressedSize());
archive.readFully(dd);
final long ddSig = LittleEndian.readUInt(dd, 0);
localCrc = ddSig == ZIP.DD_SIG
? LittleEndian.readUInt(dd, 4)
: ddSig;
} else {
// The CRC-32 is in the Local File Header.
localCrc = LittleEndian.readUInt(lfh, 14);
}
if (entry.getCrc() != localCrc)
throw new CRC32Exception(name, entry.getCrc(), localCrc);
}
final IntervalInputStream iis
= new IntervalInputStream(offset, entry.getCompressedSize());
final int bufSize = getBufferSize(entry);
InputStream in = iis;
switch (entry.getMethod()) {
case ZIP.DEFLATED:
if (inflate) {
iis.addDummy();
in = new PooledInflaterInputStream(in, bufSize);
if (check)
in = new CheckedInputStream(in, entry, bufSize);
break;
} else {
if (check)
in = new RawCheckedInputStream(in, entry, bufSize);
}
break;
case ZIP.STORED:
if (check)
in = new CheckedInputStream(in, entry, bufSize);
break;
default:
assert false : "This should already have been checked by mountCentralDirectory()!";
}
return in;
}
private static int getBufferSize(final ZipEntry entry) {
long size = entry.getSize();
if (size > ZIP.FLATER_BUF_LENGTH)
size = ZIP.FLATER_BUF_LENGTH;
else if (size < ZIP.FLATER_BUF_LENGTH / 8)
size = ZIP.FLATER_BUF_LENGTH / 8;
return (int) size;
}
/**
* Ensures that this archive is still open.
*/
private void ensureOpen() throws ZipException {
if (archive == null)
throw new ZipException("ZIP file has been closed!");
}
private static final class PooledInflaterInputStream
extends InflaterInputStream {
private boolean closed;
public PooledInflaterInputStream(InputStream in, int size) {
super(in, InflaterPool.fetch(), size);
}
public void close() throws IOException {
if (closed)
return;
closed = true;
try {
super.close();
} finally {
InflaterPool.release(inf);
}
}
} // class PooledInflaterInputStream
private static final class CheckedInputStream
extends java.util.zip.CheckedInputStream {
private final ZipEntry entry;
private final int size;
public CheckedInputStream(
final InputStream in,
final ZipEntry entry,
final int size) {
super(in, new CRC32());
this.entry = entry;
this.size = size;
}
public long skip(long toSkip) throws IOException {
return skipWithBuffer(this, toSkip, new byte[size]);
}
public void close() throws IOException {
try {
while (skip(Long.MAX_VALUE) > 0) // process CRC-32 until EOF - this version makes FindBugs happy!
;
} finally {
super.close();
}
final long expectedCrc = entry.getCrc();
final long actualCrc = getChecksum().getValue();
if (expectedCrc != actualCrc)
throw new CRC32Exception(
entry.getName(), expectedCrc, actualCrc);
}
} // class CheckedInputStream
/**
* This method skips {@code toSkip} bytes in the given input stream
* using the given buffer unless EOF or IOException.
*/
private static long skipWithBuffer(
final InputStream in,
final long toSkip,
final byte[] buf)
throws IOException {
long total = 0;
for (long len; (len = toSkip - total) > 0; total += len) {
len = in.read(buf, 0, len < buf.length ? (int) len : buf.length);
if (len < 0)
break;
}
return total;
}
/**
* A stream which reads and returns deflated data from its input
* while a CRC-32 checksum is computed over the inflated data and
* checked in the method {@code close}.
*/
private static final class RawCheckedInputStream extends FilterInputStream {
private final Checksum crc = new CRC32();
private final byte[] singleByteBuf = new byte[1];
private final Inflater inf;
private final byte[] infBuf; // contains inflated data!
private final ZipEntry entry;
private boolean closed;
public RawCheckedInputStream(
final InputStream in,
final ZipEntry entry,
final int size) {
super(in);
this.inf = InflaterPool.fetch();
this.infBuf = new byte[size];
this.entry = entry;
}
private void ensureOpen()
throws IOException {
if (closed)
throw new IOException("input stream has been closed");
}
public int read()
throws IOException {
int read;
while ((read = read(singleByteBuf, 0, 1)) == 0) // reading nothing is not acceptible!
;
return read > 0 ? singleByteBuf[0] & 0xff : -1;
}
public int read(final byte[] buf, final int off, final int len)
throws IOException {
if (len == 0)
return 0; // be fault-tolerant and compatible to FileInputStream
// Check state.
ensureOpen();
// Check parameters.
if (buf == null)
throw new NullPointerException();
final int offPlusLen = off + len;
if ((off | len | offPlusLen | buf.length - offPlusLen) < 0)
throw new IndexOutOfBoundsException();
// Read data.
final int read = in.read(buf, off, len);
// Feed inflater.
if (read >= 0) {
inf.setInput(buf, off, read);
} else {
buf[off] = 0;
inf.setInput(buf, off, 1); // provide dummy byte
}
// Inflate and update checksum.
try {
int inflated;
while ((inflated = inf.inflate(infBuf, 0, infBuf.length)) > 0)
crc.update(infBuf, 0, inflated);
} catch (DataFormatException ex) {
throw (IOException) new IOException(ex.toString()).initCause(ex);
}
// Check inflater invariants.
assert read >= 0 || inf.finished();
assert read < 0 || inf.needsInput();
assert !inf.needsDictionary();
return read;
}
public long skip(long toSkip) throws IOException {
return skipWithBuffer(this, toSkip, new byte[infBuf.length]);
}
public void close() throws IOException {
if (closed)
return;
// Order is important!
try {
while (skip(Long.MAX_VALUE) > 0) // process CRC-32 until EOF - this version makes FindBugs happy!
;
} finally {
closed = true;
InflaterPool.release(inf);
super.close();
}
long expectedCrc = entry.getCrc();
long actualCrc = crc.getValue();
if (expectedCrc != actualCrc)
throw new CRC32Exception(
entry.getName(), expectedCrc, actualCrc);
}
public void mark(int readlimit) {
}
public void reset()
throws IOException {
throw new IOException("mark()/reset() not supported");
}
public boolean markSupported() {
return false;
}
} // class RawCheckedInputStream
/**
* Closes the file.
* This closes any open input streams reading from this ZIP file.
*
* @throws IOException if an error occurs closing the file.
*/
public void close() throws IOException {
// Order is important here!
if (archive != null) {
final ReadOnlyFile oldArchive = archive;
archive = null;
oldArchive.close();
}
}
/**
* InputStream that delegates requests to the underlying
* RandomAccessFile, making sure that only bytes from a certain
* range can be read.
* Calling close() on the enclosing BasicZipFile instance causes all
* corresponding instances of this member class to get close()d, too.
* Note that this class is not thread safe!
*/
private class IntervalInputStream extends AccountedInputStream {
private long remaining;
private long fp;
private boolean addDummyByte;
/**
* @param start The start address (not offset) in {@code archive}.
* @param remaining The remaining bytes allowed to be read in
* {@code archive}.
*/
IntervalInputStream(long start, long remaining) {
assert start >= 0;
assert remaining >= 0;
this.remaining = remaining;
fp = start;
}
public int read()
throws IOException {
ensureOpen();
if (remaining <= 0) {
if (addDummyByte) {
addDummyByte = false;
return 0;
}
return -1;
}
archive.seek(fp);
final int ret = archive.read();
if (ret >= 0) {
fp++;
remaining--;
}
return ret;
}
public int read(final byte[] b, final int off, int len)
throws IOException {
if (len <= 0) {
if (len < 0)
throw new IndexOutOfBoundsException();
return 0;
}
ensureOpen();
if (remaining <= 0) {
if (addDummyByte) {
addDummyByte = false;
b[off] = 0;
return 1;
}
return -1;
}
if (len > remaining)
len = (int) remaining;
archive.seek(fp);
final int ret = archive.read(b, off, len);
if (ret > 0) {
fp += ret;
remaining -= ret;
}
return ret;
}
/**
* Inflater needs an extra dummy byte for nowrap - see
* Inflater's javadocs.
*/
void addDummy() {
addDummyByte = true;
}
/**
* @return The number of bytes remaining in this entry, yet maximum
* {@code Integer.MAX_VALUE}.
* Note that this is only relevant for entries which have been
* stored with the {@code STORED} method.
* For entries stored according to the {@code DEFLATED}
* method, the value returned by this method on the
* {@code InputStream} returned by {@link #getInputStream}
* is actually determined by an {@link InflaterInputStream}.
*/
public int available()
throws IOException {
ensureOpen();
long available = remaining;
if (addDummyByte)
available++;
return available > Integer.MAX_VALUE
? Integer.MAX_VALUE
: (int) available;
}
} // class IntervalInputStream
private abstract class AccountedInputStream extends InputStream {
private boolean closed;
public AccountedInputStream() {
openStreams++;
}
public void close() throws IOException {
// Order is important here!
if (!closed) {
closed = true;
openStreams--;
super.close();
}
}
} // class AccountedInputStream
private static class OffsetMapper {
long location(long offset) {
return offset;
}
} // class OffsetMapper
private static class IrregularOffsetMapper extends OffsetMapper {
final long start;
IrregularOffsetMapper(long start) {
this.start = start;
}
long location(long offset) {
return start + offset;
}
} // class IrregularOffsetMapper
}