
net.java.truevfs.comp.zip.AbstractZipFile Maven / Gradle / Ivy
Show all versions of truevfs-comp-zip Show documentation
/*
* Copyright (C) 2005-2015 Schlichtherle IT Services.
* All rights reserved. Use is subject to license terms.
*/
package net.java.truevfs.comp.zip;
import edu.umd.cs.findbugs.annotations.CleanupObligation;
import edu.umd.cs.findbugs.annotations.CreatesObligation;
import edu.umd.cs.findbugs.annotations.DischargesObligation;
import net.java.truecommons.io.*;
import net.java.truecommons.shed.HashMaps;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import javax.annotation.WillCloseWhenClosed;
import javax.annotation.WillNotClose;
import javax.annotation.concurrent.NotThreadSafe;
import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.Charset;
import java.util.*;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import java.util.zip.Inflater;
import java.util.zip.ZipException;
import static net.java.truevfs.comp.zip.Constants.*;
import static net.java.truevfs.comp.zip.ExtraField.WINZIP_AES_ID;
import static net.java.truevfs.comp.zip.WinZipAesExtraField.VV_AE_2;
import static net.java.truevfs.comp.zip.WinZipAesUtils.overhead;
import static net.java.truevfs.comp.zip.ZipEntry.*;
import static net.java.truevfs.comp.zip.ZipParametersUtils.parameters;
/**
* Provides unsafe (raw) access to a ZIP file using 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 AbstractZipOutputStream
* @author Christian Schlichtherle
*/
@NotThreadSafe
@CleanupObligation
public abstract class AbstractZipFile
implements Closeable, Iterable {
private static final int LFH_FILE_NAME_LENGTH_POS =
/* 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 seekable byte channel. */
private @CheckForNull SeekableByteChannel channel;
/** The total number of bytes in the ZIP channel. */
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;
/** Maps entry names to zip entries. */
private Map entries;
/** Maps 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 source the source for reading the ZIP file from.
* @param param the parameters for reading the ZIP file.
* @throws ZipException if the source data is not compatible to the ZIP
* File Format Specification.
* @throws EOFException on unexpected end-of-file.
* @throws IOException on any I/O error.
* @see #recoverLostEntries()
*/
@CreatesObligation
protected AbstractZipFile(
final Source source,
final ZipFileParameters param)
throws ZipException, EOFException, IOException {
this.param = param;
final SeekableByteChannel channel = this.channel = source.channel();
try {
length = channel.size();
charset = param.getCharset();
final @WillNotClose SeekableByteChannel
bchannel = new SafeBufferedReadOnlyChannel(channel, length);
if (!param.getPreambled()) checkZipFileSignature(bchannel);
final int numEntries = findCentralDirectory(bchannel, param.getPostambled());
mountCentralDirectory(bchannel, numEntries);
if (preamble + postamble >= length) {
assert 0 == numEntries;
if (param.getPreambled()) // otherwise already checked
checkZipFileSignature(bchannel);
}
assert null != channel;
assert null != charset;
assert null != entries;
assert null != mapper;
// Do NOT close bchannel - would close channel as well!
} catch (final Throwable e1) {
try {
channel.close();
} catch (final Throwable e2) {
e1.addSuppressed(e2);
}
throw e1;
}
}
private void checkZipFileSignature(final SeekableByteChannel channel)
throws IOException {
final long sig = MutableBuffer
.allocate(4)
.littleEndian()
.load(channel.position(preamble))
.getUInt();
// 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 != sig
&& ZIP64_EOCDR_SIG != sig
&& EOCDR_SIG != sig)
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 SeekableByteChannel channel,
final boolean postambled)
throws IOException {
// Search for End Of Central Directory Record.
final MutableBuffer eocdr = MutableBuffer
.allocate(EOCDR_MIN_LEN)
.littleEndian();
final long max = length - EOCDR_MIN_LEN;
final long min = !postambled && max >= 0xffff ? max - 0xffff : 0;
for (long eocdrPos = max; eocdrPos >= min; eocdrPos--) {
eocdr.rewind().limit(4).load(channel.position(eocdrPos));
// end of central dir signature 4 bytes (0x06054b50)
if (EOCDR_SIG != eocdr.getUInt()) continue;
try {
// Process End Of Central Directory Record.
eocdr.limit(EOCDR_MIN_LEN).load(channel);
// number of this disk 2 bytes
long diskNo = eocdr.getUShort();
// number of the disk with the
// start of the central directory 2 bytes
long cdDiskNo = eocdr.getUShort();
// total number of entries in the
// central directory on this disk 2 bytes
long cdEntriesDisk = eocdr.getUShort();
// total number of entries in
// the central directory 2 bytes
long cdEntries = eocdr.getUShort();
if (0 != diskNo || 0 != cdDiskNo || cdEntriesDisk != cdEntries)
throw new ZipException(
"ZIP file spanning/splitting is not supported!");
// size of the central directory 4 bytes
long cdSize = eocdr.getUInt();
// offset of start of central
// directory with respect to
// the starting disk number 4 bytes
long cdPos = eocdr.getUInt();
// .ZIP file comment length 2 bytes
int commentLen = eocdr.getUShort();
// .ZIP file comment (variable size)
if (0 < commentLen)
comment = MutableBuffer
.allocate(commentLen)
.load(channel)
.array();
preamble = eocdrPos;
postamble = length - channel.position();
// Check for ZIP64 End Of Central Directory Locator.
final long eocdlPos = eocdrPos - ZIP64_EOCDL_LEN;
final MutableBuffer zip64eocdl = MutableBuffer
.allocate(ZIP64_EOCDL_LEN)
.littleEndian();
// zip64 end of central dir locator
// signature 4 bytes (0x07064b50)
if (0 > eocdlPos || ZIP64_EOCDL_SIG != zip64eocdl
.load(channel.position(eocdlPos))
.getUInt()) {
// Seek and check first CFH, probably requiring an offset mapper.
long offset = eocdrPos - cdSize;
channel.position(offset);
offset -= cdPos;
if (0 != offset) mapper = new OffsetPositionMapper(offset);
return (int) cdEntries;
}
// number of the disk with the
// start of the zip64 end of
// central directory 4 bytes
final long zip64eocdrDisk = zip64eocdl.getUInt();
// relative offset of the zip64
// end of central directory record 8 bytes
final long zip64eocdrPos = zip64eocdl.getLong();
// total number of disks 4 bytes
final long totalDisks = zip64eocdl.getUInt();
if (0 != zip64eocdrDisk || 1 != totalDisks)
throw new ZipException(
"ZIP file spanning/splitting is not supported!");
// Read Zip64 End Of Central Directory Record.
final MutableBuffer zip64eocdr = MutableBuffer
.allocate(ZIP64_EOCDR_MIN_LEN)
.littleEndian()
.load(channel.position(zip64eocdrPos));
// zip64 end of central dir
// signature 4 bytes (0x06064b50)
if (ZIP64_EOCDR_SIG != zip64eocdr.getUInt())
throw new ZipException(
"Expected ZIP64 end of central directory record!");
// size of zip64 end of central
// directory record 8 bytes
// version made by 2 bytes
// version needed to extract 2 bytes
zip64eocdr.skip(8 + 2 + 2);
// number of this disk 4 bytes
diskNo = zip64eocdr.getUInt();
// number of the disk with the
// start of the central directory 4 bytes
cdDiskNo = zip64eocdr.getUInt();
// total number of entries in the
// central directory on this disk 8 bytes
cdEntriesDisk = zip64eocdr.getLong();
// total number of entries in the
// central directory 8 bytes
cdEntries = zip64eocdr.getLong();
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 = zip64eocdr.getLong();
zip64eocdr.skip(8);
// offset of start of central
// directory with respect to
// the starting disk number 8 bytes
cdPos = zip64eocdr.getLong();
// zip64 extensible data sector (variable size)
channel.position(cdPos);
preamble = zip64eocdrPos;
return (int) cdEntries;
} catch (RuntimeException e) {
throw (ZipException) new ZipException(
"Invalid (ZIP64) End Of Central Directory Record")
.initCause(e);
}
}
// Start recovering file entries from min.
preamble = min;
postamble = length - min;
return 0;
}
/**
* Reads the central directory from the given seekable byte channel 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 I/O error.
*/
private void mountCentralDirectory(
final SeekableByteChannel channel,
int numEntries)
throws IOException {
final MutableBuffer cfh = MutableBuffer
.allocate(CFH_MIN_LEN)
.littleEndian();
final Map entries = new LinkedHashMap<>(
Math.max(HashMaps.initialCapacity(numEntries), 16));
for (; ; numEntries--) {
cfh.rewind().limit(4).load(channel);
// central file header signature 4 bytes (0x02014b50)
if (CFH_SIG != cfh.getUInt()) break;
cfh.limit(CFH_MIN_LEN).load(channel);
final int gpbf = cfh.position(8).getUShort();
final int nameLen = cfh.position(28).getUShort();
final MutableBuffer name = MutableBuffer
.allocate(nameLen)
.load(channel);
// See appendix D of PKWARE's ZIP File Format Specification.
final boolean utf8 = 0 != (gpbf & GPBF_UTF8);
if (utf8) charset = UTF8;
final E entry = param.newEntry(decode(name.array()));
try {
// central file header signature 4 bytes (0x02014b50)
cfh.position(4);
// version made by 2 bytes
entry.setRawPlatform(cfh.getUShort() >> 8);
// version needed to extract 2 bytes
// general purpose bit flag 2 bytes
cfh.skip(2 + 2);
entry.setGeneralPurposeBitFlags(gpbf);
// compression method 2 bytes
entry.setRawMethod(cfh.getUShort());
// last mod file time 2 bytes
// last mod file date 2 bytes
entry.setRawTime(cfh.getUInt());
// crc-32 4 bytes
entry.setRawCrc(cfh.getUInt());
// compressed size 4 bytes
entry.setRawCompressedSize(cfh.getUInt());
// uncompressed size 4 bytes
entry.setRawSize(cfh.getUInt());
// file name length 2 bytes
cfh.skip(2);
// extra field length 2 bytes
final int extraLen = cfh.getUShort();
// file comment length 2 bytes
final int commentLen = cfh.getUShort();
// disk number start 2 bytes
// internal file attributes 2 bytes
cfh.skip(2 + 2);
//entry.setEncodedInternalAttributes(readUShort(cfh, off));
// external file attributes 4 bytes
entry.setRawExternalAttributes(cfh.getUInt());
// relative offset of local header 4 bytes
long lfhOff = cfh.getUInt();
entry.setRawOffset(lfhOff); // must be unmapped!
// extra field (variable size)
if (0 < extraLen)
entry.setRawExtraFields(MutableBuffer
.allocate(extraLen)
.load(channel)
.array());
// file comment (variable size)
if (0 < commentLen)
entry.setRawComment(decode(MutableBuffer
.allocate(commentLen)
.load(channel)
.array()));
// Re-load 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.map(entry.getOffset());
if (lfhOff < preamble) preamble = lfhOff;
} catch (RuntimeException e) {
throw (ZipException) new ZipException(
entry.getName() + " (invalid Central File Header)")
.initCause(e);
}
// 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()}.
*
* @return {@code this}
* @throws ZipException if an invalid entry is found.
* @throws EOFException on unexpected end-of-file.
* @throws IOException on any I/O error.
*/
public AbstractZipFile recoverLostEntries()
throws ZipException, EOFException, IOException {
final long length = this.length;
final SeekableByteChannel
channel = new SafeBufferedReadOnlyChannel(channel(), length);
while (0 < postamble) {
long pos = length - postamble;
final MutableBuffer lfh = MutableBuffer
.allocate(LFH_MIN_LEN)
.littleEndian()
.load(channel.position(pos));
if (LFH_SIG != lfh.getUInt())
throw new ZipException("Expected local file header!");
final int gpbf = lfh.position(6).getUShort();
final int nameLen = lfh.position(26).getUShort();
// See appendix D of PKWARE's ZIP File Format Specification.
if (0 != (gpbf & GPBF_UTF8)) charset = UTF8;
final E entry = param.newEntry(decode(MutableBuffer
.allocate(nameLen)
.load(channel)
.array()));
try {
// local file header signature 4 bytes (0x04034b50)
// version needed to extract 2 bytes
// general purpose bit flag 2 bytes
entry.setGeneralPurposeBitFlags(gpbf);
lfh.position(8);
// compression method 2 bytes
entry.setRawMethod(lfh.getUShort());
// last mod file time 2 bytes
// last mod file date 2 bytes
entry.setRawTime(lfh.getUInt());
// crc-32 4 bytes
entry.setRawCrc(lfh.getUInt());
// compressed size 4 bytes
entry.setRawCompressedSize(lfh.getUInt());
// uncompressed size 4 bytes
entry.setRawSize(lfh.getUInt());
// file name length 2 bytes
lfh.skip(2);
// extra field length 2 bytes
final int extraLen = lfh.getUShort();
entry.setRawOffset(mapper.unmap(pos));
// extra field (variable size)
if (0 < extraLen)
entry.setRawExtraFields(MutableBuffer
.allocate(extraLen)
.load(channel)
.array());
// 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 = pos = channel.position();
SeekableByteChannel echannel = new IntervalReadOnlyChannel(
channel, pos, length - pos);
WinZipAesExtraField 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)");
echannel = new WinZipAesReadOnlyChannel(echannel,
new WinZipAesEntryParameters(
parameters(
WinZipAesParameters.class,
getCryptoParameters()),
entry));
field = (WinZipAesExtraField)
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 DummyByteChannelInputStream(echannel),
bufSize);
break;
case BZIP2:
din = new CountingInputStream(
new ChannelInputStream(echannel));
try {
in = new BZip2CompressorInputStream(din);
} catch (final Throwable e1) {
try {
din.close();
} catch (final Throwable e2) {
e1.addSuppressed(e2);
}
throw e1;
}
break;
default:
throw new ZipException(entry.getName()
+ " (compression method "
+ method
+ " is not supported)");
}
try (final CheckedInputStream cin = new CheckedInputStream(in, new CRC32())) {
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:
final Inflater inf = ((ZipInflaterInputStream) in)
.getInflater();
assert inf.finished();
pos += inf.getBytesRead();
break;
case BZIP2:
pos += din.getBytesRead();
break;
default:
throw new AssertionError();
}
}
if (null != field) pos += overhead(field.getKeyStrength());
entry.setRawCompressedSize(pos - start);
// We have reconstituted all meta data for the entry.
// Next comes the Data Descriptor.
// Let's parse and check it.
final MutableBuffer dd = MutableBuffer
.allocate(entry.isZip64ExtensionsRequired()
? 4 + 8 + 8
: 4 + 4 + 4)
.littleEndian()
.limit(4)
.load(channel.position(pos));
long crc = dd.getUInt();
// Note the Data Descriptor's Signature is optional:
// All newer apps should write it (and so does TrueVFS),
// but older apps might not.
if (DD_SIG == crc) dd.rewind();
dd.limit(dd.capacity()).load(channel);
crc = dd.position(0).getUInt();
final long csize;
final long size;
if (entry.isZip64ExtensionsRequired()) {
csize = dd.getLong();
size = dd.getLong();
} else {
csize = dd.getUInt();
size = dd.getUInt();
}
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.
pos += entry.getCompressedSize();
channel.position(pos - 1);
if (pos > length || channel.position() >= channel.size())
throw new ZipException(entry.getName()
+ " (truncated entry data)");
}
} catch (RuntimeException e) {
throw (ZipException) new ZipException(
entry.getName() + " (invalid Local File Header or Data Descriptor)")
.initCause(e);
}
// Entry is almost recovered. Update the postamble length.
postamble = length - channel.position();
// 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);
}
return this;
}
@SuppressWarnings("ReturnOfCollectionOrArrayField")
final Map getRawEntries() { return entries; }
private String decode(byte[] buffer) {
return new String(buffer, 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.
*/
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 {@code name} or {@code null} if no entry
* with this name exists in this ZIP file.
* Note that the returned entry is shared with this instance - it is an
* error to change its state!
*
* @param name the name of the ZIP entry.
* @return The entry for the given {@code name} or {@code null} if no entry
* with this name exists in this ZIP file.
*/
public E entry(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 load 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 SeekableByteChannel}
* allocated for it. Instead, all streams returned by this method share
* the underlying {@code SeekableByteChannel} of this {@code ZipFile}.
* This allows to close this object (and hence the underlying
* {@code SeekableByteChannel}) 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 {
return new ChannelInputStream(
new EntryReadOnlyChannel(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 load 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 SeekableByteChannel}
* allocated for it. Instead, all streams returned by this method share
* the underlying {@code SeekableByteChannel} of this {@code ZipFile}.
* This allows to close this object (and hence the underlying
* {@code SeekableByteChannel}) 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 {
channel();
return new ChannelInputStream(
new EntryReadOnlyChannel(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.
*/
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(name, true, true)}.
*/
@CreatesObligation
public final @Nullable InputStream getCheckedInputStream(String name)
throws IOException {
return getInputStream(name, 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
protected @Nullable InputStream getInputStream(
final String name,
@CheckForNull Boolean check,
final boolean process)
throws ZipException, IOException {
final SeekableByteChannel channel = channel();
Objects.requireNonNull(name);
final ZipEntry entry = entries.get(name);
if (null == entry) return null;
long pos = entry.getOffset();
assert UNKNOWN != pos;
pos = mapper.map(pos);
final MutableBuffer lfh = MutableBuffer
.allocate(LFH_MIN_LEN)
.littleEndian()
.load(channel.position(pos));
if (LFH_SIG != lfh.getUInt())
throw new ZipException(name + " (expected local file header)");
lfh.position(LFH_FILE_NAME_LENGTH_POS);
pos += LFH_MIN_LEN
+ lfh.getUShort() // file name length
+ lfh.getUShort(); // extra field length
SeekableByteChannel echannel;
try {
echannel = new EntryReadOnlyChannel(
pos, entry.getCompressedSize());
} catch (RuntimeException e) {
throw (ZipException) new ZipException(
name + " (invalid Local File Header, Data Descriptor or Central File Header)")
.initCause(e);
}
try {
if (!process) {
assert UNKNOWN != entry.getCrc();
return new ChannelInputStream(echannel);
}
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 WinZipAesReadOnlyChannel
eechannel = new WinZipAesReadOnlyChannel(echannel,
new WinZipAesEntryParameters(
parameters(
WinZipAesParameters.class,
getCryptoParameters()),
entry));
echannel = eechannel;
if (check) {
eechannel.authenticate();
// Disable redundant CRC-32 check.
check = false;
}
final WinZipAesExtraField field
= (WinZipAesExtraField) 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 TrueVFS),
// but older apps might not.
final MutableBuffer dd = MutableBuffer
.allocate(8)
.littleEndian()
.load(channel.position(pos + entry.getCompressedSize()));
localCrc = dd.getUInt();
if (DD_SIG == localCrc) localCrc = dd.getUInt();
} else {
// The CRC32 in the Local File Header.
localCrc = lfh.position(14).getUInt();
}
if (entry.getCrc() != localCrc)
throw new Crc32Exception(name, entry.getCrc(), localCrc);
}
final int bufSize = getBufferSize(entry);
InputStream in;
switch (method) {
case STORED:
in = new ChannelInputStream(echannel);
break;
case DEFLATED:
in = new ZipInflaterInputStream(
new DummyByteChannelInputStream(echannel),
bufSize);
break;
case BZIP2:
in = new BZip2CompressorInputStream(
new ChannelInputStream(echannel));
break;
default:
throw new ZipException(name
+ " (compression method "
+ method
+ " is not supported)");
}
if (check) in = new Crc32InputStream(in, bufSize, entry);
return in;
} catch (final Throwable e1) {
try {
echannel.close();
} catch (final Throwable e2) {
e1.addSuppressed(e2);
}
throw e1;
}
}
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 SeekableByteChannel channel() throws ZipException {
final SeekableByteChannel channel = this.channel;
if (null == channel) throw new ZipException("File closed!");
return channel;
}
/**
* Closes the file.
* This closes any allocated input streams reading from this ZIP file.
*
* @throws IOException if an error occurs closing the file.
*/
@Override
@DischargesObligation
public void close() throws IOException {
final SeekableByteChannel channel = this.channel;
if (null != channel) {
channel.close();
this.channel = null;
}
}
/**
* An interval read-only channel which accounts for itself until it gets
* closed.
* Note that when an object of this class gets closed, the decorated
* read-only channel, i.e. the raw file does NOT get closed!
*/
private final class EntryReadOnlyChannel extends ReadOnlyChannel {
boolean closed;
@CreatesObligation
EntryReadOnlyChannel(final long start, final long size)
throws IOException {
super(new IntervalReadOnlyChannel(channel(), start, size));
AbstractZipFile.this.open++;
}
@Override
public void close() throws IOException {
if (closed) return;
// Never close the channel!
//super.close();
AbstractZipFile.this.open--;
closed = true;
}
} // EntryReadOnlyChannel
/**
* A buffered load 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 SafeBufferedReadOnlyChannel
extends BufferedReadOnlyChannel {
final long size;
@CreatesObligation
SafeBufferedReadOnlyChannel(
final @WillCloseWhenClosed SeekableByteChannel channel,
final long size) {
super(channel);
this.size = size;
}
@Override
public long size() throws IOException {
checkOpen();
return size;
}
} // SafeBufferedReadOnlyChannel
}