de.schlichtherle.truezip.zip.RawZipFile Maven / Gradle / Ivy
Show all versions of truezip-driver-zip Show documentation
/*
* Copyright (C) 2005-2013 Schlichtherle IT Services.
* All rights reserved. Use is subject to license terms.
*/
package de.schlichtherle.truezip.zip;
import de.schlichtherle.truezip.rof.BufferedReadOnlyFile;
import de.schlichtherle.truezip.rof.IntervalReadOnlyFile;
import de.schlichtherle.truezip.rof.ReadOnlyFile;
import de.schlichtherle.truezip.rof.ReadOnlyFileInputStream;
import static de.schlichtherle.truezip.util.HashMaps.initialCapacity;
import de.schlichtherle.truezip.util.Pool;
import static de.schlichtherle.truezip.zip.Constants.*;
import static de.schlichtherle.truezip.zip.ExtraField.WINZIP_AES_ID;
import static de.schlichtherle.truezip.zip.LittleEndian.*;
import static de.schlichtherle.truezip.zip.WinZipAesEntryExtraField.VV_AE_2;
import static de.schlichtherle.truezip.zip.WinZipAesUtils.overhead;
import static de.schlichtherle.truezip.zip.ZipEntry.*;
import static de.schlichtherle.truezip.zip.ZipParametersUtils.parameters;
import edu.umd.cs.findbugs.annotations.CreatesObligation;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import java.util.zip.Inflater;
import java.util.zip.ZipException;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import javax.annotation.WillCloseWhenClosed;
import javax.annotation.concurrent.NotThreadSafe;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
/**
* Provides unsafe (raw) access to a ZIP file using unsynchronized methods and
* shared {@link ZipEntry} instances.
*
* Warning: This class is not intended for public use
* - its API may change at will without prior notification!
*
* 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 Flags,
* 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.
*
* @param the type of the ZIP entries.
* @see RawZipOutputStream
* @author Christian Schlichtherle
*/
@NotThreadSafe
public abstract class RawZipFile
implements Iterable, Closeable {
private static final int LFH_FILE_NAME_LENGTH_OFF =
/* Local File Header signature */ 4 +
/* Version Needed To Extract */ 2 +
/* General Purpose Bit Flags */ 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 files.
* This is {@code "UTF-8"} for compatibility with Sun's JDK implementation.
* Note that you should use "IBM437" for ordinary ZIP files
* instead.
*/
public static final Charset DEFAULT_CHARSET = Constants.DEFAULT_CHARSET;
/** The nullable data source. */
private @CheckForNull ReadOnlyFile rof;
/** The total number of bytes in the ZIP file. */
private long length;
/** The number of bytes in the preamble of this ZIP file. */
private long preamble;
/** The number of bytes in the postamble of this ZIP file. */
private long postamble;
private final ZipEntryFactory param;
/** The charset to use for entry names and comments. */
private Charset charset;
/** The encoded file comment. */
private @CheckForNull byte[] comment;
/** HashMaps entry names to zip entries. */
private Map entries;
/** HashMaps offsets specified in the ZIP file to real offsets in the file. */
private PositionMapper mapper = new PositionMapper();
/** The number of open resources for reading the entries in this ZIP file. */
private int open;
/**
* Reads the given {@code zip} file in order to provide random access
* to its entries.
*
* @param zip the ZIP file to be read.
* @param param the parameters for reading the ZIP file.
* @throws ZipException if the file is not compatible to the ZIP
* File Format Specification.
* @throws IOException on any other I/O related issue.
* @see #recoverLostEntries()
*/
@CreatesObligation
protected RawZipFile(
@WillCloseWhenClosed ReadOnlyFile zip,
ZipFileParameters param)
throws IOException {
this(new SingleReadOnlyFilePool(zip), param);
}
@edu.umd.cs.findbugs.annotations.SuppressWarnings("OBL_UNSATISFIED_OBLIGATION")
RawZipFile(
final Pool source,
final ZipFileParameters param)
throws IOException {
if (null == param)
throw new NullPointerException();
final ReadOnlyFile rof = source.allocate();
try {
this.rof = rof;
this.length = rof.length();
this.param = param;
this.charset = param.getCharset();
final ReadOnlyFile
brof = new SafeBufferedReadOnlyFile(rof, this.length);
if (!param.getPreambled())
checkZipFileSignature(brof);
final int numEntries = findCentralDirectory(brof, param.getPostambled());
mountCentralDirectory(brof, numEntries);
if (this.preamble + this.postamble >= this.length) {
assert 0 == numEntries;
if (param.getPreambled()) // otherwise already checked
checkZipFileSignature(brof);
}
// Do NOT close brof - would close rof as well!
} catch (IOException ex) {
source.release(rof);
throw ex;
}
assert null != this.rof;
//assert null != this.param; // make FindBugs happy
assert null != this.charset;
assert null != this.entries;
assert null != this.mapper;
}
private void checkZipFileSignature(final ReadOnlyFile rof)
throws IOException {
final byte[] sig = new byte[4];
rof.seek(this.preamble);
rof.readFully(sig);
final long signature = 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.
if (LFH_SIG != signature
&& ZIP64_EOCDR_SIG != signature
&& EOCDR_SIG != signature)
throw new ZipException(
"Expected Local File Header or (ZIP64) End Of Central Directory Record!");
}
/**
* Positions the file pointer at the first Central File Header.
* Performs some means to check that this is really a ZIP file.
*
* As a side effect, the following fields will get initialized:
*
* - {@link #preamble}
*
- {@link #postamble}
*
*
* The following fields may get updated:
*
* - {@link #comment}
*
- {@link #mapper}
*
*
* @throws ZipException If the file is not compatible to the ZIP File
* Format Specification.
* @throws IOException On any other I/O error.
*/
private int findCentralDirectory(
final ReadOnlyFile rof,
final boolean postambled)
throws IOException {
final byte[] sig = new byte[4];
final long max = this.length - EOCDR_MIN_LEN;
final long min = !postambled && max >= 0xffff ? max - 0xffff : 0;
for (long eocdrOff = max; eocdrOff >= min; eocdrOff--) {
rof.seek(eocdrOff);
rof.readFully(sig);
if (EOCDR_SIG != readUInt(sig, 0))
continue;
// Process End Of Central Directory Record.
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
final byte[] eocdr = new byte[EOCDR_MIN_LEN - sig.length];
rof.readFully(eocdr);
int off = 0;
diskNo = readUShort(eocdr, off);
off += 2;
cdDiskNo = readUShort(eocdr, off);
off += 2;
cdEntriesDisk = readUShort(eocdr, off);
off += 2;
cdEntries = readUShort(eocdr, off);
off += 2;
if (0 != diskNo || 0 != cdDiskNo || cdEntriesDisk != cdEntries)
throw new ZipException(
"ZIP file spanning/splitting is not supported!");
cdSize = readUInt(eocdr, off);
off += 4;
cdOffset = readUInt(eocdr, off);
off += 4;
commentLen = readUShort(eocdr, off);
//off += 2;
if (0 < commentLen) {
final byte[] comment = new byte[commentLen];
rof.readFully(comment);
this.comment = comment;
}
this.preamble = eocdrOff;
this.postamble = this.length - rof.getFilePointer();
// Check for ZIP64 End Of Central Directory Locator.
final long eocdlOff = eocdrOff - ZIP64_EOCDL_LEN;
if (0 > eocdlOff) {
// Seek and check first CFH, probably using an offset mapper.
long offset = eocdrOff - cdSize;
rof.seek(offset);
offset -= cdOffset;
if (0 != offset)
this.mapper = new OffsetPositionMapper(offset);
return (int) cdEntries;
}
rof.seek(eocdlOff);
final byte[] zip64eocdl = new byte[ZIP64_EOCDL_LEN];
rof.readFully(zip64eocdl);
if (ZIP64_EOCDL_SIG != readUInt(zip64eocdl, 0)) {
// dito
long offset = eocdrOff - cdSize;
rof.seek(offset);
offset -= cdOffset;
if (0 != offset)
this.mapper = new OffsetPositionMapper(offset);
return (int) cdEntries;
}
final long zip64eocdrDisk; // number of the disk with the start of the zip64 end of central directory record
final long zip64eocdrOff; // relative offset of the zip64 end of central directory record
final long totalDisks; // total number of disks
off = 4; // reuse
zip64eocdrDisk = readUInt(zip64eocdl, off);
off += 4;
zip64eocdrOff = readLong(zip64eocdl, off);
off += 8;
totalDisks = readUInt(zip64eocdl, off);
//off += 4;
if (0 != zip64eocdrDisk || 1 != totalDisks)
throw new ZipException(
"ZIP file spanning/splitting is not supported!");
// Read Zip64 End Of Central Directory Record.
final byte[] zip64eocdr = new byte[ZIP64_EOCDR_MIN_LEN];
rof.seek(zip64eocdrOff);
rof.readFully(zip64eocdr);
off = 0; // reuse
// zip64 end of central dir
// signature 4 bytes (0x06064b50)
if (ZIP64_EOCDR_SIG != readUInt(zip64eocdr, off))
throw new ZipException(
"Expected ZIP64 End Of Central Directory Record!");
off += 4;
// size of zip64 end of central
// directory record 8 bytes
off += 8;
// version made by 2 bytes
off += 2;
// version needed to extract 2 bytes
off += 2;
// number of this disk 4 bytes
diskNo = readUInt(zip64eocdr, off);
off += 4;
// number of the disk with the
// start of the central directory 4 bytes
cdDiskNo = readUInt(zip64eocdr, off);
off += 4;
// total number of entries in the
// central directory on this disk 8 bytes
cdEntriesDisk = readLong(zip64eocdr, off);
off += 8;
// total number of entries in the
// central directory 8 bytes
cdEntries = readLong(zip64eocdr, off);
off += 8;
if (0 != diskNo || 0 != cdDiskNo || cdEntriesDisk != cdEntries)
throw new ZipException(
"ZIP file spanning/splitting is not supported!");
if (cdEntries < 0 || Integer.MAX_VALUE < cdEntries)
throw new ZipException(
"Total Number Of Entries In The Central Directory out of range!");
// size of the central directory 8 bytes
//cdSize = readLong(zip64eocdr, off);
off += 8;
// offset of start of central
// directory with respect to
// the starting disk number 8 bytes
cdOffset = readLong(zip64eocdr, off);
//off += 8;
// zip64 extensible data sector (variable size)
rof.seek(cdOffset);
this.preamble = zip64eocdrOff;
return (int) cdEntries;
}
// Start recovering file entries from min.
this.preamble = min;
this.postamble = this.length - min;
return 0;
}
/**
* Reads the central directory from the given read only file 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.
*
* As a side effect, the following fields will get initialized:
*
* - {@link #entries}
*
*
* The following fields may get updated:
*
* - {@link #preamble}
*
- {@link #charset}
*
*
* @throws ZipException If the file is not compatible to the ZIP File
* Format Specification.
* @throws IOException On any other I/O related issue.
*/
private void mountCentralDirectory(final ReadOnlyFile rof, int numEntries)
throws IOException {
final Map entries = new LinkedHashMap(
Math.max(initialCapacity(numEntries), 16));
final byte[] cfh = new byte[CFH_MIN_LEN];
for (; ; numEntries--) {
rof.readFully(cfh, 0, 4);
// central file header signature 4 bytes (0x02014b50)
if (CFH_SIG != readUInt(cfh, 0))
break;
rof.readFully(cfh, 4, CFH_MIN_LEN - 4);
final int gpbf = readUShort(cfh, 8);
final int nameLen = readUShort(cfh, 28);
final byte[] name = new byte[nameLen];
rof.readFully(name);
// See appendix D of PKWARE's ZIP File Format Specification.
final boolean utf8 = 0 != (gpbf & GPBF_UTF8);
if (utf8)
this.charset = UTF8;
final E entry = this.param.newEntry(decode(name));
try {
int off = 0;
// central file header signature 4 bytes (0x02014b50)
off += 4;
// version made by 2 bytes
entry.setRawPlatform(readUShort(cfh, off) >> 8);
off += 2;
// version needed to extract 2 bytes
off += 2;
// general purpose bit flag 2 bytes
entry.setGeneralPurposeBitFlags(gpbf);
off += 2; // General Purpose Bit Flags
// compression method 2 bytes
entry.setRawMethod(readUShort(cfh, off));
off += 2;
// last mod file time 2 bytes
// last mod file date 2 bytes
entry.setRawTime(readUInt(cfh, off));
off += 4;
// crc-32 4 bytes
entry.setRawCrc(readUInt(cfh, off));
off += 4;
// compressed size 4 bytes
entry.setRawCompressedSize(readUInt(cfh, off));
off += 4;
// uncompressed size 4 bytes
entry.setRawSize(readUInt(cfh, off));
off += 4;
// file name length 2 bytes
off += 2;
// extra field length 2 bytes
final int extraLen = readUShort(cfh, off);
off += 2;
// file comment length 2 bytes
final int commentLen = readUShort(cfh, off);
off += 2;
// disk number start 2 bytes
off += 2;
// internal file attributes 2 bytes
//entry.setEncodedInternalAttributes(readUShort(cfh, off));
off += 2;
// external file attributes 4 bytes
entry.setRawExternalAttributes(readUInt(cfh, off));
off += 4;
// relative offset of local header 4 bytes
long lfhOff = readUInt(cfh, off);
entry.setRawOffset(lfhOff); // must be unmapped!
//off += 4;
// extra field (variable size)
if (0 < extraLen) {
final byte[] extra = new byte[extraLen];
rof.readFully(extra);
entry.setRawExtraFields(extra);
}
// file comment (variable size)
if (0 < commentLen) {
final byte[] comment = new byte[commentLen];
rof.readFully(comment);
entry.setRawComment(decode(comment));
}
// 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 = this.mapper.map(entry.getOffset());
if (lfhOff < this.preamble)
this.preamble = lfhOff;
} catch (IllegalArgumentException cause) {
throw (ZipException) new ZipException(entry.getName()
+ " (invalid meta data)").initCause(cause);
}
// Map the entry using the name that has been determined
// by the ZipEntryFactory.
// 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 (0 != numEntries % 0x10000)
throw new ZipException(
"Expected " +
Math.abs(numEntries) +
(numEntries > 0 ? " more" : " less") +
" entries in the Central Directory!");
// Commit map of entries.
this.entries = entries;
}
/**
* Recovers any lost entries which have been added to the ZIP file after
* the (last) End Of Central Directory Record (EOCDR).
* This method should be called immediately after the constructor.
* It requires a fully initialized object, hence it's not part of the
* constructor.
* For example, to recover encrypted entries, it may require
* {@link #getCryptoParameters() crypto parameters}.
*
* This method starts parsing entries at the start of the postamble and
* continues until it hits EOF or any non-entry data.
* As a side effect, it will not only add any found entries to its internal
* map, but will also cut the start of the postamble accordingly.
*
* Note that it's very likely that this method terminates with an
* exception unless the postamble is empty or contains only valid ZIP
* entries.
* Therefore it may be a good idea to log or silently ignore any exception
* thrown by this method.
* If an exception is thrown, you can check and recover the remaining
* postamble for post-mortem analysis by calling
* {@link #getPostambleLength()} and {@link #getPostambleInputStream()}.
*
* @throws ZipException if an invalid entry is found.
* @throws IOException if any other I/O error occurs.
*/
public void recoverLostEntries() throws IOException {
final long length = this.length;
final ReadOnlyFile rof = new SafeBufferedReadOnlyFile(rof(), length);
while (0 < this.postamble) {
long fp = length - postamble;
rof.seek(fp);
final byte[] lfh = new byte[LFH_MIN_LEN];
rof.readFully(lfh);
if (LFH_SIG != readUInt(lfh, 0))
throw new ZipException("Expected Local File Header!");
final int gpbf = readUShort(lfh, 6);
final int nameLen = readUShort(lfh, 26);
final byte[] name = new byte[nameLen];
rof.readFully(name);
// See appendix D of PKWARE's ZIP File Format Specification.
final boolean utf8 = 0 != (gpbf & GPBF_UTF8);
if (utf8)
this.charset = UTF8;
final E entry = this.param.newEntry(decode(name));
try {
int off = 0;
// local file header signature 4 bytes (0x04034b50)
off += 4;
// version needed to extract 2 bytes
off += 2;
// general purpose bit flag 2 bytes
entry.setGeneralPurposeBitFlags(gpbf);
off += 2; // General Purpose Bit Flags
// compression method 2 bytes
entry.setRawMethod(readUShort(lfh, off));
off += 2;
// last mod file time 2 bytes
// last mod file date 2 bytes
entry.setRawTime(readUInt(lfh, off));
off += 4;
// crc-32 4 bytes
entry.setRawCrc(readUInt(lfh, off));
off += 4;
// compressed size 4 bytes
entry.setRawCompressedSize(readUInt(lfh, off));
off += 4;
// uncompressed size 4 bytes
entry.setRawSize(readUInt(lfh, off));
off += 4;
// file name length 2 bytes
off += 2;
// extra field length 2 bytes
final int extraLen = readUShort(lfh, off);
//off += 2;
entry.setRawOffset(mapper.unmap(fp));
// extra field (variable size)
if (0 < extraLen) {
final byte[] extra = new byte[extraLen];
rof.readFully(extra);
entry.setRawExtraFields(extra);
}
// Process entry contents.
if (entry.getGeneralPurposeBitFlag(GPBF_DATA_DESCRIPTOR)) {
// HC SVNT DRACONES!
// This is the tough one.
// We need to process the entry as if we were unzipping
// it because the CRC-32, the compressed size and the
// uncompressed size are unknown.
// Once we have done this, we compare our findings to
// the Data Descriptor which comes next.
final long start = fp = rof.getFilePointer();
ReadOnlyFile erof = new IntervalReadOnlyFile(rof,
fp, length - fp);
WinZipAesEntryExtraField field = null;
int method = entry.getMethod();
if (entry.isEncrypted()) {
if (WINZIP_AES != method)
throw new ZipException(entry.getName()
+ " (encrypted compression method "
+ method
+ " is not supported)");
erof = new WinZipAesEntryReadOnlyFile(erof,
new WinZipAesEntryParameters(
parameters(
WinZipAesParameters.class,
getCryptoParameters()),
entry));
field = (WinZipAesEntryExtraField)
entry.getExtraField(WINZIP_AES_ID);
method = field.getMethod();
}
final int bufSize = getBufferSize(entry);
CountingInputStream din = null;
InputStream in;
switch (method) {
case DEFLATED:
in = new ZipInflaterInputStream(
new DummyByteInputStream(erof),
bufSize);
break;
case BZIP2:
din = new CountingInputStream(
new ReadOnlyFileInputStream(erof));
in = new BZip2CompressorInputStream(din);
break;
default:
throw new ZipException(entry.getName()
+ " (compression method "
+ method
+ " is not supported)");
}
final CheckedInputStream
cin = new CheckedInputStream(in, new CRC32());
try {
entry.setRawSize(cin.skip(Long.MAX_VALUE));
if (null != field && field.getVendorVersion() == VV_AE_2)
entry.setRawCrc(0);
else
entry.setRawCrc(cin.getChecksum().getValue());
// Sync file pointer on deflated input again.
switch (method) {
case DEFLATED:
Inflater inf = ((ZipInflaterInputStream) in)
.getInflater();
assert inf.finished(); // JDK6: R/W 1210/2057; JDK 7: R/W 1193/2057
fp += inf.getBytesRead();
break;
case BZIP2:
fp += din.getBytesRead();
break;
default:
throw new AssertionError();
}
} finally {
cin.close();
}
if (null != field)
fp += overhead(field.getKeyStrength());
entry.setRawCompressedSize(fp - start);
// We have reconstituted all meta data for the entry.
// Next comes the Data Descriptor.
// Let's parse and check it.
final byte[] dd = new byte[
entry.isZip64ExtensionsRequired()
? 4 + 8 + 8
: 4 + 4 + 4];
rof.seek(fp);
rof.readFully(dd, 0, 4);
long crc = readUInt(dd, 0);
// Note the Data Descriptor's Signature is optional:
// All newer apps should write it (and so does TrueZIP),
// but older apps might not.
if (DD_SIG == crc)
rof.readFully(dd);
else
rof.readFully(dd, 4, dd.length - 4);
crc = readUInt(dd, 0);
final long csize;
final long size;
if (entry.isZip64ExtensionsRequired()) {
csize = readLong(dd, 4);
size = readLong(dd, 12);
} else {
csize = readUInt(dd, 4);
size = readUInt(dd, 8);
}
if (entry.getCrc() != crc)
throw new CRC32Exception(entry.getName(),
entry.getCrc(), crc);
if (entry.getCompressedSize() != csize)
throw new ZipException(entry.getName()
+ " (invalid compressed size in Data Descriptor)");
if (entry.getSize() != size)
throw new ZipException(entry.getName()
+ " (invalid uncompressed size in Data Descriptor)");
} else {
// This is the easy one.
// The entry is not using a Data Descriptor, so we can
// use the properties parsed from the Local File Header.
fp += entry.getCompressedSize();
rof.seek(fp - 1);
if (fp > length || -1 == rof.read())
throw new ZipException(entry.getName()
+ " (truncated ZIP entry)");
}
} catch (final IllegalArgumentException ex) {
throw (ZipException) new ZipException(entry.getName()
+ " (invalid meta data)").initCause(ex);
}
// Entry is almost recovered. Update the postamble length.
postamble = length - rof.getFilePointer();
// Map the entry using the name that has been determined
// by the ZipEntryFactory.
// Note that this name may differ from what has been found
// in the ZIP file!
entries.put(entry.getName(), entry);
}
}
@SuppressWarnings("ReturnOfCollectionOrArrayField")
final Map getRawEntries() { return entries; }
private String decode(byte[] bytes) {
return new String(bytes, charset);
}
@SuppressWarnings("ReturnOfCollectionOrArrayField")
@CheckForNull final byte[] getRawComment() {
return this.comment;
}
/**
* Returns the file comment.
*
* @return The file comment.
*/
public @Nullable String getComment() {
final byte[] comment = this.comment;
return null == comment ? null : decode(comment);
}
/**
* Returns {@code true} if and only if this ZIP file is busy reading
* one or more entries.
*/
public boolean busy() {
return 0 < open;
}
/**
* Returns the character set which is effectively used for
* decoding entry names and the file comment.
* Depending on the ZIP file contents, this may differ from the character
* set provided to the constructor.
*
* @since TrueZIP 7.3
*/
public Charset getRawCharset() {
return charset;
}
/**
* Returns the name of the character set which is effectively used for
* decoding entry names and the file comment.
* Depending on the ZIP file contents, this may differ from the character
* set provided to the constructor.
*/
public String getCharset() {
return charset.name();
}
/**
* Returns the number of entries in this ZIP file.
*/
public int size() {
return entries.size();
}
/**
* Returns an iteration of all entries in this ZIP file.
* Note that the iterated entries are shared with this instance.
* It is illegal to change their state!
*/
@Override
public Iterator iterator() {
return Collections.unmodifiableCollection(entries.values()).iterator();
}
/**
* Returns the entry for the given name or {@code null} if no entry with
* this name exists.
* Note that the returned entry is shared with this instance.
* It is illegal to change its state!
*
* @param name the name of the ZIP entry.
*/
public E getEntry(String name) {
return entries.get(name);
}
/**
* Returns the file length of this ZIP file in bytes.
*/
public long length() {
return length;
}
/**
* Returns the size of the preamble of this ZIP file in bytes.
*
* @return A positive value or zero to indicate that this ZIP file does
* not have a preamble.
*/
public long getPreambleLength() {
return preamble;
}
/**
* Returns an {@link InputStream} to read the preamble of this ZIP 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).
*
* @throws ZipException If this ZIP file has been closed.
*/
@CreatesObligation
public InputStream getPreambleInputStream() throws IOException {
rof();
return new ReadOnlyFileInputStream(
new EntryReadOnlyFile(0, preamble));
}
/**
* Returns the size of the postamble of this ZIP file in bytes.
*
* @return A positive value or zero to indicate that this ZIP file does
* not have a postamble.
*/
public long getPostambleLength() {
return postamble;
}
/**
* Returns an {@link InputStream} to read the postamble of this ZIP 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).
*
* @throws ZipException If this ZIP file has been closed.
*/
@CreatesObligation
public InputStream getPostambleInputStream() throws IOException {
rof();
return new ReadOnlyFileInputStream(
new EntryReadOnlyFile(length - postamble, postamble));
}
final PositionMapper getOffsetMapper() {
return mapper;
}
/**
* 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 0 == mapper.map(0);
}
/**
* Returns the parameters for encryption or authentication of entries.
*
* Returns The parameters for encryption or authentication of entries.
* @since TrueZIP 7.3
*/
protected abstract @CheckForNull ZipCryptoParameters getCryptoParameters();
/**
* Equivalent to {@link #getInputStream(String, Boolean, boolean)
* getInputStream(name, null, true)}.
*/
@CreatesObligation
public final @Nullable InputStream getInputStream(String name)
throws IOException {
return getInputStream(name, null, true);
}
/**
* Equivalent to {@link #getInputStream(String, Boolean, boolean)
* getInputStream(entry.getName(), null, true)} instead.
*/
@CreatesObligation
public final @Nullable InputStream getInputStream(ZipEntry entry)
throws IOException {
return getInputStream(entry.getName(), null, true);
}
/**
* Equivalent to {@link #getInputStream(String, Boolean, boolean)
* getInputStream(name, true, true)}.
*/
@CreatesObligation
public final @Nullable InputStream getCheckedInputStream(String name)
throws IOException {
return getInputStream(name, true, true);
}
/**
* Equivalent to {@link #getInputStream(String, Boolean, boolean)
* getInputStream(entry.getName(), true, true)} instead.
*/
@CreatesObligation
public final @Nullable InputStream getCheckedInputStream(ZipEntry entry)
throws IOException {
return getInputStream(entry.getName(), true, true);
}
/**
* Returns an {@code InputStream} for reading the contents 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.
* @param check Whether or not the entry content gets checked/authenticated.
* If the parameter {@code process} is {@code false}, then this
* parameter is ignored.
* Otherwise, if this parameter is {@code null}, then it is set to
* the {@link ZipEntry#isEncrypted()} property of the given entry.
* Finally, if this parameter is {@code true},
* then the following additional check is performed for the entry:
*
* - If the entry is encrypted, then the Message Authentication
* Code (MAC) value gets computed and checked.
* If this check fails, then a
* {@link ZipAuthenticationException} gets thrown from this
* method (pre-check).
*
- If the entry is not encrypted, then the CRC-32
* value gets computed and checked.
* First, the local file header is checked to hold the same
* CRC-32 value than the central directory record.
* Second, the CRC-32 value is computed and checked.
* If this check fails, then a {@link CRC32Exception}
* gets thrown when {@link InputStream#close} is called on the
* returned entry stream (post-check).
*
* @param process Whether or not the entry contents should get processed,
* e.g. inflated.
* This should be set to {@code false} if and only if the
* application is going to copy entries from an input ZIP file to
* an output ZIP file.
* @return A stream to read the entry data from or {@code null} if the
* entry does not exist.
* @throws ZipAuthenticationException If the entry is encrypted and
* checking the MAC fails.
* @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.
*/
@CreatesObligation
@edu.umd.cs.findbugs.annotations.SuppressWarnings("OBL_UNSATISFIED_OBLIGATION")
protected @Nullable InputStream getInputStream(
final String name,
@CheckForNull Boolean check,
final boolean process)
throws IOException {
final ReadOnlyFile rof = rof();
if (name == null)
throw new NullPointerException();
final ZipEntry entry = entries.get(name);
if (entry == null)
return null;
long fp = entry.getOffset();
assert UNKNOWN != fp;
fp = mapper.map(fp);
rof.seek(fp);
final byte[] lfh = new byte[LFH_MIN_LEN];
rof.readFully(lfh);
if (LFH_SIG != readUInt(lfh, 0))
throw new ZipException(name + " (expected Local File Header)");
fp += LFH_MIN_LEN
+ readUShort(lfh, LFH_FILE_NAME_LENGTH_OFF) // file name length
+ readUShort(lfh, LFH_FILE_NAME_LENGTH_OFF + 2); // extra field length
ReadOnlyFile erof;
try {
erof = new EntryReadOnlyFile(
fp, entry.getCompressedSize());
} catch (IllegalArgumentException ex) {
throw (IOException) new ZipException(name +
" (invalid meta data in Local File Header or Central Directory Record)"
).initCause(ex);
}
try {
if (!process) {
assert UNKNOWN != entry.getCrc();
return new ReadOnlyFileInputStream(erof);
}
if (null == check)
check = entry.isEncrypted();
int method = entry.getMethod();
if (entry.isEncrypted()) {
if (WINZIP_AES != method)
throw new ZipException(name
+ " (encrypted compression method "
+ method
+ " is not supported)");
final WinZipAesEntryReadOnlyFile
eerof = new WinZipAesEntryReadOnlyFile(erof,
new WinZipAesEntryParameters(
parameters(
WinZipAesParameters.class,
getCryptoParameters()),
entry));
erof = eerof;
if (check) {
eerof.authenticate();
// Disable redundant CRC-32 check.
check = false;
}
final WinZipAesEntryExtraField field
= (WinZipAesEntryExtraField) entry.getExtraField(WINZIP_AES_ID);
method = field.getMethod();
}
if (check) {
// Check CRC32 in the Local File Header or Data Descriptor.
long localCrc;
if (entry.getGeneralPurposeBitFlag(GPBF_DATA_DESCRIPTOR)) {
// The CRC32 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];
rof.seek(fp + entry.getCompressedSize());
rof.readFully(dd);
localCrc = readUInt(dd, 0);
if (DD_SIG == localCrc)
localCrc = readUInt(dd, 4);
} else {
// The CRC32 in the Local File Header.
localCrc = readUInt(lfh, 14);
}
if (entry.getCrc() != localCrc)
throw new CRC32Exception(name, entry.getCrc(), localCrc);
}
final int bufSize = getBufferSize(entry);
InputStream in;
switch (method) {
case STORED:
in = new ReadOnlyFileInputStream(erof);
break;
case DEFLATED:
in = new ZipInflaterInputStream(new DummyByteInputStream(erof),
bufSize);
break;
case BZIP2:
in = new BZip2CompressorInputStream(
new ReadOnlyFileInputStream(erof));
break;
default:
throw new ZipException(name
+ " (compression method "
+ method
+ " is not supported)");
}
if (check)
in = new Crc32InputStream(in, entry, bufSize);
return in;
} catch (final IOException ex) {
erof.close();
throw ex;
}
}
private static int getBufferSize(final ZipEntry entry) {
long size = entry.getSize();
if (MAX_FLATER_BUF_LENGTH < size)
size = MAX_FLATER_BUF_LENGTH;
else if (size < MIN_FLATER_BUF_LENGTH)
size = MIN_FLATER_BUF_LENGTH;
return (int) size;
}
/** Checks that this ZIP file is still open for reading its entries. */
private ReadOnlyFile rof() throws ZipException {
final ReadOnlyFile rof = this.rof;
if (null == rof)
throw new ZipException("ZIP file closed!");
return rof;
}
/**
* Closes the file.
* This closes any allocate input streams reading from this ZIP file.
*
* @throws IOException if an error occurs closing the file.
*/
@Override
public void close() throws IOException {
final ReadOnlyFile rof = this.rof;
if (null == rof)
return;
rof.close();
this.rof = null;
}
/**
* An interval read only file which accounts for itself until it gets
* closed.
* Note that when an object of this class gets closed, the decorated read
* only file, i.e. the raw zip file does NOT get closed!
*/
private final class EntryReadOnlyFile extends IntervalReadOnlyFile {
private boolean closed;
@CreatesObligation
@edu.umd.cs.findbugs.annotations.SuppressWarnings("OBL_UNSATISFIED_OBLIGATION")
EntryReadOnlyFile(final long start, final long length)
throws IOException {
super(rof(), start, length);
RawZipFile.this.open++;
}
@Override
public void close() throws IOException {
if (this.closed)
return;
// Never close the raw ZIP file!
//super.close();
this.closed = true;
RawZipFile.this.open--;
}
} // EntryReadOnlyFile
/**
* A buffered read only file which is safe for use with a concurrently
* growing file, e.g. when another thread is appending to it.
*/
private static final class SafeBufferedReadOnlyFile
extends BufferedReadOnlyFile {
final long length;
@CreatesObligation
@edu.umd.cs.findbugs.annotations.SuppressWarnings("OBL_UNSATISFIED_OBLIGATION")
public SafeBufferedReadOnlyFile(
final @WillCloseWhenClosed ReadOnlyFile rof,
final long length)
throws IOException {
super(rof);
assert length <= rof.length();
this.length = length;
}
@Override
public long length() throws IOException {
assertOpen();
return length;
}
} // SafeBufferedReadOnlyFile
}