From 86d151d95356d925c7ae28adebc65272f219c014 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Fri, 10 Nov 2017 00:47:48 +0300 Subject: [PATCH] Add support for more java.util.zip --- checkstyle.xml | 1 - .../classlib/java/util/zip/TAdler32.java | 52 +++ .../teavm/classlib/java/util/zip/TCRC32.java | 55 +++ .../java/util/zip/TCheckedInputStream.java | 73 ++++ .../java/util/zip/TCheckedOutputStream.java | 46 +++ .../classlib/java/util/zip/TChecksum.java | 27 ++ .../java/util/zip/TDataFormatException.java | 9 +- .../classlib/java/util/zip/TDeflater.java | 222 ++++++++++++ .../java/util/zip/TDeflaterOutputStream.java | 87 ++++- .../java/util/zip/TGZIPInputStream.java | 161 +++++++- .../java/util/zip/TGZIPOutputStream.java | 57 ++- .../classlib/java/util/zip/TInflater.java | 201 ++++++++++ .../java/util/zip/TInflaterInputStream.java | 167 ++++++++- .../classlib/java/util/zip/TZipConstants.java | 62 ++++ .../classlib/java/util/zip/TZipEntry.java | 310 ++++++++++++++++ .../classlib/java/util/zip/TZipException.java | 29 ++ .../classlib/java/util/zip/TZipFile.java | 343 ++++++++++++++++++ .../java/util/zip/TZipInputStream.java | 330 +++++++++++++++++ .../java/util/zip/TZipOutputStream.java | 330 +++++++++++++++++ 19 files changed, 2527 insertions(+), 35 deletions(-) create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/zip/TAdler32.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/zip/TCRC32.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/zip/TCheckedInputStream.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/zip/TCheckedOutputStream.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/zip/TChecksum.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/zip/TDeflater.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/zip/TInflater.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipConstants.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipEntry.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipException.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipFile.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipInputStream.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipOutputStream.java diff --git a/checkstyle.xml b/checkstyle.xml index b7fca0c6d..a6bbf8e3a 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -20,7 +20,6 @@ - diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TAdler32.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TAdler32.java new file mode 100644 index 000000000..7dc132f14 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TAdler32.java @@ -0,0 +1,52 @@ +/* + * 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 com.jcraft.jzlib.Adler32; + +public class TAdler32 implements TChecksum { + private Adler32 impl = new Adler32(); + + @Override + public long getValue() { + return impl.getValue(); + } + + @Override + public void reset() { + impl.reset(); + } + + @Override + public void update(int i) { + update(new byte[] { (byte) i }); + } + + public void update(byte[] buf) { + update(buf, 0, buf.length); + } + + @Override + public void update(byte[] buf, int off, int nbytes) { + // avoid int overflow, check null buf + if (off <= buf.length && nbytes >= 0 && off >= 0 && buf.length - off >= nbytes) { + impl.update(buf, off, nbytes); + } else { + throw new ArrayIndexOutOfBoundsException(); + } + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TCRC32.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TCRC32.java new file mode 100644 index 000000000..998b85d1a --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TCRC32.java @@ -0,0 +1,55 @@ +/* + * 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 com.jcraft.jzlib.CRC32; + +public class TCRC32 implements TChecksum { + private CRC32 impl = new CRC32(); + long tbytes; + + @Override + public long getValue() { + return impl.getValue(); + } + + @Override + public void reset() { + impl.reset(); + tbytes = 0; + } + + @Override + public void update(int val) { + impl.update(new byte[] { (byte) val }, 0, 1); + } + + public void update(byte[] buf) { + update(buf, 0, buf.length); + } + + @Override + public void update(byte[] buf, int off, int nbytes) { + // avoid int overflow, check null buf + if (off <= buf.length && nbytes >= 0 && off >= 0 && buf.length - off >= nbytes) { + impl.update(buf, off, nbytes); + tbytes += nbytes; + } else { + throw new ArrayIndexOutOfBoundsException(); + } + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TCheckedInputStream.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TCheckedInputStream.java new file mode 100644 index 000000000..8bcd748e3 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TCheckedInputStream.java @@ -0,0 +1,73 @@ +/* + * 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.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class TCheckedInputStream extends FilterInputStream { + private final TChecksum check; + + public TCheckedInputStream(InputStream is, TChecksum csum) { + super(is); + check = csum; + } + + @Override + public int read() throws IOException { + int x = in.read(); + if (x != -1) { + check.update(x); + } + return x; + } + + @Override + public int read(byte[] buf, int off, int nbytes) throws IOException { + int x = in.read(buf, off, nbytes); + if (x != -1) { + check.update(buf, off, x); + } + return x; + } + + public TChecksum getChecksum() { + return check; + } + + @Override + public long skip(long nbytes) throws IOException { + if (nbytes < 1) { + return 0; + } + long skipped = 0; + byte[] b = new byte[(int) Math.min(nbytes, 2048L)]; + int x; + int v; + while (skipped != nbytes) { + v = (int) (nbytes - skipped); + x = in.read(b, 0, v > b.length ? b.length : v); + if (x == -1) { + return skipped; + } + check.update(b, 0, x); + skipped += x; + } + return skipped; + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TCheckedOutputStream.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TCheckedOutputStream.java new file mode 100644 index 000000000..d66003472 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TCheckedOutputStream.java @@ -0,0 +1,46 @@ +/* + * 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.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class TCheckedOutputStream extends FilterOutputStream { + private final TChecksum check; + + public TCheckedOutputStream(OutputStream os, TChecksum cs) { + super(os); + check = cs; + } + + public TChecksum getChecksum() { + return check; + } + + @Override + public void write(int val) throws IOException { + out.write(val); + check.update(val); + } + + @Override + public void write(byte[] buf, int off, int nbytes) throws IOException { + out.write(buf, off, nbytes); + check.update(buf, off, nbytes); + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TChecksum.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TChecksum.java new file mode 100644 index 000000000..75437fefb --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TChecksum.java @@ -0,0 +1,27 @@ +/* + * 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; + +public interface TChecksum { + long getValue(); + + void reset(); + + void update(byte[] buf, int off, int nbytes); + + void update(int val); +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TDataFormatException.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TDataFormatException.java index 4236f1432..c802f7325 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TDataFormatException.java +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TDataFormatException.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 Alexey Andreev. + * 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. @@ -13,16 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.teavm.classlib.java.util.zip; public class TDataFormatException extends Exception { - private static final long serialVersionUID = 7856637411580418624L; - public TDataFormatException() { super(); } - public TDataFormatException(String message) { - super(message); + public TDataFormatException(String detailMessage) { + super(detailMessage); } } diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TDeflater.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TDeflater.java new file mode 100644 index 000000000..8ec395373 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TDeflater.java @@ -0,0 +1,222 @@ +/* + * 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 com.jcraft.jzlib.Deflater; +import com.jcraft.jzlib.GZIPException; +import com.jcraft.jzlib.JZlib; +import java.util.Arrays; + +public class TDeflater { + public static final int BEST_COMPRESSION = 9; + public static final int BEST_SPEED = 1; + public static final int DEFAULT_COMPRESSION = -1; + public static final int DEFAULT_STRATEGY = 0; + public static final int DEFLATED = 8; + public static final int FILTERED = 1; + public static final int HUFFMAN_ONLY = 2; + public static final int NO_COMPRESSION = 0; + static final int Z_NO_FLUSH = 0; + static final int Z_SYNC_FLUSH = 2; + static final int Z_FINISH = 4; + private int flushParm = Z_NO_FLUSH; + private boolean finished; + private int compressLevel = DEFAULT_COMPRESSION; + private int strategy = DEFAULT_STRATEGY; + private Deflater impl; + private int inRead; + private int inLength; + private boolean nowrap; + + public TDeflater() { + this(DEFAULT_COMPRESSION, false); + } + + public TDeflater(int level) { + this(level, false); + } + + public TDeflater(int level, boolean noHeader) { + super(); + if (level < DEFAULT_COMPRESSION || level > BEST_COMPRESSION) { + throw new IllegalArgumentException(); + } + compressLevel = level; + try { + impl = new Deflater(compressLevel, strategy, noHeader); + } catch (GZIPException e) { + // do nothing + } + nowrap = noHeader; + } + + public int deflate(byte[] buf) { + return deflate(buf, 0, buf.length); + } + + public int deflate(byte[] buf, int off, int nbytes) { + return deflate(buf, off, nbytes, flushParm); + } + + int deflate(byte[] buf, int off, int nbytes, int flushParam) { + if (impl == null) { + throw new IllegalStateException(); + } + // avoid int overflow, check null buf + if (off > buf.length || nbytes < 0 || off < 0 || buf.length - off < nbytes) { + throw new ArrayIndexOutOfBoundsException(); + } + + long sin = impl.total_in; + long sout = impl.total_out; + impl.setOutput(buf, off, nbytes); + int err = impl.deflate(flushParam); + switch (err) { + case JZlib.Z_OK: + break; + case JZlib.Z_STREAM_END: + finished = true; + break; + default: + throw new RuntimeException("Error: " + err); + } + + inRead += impl.total_in - sin; + return (int) (impl.total_out - sout); + } + public void end() { + impl = null; + } + + @Override + protected void finalize() { + end(); + } + + public void finish() { + flushParm = Z_FINISH; + } + + public boolean finished() { + return finished; + } + + public int getAdler() { + if (impl == null) { + throw new IllegalStateException(); + } + + return (int) impl.getAdler(); + } + + public int getTotalIn() { + if (impl == null) { + throw new IllegalStateException(); + } + + return (int) impl.getTotalIn(); + } + + public int getTotalOut() { + if (impl == null) { + throw new IllegalStateException(); + } + + return (int) impl.getTotalOut(); + } + + public boolean needsInput() { + return inRead == inLength; + } + + public void reset() { + if (impl == null) { + throw new NullPointerException(); + } + + flushParm = Z_NO_FLUSH; + finished = false; + impl.init(compressLevel, strategy, nowrap); + } + + public void setDictionary(byte[] buf) { + setDictionary(buf, 0, buf.length); + } + + public void setDictionary(byte[] buf, int off, int nbytes) { + if (impl == null) { + throw new IllegalStateException(); + } + // avoid int overflow, check null buf + if (off <= buf.length && nbytes >= 0 && off >= 0 && buf.length - off >= nbytes) { + impl.setDictionary(Arrays.copyOfRange(buf, off, buf.length), nbytes); + } else { + throw new ArrayIndexOutOfBoundsException(); + } + } + + public void setInput(byte[] buf) { + setInput(buf, 0, buf.length); + } + + public void setInput(byte[] buf, int off, int nbytes) { + if (impl == null) { + throw new IllegalStateException(); + } + // avoid int overflow, check null buf + if (off <= buf.length && nbytes >= 0 && off >= 0 && buf.length - off >= nbytes) { + inLength = nbytes; + inRead = 0; + if (impl.next_in == null) { + impl.init(compressLevel, strategy, nowrap); + } + impl.setInput(buf, off, nbytes, false); + } else { + throw new ArrayIndexOutOfBoundsException(); + } + } + + public void setLevel(int level) { + if (level < DEFAULT_COMPRESSION || level > BEST_COMPRESSION) { + throw new IllegalArgumentException(); + } + compressLevel = level; + } + + public void setStrategy(int strategy) { + if (strategy < DEFAULT_STRATEGY || strategy > HUFFMAN_ONLY) { + throw new IllegalArgumentException(); + } + this.strategy = strategy; + } + + public long getBytesRead() { + // Throw NPE here + if (impl == null) { + throw new NullPointerException(); + } + return impl.getTotalIn(); + } + + public long getBytesWritten() { + // Throw NPE here + if (impl == null) { + throw new NullPointerException(); + } + return impl.getTotalOut(); + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TDeflaterOutputStream.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TDeflaterOutputStream.java index 920290ffb..848ea340e 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TDeflaterOutputStream.java +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TDeflaterOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 Alexey Andreev. + * 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. @@ -13,16 +13,93 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.teavm.classlib.java.util.zip; -import com.jcraft.jzlib.DeflaterOutputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; public class TDeflaterOutputStream extends FilterOutputStream { - public TDeflaterOutputStream(OutputStream out) throws IOException { - super(out); - this.out = new DeflaterOutputStream(out); + static final int BUF_SIZE = 512; + protected byte[] buf; + protected TDeflater def; + boolean done; + + public TDeflaterOutputStream(OutputStream os, TDeflater def) { + this(os, def, BUF_SIZE); + } + + public TDeflaterOutputStream(OutputStream os) { + this(os, new TDeflater()); + } + + public TDeflaterOutputStream(OutputStream os, TDeflater def, int bsize) { + super(os); + if (os == null || def == null) { + throw new NullPointerException(); + } + if (bsize <= 0) { + throw new IllegalArgumentException(); + } + this.def = def; + buf = new byte[bsize]; + } + + protected void deflate() throws IOException { + int x; + do { + x = def.deflate(buf); + out.write(buf, 0, x); + } while (!def.needsInput()); + } + + @Override + public void close() throws IOException { + if (!def.finished()) { + finish(); + } + def.end(); + out.close(); + } + + public void finish() throws IOException { + if (done) { + return; + } + def.finish(); + int x = 0; + while (!def.finished()) { + if (def.needsInput()) { + def.setInput(buf, 0, 0); + } + x = def.deflate(buf); + out.write(buf, 0, x); + } + done = true; + } + + @Override + public void write(int i) throws IOException { + byte[] b = new byte[1]; + b[0] = (byte) i; + write(b, 0, 1); + } + + @Override + public void write(byte[] buffer, int off, int nbytes) throws IOException { + if (done) { + throw new IOException(); + } + // avoid int overflow, check null buf + if (off <= buffer.length && nbytes >= 0 && off >= 0 && buffer.length - off >= nbytes) { + if (!def.needsInput()) { + throw new IOException(); + } + def.setInput(buffer, off, nbytes); + deflate(); + } else { + throw new ArrayIndexOutOfBoundsException(); + } } } diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TGZIPInputStream.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TGZIPInputStream.java index 1c35cb202..5804d0347 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TGZIPInputStream.java +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TGZIPInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 Alexey Andreev. + * 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. @@ -13,20 +13,165 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.teavm.classlib.java.util.zip; -import com.jcraft.jzlib.GZIPInputStream; +import java.io.EOFException; import java.io.IOException; import java.io.InputStream; public class TGZIPInputStream extends TInflaterInputStream { - public TGZIPInputStream(InputStream in, int size) throws IOException { - super(in); - this.in = new GZIPInputStream(in, size, false); + private static final int FCOMMENT = 16; + private static final int FEXTRA = 4; + private static final int FHCRC = 2; + private static final int FNAME = 8; + public final static int GZIP_MAGIC = 0x8b1f; + protected TCRC32 crc = new TCRC32(); + protected boolean eos; + + public TGZIPInputStream(InputStream is) throws IOException { + this(is, BUF_SIZE); } - public TGZIPInputStream(InputStream in) throws IOException { - super(in); - this.in = new GZIPInputStream(in); + public TGZIPInputStream(InputStream is, int size) throws IOException { + super(is, new TInflater(true), size); + byte[] header = new byte[10]; + readFully(header, 0, header.length); + if (getShort(header, 0) != GZIP_MAGIC) { + throw new IOException(); + } + int flags = header[3]; + boolean hcrc = (flags & FHCRC) != 0; + if (hcrc) { + crc.update(header, 0, header.length); + } + if ((flags & FEXTRA) != 0) { + readFully(header, 0, 2); + if (hcrc) { + crc.update(header, 0, 2); + } + int length = getShort(header, 0); + while (length > 0) { + int max = length > buf.length ? buf.length : length; + int result = in.read(buf, 0, max); + if (result == -1) { + throw new EOFException(); + } + if (hcrc) { + crc.update(buf, 0, result); + } + length -= result; + } + } + if ((flags & FNAME) != 0) { + readZeroTerminated(hcrc); + } + if ((flags & FCOMMENT) != 0) { + readZeroTerminated(hcrc); + } + if (hcrc) { + readFully(header, 0, 2); + int crc16 = getShort(header, 0); + if ((crc.getValue() & 0xffff) != crc16) { + throw new IOException(); + } + crc.reset(); + } + } + + @Override + public void close() throws IOException { + eos = true; + super.close(); + } + + 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 int getShort(byte[] buffer, int off) { + return (buffer[off] & 0xFF) | ((buffer[off + 1] & 0xFF) << 8); + } + + @Override + public int read(byte[] buffer, int off, int nbytes) throws IOException { + if (closed) { + throw new IOException(); + } + if (eos) { + return -1; + } + // avoid int overflow, check null buffer + if (off > buffer.length || nbytes < 0 || off < 0 || buffer.length - off < nbytes) { + throw new ArrayIndexOutOfBoundsException(); + } + + int bytesRead; + try { + bytesRead = super.read(buffer, off, nbytes); + } finally { + eos = eof; // update eos after every read(), even when it throws + } + + if (bytesRead != -1) { + crc.update(buffer, off, bytesRead); + } + + if (eos) { + verifyCrc(); + } + + return bytesRead; + } + + private void verifyCrc() throws IOException { + // Get non-compressed bytes read by fill + int size = inf.getRemaining(); + final int trailerSize = 8; // crc (4 bytes) + total out (4 bytes) + byte[] b = new byte[trailerSize]; + int copySize = (size > trailerSize) ? trailerSize : size; + + System.arraycopy(buf, len - size, b, 0, copySize); + readFully(b, copySize, trailerSize - copySize); + + if (getLong(b, 0) != crc.getValue()) { + throw new IOException(); + } + if ((int) getLong(b, 4) != inf.getTotalOut()) { + throw new IOException(); + } + } + + private void readFully(byte[] buffer, int offset, int length) throws IOException { + int result; + while (length > 0) { + result = in.read(buffer, offset, length); + if (result == -1) { + throw new EOFException(); + } + offset += result; + length -= result; + } + } + + private void readZeroTerminated(boolean hcrc) throws IOException { + int result; + while ((result = in.read()) > 0) { + if (hcrc) { + crc.update(result); + } + } + if (result == -1) { + throw new EOFException(); + } + // Add the zero + if (hcrc) { + crc.update(result); + } } } diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TGZIPOutputStream.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TGZIPOutputStream.java index f47ff944c..5ebbdf664 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TGZIPOutputStream.java +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TGZIPOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 Alexey Andreev. + * 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. @@ -13,27 +13,62 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.teavm.classlib.java.util.zip; -import com.jcraft.jzlib.GZIPOutputStream; import java.io.IOException; import java.io.OutputStream; public class TGZIPOutputStream extends TDeflaterOutputStream { - public TGZIPOutputStream(OutputStream out, int size, boolean syncFlush) throws IOException { - super(out); - this.out = new GZIPOutputStream(out, size, syncFlush); + protected TCRC32 crc = new TCRC32(); + + public TGZIPOutputStream(OutputStream os) throws IOException { + this(os, BUF_SIZE); } - public TGZIPOutputStream(OutputStream out, boolean syncFlush) throws IOException { - this(out, 512, syncFlush); + public TGZIPOutputStream(OutputStream os, int size) throws IOException { + super(os, new TDeflater(java.util.zip.Deflater.DEFAULT_COMPRESSION, true), size); + writeShort(TGZIPInputStream.GZIP_MAGIC); + out.write(java.util.zip.Deflater.DEFLATED); + out.write(0); // flags + writeLong(0); // mod time + out.write(0); // extra flags + out.write(0); // operating system } - public TGZIPOutputStream(OutputStream out, int size) throws IOException { - this(out, size, true); + @Override + public void flush() throws IOException { + int count = def.deflate(buf, 0, buf.length, TDeflater.Z_SYNC_FLUSH); + out.write(buf, 0, count); + out.flush(); } - public TGZIPOutputStream(OutputStream out) throws IOException { - this(out, 512, true); + @Override + public void finish() throws IOException { + super.finish(); + writeLong(crc.getValue()); + writeLong(crc.tbytes); + } + + @Override + public void write(byte[] buffer, int off, int nbytes) throws IOException { + super.write(buffer, off, nbytes); + crc.update(buffer, off, nbytes); + } + + private long writeLong(long i) throws IOException { + // Write out the long value as an unsigned int + int unsigned = (int) i; + out.write(unsigned & 0xFF); + out.write((unsigned >> 8) & 0xFF); + out.write((unsigned >> 16) & 0xFF); + out.write((unsigned >> 24) & 0xFF); + return i; + } + + private int writeShort(int i) throws IOException { + out.write(i & 0xFF); + out.write((i >> 8) & 0xFF); + return i; } } diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TInflater.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TInflater.java new file mode 100644 index 000000000..7838741ba --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TInflater.java @@ -0,0 +1,201 @@ +/* + * 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 com.jcraft.jzlib.GZIPException; +import com.jcraft.jzlib.Inflater; +import com.jcraft.jzlib.JZlib; +import java.util.Arrays; + +public class TInflater { + private boolean finished; + private boolean nowrap; + int inLength; + int inRead; + private boolean needsDictionary; + private Inflater impl; + + public TInflater() { + this(false); + } + + public TInflater(boolean noHeader) { + nowrap = noHeader; + try { + impl = new Inflater(noHeader); + } catch (GZIPException e) { + // Do nothing + } + } + + public void end() { + inRead = 0; + inLength = 0; + impl = null; + } + + @Override + protected void finalize() { + end(); + } + + public boolean finished() { + return finished; + } + + public int getAdler() { + if (impl == null) { + throw new IllegalStateException(); + } + return (int) impl.getAdler(); + } + + private native int getAdlerImpl(long handle); + + public long getBytesRead() { + if (impl == null) { + throw new IllegalStateException(); + } + return impl.getTotalIn(); + } + + public long getBytesWritten() { + if (impl == null) { + throw new IllegalStateException(); + } + return impl.getTotalOut(); + } + + public int getRemaining() { + return inLength - inRead; + } + + public int getTotalIn() { + return (int) getBytesRead(); + } + + private native long getTotalInImpl(long handle); + + public int getTotalOut() { + return (int) getBytesWritten(); + } + + private native long getTotalOutImpl(long handle); + + public int inflate(byte[] buf) throws TDataFormatException { + return inflate(buf, 0, buf.length); + } + + public int inflate(byte[] buf, int off, int nbytes) throws TDataFormatException { + // avoid int overflow, check null buf + if (off > buf.length || nbytes < 0 || off < 0 || buf.length - off < nbytes) { + throw new ArrayIndexOutOfBoundsException(); + } + + if (impl == null) { + throw new IllegalStateException(); + } + + if (needsInput()) { + return 0; + } + + long lastInSize = impl.total_in; + long lastOutSize = impl.total_out; + boolean neededDict = needsDictionary; + needsDictionary = false; + impl.setOutput(buf, off, nbytes); + + int errCode = impl.inflate(0); + switch (errCode) { + case JZlib.Z_OK: + break; + case JZlib.Z_NEED_DICT: + needsDictionary = true; + break; + case JZlib.Z_STREAM_END: + finished = true; + break; + default: + throw new TDataFormatException("Error occurred: " + errCode); + } + + if (needsDictionary && neededDict) { + throw new TDataFormatException(); + } + + inRead += impl.total_in - lastInSize; + return (int) (impl.total_out - lastOutSize); + } + + public boolean needsDictionary() { + return needsDictionary; + } + + public boolean needsInput() { + return inRead == inLength; + } + + public void reset() { + if (impl == null) { + throw new NullPointerException(); + } + finished = false; + needsDictionary = false; + inLength = 0; + inRead = 0; + impl.init(nowrap); + } + + private native void resetImpl(long handle); + + public void setDictionary(byte[] buf) { + setDictionary(buf, 0, buf.length); + } + + public void setDictionary(byte[] buf, int off, int nbytes) { + if (impl == null) { + throw new IllegalStateException(); + } + // avoid int overflow, check null buf + if (off <= buf.length && nbytes >= 0 && off >= 0 && buf.length - off >= nbytes) { + if (off > 0) { + buf = Arrays.copyOfRange(buf, off, buf.length); + } + impl.setDictionary(buf, nbytes); + } else { + throw new ArrayIndexOutOfBoundsException(); + } + } + public void setInput(byte[] buf) { + setInput(buf, 0, buf.length); + } + + public void setInput(byte[] buf, int off, int nbytes) { + if (impl == null) { + throw new IllegalStateException(); + } + // avoid int overflow, check null buf + if (off <= buf.length && nbytes >= 0 && off >= 0 && buf.length - off >= nbytes) { + inRead = 0; + inLength = nbytes; + impl.setInput(buf, off, nbytes, false); + } else { + throw new ArrayIndexOutOfBoundsException(); + } + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TInflaterInputStream.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TInflaterInputStream.java index 08d1071d5..38d489fb9 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TInflaterInputStream.java +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TInflaterInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 Alexey Andreev. + * 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. @@ -13,16 +13,173 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.teavm.classlib.java.util.zip; -import com.jcraft.jzlib.InflaterInputStream; +import java.io.EOFException; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; public class TInflaterInputStream extends FilterInputStream { - public TInflaterInputStream(InputStream in) throws IOException { - super(in); - this.in = new InflaterInputStream(in); + protected TInflater inf; + protected byte[] buf; + protected int len; + boolean closed; + boolean eof; + static final int BUF_SIZE = 512; + + public TInflaterInputStream(InputStream is) { + this(is, new TInflater(), BUF_SIZE); + } + + public TInflaterInputStream(InputStream is, TInflater inf) { + this(is, inf, BUF_SIZE); + } + + public TInflaterInputStream(InputStream is, TInflater inf, int bsize) { + super(is); + if (is == null || inf == null) { + throw new NullPointerException(); + } + if (bsize <= 0) { + throw new IllegalArgumentException(); + } + this.inf = inf; + buf = new byte[bsize]; + } + + @Override + public int read() throws IOException { + byte[] b = new byte[1]; + if (read(b, 0, 1) == -1) { + return -1; + } + return b[0] & 0xff; + } + + @Override + public int read(byte[] buffer, int off, int nbytes) throws IOException { + if (closed) { + throw new IOException("Stream is closed"); + } + + if (null == buffer) { + throw new NullPointerException(); + } + + if (off < 0 || nbytes < 0 || off + nbytes > buffer.length) { + throw new IndexOutOfBoundsException(); + } + + if (nbytes == 0) { + return 0; + } + + if (eof) { + return -1; + } + + // avoid int overflow, check null buffer + if (off > buffer.length || nbytes < 0 || off < 0 || buffer.length - off < nbytes) { + throw new ArrayIndexOutOfBoundsException(); + } + + do { + if (inf.needsInput()) { + fill(); + } + // Invariant: if reading returns -1 or throws, eof must be true. + // It may also be true if the next read() should return -1. + try { + int result = inf.inflate(buffer, off, nbytes); + eof = inf.finished(); + if (result > 0) { + return result; + } else if (eof) { + return -1; + } else if (inf.needsDictionary()) { + eof = true; + return -1; + } else if (len == -1) { + eof = true; + throw new EOFException(); + // If result == 0, fill() and try again + } + } catch (TDataFormatException e) { + eof = true; + if (len == -1) { + throw new EOFException(); + } + throw new IOException(e); + } + } while (true); + } + + protected void fill() throws IOException { + if (closed) { + throw new IOException(); + } + len = in.read(buf); + if (len > 0) { + inf.setInput(buf, 0, len); + } + } + + @Override + public long skip(long nbytes) throws IOException { + if (nbytes >= 0) { + if (buf == null) { + buf = new byte[(int) Math.min(nbytes, BUF_SIZE)]; + } + long count = 0; + long rem; + while (count < nbytes) { + rem = nbytes - count; + int x = read(buf, 0, rem > buf.length ? buf.length : (int) rem); + if (x == -1) { + return count; + } + count += x; + } + return count; + } + throw new IllegalArgumentException(); + } + + @Override + public int available() throws IOException { + if (closed) { + throw new IOException("Stream is closed"); + } + if (eof) { + return 0; + } + return 1; + } + + @Override + public void close() throws IOException { + if (!closed) { + inf.end(); + closed = true; + eof = true; + super.close(); + } + } + + @Override + public void mark(int readlimit) { + // do nothing + } + + @Override + public void reset() throws IOException { + throw new IOException(); + } + + @Override + public boolean markSupported() { + return false; } } diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipConstants.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipConstants.java new file mode 100644 index 000000000..5359e6ec3 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipConstants.java @@ -0,0 +1,62 @@ +/* + * 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; + +interface TZipConstants { + + long LOCSIG = 0x4034b50; + long EXTSIG = 0x8074b50; + long CENSIG = 0x2014b50; + long ENDSIG = 0x6054b50; + + int LOCHDR = 30; + int EXTHDR = 16; + int CENHDR = 46; + int ENDHDR = 22; + int LOCVER = 4; + int LOCFLG = 6; + int LOCHOW = 8; + int LOCTIM = 10; + int LOCCRC = 14; + int LOCSIZ = 18; + int LOCLEN = 22; + int LOCNAM = 26; + int LOCEXT = 28; + int EXTCRC = 4; + int EXTSIZ = 8; + int EXTLEN = 12; + int CENVEM = 4; + int CENVER = 6; + int CENFLG = 8; + int CENHOW = 10; + int CENTIM = 12; + int CENCRC = 16; + int CENSIZ = 20; + int CENLEN = 24; + int CENNAM = 28; + int CENEXT = 30; + int CENCOM = 32; + int CENDSK = 34; + int CENATT = 36; + int CENATX = 38; + int CENOFF = 42; + int ENDSUB = 8; + int ENDTOT = 10; + int ENDSIZ = 12; + int ENDOFF = 16; + int ENDCOM = 20; +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipEntry.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipEntry.java new file mode 100644 index 000000000..4e3c35dfb --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipEntry.java @@ -0,0 +1,310 @@ +/* + * 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.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +public class TZipEntry implements TZipConstants, Cloneable { + String name; + String comment; + long compressedSize = -1; + long crc = -1; + long size = -1; + int compressionMethod = -1; + int time = -1; + int modDate = -1; + byte[] extra; + int nameLen = -1; + long mLocalHeaderRelOffset = -1; + + public static final int DEFLATED = 8; + public static final int STORED = 0; + + public TZipEntry(String name) { + if (name == null) { + throw new NullPointerException(); + } + if (name.length() > 0xFFFF) { + throw new IllegalArgumentException(); + } + this.name = name; + } + + public String getComment() { + return comment; + } + + public long getCompressedSize() { + return compressedSize; + } + + public long getCrc() { + return crc; + } + + public byte[] getExtra() { + return extra; + } + + public int getMethod() { + return compressionMethod; + } + + public String getName() { + return name; + } + + public long getSize() { + return size; + } + + public long getTime() { + if (time != -1) { + GregorianCalendar cal = new GregorianCalendar(); + cal.set(Calendar.MILLISECOND, 0); + cal.set(1980 + ((modDate >> 9) & 0x7f), ((modDate >> 5) & 0xf) - 1, + modDate & 0x1f, (time >> 11) & 0x1f, (time >> 5) & 0x3f, + (time & 0x1f) << 1); + return cal.getTime().getTime(); + } + return -1; + } + + public boolean isDirectory() { + return name.charAt(name.length() - 1) == '/'; + } + + public void setComment(String string) { + if (string == null || string.length() <= 0xFFFF) { + comment = string; + } else { + throw new IllegalArgumentException(); + } + } + + public void setCompressedSize(long value) { + compressedSize = value; + } + + public void setCrc(long value) { + if (value >= 0 && value <= 0xFFFFFFFFL) { + crc = value; + } else { + throw new IllegalArgumentException(); + } + } + + public void setExtra(byte[] data) { + if (data == null || data.length <= 0xFFFF) { + extra = data; + } else { + throw new IllegalArgumentException(); + } + } + + public void setMethod(int value) { + if (value != STORED && value != DEFLATED) { + throw new IllegalArgumentException(); + } + compressionMethod = value; + } + + public void setSize(long value) { + if (value >= 0 && value <= 0xFFFFFFFFL) { + size = value; + } else { + throw new IllegalArgumentException(); + } + } + + public void setTime(long value) { + GregorianCalendar cal = new GregorianCalendar(); + cal.setTime(new Date(value)); + int year = cal.get(Calendar.YEAR); + if (year < 1980) { + modDate = 0x21; + time = 0; + } else { + modDate = cal.get(Calendar.DATE); + modDate = (cal.get(Calendar.MONTH) + 1 << 5) | modDate; + modDate = ((cal.get(Calendar.YEAR) - 1980) << 9) | modDate; + time = cal.get(Calendar.SECOND) >> 1; + time = (cal.get(Calendar.MINUTE) << 5) | time; + time = (cal.get(Calendar.HOUR_OF_DAY) << 11) | time; + } + } + + @Override + public String toString() { + return name; + } + + public TZipEntry(TZipEntry ze) { + name = ze.name; + comment = ze.comment; + time = ze.time; + size = ze.size; + compressedSize = ze.compressedSize; + crc = ze.crc; + compressionMethod = ze.compressionMethod; + modDate = ze.modDate; + extra = ze.extra; + nameLen = ze.nameLen; + mLocalHeaderRelOffset = ze.mLocalHeaderRelOffset; + } + + @Override + public Object clone() { + return new TZipEntry(this); + } + + /* + * Internal constructor. Creates a new TZipEntry by reading the + * Central Directory Entry from "in", which must be positioned at + * the CDE signature. + * + * On exit, "in" will be positioned at the start of the next entry. + */ + TZipEntry(LittleEndianReader ler, InputStream in) throws IOException { + + /* + * We're seeing performance issues when we call readShortLE and + * readIntLE, so we're going to read the entire header at once + * and then parse the results out without using any function calls. + * Uglier, but should be much faster. + * + * Note that some lines look a bit different, because the corresponding + * fields or locals are long and so we need to do & 0xffffffffl to avoid + * problems induced by sign extension. + */ + + byte[] hdrBuf = ler.hdrBuf; + myReadFully(in, hdrBuf); + + long sig = (hdrBuf[0] & 0xff) | ((hdrBuf[1] & 0xff) << 8) + | ((hdrBuf[2] & 0xff) << 16) | ((hdrBuf[3] << 24) & 0xffffffffL); + if (sig != CENSIG) { + throw new TZipException(); + } + + compressionMethod = (hdrBuf[10] & 0xff) | ((hdrBuf[11] & 0xff) << 8); + time = (hdrBuf[12] & 0xff) | ((hdrBuf[13] & 0xff) << 8); + modDate = (hdrBuf[14] & 0xff) | ((hdrBuf[15] & 0xff) << 8); + crc = (hdrBuf[16] & 0xff) | ((hdrBuf[17] & 0xff) << 8) + | ((hdrBuf[18] & 0xff) << 16) + | ((hdrBuf[19] << 24) & 0xffffffffL); + compressedSize = (hdrBuf[20] & 0xff) | ((hdrBuf[21] & 0xff) << 8) + | ((hdrBuf[22] & 0xff) << 16) + | ((hdrBuf[23] << 24) & 0xffffffffL); + size = (hdrBuf[24] & 0xff) | ((hdrBuf[25] & 0xff) << 8) + | ((hdrBuf[26] & 0xff) << 16) + | ((hdrBuf[27] << 24) & 0xffffffffL); + nameLen = (hdrBuf[28] & 0xff) | ((hdrBuf[29] & 0xff) << 8); + int extraLen = (hdrBuf[30] & 0xff) | ((hdrBuf[31] & 0xff) << 8); + int commentLen = (hdrBuf[32] & 0xff) | ((hdrBuf[33] & 0xff) << 8); + mLocalHeaderRelOffset = (hdrBuf[42] & 0xff) | ((hdrBuf[43] & 0xff) << 8) + | ((hdrBuf[44] & 0xff) << 16) + | ((hdrBuf[45] << 24) & 0xffffffffL); + + byte[] nameBytes = new byte[nameLen]; + myReadFully(in, nameBytes); + + byte[] commentBytes = null; + if (commentLen > 0) { + commentBytes = new byte[commentLen]; + myReadFully(in, commentBytes); + } + + if (extraLen > 0) { + extra = new byte[extraLen]; + myReadFully(in, extra); + } + + try { + /* + * The actual character set is "IBM Code Page 437". As of + * Sep 2006, the Zip spec (APPNOTE.TXT) supports UTF-8. When + * bit 11 of the GP flags field is set, the file name and + * comment fields are UTF-8. + * + * TODO: add correct UTF-8 support. + */ + name = new String(nameBytes, "ISO-8859-1"); + comment = commentBytes != null ? new String(commentBytes, "ISO-8859-1") : null; + } catch (UnsupportedEncodingException uee) { + throw new InternalError(uee.getMessage()); + } + } + + private void myReadFully(InputStream in, byte[] b) throws IOException { + int len = b.length; + int off = 0; + + while (len > 0) { + int count = in.read(b, off, len); + if (count <= 0) { + throw new EOFException(); + } + off += count; + len -= count; + } + } + + static long readIntLE(RandomAccessFile raf) throws IOException { + int b0 = raf.read(); + int b1 = raf.read(); + int b2 = raf.read(); + int b3 = raf.read(); + + if (b3 < 0) { + throw new EOFException(); + } + return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24); // ATTENTION: DOES SIGN EXTENSION: IS THIS WANTED? + } + + static class LittleEndianReader { + private byte[] b = new byte[4]; + byte[] hdrBuf = new byte[CENHDR]; + + int readShortLE(InputStream in) throws IOException { + if (in.read(b, 0, 2) == 2) { + return (b[0] & 0XFF) | ((b[1] & 0XFF) << 8); + } else { + throw new EOFException(); + } + } + + long readIntLE(InputStream in) throws IOException { + if (in.read(b, 0, 4) == 4) { + return ((b[0] & 0XFF) + | ((b[1] & 0XFF) << 8) + | ((b[2] & 0XFF) << 16) + | ((b[3] & 0XFF) << 24)) + & 0XFFFFFFFFL; // Here for sure NO sign extension is wanted. + } else { + throw new EOFException(); + } + } + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipException.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipException.java new file mode 100644 index 000000000..4b1b88fd4 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipException.java @@ -0,0 +1,29 @@ +/* + * 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.IOException; + +public class TZipException extends IOException { + public TZipException() { + super(); + } + + public TZipException(String detailMessage) { + super(detailMessage); + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipFile.java b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipFile.java new file mode 100644 index 000000000..251e92e75 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/zip/TZipFile.java @@ -0,0 +1,343 @@ +/* + * 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.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedHashMap; +import org.teavm.classlib.java.util.zip.TZipEntry.LittleEndianReader; + +public class TZipFile implements TZipConstants { + public static final int OPEN_READ = 1; + public static final int OPEN_DELETE = 4; + + private final String fileName; + private File fileToDeleteOnClose; + private RandomAccessFile mRaf; + private final LittleEndianReader ler = new LittleEndianReader(); + + private final LinkedHashMap mEntries = new LinkedHashMap<>(); + + public TZipFile(File file) throws TZipException, IOException { + this(file, OPEN_READ); + } + + public TZipFile(File file, int mode) throws IOException { + fileName = file.getPath(); + if (mode != OPEN_READ && mode != (OPEN_READ | OPEN_DELETE)) { + throw new IllegalArgumentException(); + } + + if ((mode & OPEN_DELETE) != 0) { + fileToDeleteOnClose = file; // file.deleteOnExit(); + } else { + fileToDeleteOnClose = null; + } + + mRaf = new RandomAccessFile(fileName, "r"); + + readCentralDir(); + } + + public TZipFile(String name) throws IOException { + this(new File(name), OPEN_READ); + } + + @Override + protected void finalize() throws IOException { + close(); + } + + public void close() throws IOException { + RandomAccessFile raf = mRaf; + + if (raf != null) { + mRaf = null; + raf.close(); + if (fileToDeleteOnClose != null) { + new File(fileName).delete(); + // fileToDeleteOnClose.delete(); + fileToDeleteOnClose = null; + } + } + } + + private void checkNotClosed() { + if (mRaf == null) { + throw new IllegalStateException(); + } + } + + public Enumeration entries() { + checkNotClosed(); + final Iterator iterator = mEntries.values().iterator(); + + return new Enumeration() { + @Override + public boolean hasMoreElements() { + checkNotClosed(); + return iterator.hasNext(); + } + + @Override + public TZipEntry nextElement() { + checkNotClosed(); + return iterator.next(); + } + }; + } + + public TZipEntry getEntry(String entryName) { + checkNotClosed(); + if (entryName == null) { + throw new NullPointerException(); + } + + TZipEntry ze = mEntries.get(entryName); + if (ze == null) { + ze = mEntries.get(entryName + "/"); + } + return ze; + } + + public InputStream getInputStream(TZipEntry entry) throws IOException { + /* + * Make sure this TZipEntry is in this Zip file. We run it through + * the name lookup. + */ + entry = getEntry(entry.getName()); + if (entry == null) { + return null; + } + + /* + * Create a TZipInputStream at the right part of the file. + */ + RandomAccessFile raf = mRaf; + // We don't know the entry data's start position. All we have is the + // position of the entry's local header. At position 28 we find the + // length of the extra data. In some cases this length differs from + // the one coming in the central header. + RAFStream rafstrm = new RAFStream(raf, entry.mLocalHeaderRelOffset + 28); + int localExtraLenOrWhatever = ler.readShortLE(rafstrm); + // Skip the name and this "extra" data or whatever it is: + rafstrm.skip(entry.nameLen + localExtraLenOrWhatever); + rafstrm.mLength = rafstrm.mOffset + entry.compressedSize; + if (entry.compressionMethod == java.util.zip.ZipEntry.DEFLATED) { + int bufSize = Math.max(1024, (int) Math.min(entry.getSize(), 65535L)); + return new ZipInflaterInputStream(rafstrm, new TInflater(true), bufSize, entry); + } else { + return rafstrm; + } + } + + public String getName() { + return fileName; + } + + public int size() { + checkNotClosed(); + return mEntries.size(); + } + + /** + * Find the central directory and read the contents. + * + *

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 entries = new ArrayList<>(); + private int compressMethod = DEFLATED; + private int compressLevel = TDeflater.DEFAULT_COMPRESSION; + private ByteArrayOutputStream cDir = new ByteArrayOutputStream(); + private TZipEntry currentEntry; + private final TCRC32 crc = new TCRC32(); + private int offset; + private int curOffset; + private int nameLength; + private byte[] nameBytes; + + public TZipOutputStream(OutputStream p1) { + super(p1, new TDeflater(TDeflater.DEFAULT_COMPRESSION, true)); + } + + @Override + public void close() throws IOException { + if (out != null) { + finish(); + out.close(); + out = null; + } + } + + public void closeEntry() throws IOException { + if (cDir == null) { + throw new IOException(); + } + if (currentEntry == null) { + return; + } + if (currentEntry.getMethod() == DEFLATED) { + super.finish(); + } + + // Verify values for STORED types + if (currentEntry.getMethod() == STORED) { + if (crc.getValue() != currentEntry.crc) { + throw new TZipException(); + } + if (currentEntry.size != crc.tbytes) { + throw new TZipException(); + } + } + curOffset = LOCHDR; + + // Write the DataDescriptor + if (currentEntry.getMethod() != STORED) { + curOffset += EXTHDR; + writeLong(out, EXTSIG); + currentEntry.crc = crc.getValue(); + writeLong(out, currentEntry.crc); + currentEntry.compressedSize = def.getTotalOut(); + writeLong(out, currentEntry.compressedSize); + currentEntry.size = def.getTotalIn(); + writeLong(out, currentEntry.size); + } + // Update the CentralDirectory + writeLong(cDir, CENSIG); + writeShort(cDir, ZIPLocalHeaderVersionNeeded); // Version created + writeShort(cDir, ZIPLocalHeaderVersionNeeded); // Version to extract + writeShort(cDir, currentEntry.getMethod() == STORED ? 0 : ZIPDataDescriptorFlag); + writeShort(cDir, currentEntry.getMethod()); + writeShort(cDir, currentEntry.time); + writeShort(cDir, currentEntry.modDate); + writeLong(cDir, crc.getValue()); + if (currentEntry.getMethod() == DEFLATED) { + curOffset += writeLong(cDir, def.getTotalOut()); + writeLong(cDir, def.getTotalIn()); + } else { + curOffset += writeLong(cDir, crc.tbytes); + writeLong(cDir, crc.tbytes); + } + curOffset += writeShort(cDir, nameLength); + if (currentEntry.extra != null) { + curOffset += writeShort(cDir, currentEntry.extra.length); + } else { + writeShort(cDir, 0); + } + String c = currentEntry.getComment(); + writeShort(cDir, c != null ? c.length() : 0); + writeShort(cDir, 0); // Disk Start + writeShort(cDir, 0); // Internal File Attributes + writeLong(cDir, 0); // External File Attributes + writeLong(cDir, offset); + cDir.write(nameBytes); + nameBytes = null; + if (currentEntry.extra != null) { + cDir.write(currentEntry.extra); + } + offset += curOffset; + if (c != null) { + cDir.write(c.getBytes()); + } + currentEntry = null; + crc.reset(); + def.reset(); + done = false; + } + + @Override + public void finish() throws IOException { + if (out == null) { + throw new IOException(); + } + if (cDir == null) { + return; + } + if (entries.size() == 0) { + throw new TZipException(); + } + if (currentEntry != null) { + closeEntry(); + } + int cdirSize = cDir.size(); + // Write Central Dir End + writeLong(cDir, ENDSIG); + writeShort(cDir, 0); // Disk Number + writeShort(cDir, 0); // Start Disk + writeShort(cDir, entries.size()); // Number of entries + writeShort(cDir, entries.size()); // Number of entries + writeLong(cDir, cdirSize); // Size of central dir + writeLong(cDir, offset); // Offset of central dir + if (comment != null) { + writeShort(cDir, comment.length()); + cDir.write(comment.getBytes()); + } else { + writeShort(cDir, 0); + } + // Write the central dir + out.write(cDir.toByteArray()); + cDir = null; + + } + + public void putNextEntry(TZipEntry ze) throws IOException { + if (currentEntry != null) { + closeEntry(); + } + if (ze.getMethod() == STORED + || (compressMethod == STORED && ze.getMethod() == -1)) { + if (ze.crc == -1) { + throw new TZipException("Crc mismatch"); + } + if (ze.size == -1 && ze.compressedSize == -1) { + throw new TZipException("Size mismatch"); + } + if (ze.size != ze.compressedSize && ze.compressedSize != -1 && ze.size != -1) { + throw new TZipException("Size mismatch"); + } + } + if (cDir == null) { + throw new IOException("Stream is closed"); + } + if (entries.contains(ze.name)) { + throw new TZipException("Entry already exists: " + ze.name); + } + nameLength = utf8Count(ze.name); + if (nameLength > 0xffff) { + throw new IllegalArgumentException("Name too long: " + ze.name); + } + + def.setLevel(compressLevel); + currentEntry = ze; + entries.add(currentEntry.name); + if (currentEntry.getMethod() == -1) { + currentEntry.setMethod(compressMethod); + } + writeLong(out, LOCSIG); // Entry header + writeShort(out, ZIPLocalHeaderVersionNeeded); // Extraction version + writeShort(out, currentEntry.getMethod() == STORED ? 0 : ZIPDataDescriptorFlag); + writeShort(out, currentEntry.getMethod()); + if (currentEntry.getTime() == -1) { + currentEntry.setTime(System.currentTimeMillis()); + } + writeShort(out, currentEntry.time); + writeShort(out, currentEntry.modDate); + + if (currentEntry.getMethod() == STORED) { + if (currentEntry.size == -1) { + currentEntry.size = currentEntry.compressedSize; + } else if (currentEntry.compressedSize == -1) { + currentEntry.compressedSize = currentEntry.size; + } + writeLong(out, currentEntry.crc); + writeLong(out, currentEntry.size); + writeLong(out, currentEntry.size); + } else { + writeLong(out, 0); + writeLong(out, 0); + writeLong(out, 0); + } + writeShort(out, nameLength); + writeShort(out, currentEntry.extra != null ? currentEntry.extra.length : 0); + nameBytes = toUTF8Bytes(currentEntry.name, nameLength); + out.write(nameBytes); + if (currentEntry.extra != null) { + out.write(currentEntry.extra); + } + } + + public void setComment(String comment) { + if (comment.length() > 0xFFFF) { + throw new IllegalArgumentException(); + } + this.comment = comment; + } + + public void setLevel(int level) { + if (level < TDeflater.DEFAULT_COMPRESSION || level > TDeflater.BEST_COMPRESSION) { + throw new IllegalArgumentException(); + } + compressLevel = level; + } + + public void setMethod(int method) { + if (method != STORED && method != DEFLATED) { + throw new IllegalArgumentException(); + } + compressMethod = method; + + } + + private long writeLong(OutputStream os, long i) throws IOException { + // Write out the long value as an unsigned int + os.write((int) (i & 0xFF)); + os.write((int) (i >> 8) & 0xFF); + os.write((int) (i >> 16) & 0xFF); + os.write((int) (i >> 24) & 0xFF); + return i; + } + + private int writeShort(OutputStream os, int i) throws IOException { + os.write(i & 0xFF); + os.write((i >> 8) & 0xFF); + return i; + + } + + /** + * Writes data for the current entry to the underlying stream. + * + * @exception IOException + * If an error occurs writing to the stream + */ + @Override + public void write(byte[] buffer, int off, int nbytes) + throws IOException { + // avoid int overflow, check null buf + if ((off < 0 || (nbytes < 0) || off > buffer.length) || (buffer.length - off < nbytes)) { + throw new IndexOutOfBoundsException(); + } + + if (currentEntry == null) { + throw new TZipException("No active entry"); + } + + if (currentEntry.getMethod() == STORED) { + out.write(buffer, off, nbytes); + } else { + super.write(buffer, off, nbytes); + } + crc.update(buffer, off, nbytes); + } + + static int utf8Count(String value) { + int total = 0; + for (int i = value.length(); --i >= 0;) { + char ch = value.charAt(i); + if (ch < 0x80) { + total++; + } else if (ch < 0x800) { + total += 2; + } else { + total += 3; + } + } + return total; + } + + static byte[] toUTF8Bytes(String value, int length) { + byte[] result = new byte[length]; + int pos = result.length; + for (int i = value.length(); --i >= 0;) { + char ch = value.charAt(i); + if (ch < 0x80) { + result[--pos] = (byte) ch; + } else if (ch < 0x800) { + result[--pos] = (byte) (0x80 | (ch & 0x3f)); + result[--pos] = (byte) (0xc0 | (ch >> 6)); + } else { + result[--pos] = (byte) (0x80 | (ch & 0x3f)); + result[--pos] = (byte) (0x80 | ((ch >> 6) & 0x3f)); + result[--pos] = (byte) (0xe0 | (ch >> 12)); + } + } + return result; + } +}