| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 java.util.zip; |
| |
| import java.io.EOFException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.PushbackInputStream; |
| import java.nio.charset.ModifiedUtf8; |
| import java.util.jar.Attributes; |
| import java.util.jar.JarEntry; |
| |
| /** |
| * This class provides an implementation of {@code FilterInputStream} that |
| * uncompresses data from a <i>ZIP-archive</i> input stream. |
| * <p> |
| * A <i>ZIP-archive</i> is a collection of compressed (or uncompressed) files - |
| * the so called ZIP entries. Therefore when reading from a {@code |
| * ZipInputStream} first the entry's attributes will be retrieved with {@code |
| * getNextEntry} before its data is read. |
| * <p> |
| * While {@code InflaterInputStream} can read a compressed <i>ZIP-archive</i> |
| * entry, this extension can read uncompressed entries as well. |
| * <p> |
| * Use {@code ZipFile} if you can access the archive as a file directly. |
| * |
| * @see ZipEntry |
| * @see ZipFile |
| */ |
| public class ZipInputStream extends InflaterInputStream implements ZipConstants { |
| static final int DEFLATED = 8; |
| |
| static final int STORED = 0; |
| |
| static final int ZIPLocalHeaderVersionNeeded = 20; |
| |
| private boolean entriesEnd = false; |
| |
| private boolean hasDD = false; |
| |
| private int entryIn = 0; |
| |
| private int inRead, lastRead = 0; |
| |
| ZipEntry currentEntry; |
| |
| private final byte[] hdrBuf = new byte[LOCHDR - LOCVER]; |
| |
| private final CRC32 crc = new CRC32(); |
| |
| private byte[] nameBuf = new byte[256]; |
| |
| private char[] charBuf = new char[256]; |
| |
| /** |
| * Constructs a new {@code ZipInputStream} from the specified input stream. |
| * |
| * @param stream |
| * the input stream to representing a ZIP archive. |
| */ |
| public ZipInputStream(InputStream stream) { |
| super(new PushbackInputStream(stream, BUF_SIZE), new Inflater(true)); |
| if (stream == null) { |
| throw new NullPointerException(); |
| } |
| } |
| |
| /** |
| * Closes this {@code ZipInputStream}. |
| * |
| * @throws IOException |
| * if an {@code IOException} occurs. |
| */ |
| @Override |
| public void close() throws IOException { |
| if (!closed) { |
| closeEntry(); // Close the current entry |
| super.close(); |
| } |
| } |
| |
| /** |
| * Closes the current ZIP entry and positions to read the next entry. |
| * |
| * @throws IOException |
| * if an {@code IOException} occurs. |
| */ |
| public void closeEntry() throws IOException { |
| checkClosed(); |
| if (currentEntry == null) { |
| return; |
| } |
| if (currentEntry instanceof java.util.jar.JarEntry) { |
| Attributes temp = ((JarEntry) currentEntry).getAttributes(); |
| if (temp != null && temp.containsKey("hidden")) { |
| return; |
| } |
| } |
| |
| /* |
| * The following code is careful to leave the ZipInputStream in a |
| * consistent state, even when close() results in an exception. It does |
| * so by: |
| * - pushing bytes back into the source stream |
| * - reading a data descriptor footer from the source stream |
| * - resetting fields that manage the entry being closed |
| */ |
| |
| // Ensure all entry bytes are read |
| Exception failure = null; |
| try { |
| skip(Long.MAX_VALUE); |
| } catch (Exception e) { |
| failure = e; |
| } |
| |
| int inB, out; |
| if (currentEntry.compressionMethod == DEFLATED) { |
| inB = inf.getTotalIn(); |
| out = inf.getTotalOut(); |
| } else { |
| inB = inRead; |
| out = inRead; |
| } |
| int diff = entryIn - inB; |
| // Pushback any required bytes |
| if (diff != 0) { |
| ((PushbackInputStream) in).unread(buf, len - diff, diff); |
| } |
| |
| try { |
| readAndVerifyDataDescriptor(inB, out); |
| } catch (Exception e) { |
| if (failure == null) { // otherwise we're already going to throw |
| failure = e; |
| } |
| } |
| |
| inf.reset(); |
| lastRead = inRead = entryIn = len = 0; |
| crc.reset(); |
| currentEntry = null; |
| |
| if (failure != null) { |
| if (failure instanceof IOException) { |
| throw (IOException) failure; |
| } else if (failure instanceof RuntimeException) { |
| throw (RuntimeException) failure; |
| } |
| AssertionError error = new AssertionError(); |
| error.initCause(failure); |
| throw error; |
| } |
| } |
| |
| private void readAndVerifyDataDescriptor(int inB, int out) throws IOException { |
| if (hasDD) { |
| in.read(hdrBuf, 0, EXTHDR); |
| long sig = getLong(hdrBuf, 0); |
| if (sig != EXTSIG) { |
| throw new ZipException(String.format("unknown format (EXTSIG=%x)", sig)); |
| } |
| currentEntry.crc = getLong(hdrBuf, EXTCRC); |
| currentEntry.compressedSize = getLong(hdrBuf, EXTSIZ); |
| currentEntry.size = getLong(hdrBuf, EXTLEN); |
| } |
| if (currentEntry.crc != crc.getValue()) { |
| throw new ZipException("CRC mismatch"); |
| } |
| if (currentEntry.compressedSize != inB || currentEntry.size != out) { |
| throw new ZipException("Size mismatch"); |
| } |
| } |
| |
| /** |
| * Reads the next entry from this {@code ZipInputStream} or {@code null} if |
| * no more entries are present. |
| * |
| * @return the next {@code ZipEntry} contained in the input stream. |
| * @throws IOException |
| * if an {@code IOException} occurs. |
| * @see ZipEntry |
| */ |
| public ZipEntry getNextEntry() throws IOException { |
| closeEntry(); |
| if (entriesEnd) { |
| return null; |
| } |
| |
| int x = 0, count = 0; |
| while (count != 4) { |
| count += x = in.read(hdrBuf, count, 4 - count); |
| if (x == -1) { |
| return null; |
| } |
| } |
| long hdr = getLong(hdrBuf, 0); |
| if (hdr == CENSIG) { |
| entriesEnd = true; |
| return null; |
| } |
| if (hdr != LOCSIG) { |
| return null; |
| } |
| |
| // Read the local header |
| count = 0; |
| while (count != (LOCHDR - LOCVER)) { |
| count += x = in.read(hdrBuf, count, (LOCHDR - LOCVER) - count); |
| if (x == -1) { |
| throw new EOFException(); |
| } |
| } |
| int version = getShort(hdrBuf, 0) & 0xff; |
| if (version > ZIPLocalHeaderVersionNeeded) { |
| throw new ZipException("Cannot read local header version " + version); |
| } |
| int flags = getShort(hdrBuf, LOCFLG - LOCVER); |
| hasDD = ((flags & ZipFile.GPBF_DATA_DESCRIPTOR_FLAG) != 0); |
| int cetime = getShort(hdrBuf, LOCTIM - LOCVER); |
| int cemodDate = getShort(hdrBuf, LOCTIM - LOCVER + 2); |
| int cecompressionMethod = getShort(hdrBuf, LOCHOW - LOCVER); |
| long cecrc = 0, cecompressedSize = 0, cesize = -1; |
| if (!hasDD) { |
| cecrc = getLong(hdrBuf, LOCCRC - LOCVER); |
| cecompressedSize = getLong(hdrBuf, LOCSIZ - LOCVER); |
| cesize = getLong(hdrBuf, LOCLEN - LOCVER); |
| } |
| int flen = getShort(hdrBuf, LOCNAM - LOCVER); |
| if (flen == 0) { |
| throw new ZipException("Entry is not named"); |
| } |
| int elen = getShort(hdrBuf, LOCEXT - LOCVER); |
| |
| count = 0; |
| if (flen > nameBuf.length) { |
| nameBuf = new byte[flen]; |
| charBuf = new char[flen]; |
| } |
| while (count != flen) { |
| count += x = in.read(nameBuf, count, flen - count); |
| if (x == -1) { |
| throw new EOFException(); |
| } |
| } |
| currentEntry = createZipEntry(ModifiedUtf8.decode(nameBuf, charBuf, 0, flen)); |
| currentEntry.time = cetime; |
| currentEntry.modDate = cemodDate; |
| currentEntry.setMethod(cecompressionMethod); |
| if (cesize != -1) { |
| currentEntry.setCrc(cecrc); |
| currentEntry.setSize(cesize); |
| currentEntry.setCompressedSize(cecompressedSize); |
| } |
| if (elen > 0) { |
| count = 0; |
| byte[] e = new byte[elen]; |
| while (count != elen) { |
| count += x = in.read(e, count, elen - count); |
| if (x == -1) { |
| throw new EOFException(); |
| } |
| } |
| currentEntry.setExtra(e); |
| } |
| return currentEntry; |
| } |
| |
| /* Read 4 bytes from the buffer and store it as an int */ |
| |
| /** |
| * Reads up to the specified number of uncompressed bytes into the buffer |
| * starting at the offset. |
| * |
| * @param buffer |
| * a byte array |
| * @param start |
| * the starting offset into the buffer |
| * @param length |
| * the number of bytes to read |
| * @return the number of bytes read |
| */ |
| @Override |
| public int read(byte[] buffer, int start, int length) throws IOException { |
| checkClosed(); |
| if (inf.finished() || currentEntry == null) { |
| return -1; |
| } |
| // avoid int overflow, check null buffer |
| if (start > buffer.length || length < 0 || start < 0 |
| || buffer.length - start < length) { |
| throw new ArrayIndexOutOfBoundsException(); |
| } |
| |
| if (currentEntry.compressionMethod == STORED) { |
| int csize = (int) currentEntry.size; |
| if (inRead >= csize) { |
| return -1; |
| } |
| if (lastRead >= len) { |
| lastRead = 0; |
| if ((len = in.read(buf)) == -1) { |
| eof = true; |
| return -1; |
| } |
| entryIn += len; |
| } |
| int toRead = length > (len - lastRead) ? len - lastRead : length; |
| if ((csize - inRead) < toRead) { |
| toRead = csize - inRead; |
| } |
| System.arraycopy(buf, lastRead, buffer, start, toRead); |
| lastRead += toRead; |
| inRead += toRead; |
| crc.update(buffer, start, toRead); |
| return toRead; |
| } |
| if (inf.needsInput()) { |
| fill(); |
| if (len > 0) { |
| entryIn += len; |
| } |
| } |
| int read; |
| try { |
| read = inf.inflate(buffer, start, length); |
| } catch (DataFormatException e) { |
| throw new ZipException(e.getMessage()); |
| } |
| if (read == 0 && inf.finished()) { |
| return -1; |
| } |
| crc.update(buffer, start, read); |
| return read; |
| } |
| |
| /** |
| * Skips up to the specified number of bytes in the current ZIP entry. |
| * |
| * @param value |
| * the number of bytes to skip. |
| * @return the number of bytes skipped. |
| * @throws IOException |
| * if an {@code IOException} occurs. |
| */ |
| @Override |
| public long skip(long value) throws IOException { |
| if (value < 0) { |
| throw new IllegalArgumentException(); |
| } |
| |
| long skipped = 0; |
| byte[] b = new byte[(int)Math.min(value, 2048L)]; |
| while (skipped != value) { |
| long rem = value - skipped; |
| int x = read(b, 0, (int) (b.length > rem ? rem : b.length)); |
| if (x == -1) { |
| return skipped; |
| } |
| skipped += x; |
| } |
| return skipped; |
| } |
| |
| @Override |
| public int available() throws IOException { |
| checkClosed(); |
| // The InflaterInputStream contract says we must only return 0 or 1. |
| return (currentEntry == null || inRead < currentEntry.size) ? 1 : 0; |
| } |
| |
| /** |
| * creates a {@link ZipEntry } with the given name. |
| * |
| * @param name |
| * the name of the entry. |
| * @return the created {@code ZipEntry}. |
| */ |
| protected ZipEntry createZipEntry(String name) { |
| return new ZipEntry(name); |
| } |
| |
| private int getShort(byte[] buffer, int off) { |
| return (buffer[off] & 0xFF) | ((buffer[off + 1] & 0xFF) << 8); |
| } |
| |
| private long getLong(byte[] buffer, int off) { |
| long l = 0; |
| l |= (buffer[off] & 0xFF); |
| l |= (buffer[off + 1] & 0xFF) << 8; |
| l |= (buffer[off + 2] & 0xFF) << 16; |
| l |= ((long) (buffer[off + 3] & 0xFF)) << 24; |
| return l; |
| } |
| |
| private void checkClosed() throws IOException { |
| if (closed) { |
| throw new IOException("Stream is closed"); |
| } |
| } |
| } |