The central directory can be followed by a variable-length comment + * field, so we have to scan through it backwards. The comment is at + * most 64K, plus we have 18 bytes for the end-of-central-dir stuff + * itself, plus apparently sometimes people throw random junk on the end + * just for the fun of it. + * + *
This is all a little wobbly. If the wrong value ends up in the EOCD
+ * area, we're hosed. This appears to be the way that everybody handles
+ * it though, so we're in good company if this fails.
+ */
+ private void readCentralDir() throws IOException {
+ /*
+ * Scan back, looking for the End Of Central Directory field. If
+ * the archive doesn't have a comment, we'll hit it on the first
+ * try.
+ *
+ * No need to synchronize mRaf here -- we only do this when we
+ * first open the Zip file.
+ */
+ long scanOffset = mRaf.length() - ENDHDR;
+ if (scanOffset < 0) {
+ throw new TZipException();
+ }
+
+ long stopOffset = scanOffset - 65536;
+ if (stopOffset < 0) {
+ stopOffset = 0;
+ }
+
+ while (true) {
+ mRaf.seek(scanOffset);
+ if (TZipEntry.readIntLE(mRaf) == 101010256L) {
+ break;
+ }
+
+ scanOffset--;
+ if (scanOffset < stopOffset) {
+ throw new TZipException();
+ }
+ }
+
+ /*
+ * Found it, read the EOCD.
+ *
+ * For performance we want to use buffered I/O when reading the
+ * file. We wrap a buffered stream around the random-access file
+ * object. If we just read from the RandomAccessFile we'll be
+ * doing a read() system call every time.
+ */
+ RAFStream rafs = new RAFStream(mRaf, mRaf.getFilePointer());
+ BufferedInputStream bin = new BufferedInputStream(rafs, ENDHDR);
+
+ int diskNumber = ler.readShortLE(bin);
+ int diskWithCentralDir = ler.readShortLE(bin);
+ int numEntries = ler.readShortLE(bin);
+ int totalNumEntries = ler.readShortLE(bin);
+ /*centralDirSize =*/ ler.readIntLE(bin);
+ long centralDirOffset = ler.readIntLE(bin);
+ /*commentLen =*/ ler.readShortLE(bin);
+
+ if (numEntries != totalNumEntries || diskNumber != 0 || diskWithCentralDir != 0) {
+ throw new TZipException();
+ }
+
+ /*
+ * Seek to the first CDE and read all entries.
+ * However, when Z_SYNC_FLUSH is used the offset may not point directly
+ * to the CDE so skip over until we find it.
+ * At most it will be 6 bytes away (one or two bytes for empty block, 4 bytes for
+ * empty block signature).
+ */
+ scanOffset = centralDirOffset;
+ stopOffset = scanOffset + 6;
+
+ while (true) {
+ mRaf.seek(scanOffset);
+ if (TZipEntry.readIntLE(mRaf) == CENSIG) {
+ break;
+ }
+
+ scanOffset++;
+ if (scanOffset > stopOffset) {
+ throw new TZipException();
+ }
+ }
+
+ // If CDE is found then go and read all the entries
+ rafs = new RAFStream(mRaf, scanOffset);
+ bin = new BufferedInputStream(rafs, 4096);
+ for (int i = 0; i < numEntries; i++) {
+ TZipEntry newEntry = new TZipEntry(ler, bin);
+ mEntries.put(newEntry.getName(), newEntry);
+ }
+ }
+
+ static class RAFStream extends InputStream {
+
+ RandomAccessFile mSharedRaf;
+ long mOffset;
+ long mLength;
+
+ public RAFStream(RandomAccessFile raf, long pos) throws IOException {
+ mSharedRaf = raf;
+ mOffset = pos;
+ mLength = raf.length();
+ }
+
+ @Override
+ public int available() throws IOException {
+ if (mLength > mOffset) {
+ if (mLength - mOffset < Integer.MAX_VALUE) {
+ return (int) (mLength - mOffset);
+ } else {
+ return Integer.MAX_VALUE;
+ }
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public int read() throws IOException {
+ byte[] singleByteBuf = new byte[1];
+ if (read(singleByteBuf, 0, 1) == 1) {
+ return singleByteBuf[0] & 0XFF;
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ mSharedRaf.seek(mOffset);
+ if (len > mLength - mOffset) {
+ len = (int) (mLength - mOffset);
+ }
+ int count = mSharedRaf.read(b, off, len);
+ if (count > 0) {
+ mOffset += count;
+ return count;
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ if (n > mLength - mOffset) {
+ n = mLength - mOffset;
+ }
+ mOffset += n;
+ return n;
+ }
+ }
+
+ static class ZipInflaterInputStream extends TInflaterInputStream {
+ TZipEntry entry;
+ long bytesRead;
+
+ public ZipInflaterInputStream(InputStream is, TInflater inf, int bsize, TZipEntry entry) {
+ super(is, inf, bsize);
+ this.entry = entry;
+ }
+
+ @Override
+ public int read(byte[] buffer, int off, int nbytes) throws IOException {
+ int i = super.read(buffer, off, nbytes);
+ if (i != -1) {
+ bytesRead += i;
+ }
+ return i;
+ }
+
+ @Override
+ public int available() throws IOException {
+ return super.available() == 0 ? 0 : (int) (entry.getSize() - bytesRead);
+ }
+ }
+}
diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipInputStream.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipInputStream.java
new file mode 100644
index 000000000..3a9b1c18a
--- /dev/null
+++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipInputStream.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright 2017 Alexey Andreev.
+ *
+ * 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 org.teavm.classlib.java.util.zip;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PushbackInputStream;
+
+public class TZipInputStream extends TInflaterInputStream implements TZipConstants {
+ static final int DEFLATED = 8;
+ static final int STORED = 0;
+ static final int ZIPDataDescriptorFlag = 8;
+ static final int ZIPLocalHeaderVersionNeeded = 20;
+ private boolean entriesEnd;
+ private boolean hasDD;
+ private int entryIn;
+ private int inRead;
+ private int lastRead;
+ TZipEntry currentEntry;
+ private final byte[] hdrBuf = new byte[LOCHDR - LOCVER];
+ private final TCRC32 crc = new TCRC32();
+ private byte[] nameBuf = new byte[256];
+ private char[] charBuf = new char[256];
+
+ public TZipInputStream(InputStream stream) {
+ super(new PushbackInputStream(stream, 512), new TInflater(true));
+ if (stream == null) {
+ throw new NullPointerException();
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (!closed) {
+ closeEntry(); // Close the current entry
+ super.close();
+ }
+ }
+
+ public void closeEntry() throws IOException {
+ if (closed) {
+ throw new IOException();
+ }
+ if (currentEntry == null) {
+ return;
+ }
+
+ /*
+ * The following code is careful to leave the TZipInputStream 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;
+ int 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 = 0;
+ inRead = 0;
+ entryIn = 0;
+ len = 0;
+ crc.reset();
+ currentEntry = null;
+
+ if (failure != null) {
+ if (failure instanceof IOException) {
+ throw (IOException) failure;
+ } else if (failure instanceof RuntimeException) {
+ throw (RuntimeException) failure;
+ }
+ throw new AssertionError(failure);
+ }
+ }
+
+ private void readAndVerifyDataDescriptor(int inB, int out) throws IOException {
+ if (hasDD) {
+ in.read(hdrBuf, 0, EXTHDR);
+ if (getLong(hdrBuf, 0) != EXTSIG) {
+ throw new TZipException();
+ }
+ currentEntry.crc = getLong(hdrBuf, EXTCRC);
+ currentEntry.compressedSize = getLong(hdrBuf, EXTSIZ);
+ currentEntry.size = getLong(hdrBuf, EXTLEN);
+ }
+ if (currentEntry.crc != crc.getValue()) {
+ throw new TZipException();
+ }
+ if (currentEntry.compressedSize != inB || currentEntry.size != out) {
+ throw new TZipException();
+ }
+ }
+
+ public TZipEntry getNextEntry() throws IOException {
+ closeEntry();
+ if (entriesEnd) {
+ return null;
+ }
+
+ int x = 0;
+ int count = 0;
+ while (count != 4) {
+ x = in.read(hdrBuf, count, 4 - count);
+ count += x;
+ 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) {
+ x = in.read(hdrBuf, count, (LOCHDR - LOCVER) - count);
+ count += x;
+ if (x == -1) {
+ throw new EOFException();
+ }
+ }
+ int version = getShort(hdrBuf, 0) & 0xff;
+ if (version > ZIPLocalHeaderVersionNeeded) {
+ throw new TZipException();
+ }
+ int flags = getShort(hdrBuf, LOCFLG - LOCVER);
+ hasDD = (flags & ZIPDataDescriptorFlag) == ZIPDataDescriptorFlag;
+ int cetime = getShort(hdrBuf, LOCTIM - LOCVER);
+ int cemodDate = getShort(hdrBuf, LOCTIM - LOCVER + 2);
+ int cecompressionMethod = getShort(hdrBuf, LOCHOW - LOCVER);
+ long cecrc = 0;
+ long cecompressedSize = 0;
+ long 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 TZipException();
+ }
+ int elen = getShort(hdrBuf, LOCEXT - LOCVER);
+
+ count = 0;
+ if (flen > nameBuf.length) {
+ nameBuf = new byte[flen];
+ charBuf = new char[flen];
+ }
+ while (count != flen) {
+ x = in.read(nameBuf, count, flen - count);
+ count += x;
+ if (x == -1) {
+ throw new EOFException();
+ }
+ }
+ currentEntry = createZipEntry(new String(nameBuf, 0, flen, "UTF-8"));
+ 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) {
+ x = in.read(e, count, elen - count);
+ count += x;
+ if (x == -1) {
+ throw new EOFException();
+ }
+ }
+ currentEntry.setExtra(e);
+ }
+ return currentEntry;
+ }
+
+ @Override
+ public int read(byte[] buffer, int start, int length) throws IOException {
+ if (closed) {
+ throw new IOException();
+ }
+ 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;
+ len = in.read(buf);
+ if (len == -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 (TDataFormatException e) {
+ throw new TZipException(e.getMessage());
+ }
+ if (read == 0 && inf.finished()) {
+ return -1;
+ }
+ crc.update(buffer, start, read);
+ return read;
+ }
+
+ @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 {
+ if (closed) {
+ throw new IOException();
+ }
+ return (currentEntry == null || inRead < currentEntry.size) ? 1 : 0;
+ }
+
+ protected TZipEntry createZipEntry(String name) {
+ return new TZipEntry(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;
+ }
+}
diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipOutputStream.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipOutputStream.java
new file mode 100644
index 000000000..2b64a128f
--- /dev/null
+++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipOutputStream.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright 2017 Alexey Andreev.
+ *
+ * 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 org.teavm.classlib.java.util.zip;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+public class TZipOutputStream extends TDeflaterOutputStream implements TZipConstants {
+ public static final int DEFLATED = 8;
+ public static final int STORED = 0;
+
+ static final int ZIPDataDescriptorFlag = 8;
+ static final int ZIPLocalHeaderVersionNeeded = 20;
+ private String comment;
+ private final List