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 pa = () -> { System.loadLibrary("zip"); return null; }; + AccessController.doPrivileged(pa); + } + } +} + diff --git a/src/test/io/github/scala_cli/zip/CustomZipInputStreamTests.scala b/src/test/io/github/scala_cli/zip/CustomZipInputStreamTests.scala new file mode 100644 index 0000000..5322672 --- /dev/null +++ b/src/test/io/github/scala_cli/zip/CustomZipInputStreamTests.scala @@ -0,0 +1,48 @@ +//> using scala "2.13.8" +//> using lib "com.lihaoyi::utest::0.7.10" +//> using lib "io.get-coursier:interface:1.0.7" + +package io.github.scala_cli.zip + +import coursierapi._ +import utest._ + +import java.io.{FileInputStream, InputStream} +import java.util.zip.ZipEntry + +import scala.collection.mutable + +object CustomZipInputStreamTests extends TestSuite { + val tests = Tests { + test("simple test") { + + val cache = Cache.create() + val f = cache.get(Artifact.of("https://repo1.maven.org/maven2/org/scala-lang/scala-library/2.13.8/scala-library-2.13.8.jar")) + + var entries = new mutable.ListBuffer[(String, Int)] + + var is: InputStream = null + try { + is = new FileInputStream(f) + val zis = new ZipInputStream(is) + var ent: ZipEntry = null + while ({ + ent = zis.getNextEntry() + ent != null + }) { + val b = zis.readAllBytes() + entries += ent.getName -> b.length + } + } + finally { + if (is != null) + is.close() + } + + val map = entries.toMap + + assert(map.get("scala/util/hashing/package.class").contains(792)) + assert(map.get("scala/util/Right.class").contains(5011)) + } + } +} \ No newline at end of file