diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9960204 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c34010b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI +on: + push: + branches: + - main + tags: + - "v*" + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: coursier/cache-action@v6.3 + - uses: coursier/setup-action@v1.2.0-M3 + with: + jvm: temurin:17 + - name: Test + run: ./scala-cli test . --cross --require-tests + + publish: + needs: test + if: github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: coursier/cache-action@v6.3 + - uses: coursier/setup-action@v1.2.0-M3 + with: + jvm: temurin:17 + - name: Release + run: ./scala-cli publish . --cross + env: + PUBLISH_USER: ${{ secrets.PUBLISH_USER }} + PUBLISH_PASSWORD: ${{ secrets.PUBLISH_PASSWORD }} + PUBLISH_SECRET_KEY: ${{ secrets.PUBLISH_SECRET_KEY }} + PUBLISH_SECRET_KEY_PASSWORD: ${{ secrets.PUBLISH_SECRET_KEY_PASSWORD }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82f3f2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.bsp/ +/.scala-build/ diff --git a/scala-cli b/scala-cli new file mode 100755 index 0000000..5300e33 --- /dev/null +++ b/scala-cli @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -e +SC_EXEC="$(cs get "https://github.com/scala-cli/no-crc32-zip-input-stream/releases/download/scala-cli-launcher/scala-cli-x86_64-pc-linux-v2.gz" --archive)" +chmod +x "$SC_EXEC" +exec "$SC_EXEC" "$@" diff --git a/src/config.java b/src/config.java new file mode 100644 index 0000000..4f813ef --- /dev/null +++ b/src/config.java @@ -0,0 +1,14 @@ +//> using publish.organization "io.github.alexarchambault.scala-cli.tmp" +//> using publish.moduleName "zip-input-stream" +//> using publish.computeVersion "git:tag" + +//> using publish.repository "central-s01" +//> using publish.user "env:PUBLISH_USER" +//> using publish.password "env:PUBLISH_PASSWORD" +//> using publish.secretKey "env:PUBLISH_SECRET_KEY" +//> using publish.secretKeyPassword "env:PUBLISH_SECRET_KEY_PASSWORD" + +//> using publish.license "GPL-2.0-with-classpath-exception" +//> using publish.url "https://github.com/scala-cli/no-crc32-zip-input-stream" +//> using publish.versionControl "github:scala-cli/no-crc32-zip-input-stream" +//> using publish.developer "alexarchambault|Alex Archambault|https://github.com/alexarchambault" diff --git a/src/io/github/scala_cli/zip/ZipCoder.java b/src/io/github/scala_cli/zip/ZipCoder.java new file mode 100644 index 0000000..d401a47 --- /dev/null +++ b/src/io/github/scala_cli/zip/ZipCoder.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 1996, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package io.github.scala_cli.zip; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CodingErrorAction; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Utility class for zipfile name and comment decoding and encoding + */ + +class ZipCoder { + + static final class UTF8 extends ZipCoder { + + UTF8(Charset utf8) { + super(utf8); + } + + @Override + boolean isUTF8() { + return true; + } + + @Override + String toString(byte[] ba, int off, int length) { + try { + return new String(ba, off, length, "UTF-8"); + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException(ex); + } + } + + @Override + byte[] getBytes(String s) { + try { + return s.getBytes("UTF-8"); + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException(ex); + } + } + } + + // UTF_8.ArrayEn/Decoder is stateless, so make it singleton. + private static ZipCoder utf8 = new UTF8(UTF_8); + + public static ZipCoder get(Charset charset) { + if (charset == UTF_8) + return utf8; + return new ZipCoder(charset); + } + + String toString(byte[] ba, int off, int length) { + try { + return decoder().decode(ByteBuffer.wrap(ba, off, length)).toString(); + } catch (CharacterCodingException x) { + throw new IllegalArgumentException(x); + } + } + + String toString(byte[] ba, int length) { + return toString(ba, 0, length); + } + + String toString(byte[] ba) { + return toString(ba, 0, ba.length); + } + + byte[] getBytes(String s) { + try { + ByteBuffer bb = encoder().encode(CharBuffer.wrap(s)); + int pos = bb.position(); + int limit = bb.limit(); + if (bb.hasArray() && pos == 0 && limit == bb.capacity()) { + return bb.array(); + } + byte[] bytes = new byte[bb.limit() - bb.position()]; + bb.get(bytes); + return bytes; + } catch (CharacterCodingException x) { + throw new IllegalArgumentException(x); + } + } + + // assume invoked only if "this" is not utf8 + byte[] getBytesUTF8(String s) { + return utf8.getBytes(s); + } + + static String toStringUTF8(byte[] ba, int len) { + return utf8.toString(ba, 0, len); + } + + static String toStringUTF8(byte[] ba, int off, int len) { + return utf8.toString(ba, off, len); + } + + boolean isUTF8() { + return false; + } + + private Charset cs; + private CharsetDecoder dec; + private CharsetEncoder enc; + + private ZipCoder(Charset cs) { + this.cs = cs; + } + + protected CharsetDecoder decoder() { + if (dec == null) { + dec = cs.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + } + return dec; + } + + protected CharsetEncoder encoder() { + if (enc == null) { + enc = cs.newEncoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + } + return enc; + } +} + diff --git a/src/io/github/scala_cli/zip/ZipConstants.java b/src/io/github/scala_cli/zip/ZipConstants.java new file mode 100644 index 0000000..8612c04 --- /dev/null +++ b/src/io/github/scala_cli/zip/ZipConstants.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 1996, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package io.github.scala_cli.zip; + +/* + * This interface defines the constants that are used by the classes + * which manipulate ZIP files. + * + * @author David Connelly + * @since 1.1 + */ +interface ZipConstants { + /* + * Header signatures + */ + static long LOCSIG = 0x04034b50L; // "PK\003\004" + static long EXTSIG = 0x08074b50L; // "PK\007\008" + static long CENSIG = 0x02014b50L; // "PK\001\002" + static long ENDSIG = 0x06054b50L; // "PK\005\006" + + /* + * Header sizes in bytes (including signatures) + */ + static final int LOCHDR = 30; // LOC header size + static final int EXTHDR = 16; // EXT header size + static final int CENHDR = 46; // CEN header size + static final int ENDHDR = 22; // END header size + + /* + * Local file (LOC) header field offsets + */ + static final int LOCVER = 4; // version needed to extract + static final int LOCFLG = 6; // general purpose bit flag + static final int LOCHOW = 8; // compression method + static final int LOCTIM = 10; // modification time + static final int LOCCRC = 14; // uncompressed file crc-32 value + static final int LOCSIZ = 18; // compressed size + static final int LOCLEN = 22; // uncompressed size + static final int LOCNAM = 26; // filename length + static final int LOCEXT = 28; // extra field length + + /* + * Extra local (EXT) header field offsets + */ + static final int EXTCRC = 4; // uncompressed file crc-32 value + static final int EXTSIZ = 8; // compressed size + static final int EXTLEN = 12; // uncompressed size + + /* + * Central directory (CEN) header field offsets + */ + static final int CENVEM = 4; // version made by + static final int CENVER = 6; // version needed to extract + static final int CENFLG = 8; // encrypt, decrypt flags + static final int CENHOW = 10; // compression method + static final int CENTIM = 12; // modification time + static final int CENCRC = 16; // uncompressed file crc-32 value + static final int CENSIZ = 20; // compressed size + static final int CENLEN = 24; // uncompressed size + static final int CENNAM = 28; // filename length + static final int CENEXT = 30; // extra field length + static final int CENCOM = 32; // comment length + static final int CENDSK = 34; // disk number start + static final int CENATT = 36; // internal file attributes + static final int CENATX = 38; // external file attributes + static final int CENOFF = 42; // LOC header offset + + /* + * End of central directory (END) header field offsets + */ + static final int ENDSUB = 8; // number of entries on this disk + static final int ENDTOT = 10; // total number of entries + static final int ENDSIZ = 12; // central directory size in bytes + static final int ENDOFF = 16; // offset of first CEN header + static final int ENDCOM = 20; // zip file comment length +} + diff --git a/src/io/github/scala_cli/zip/ZipConstants64.java b/src/io/github/scala_cli/zip/ZipConstants64.java new file mode 100644 index 0000000..c7455fa --- /dev/null +++ b/src/io/github/scala_cli/zip/ZipConstants64.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 1996, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package io.github.scala_cli.zip; + +/* + * This class defines the constants that are used by the classes + * which manipulate Zip64 files. + */ + +class ZipConstants64 { + + /* + * ZIP64 constants + */ + static final long ZIP64_ENDSIG = 0x06064b50L; // "PK\006\006" + static final long ZIP64_LOCSIG = 0x07064b50L; // "PK\006\007" + static final int ZIP64_ENDHDR = 56; // ZIP64 end header size + static final int ZIP64_LOCHDR = 20; // ZIP64 end loc header size + static final int ZIP64_EXTHDR = 24; // EXT header size + static final int ZIP64_EXTID = 0x0001; // Extra field Zip64 header ID + + static final int ZIP64_MAGICCOUNT = 0xFFFF; + static final long ZIP64_MAGICVAL = 0xFFFFFFFFL; + + /* + * Zip64 End of central directory (END) header field offsets + */ + static final int ZIP64_ENDLEN = 4; // size of zip64 end of central dir + static final int ZIP64_ENDVEM = 12; // version made by + static final int ZIP64_ENDVER = 14; // version needed to extract + static final int ZIP64_ENDNMD = 16; // number of this disk + static final int ZIP64_ENDDSK = 20; // disk number of start + static final int ZIP64_ENDTOD = 24; // total number of entries on this disk + static final int ZIP64_ENDTOT = 32; // total number of entries + static final int ZIP64_ENDSIZ = 40; // central directory size in bytes + static final int ZIP64_ENDOFF = 48; // offset of first CEN header + static final int ZIP64_ENDEXT = 56; // zip64 extensible data sector + + /* + * Zip64 End of central directory locator field offsets + */ + static final int ZIP64_LOCDSK = 4; // disk number start + static final int ZIP64_LOCOFF = 8; // offset of zip64 end + static final int ZIP64_LOCTOT = 16; // total number of disks + + /* + * Zip64 Extra local (EXT) header field offsets + */ + static final int ZIP64_EXTCRC = 4; // uncompressed file crc-32 value + static final int ZIP64_EXTSIZ = 8; // compressed size, 8-byte + static final int ZIP64_EXTLEN = 16; // uncompressed size, 8-byte + + /* + * Language encoding flag (general purpose flag bit 11) + * + * If this bit is set the filename and comment fields for this + * entry must be encoded using UTF-8. + */ + static final int USE_UTF8 = 0x800; + + /* + * Constants below are defined here (instead of in ZipConstants) + * to avoid being exposed as public fields of ZipFile, ZipEntry, + * ZipInputStream and ZipOutputstream. + */ + + /* + * Extra field header ID + */ + static final int EXTID_ZIP64 = 0x0001; // Zip64 + static final int EXTID_NTFS = 0x000a; // NTFS + static final int EXTID_UNIX = 0x000d; // UNIX + static final int EXTID_EXTT = 0x5455; // Info-ZIP Extended Timestamp + + /* + * EXTT timestamp flags + */ + static final int EXTT_FLAG_LMT = 0x1; // LastModifiedTime + static final int EXTT_FLAG_LAT = 0x2; // LastAccessTime + static final int EXTT_FLAT_CT = 0x4; // CreationTime + + private ZipConstants64() {} +} + diff --git a/src/io/github/scala_cli/zip/ZipInputStream.java b/src/io/github/scala_cli/zip/ZipInputStream.java new file mode 100644 index 0000000..9cf99b5 --- /dev/null +++ b/src/io/github/scala_cli/zip/ZipInputStream.java @@ -0,0 +1,444 @@ +/* + * Copyright (c) 1996, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package io.github.scala_cli.zip; + +import java.util.zip.*; +import java.io.InputStream; +import java.io.IOException; +import java.io.EOFException; +import java.io.PushbackInputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import static io.github.scala_cli.zip.ZipConstants64.*; +import static io.github.scala_cli.zip.ZipUtils.*; + +/** + * This class implements an input stream filter for reading files in the + * ZIP file format. Includes support for both compressed and uncompressed + * entries. + * + * @author David Connelly + * @since 1.1 + */ +public +class ZipInputStream extends InflaterInputStream implements ZipConstants { + private ZipEntry entry; + private int flag; + private CRC32 crc = new CRC32(); + private int crcCount = 0; + private long remaining; + private byte[] tmpbuf = new byte[512]; + + private static final int STORED = ZipEntry.STORED; + private static final int DEFLATED = ZipEntry.DEFLATED; + + private boolean closed = false; + // this flag is set to true after EOF has reached for + // one entry + private boolean entryEOF = false; + + private ZipCoder zc; + + private boolean debugCrc = false; + + /** + * Check to make sure that this stream has not been closed + */ + private void ensureOpen() throws IOException { + if (closed) { + throw new IOException("Stream closed"); + } + } + + /** + * Creates a new ZIP input stream. + * + *
The UTF-8 {@link java.nio.charset.Charset charset} is used to + * decode the entry names. + * + * @param in the actual input stream + */ + public ZipInputStream(InputStream in) { + this(in, StandardCharsets.UTF_8); + } + + /** + * Creates a new ZIP input stream. + * + * @param in the actual input stream + * + * @param charset + * The {@linkplain java.nio.charset.Charset charset} to be + * used to decode the ZIP entry name (ignored if the + * language + * encoding bit of the ZIP entry's general purpose bit + * flag is set). + * + * @since 1.7 + */ + public ZipInputStream(InputStream in, Charset charset) { + super(new PushbackInputStream(in, 512), new Inflater(true), 512); + // usesDefaultInflater = true; + if (in == null) { + throw new NullPointerException("in is null"); + } + if (charset == null) + throw new NullPointerException("charset is null"); + this.zc = ZipCoder.get(charset); + this.debugCrc = Boolean.getBoolean("scala-cli.zis.vendored.verbose"); + } + + /** + * Reads the next ZIP file entry and positions the stream at the + * beginning of the entry data. + * @return the next ZIP file entry, or null if there are no more entries + * @exception ZipException if a ZIP file error has occurred + * @exception IOException if an I/O error has occurred + */ + public ZipEntry getNextEntry() throws IOException { + ensureOpen(); + if (entry != null) { + closeEntry(); + } + if (debugCrc) + System.err.println("Reset CRC"); + crc.reset(); + crcCount = 0; + inf.reset(); + if ((entry = readLOC()) == null) { + return null; + } + if (entry.getMethod() == STORED) { + remaining = entry.getSize(); + } + entryEOF = false; + return entry; + } + + /** + * Closes the current ZIP entry and positions the stream for reading the + * next entry. + * @exception ZipException if a ZIP file error has occurred + * @exception IOException if an I/O error has occurred + */ + public void closeEntry() throws IOException { + ensureOpen(); + while (read(tmpbuf, 0, tmpbuf.length) != -1) ; + entryEOF = true; + } + + /** + * Returns 0 after EOF has reached for the current entry data, + * otherwise always return 1. + *
+ * Programs should not count on this method to return the actual number
+ * of bytes that could be read without blocking.
+ *
+ * @return 1 before EOF and 0 after EOF has reached for current entry.
+ * @exception IOException if an I/O error occurs.
+ *
+ */
+ public int available() throws IOException {
+ ensureOpen();
+ if (entryEOF) {
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+
+ /**
+ * Reads from the current ZIP entry into an array of bytes.
+ * If len
is not zero, the method
+ * blocks until some input is available; otherwise, no
+ * bytes are read and 0
is returned.
+ * @param b the buffer into which the data is read
+ * @param off the start offset in the destination array b
+ * @param len the maximum number of bytes read
+ * @return the actual number of bytes read, or -1 if the end of the
+ * entry is reached
+ * @exception NullPointerException if b
is null
.
+ * @exception IndexOutOfBoundsException if off
is negative,
+ * len
is negative, or len
is greater than
+ * b.length - off
+ * @exception ZipException if a ZIP file error has occurred
+ * @exception IOException if an I/O error has occurred
+ */
+ public int read(byte[] b, int off, int len) throws IOException {
+ ensureOpen();
+ if (off < 0 || len < 0 || off > b.length - len) {
+ throw new IndexOutOfBoundsException();
+ } else if (len == 0) {
+ return 0;
+ }
+
+ if (entry == null) {
+ return -1;
+ }
+ switch (entry.getMethod()) {
+ case DEFLATED:
+ len = super.read(b, off, len);
+ if (len == -1) {
+ readEnd(entry);
+ entryEOF = true;
+ entry = null;
+ } else {
+ crc.update(b, off, len);
+ if (debugCrc) {
+ crcCount += len;
+ System.err.println("Updating CRC: " + crcCount + " B (" + Long.toHexString(crc.getValue()) + ")");
+ }
+ }
+ return len;
+ case STORED:
+ if (remaining <= 0) {
+ entryEOF = true;
+ entry = null;
+ return -1;
+ }
+ if (len > remaining) {
+ len = (int)remaining;
+ }
+ len = in.read(b, off, len);
+ if (len == -1) {
+ throw new ZipException("unexpected EOF");
+ }
+ crc.update(b, off, len);
+ if (debugCrc) {
+ crcCount += len;
+ System.err.println("Updating CRC (stored): " + crcCount + " B (" + Long.toHexString(crc.getValue()) + ")");
+ }
+ remaining -= len;
+ if (debugCrc && remaining == 0 && entry.getCrc() != crc.getValue()) {
+ System.err.println(
+ "ignoring invalid entry CRC (expected 0x" + Long.toHexString(entry.getCrc()) +
+ " but got 0x" + Long.toHexString(crc.getValue()) + ")");
+ }
+ return len;
+ default:
+ throw new ZipException("invalid compression method");
+ }
+ }
+
+ /**
+ * Skips specified number of bytes in the current ZIP entry.
+ * @param n the number of bytes to skip
+ * @return the actual number of bytes skipped
+ * @exception ZipException if a ZIP file error has occurred
+ * @exception IOException if an I/O error has occurred
+ * @exception IllegalArgumentException if {@code n < 0}
+ */
+ public long skip(long n) throws IOException {
+ if (n < 0) {
+ throw new IllegalArgumentException("negative skip length");
+ }
+ ensureOpen();
+ int max = (int)Math.min(n, Integer.MAX_VALUE);
+ int total = 0;
+ while (total < max) {
+ int len = max - total;
+ if (len > tmpbuf.length) {
+ len = tmpbuf.length;
+ }
+ len = read(tmpbuf, 0, len);
+ if (len == -1) {
+ entryEOF = true;
+ break;
+ }
+ total += len;
+ }
+ return total;
+ }
+
+ /**
+ * Closes this input stream and releases any system resources associated
+ * with the stream.
+ * @exception IOException if an I/O error has occurred
+ */
+ public void close() throws IOException {
+ if (!closed) {
+ super.close();
+ closed = true;
+ }
+ }
+
+ private byte[] b = new byte[256];
+
+ /*
+ * Reads local file (LOC) header for next entry.
+ */
+ private ZipEntry readLOC() throws IOException {
+ try {
+ readFully(tmpbuf, 0, LOCHDR);
+ } catch (EOFException e) {
+ return null;
+ }
+ if (get32(tmpbuf, 0) != LOCSIG) {
+ return null;
+ }
+ // get flag first, we need check USE_UTF8.
+ flag = get16(tmpbuf, LOCFLG);
+ // get the entry name and create the ZipEntry first
+ int len = get16(tmpbuf, LOCNAM);
+ int blen = b.length;
+ if (len > blen) {
+ do {
+ blen = blen * 2;
+ } while (len > blen);
+ b = new byte[blen];
+ }
+ readFully(b, 0, len);
+ // Force to use UTF-8 if the USE_UTF8 bit is ON
+ ZipEntry e = createZipEntry(((flag & USE_UTF8) != 0)
+ ? ZipCoder.toStringUTF8(b, len)
+ : zc.toString(b, len));
+ // now get the remaining fields for the entry
+ if ((flag & 1) == 1) {
+ throw new ZipException("encrypted ZIP entry not supported");
+ }
+ e.setMethod(get16(tmpbuf, LOCHOW));
+ // e.xdostime =
+ get32(tmpbuf, LOCTIM);
+ if ((flag & 8) == 8) {
+ /* "Data Descriptor" present */
+ if (e.getMethod() != DEFLATED) {
+ throw new ZipException(
+ "only DEFLATED entries can have EXT descriptor");
+ }
+ } else {
+ e.setCrc(get32(tmpbuf, LOCCRC));
+ e.setCompressedSize(get32(tmpbuf, LOCSIZ));
+ e.setSize(get32(tmpbuf, LOCLEN));
+ }
+ len = get16(tmpbuf, LOCEXT);
+ if (len > 0) {
+ byte[] extra = new byte[len];
+ readFully(extra, 0, len);
+ // e.setExtra0(extra, e.getCompressedSize() == ZIP64_MAGICVAL || e.getSize() == ZIP64_MAGICVAL, true);
+ e.setExtra(extra);
+ }
+ return e;
+ }
+
+ /**
+ * Creates a new ZipEntry
object for the specified
+ * entry name.
+ *
+ * @param name the ZIP file entry name
+ * @return the ZipEntry just created
+ */
+ protected ZipEntry createZipEntry(String name) {
+ return new ZipEntry(name);
+ }
+
+ /**
+ * Reads end of deflated entry as well as EXT descriptor if present.
+ *
+ * Local headers for DEFLATED entries may optionally be followed by a
+ * data descriptor, and that data descriptor may optionally contain a
+ * leading signature (EXTSIG).
+ *
+ * From the zip spec http://www.pkware.com/documents/casestudies/APPNOTE.TXT
+ *
+ * """Although not originally assigned a signature, the value 0x08074b50
+ * has commonly been adopted as a signature value for the data descriptor
+ * record. Implementers should be aware that ZIP files may be
+ * encountered with or without this signature marking data descriptors
+ * and should account for either case when reading ZIP files to ensure
+ * compatibility."""
+ */
+ private void readEnd(ZipEntry e) throws IOException {
+ int n = inf.getRemaining();
+ if (n > 0) {
+ ((PushbackInputStream)in).unread(buf, len - n, n);
+ }
+ if ((flag & 8) == 8) {
+ /* "Data Descriptor" present */
+ if (inf.getBytesWritten() > ZIP64_MAGICVAL ||
+ inf.getBytesRead() > ZIP64_MAGICVAL) {
+ // ZIP64 format
+ readFully(tmpbuf, 0, ZIP64_EXTHDR);
+ long sig = get32(tmpbuf, 0);
+ if (sig != EXTSIG) { // no EXTSIG present
+ e.setCrc(sig);
+ e.setCompressedSize(get64(tmpbuf, ZIP64_EXTSIZ - ZIP64_EXTCRC));
+ e.setSize(get64(tmpbuf, ZIP64_EXTLEN - ZIP64_EXTCRC));
+ ((PushbackInputStream)in).unread(
+ tmpbuf, ZIP64_EXTHDR - ZIP64_EXTCRC, ZIP64_EXTCRC);
+ } else {
+ e.setCrc(get32(tmpbuf, ZIP64_EXTCRC));
+ e.setCompressedSize(get64(tmpbuf, ZIP64_EXTSIZ));
+ e.setSize(get64(tmpbuf, ZIP64_EXTLEN));
+ }
+ } else {
+ readFully(tmpbuf, 0, EXTHDR);
+ long sig = get32(tmpbuf, 0);
+ if (sig != EXTSIG) { // no EXTSIG present
+ e.setCrc(sig);
+ e.setCompressedSize(get32(tmpbuf, EXTSIZ - EXTCRC));
+ e.setSize(get32(tmpbuf, EXTLEN - EXTCRC));
+ ((PushbackInputStream)in).unread(
+ tmpbuf, EXTHDR - EXTCRC, EXTCRC);
+ } else {
+ e.setCrc(get32(tmpbuf, EXTCRC));
+ e.setCompressedSize(get32(tmpbuf, EXTSIZ));
+ e.setSize(get32(tmpbuf, EXTLEN));
+ }
+ }
+ }
+ if (e.getSize() != inf.getBytesWritten()) {
+ throw new ZipException(
+ "invalid entry size (expected " + e.getSize() +
+ " but got " + inf.getBytesWritten() + " bytes)");
+ }
+ if (e.getCompressedSize() != inf.getBytesRead()) {
+ throw new ZipException(
+ "invalid entry compressed size (expected " + e.getCompressedSize() +
+ " but got " + inf.getBytesRead() + " bytes)");
+ }
+ if (debugCrc && e.getCrc() != crc.getValue()) {
+ System.err.println(
+ "ignoring invalid entry CRC (expected 0x" + Long.toHexString(e.getCrc()) +
+ " but got 0x" + Long.toHexString(crc.getValue()) + ")");
+ }
+ }
+
+ /*
+ * Reads bytes, blocking until all bytes are read.
+ */
+ private void readFully(byte[] b, int off, int len) throws IOException {
+ while (len > 0) {
+ int n = in.read(b, off, len);
+ if (n == -1) {
+ throw new EOFException();
+ }
+ off += n;
+ len -= n;
+ }
+ }
+
+}
+
diff --git a/src/io/github/scala_cli/zip/ZipUtils.java b/src/io/github/scala_cli/zip/ZipUtils.java
new file mode 100644
index 0000000..9189ad2
--- /dev/null
+++ b/src/io/github/scala_cli/zip/ZipUtils.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (c) 1996, 2020, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package io.github.scala_cli.zip;
+
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.file.attribute.FileTime;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+import java.util.zip.ZipEntry;
+
+import static io.github.scala_cli.zip.ZipConstants.ENDHDR;
+
+class ZipUtils {
+
+ static final long DOSTIME_BEFORE_1980 = (1 << 21) | (1 << 16);
+
+ // used to adjust values between Windows and java epoch
+ private static final long WINDOWS_EPOCH_IN_MICROSECONDS = -11644473600000000L;
+
+ // used to indicate the corresponding windows time is not available
+ public static final long WINDOWS_TIME_NOT_AVAILABLE = Long.MIN_VALUE;
+
+ // static final ByteBuffer defaultBuf = ByteBuffer.allocateDirect(0);
+ static final ByteBuffer defaultBuf = ByteBuffer.allocate(0);
+
+ /**
+ * Converts Windows time (in microseconds, UTC/GMT) time to FileTime.
+ */
+ public static final FileTime winTimeToFileTime(long wtime) {
+ return FileTime.from(wtime / 10 + WINDOWS_EPOCH_IN_MICROSECONDS,
+ TimeUnit.MICROSECONDS);
+ }
+
+ /**
+ * Converts FileTime to Windows time.
+ */
+ public static final long fileTimeToWinTime(FileTime ftime) {
+ return (ftime.to(TimeUnit.MICROSECONDS) - WINDOWS_EPOCH_IN_MICROSECONDS) * 10;
+ }
+
+ /**
+ * The upper bound of the 32-bit unix time, the "year 2038 problem".
+ */
+ public static final long UPPER_UNIXTIME_BOUND = 0x7fffffff;
+
+ /**
+ * Converts "standard Unix time"(in seconds, UTC/GMT) to FileTime
+ */
+ public static final FileTime unixTimeToFileTime(long utime) {
+ return FileTime.from(utime, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Converts FileTime to "standard Unix time".
+ */
+ public static final long fileTimeToUnixTime(FileTime ftime) {
+ return ftime.to(TimeUnit.SECONDS);
+ }
+
+ /**
+ * Converts DOS time to Java time (number of milliseconds since epoch).
+ */
+ public static long dosToJavaTime(long dtime) {
+ int year = (int) (((dtime >> 25) & 0x7f) + 1980);
+ int month = (int) ((dtime >> 21) & 0x0f);
+ int day = (int) ((dtime >> 16) & 0x1f);
+ int hour = (int) ((dtime >> 11) & 0x1f);
+ int minute = (int) ((dtime >> 5) & 0x3f);
+ int second = (int) ((dtime << 1) & 0x3e);
+
+ if (month > 0 && month < 13 && day > 0 && hour < 24 && minute < 60 && second < 60) {
+ try {
+ LocalDateTime ldt = LocalDateTime.of(year, month, day, hour, minute, second);
+ return TimeUnit.MILLISECONDS.convert(ldt.toEpochSecond(
+ ZoneId.systemDefault().getRules().getOffset(ldt)), TimeUnit.SECONDS);
+ } catch (DateTimeException dte) {
+ // ignore
+ }
+ }
+ return overflowDosToJavaTime(year, month, day, hour, minute, second);
+ }
+
+ /*
+ * Deal with corner cases where an arguably mal-formed DOS time is used
+ */
+ @SuppressWarnings("deprecation") // Use of Date constructor
+ private static long overflowDosToJavaTime(int year, int month, int day,
+ int hour, int minute, int second) {
+ return new Date(year - 1900, month - 1, day, hour, minute, second).getTime();
+ }
+
+
+ /**
+ * Converts extended DOS time to Java time, where up to 1999 milliseconds
+ * might be encoded into the upper half of the returned long.
+ *
+ * @param xdostime the extended DOS time value
+ * @return milliseconds since epoch
+ */
+ public static long extendedDosToJavaTime(long xdostime) {
+ long time = dosToJavaTime(xdostime);
+ return time + (xdostime >> 32);
+ }
+
+ /**
+ * Converts Java time to DOS time.
+ */
+ private static long javaToDosTime(long time) {
+ Instant instant = Instant.ofEpochMilli(time);
+ LocalDateTime ldt = LocalDateTime.ofInstant(
+ instant, ZoneId.systemDefault());
+ int year = ldt.getYear() - 1980;
+ if (year < 0) {
+ return (1 << 21) | (1 << 16);
+ }
+ return (year << 25 |
+ ldt.getMonthValue() << 21 |
+ ldt.getDayOfMonth() << 16 |
+ ldt.getHour() << 11 |
+ ldt.getMinute() << 5 |
+ ldt.getSecond() >> 1) & 0xffffffffL;
+ }
+
+ /**
+ * Converts Java time to DOS time, encoding any milliseconds lost
+ * in the conversion into the upper half of the returned long.
+ *
+ * @param time milliseconds since epoch
+ * @return DOS time with 2s remainder encoded into upper half
+ */
+ public static long javaToExtendedDosTime(long time) {
+ if (time < 0) {
+ return DOSTIME_BEFORE_1980;
+ }
+ long dostime = javaToDosTime(time);
+ return (dostime != DOSTIME_BEFORE_1980)
+ ? dostime + ((time % 2000) << 32)
+ : DOSTIME_BEFORE_1980;
+ }
+
+ /**
+ * Fetches unsigned 16-bit value from byte array at specified offset.
+ * The bytes are assumed to be in Intel (little-endian) byte order.
+ */
+ public static final int get16(byte b[], int off) {
+ return (b[off] & 0xff) | ((b[off + 1] & 0xff) << 8);
+ }
+
+ /**
+ * Fetches unsigned 32-bit value from byte array at specified offset.
+ * The bytes are assumed to be in Intel (little-endian) byte order.
+ */
+ public static final long get32(byte b[], int off) {
+ return (get16(b, off) | ((long)get16(b, off+2) << 16)) & 0xffffffffL;
+ }
+
+ /**
+ * Fetches signed 64-bit value from byte array at specified offset.
+ * The bytes are assumed to be in Intel (little-endian) byte order.
+ */
+ public static final long get64(byte b[], int off) {
+ return get32(b, off) | (get32(b, off+4) << 32);
+ }
+
+ /**
+ * Fetches signed 32-bit value from byte array at specified offset.
+ * The bytes are assumed to be in Intel (little-endian) byte order.
+ *
+ */
+ public static final int get32S(byte b[], int off) {
+ return (get16(b, off) | (get16(b, off+2) << 16));
+ }
+
+ // fields access methods
+ static final int CH(byte[] b, int n) {
+ return b[n] & 0xff ;
+ }
+
+ static final int SH(byte[] b, int n) {
+ return (b[n] & 0xff) | ((b[n + 1] & 0xff) << 8);
+ }
+
+ static final long LG(byte[] b, int n) {
+ return ((SH(b, n)) | (SH(b, n + 2) << 16)) & 0xffffffffL;
+ }
+
+ static final long LL(byte[] b, int n) {
+ return (LG(b, n)) | (LG(b, n + 4) << 32);
+ }
+
+ static final long GETSIG(byte[] b) {
+ return LG(b, 0);
+ }
+
+ // local file (LOC) header fields
+ static final long LOCSIG(byte[] b) { return LG(b, 0); } // signature
+ static final int LOCVER(byte[] b) { return SH(b, 4); } // version needed to extract
+ static final int LOCFLG(byte[] b) { return SH(b, 6); } // general purpose bit flags
+ static final int LOCHOW(byte[] b) { return SH(b, 8); } // compression method
+ static final long LOCTIM(byte[] b) { return LG(b, 10);} // modification time
+ static final long LOCCRC(byte[] b) { return LG(b, 14);} // crc of uncompressed data
+ static final long LOCSIZ(byte[] b) { return LG(b, 18);} // compressed data size
+ static final long LOCLEN(byte[] b) { return LG(b, 22);} // uncompressed data size
+ static final int LOCNAM(byte[] b) { return SH(b, 26);} // filename length
+ static final int LOCEXT(byte[] b) { return SH(b, 28);} // extra field length
+
+ // extra local (EXT) header fields
+ static final long EXTCRC(byte[] b) { return LG(b, 4);} // crc of uncompressed data
+ static final long EXTSIZ(byte[] b) { return LG(b, 8);} // compressed size
+ static final long EXTLEN(byte[] b) { return LG(b, 12);} // uncompressed size
+
+ // end of central directory header (END) fields
+ static final int ENDSUB(byte[] b) { return SH(b, 8); } // number of entries on this disk
+ static final int ENDTOT(byte[] b) { return SH(b, 10);} // total number of entries
+ static final long ENDSIZ(byte[] b) { return LG(b, 12);} // central directory size
+ static final long ENDOFF(byte[] b) { return LG(b, 16);} // central directory offset
+ static final int ENDCOM(byte[] b) { return SH(b, 20);} // size of zip file comment
+ static final int ENDCOM(byte[] b, int off) { return SH(b, off + 20);}
+
+ // zip64 end of central directory recoder fields
+ static final long ZIP64_ENDTOD(byte[] b) { return LL(b, 24);} // total number of entries on disk
+ static final long ZIP64_ENDTOT(byte[] b) { return LL(b, 32);} // total number of entries
+ static final long ZIP64_ENDSIZ(byte[] b) { return LL(b, 40);} // central directory size
+ static final long ZIP64_ENDOFF(byte[] b) { return LL(b, 48);} // central directory offset
+ static final long ZIP64_LOCOFF(byte[] b) { return LL(b, 8);} // zip64 end offset
+
+ // central directory header (CEN) fields
+ static final long CENSIG(byte[] b, int pos) { return LG(b, pos + 0); }
+ static final int CENVEM(byte[] b, int pos) { return SH(b, pos + 4); }
+ static final int CENVER(byte[] b, int pos) { return SH(b, pos + 6); }
+ static final int CENFLG(byte[] b, int pos) { return SH(b, pos + 8); }
+ static final int CENHOW(byte[] b, int pos) { return SH(b, pos + 10);}
+ static final long CENTIM(byte[] b, int pos) { return LG(b, pos + 12);}
+ static final long CENCRC(byte[] b, int pos) { return LG(b, pos + 16);}
+ static final long CENSIZ(byte[] b, int pos) { return LG(b, pos + 20);}
+ static final long CENLEN(byte[] b, int pos) { return LG(b, pos + 24);}
+ static final int CENNAM(byte[] b, int pos) { return SH(b, pos + 28);}
+ static final int CENEXT(byte[] b, int pos) { return SH(b, pos + 30);}
+ static final int CENCOM(byte[] b, int pos) { return SH(b, pos + 32);}
+ static final int CENDSK(byte[] b, int pos) { return SH(b, pos + 34);}
+ static final int CENATT(byte[] b, int pos) { return SH(b, pos + 36);}
+ static final long CENATX(byte[] b, int pos) { return LG(b, pos + 38);}
+ static final long CENOFF(byte[] b, int pos) { return LG(b, pos + 42);}
+
+ // The END header is followed by a variable length comment of size < 64k.
+ static final long END_MAXLEN = 0xFFFF + ENDHDR;
+ static final int READBLOCKSZ = 128;
+
+ /**
+ * Loads zip native library, if not already laoded
+ */
+ static void loadLibrary() {
+ SecurityManager sm = System.getSecurityManager();
+ if (sm == null) {
+ System.loadLibrary("zip");
+ } else {
+ PrivilegedAction