All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.schlichtherle.util.zip.BasicZipFile Maven / Gradle / Ivy

/*
 * 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: *

    *
  1. All entry headers are checked to have consistent declarations * of the CRC-32 value for the inflated entry data. *
  2. 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 }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy