From bb0dba8ab4a0f709eb553cf282d7f034a184b2dd Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:57:28 +0300 Subject: [PATCH 01/16] Add shared streaming ZIP APIs to ParparVM --- .../src/com/codename1/io/gzip/Deflate.java | 9 +- .../io/zip/ZipArchiveOutputStream.java | 417 ++++ Ports/CLDC11/src/java/util/zip/Adler32.java | 11 + Ports/CLDC11/src/java/util/zip/CRC32.java | 11 + .../src/java/util/zip/CheckedInputStream.java | 10 + .../java/util/zip/CheckedOutputStream.java | 9 + Ports/CLDC11/src/java/util/zip/Checksum.java | 9 + .../java/util/zip/DataFormatException.java | 7 + Ports/CLDC11/src/java/util/zip/Deflater.java | 38 + .../java/util/zip/DeflaterOutputStream.java | 19 + .../src/java/util/zip/GZIPInputStream.java | 13 + .../src/java/util/zip/GZIPOutputStream.java | 13 + Ports/CLDC11/src/java/util/zip/Inflater.java | 24 + .../java/util/zip/InflaterInputStream.java | 21 + Ports/CLDC11/src/java/util/zip/ZipEntry.java | 28 + .../src/java/util/zip/ZipException.java | 7 + .../src/java/util/zip/ZipInputStream.java | 13 + .../src/java/util/zip/ZipOutputStream.java | 20 + .../Backend-Operations.asciidoc | 124 ++ scripts/generate-zip-api-stubs.py | 54 + scripts/sync-zip-runtime.py | 54 + .../src/com/codename1/impl/zip/Adler32.java | 154 ++ .../src/com/codename1/impl/zip/CRC32.java | 196 ++ .../src/com/codename1/impl/zip/Checksum.java | 48 + .../src/com/codename1/impl/zip/Deflate.java | 1777 +++++++++++++++++ .../src/com/codename1/impl/zip/Deflater.java | 172 ++ .../com/codename1/impl/zip/GZIPException.java | 46 + .../com/codename1/impl/zip/GZIPHeader.java | 211 ++ .../src/com/codename1/impl/zip/InfBlocks.java | 682 +++++++ .../src/com/codename1/impl/zip/InfCodes.java | 716 +++++++ .../src/com/codename1/impl/zip/InfTree.java | 517 +++++ .../src/com/codename1/impl/zip/Inflate.java | 795 ++++++++ .../src/com/codename1/impl/zip/Inflater.java | 163 ++ .../src/com/codename1/impl/zip/JZlib.java | 120 ++ .../com/codename1/impl/zip/StaticTree.java | 144 ++ .../src/com/codename1/impl/zip/Tree.java | 374 ++++ .../src/com/codename1/impl/zip/ZStream.java | 404 ++++ vm/JavaAPI/src/java/util/zip/Adler32.java | 29 + vm/JavaAPI/src/java/util/zip/CRC32.java | 29 + .../src/java/util/zip/CheckedInputStream.java | 43 + .../java/util/zip/CheckedOutputStream.java | 28 + vm/JavaAPI/src/java/util/zip/Checksum.java | 13 + .../java/util/zip/DataFormatException.java | 14 + vm/JavaAPI/src/java/util/zip/Deflater.java | 151 ++ .../java/util/zip/DeflaterOutputStream.java | 93 + .../src/java/util/zip/GZIPInputStream.java | 103 + .../src/java/util/zip/GZIPOutputStream.java | 51 + vm/JavaAPI/src/java/util/zip/Inflater.java | 110 + .../java/util/zip/InflaterInputStream.java | 109 + vm/JavaAPI/src/java/util/zip/ZipEntry.java | 109 + .../src/java/util/zip/ZipException.java | 14 + .../src/java/util/zip/ZipInputStream.java | 185 ++ .../src/java/util/zip/ZipOutputStream.java | 405 ++++ vm/README.md | 21 + .../zipdownload/com/demo/ZipDownload.java | 65 + .../translator/ZipApiIntegrationTest.java | 124 ++ .../codename1/tools/translator/ZipApiApp.java | 363 ++++ 57 files changed, 9487 insertions(+), 2 deletions(-) create mode 100644 CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java create mode 100644 Ports/CLDC11/src/java/util/zip/Adler32.java create mode 100644 Ports/CLDC11/src/java/util/zip/CRC32.java create mode 100644 Ports/CLDC11/src/java/util/zip/CheckedInputStream.java create mode 100644 Ports/CLDC11/src/java/util/zip/CheckedOutputStream.java create mode 100644 Ports/CLDC11/src/java/util/zip/Checksum.java create mode 100644 Ports/CLDC11/src/java/util/zip/DataFormatException.java create mode 100644 Ports/CLDC11/src/java/util/zip/Deflater.java create mode 100644 Ports/CLDC11/src/java/util/zip/DeflaterOutputStream.java create mode 100644 Ports/CLDC11/src/java/util/zip/GZIPInputStream.java create mode 100644 Ports/CLDC11/src/java/util/zip/GZIPOutputStream.java create mode 100644 Ports/CLDC11/src/java/util/zip/Inflater.java create mode 100644 Ports/CLDC11/src/java/util/zip/InflaterInputStream.java create mode 100644 Ports/CLDC11/src/java/util/zip/ZipEntry.java create mode 100644 Ports/CLDC11/src/java/util/zip/ZipException.java create mode 100644 Ports/CLDC11/src/java/util/zip/ZipInputStream.java create mode 100644 Ports/CLDC11/src/java/util/zip/ZipOutputStream.java create mode 100644 scripts/generate-zip-api-stubs.py create mode 100644 scripts/sync-zip-runtime.py create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/Adler32.java create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/CRC32.java create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/Checksum.java create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/Deflate.java create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/Deflater.java create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/GZIPException.java create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/GZIPHeader.java create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/InfBlocks.java create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/InfCodes.java create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/InfTree.java create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/Inflate.java create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/Inflater.java create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/JZlib.java create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/StaticTree.java create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/Tree.java create mode 100644 vm/JavaAPI/src/com/codename1/impl/zip/ZStream.java create mode 100644 vm/JavaAPI/src/java/util/zip/Adler32.java create mode 100644 vm/JavaAPI/src/java/util/zip/CRC32.java create mode 100644 vm/JavaAPI/src/java/util/zip/CheckedInputStream.java create mode 100644 vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java create mode 100644 vm/JavaAPI/src/java/util/zip/Checksum.java create mode 100644 vm/JavaAPI/src/java/util/zip/DataFormatException.java create mode 100644 vm/JavaAPI/src/java/util/zip/Deflater.java create mode 100644 vm/JavaAPI/src/java/util/zip/DeflaterOutputStream.java create mode 100644 vm/JavaAPI/src/java/util/zip/GZIPInputStream.java create mode 100644 vm/JavaAPI/src/java/util/zip/GZIPOutputStream.java create mode 100644 vm/JavaAPI/src/java/util/zip/Inflater.java create mode 100644 vm/JavaAPI/src/java/util/zip/InflaterInputStream.java create mode 100644 vm/JavaAPI/src/java/util/zip/ZipEntry.java create mode 100644 vm/JavaAPI/src/java/util/zip/ZipException.java create mode 100644 vm/JavaAPI/src/java/util/zip/ZipInputStream.java create mode 100644 vm/JavaAPI/src/java/util/zip/ZipOutputStream.java create mode 100644 vm/backend/demo/zipdownload/com/demo/ZipDownload.java create mode 100644 vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java create mode 100644 vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java diff --git a/CodenameOne/src/com/codename1/io/gzip/Deflate.java b/CodenameOne/src/com/codename1/io/gzip/Deflate.java index 0b9a0fbb142..6e9508f2efc 100644 --- a/CodenameOne/src/com/codename1/io/gzip/Deflate.java +++ b/CodenameOne/src/com/codename1/io/gzip/Deflate.java @@ -1492,11 +1492,16 @@ int deflateSetDictionary(byte[] dictionary, int dictLength) { int length = dictLength; int index = 0; - if (dictionary == null || status != INIT_STATE) { + // Raw DEFLATE begins in BUSY_STATE because it has no zlib header. + // A preset dictionary is still valid before any input is compressed. + if (dictionary == null || (status != INIT_STATE && + !(wrap == 0 && status == BUSY_STATE && strm.totalIn == 0))) { return Z_STREAM_ERROR; } - strm.adler.update(dictionary, 0, dictLength); + if (wrap != 0) { + strm.adler.update(dictionary, 0, dictLength); + } if (length < MIN_MATCH) { return Z_OK; diff --git a/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java b/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java new file mode 100644 index 00000000000..68425a7d000 --- /dev/null +++ b/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java @@ -0,0 +1,417 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * This code is licensed under GPLv2 with the Classpath exception. + */ +package com.codename1.io.zip; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashSet; +import java.util.zip.CRC32; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; + +/** + * Streaming ZIP writer with optional Unix permissions. Works on the JVM and + * ParparVM. Payloads are never buffered in full. Central-directory metadata is + * limited to 16 MiB and 65534 entries; ZIP64 is explicitly refused. + * finish() leaves the destination open; close() closes it even after a failure. + * Input streams are owned by the caller. This stream is not thread safe. + */ +@com.codename1.impl.SharedWithBackend +public class ZipArchiveOutputStream extends DeflaterOutputStream { + public static final int STORED = 0, DEFLATED = 8; + private static final long LIMIT = 0xffffffffL; + private static final int METADATA_LIMIT = 16 * 1024 * 1024; + private final ArrayList entries = new ArrayList(); + private final HashSet names = new HashSet(); + private final CRC32 crc = new CRC32(); + private final String encoding; + private final int encodingFlag; + private Record current; + private ZipEntry original; + private int method = DEFLATED, metadataSize; + private long written, entryStart, entrySize; + private boolean finished, closed; + private IOException failure; + private byte[] comment = new byte[0]; + + private static final class Record { + ZipEntry entry; + byte[] name, extra, comment; + long offset, dosTime, attributes; + int flags, platform; + } + + public ZipArchiveOutputStream(OutputStream out) { + this(out, Charset.forName("UTF-8")); + } + public ZipArchiveOutputStream(OutputStream out, Charset charset) { + super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true), 8192); + if (charset == null) + throw new NullPointerException(); + encoding = charset.displayName(); + encodingFlag = "UTF-8".equalsIgnoreCase(encoding) ? 0x800 : 0; + } + + public void setMethod(int method) { + if (method != STORED && method != DEFLATED) + throw new IllegalArgumentException("Invalid method"); + this.method = method; + } + public void setLevel(int level) { + def.setLevel(level); + } + public void setComment(String comment) { + try { + this.comment = encode(comment == null ? "" : comment); + } catch (IOException ex) { + throw new IllegalArgumentException(ex.getMessage()); + } + } + private byte[] encode(String value) throws IOException { + byte[] bytes = value.getBytes(encoding); + if (bytes.length > 65535) + throw new ZipException("Encoded ZIP field exceeds 65535 bytes"); + return bytes; + } + private void writable() throws IOException { + if (closed) + throw new IOException("ZIP stream closed"); + if (failure != null) + throw failure; + if (finished) + throw new IOException("ZIP stream finished"); + } + /** Writes an entry without Unix attributes, like java.util.zip.ZipOutputStream. */ + public void putNextEntry(ZipEntry entry) throws IOException { + putEntry(entry, 0, 0); + } + + /** + * Writes an entry with Unix permission bits (for example 0755 or 0644). + * File/directory type bits are inferred from the trailing slash. Symlinks + * and special files are not created. Values outside 0000..0777 are refused. + */ + public void putNextEntry(ZipEntry entry, int permissions) throws IOException { + if (permissions < 0 || permissions > 0777) + throw new IllegalArgumentException("Invalid permissions"); + long mode = (entry.isDirectory() ? 0040000 : 0100000) | permissions; + putEntry(entry, 3, (mode << 16) | (entry.isDirectory() ? 0x10 : 0)); + } + private void putEntry(ZipEntry entry, int platform, long attributes) throws IOException { + writable(); + if (entry == null) + throw new NullPointerException(); + closeEntry(); + if (entries.size() >= 65534) + throw new ZipException("ZIP64 entry count unsupported"); + if (names.contains(entry.getName())) + throw new ZipException("Duplicate entry: " + entry.getName()); + Record r = new Record(); + r.entry = new ZipEntry(entry); + if (r.entry.getMethod() == -1) + r.entry.setMethod(method); + if (r.entry.getTime() == -1) + r.entry.setTime(System.currentTimeMillis()); + r.name = encode(r.entry.getName()); + r.comment = encode(r.entry.getComment() == null ? "" : r.entry.getComment()); + byte[] extra = r.entry.getExtra(); + r.extra = extra == null ? new byte[0] : extra.clone(); + validateExtra(r.extra); + r.extra = timestampExtra(r.extra, r.entry.getTime()); + if (r.name.length == 0) + throw new ZipException("Empty entry name"); + checkSize(r.entry.getSize()); + checkSize(r.entry.getCompressedSize()); + if (r.entry.getMethod() == STORED) { + if (r.entry.getSize() < 0 && r.entry.getCompressedSize() >= 0) + r.entry.setSize(r.entry.getCompressedSize()); + if (r.entry.getCompressedSize() < 0) + r.entry.setCompressedSize(r.entry.getSize()); + if (r.entry.getSize() < 0 || r.entry.getCrc() < 0 || r.entry.getSize() != r.entry.getCompressedSize()) + throw new ZipException("STORED entries require size and CRC before writing"); + } + int metadata = 46 + r.name.length + r.extra.length + r.comment.length; + if (metadataSize > METADATA_LIMIT - metadata) + throw new ZipException("ZIP central directory exceeds 16 MiB"); + r.flags = encodingFlag | (r.entry.getMethod() == DEFLATED ? 8 : 0); + r.platform = platform; + r.attributes = attributes; + r.offset = written; + r.dosTime = dosTime(r.entry.getTime()); + current = r; + original = entry; + entrySize = 0; + crc.reset(); + if (r.entry.getMethod() == DEFLATED) + def.reset(); + try { + word(0x04034b50L, 4); + word(20, 2); + word(r.flags, 2); + word(r.entry.getMethod(), 2); + word(r.dosTime, 4); + word(r.entry.getMethod() == STORED ? r.entry.getCrc() : 0, 4); + word(r.entry.getMethod() == STORED ? r.entry.getSize() : 0, 4); + word(r.entry.getMethod() == STORED ? r.entry.getSize() : 0, 4); + word(r.name.length, 2); + word(r.extra.length, 2); + raw(r.name); + raw(r.extra); + entryStart = written; + entries.add(r); + names.add(r.entry.getName()); + metadataSize += metadata; + } catch (IOException ex) { + failure = ex; + throw ex; + } + } + private static void validateExtra(byte[] extra) throws ZipException { + int at = 0; + while (at < extra.length) { + if (extra.length - at < 4) + throw new ZipException("Malformed ZIP extra field"); + int id = (extra[at] & 255) | ((extra[at + 1] & 255) << 8); + int size = (extra[at + 2] & 255) | ((extra[at + 3] & 255) << 8); + if (id == 1) + throw new ZipException("ZIP64 extra field unsupported"); + if (size > extra.length - at - 4) + throw new ZipException("Malformed ZIP extra field"); + at += 4 + size; + } + } + private static void checkSize(long size) throws ZipException { + if (size >= LIMIT) + throw new ZipException("ZIP64 size unsupported"); + } + private static byte[] timestampExtra(byte[] extra, long millis) throws ZipException { + long seconds = millis / 1000; + if (millis < 0 && millis % 1000 != 0) + seconds--; + if (seconds < Integer.MIN_VALUE || seconds > Integer.MAX_VALUE) + return extra; + // Replace any caller-provided extended timestamp with the entry's time. + int kept = 0; + for (int at = 0; at < extra.length;) { + int size = 4 + (extra[at + 2] & 255) + ((extra[at + 3] & 255) << 8); + if ((extra[at] & 255) != 0x55 || (extra[at + 1] & 255) != 0x54) + kept += size; + at += size; + } + if (kept > 65535 - 9) + throw new ZipException("No room for ZIP timestamp extra field"); + byte[] result = new byte[kept + 9]; + int pos = 0; + for (int at = 0; at < extra.length;) { + int size = 4 + (extra[at + 2] & 255) + ((extra[at + 3] & 255) << 8); + if ((extra[at] & 255) != 0x55 || (extra[at + 1] & 255) != 0x54) { + System.arraycopy(extra, at, result, pos, size); + pos += size; + } + at += size; + } + result[pos] = 0x55; + result[pos + 1] = 0x54; + result[pos + 2] = 5; + result[pos + 4] = 1; + for (int i = 0; i < 4; i++) result[pos + 5 + i] = (byte) (seconds >>> (8 * i)); + return result; + } + private static long dosTime(long millis) { + // The deterministic epoch timestamp predates the DOS range in every + // timezone. Avoid calendar/timezone lookups for each such entry. + if (millis == 0) + return 0x00210000L; + Calendar c = Calendar.getInstance(); + c.setTime(new java.util.Date(millis)); + int year = c.get(Calendar.YEAR); + if (year < 1980) + return 0x00210000L; + if (year > 2107) + return 0xff9fbf7dL; + return ((long) (year - 1980) << 25) | ((c.get(Calendar.MONTH) + 1) << 21) | (c.get(Calendar.DAY_OF_MONTH) << 16) + | (c.get(Calendar.HOUR_OF_DAY) << 11) | (c.get(Calendar.MINUTE) << 5) | (c.get(Calendar.SECOND) >> 1); + } + public void write(int value) throws IOException { + write(new byte[] {(byte) value}, 0, 1); + } + public void write(byte[] data, int off, int len) throws IOException { + writable(); + if (data == null) + throw new NullPointerException(); + if (off < 0 || len < 0 || off > data.length - len) + throw new IndexOutOfBoundsException(); + if (current == null) + throw new ZipException("No current ZIP entry"); + try { + checkSize(entrySize + len); + if (current.entry.getMethod() == STORED && entrySize + len > current.entry.getSize()) + throw new ZipException("STORED entry exceeds declared size"); + crc.update(data, off, len); + entrySize += len; + if (current.entry.getMethod() == STORED) + raw(data, off, len); + else { + def.setInput(data, off, len); + while (!def.needsInput()) deflate(); + } + } catch (IOException ex) { + failure = ex; + throw ex; + } + } + protected void deflate() throws IOException { + int n = def.deflate(buf, 0, buf.length); + if (n > 0) + raw(buf, 0, n); + } + public void closeEntry() throws IOException { + writable(); + if (current == null) + return; + try { + ZipEntry e = current.entry; + if (e.getMethod() == DEFLATED) { + def.finish(); + while (!def.finished()) deflate(); + } + long compressed = written - entryStart; + checkSize(compressed); + if ((e.getSize() >= 0 && e.getSize() != entrySize) + || (e.getCompressedSize() >= 0 && e.getCompressedSize() != compressed) + || (e.getCrc() >= 0 && e.getCrc() != crc.getValue())) + throw new ZipException("ZIP entry size or CRC mismatch"); + e.setSize(entrySize); + e.setCompressedSize(compressed); + e.setCrc(crc.getValue()); + original.setSize(entrySize); + original.setCompressedSize(compressed); + original.setCrc(crc.getValue()); + if (e.getMethod() == DEFLATED) { + word(0x08074b50L, 4); + word(e.getCrc(), 4); + word(compressed, 4); + word(entrySize, 4); + } + current = null; + original = null; + } catch (IOException ex) { + failure = ex; + throw ex; + } + } + public void finish() throws IOException { + if (closed) + throw new IOException("ZIP stream closed"); + if (failure != null) + throw failure; + if (finished) + return; + closeEntry(); + try { + long start = written; + for (Record r : entries) { + ZipEntry e = r.entry; + word(0x02014b50L, 4); + word((r.platform << 8) | 20, 2); + word(20, 2); + word(r.flags, 2); + word(e.getMethod(), 2); + word(r.dosTime, 4); + word(e.getCrc(), 4); + word(e.getCompressedSize(), 4); + word(e.getSize(), 4); + word(r.name.length, 2); + word(r.extra.length, 2); + word(r.comment.length, 2); + word(0, 2); + word(0, 2); + word(r.attributes, 4); + word(r.offset, 4); + raw(r.name); + raw(r.extra); + raw(r.comment); + } + long length = written - start; + word(0x06054b50L, 4); + word(0, 2); + word(0, 2); + word(entries.size(), 2); + word(entries.size(), 2); + word(length, 4); + word(start, 4); + word(comment.length, 2); + raw(comment); + finished = true; + entries.clear(); + names.clear(); + } catch (IOException ex) { + failure = ex; + throw ex; + } + } + public void flush() throws IOException { + if (closed) + throw new IOException("ZIP stream closed"); + if (failure != null) + throw failure; + try { + out.flush(); + } catch (IOException ex) { + failure = ex; + throw ex; + } + } + public void close() throws IOException { + if (closed) + return; + // An earlier write already delivered failure to the caller. Throwing + // that same object again from close breaks try-with-resources through + // Throwable's self-suppression guard. + IOException error = null; + try { + if (failure == null) + finish(); + } catch (IOException ex) { + error = ex; + } finally { + closed = true; + def.end(); + entries.clear(); + names.clear(); + current = null; + original = null; + try { + out.close(); + } catch (IOException ex) { + if (error == null) + error = ex; + } + } + if (error != null) + throw error; + } + private void word(long value, int bytes) throws IOException { + byte[] b = new byte[bytes]; + for (int i = 0; i < bytes; i++) { + b[i] = (byte) value; + value >>>= 8; + } + raw(b); + } + private void raw(byte[] data) throws IOException { + raw(data, 0, data.length); + } + private void raw(byte[] data, int off, int len) throws IOException { + checkSize(written + len); + out.write(data, off, len); + written += len; + } +} diff --git a/Ports/CLDC11/src/java/util/zip/Adler32.java b/Ports/CLDC11/src/java/util/zip/Adler32.java new file mode 100644 index 00000000000..9e4cf619d1f --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/Adler32.java @@ -0,0 +1,11 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public class Adler32 implements java.util.zip.Checksum { + public Adler32() { throw new RuntimeException("Compile-time stub"); } + public void update(int arg0) { throw new RuntimeException("Compile-time stub"); } + public void update(byte[] arg0) { throw new RuntimeException("Compile-time stub"); } + public void update(byte[] arg0, int arg1, int arg2) { throw new RuntimeException("Compile-time stub"); } + public long getValue() { throw new RuntimeException("Compile-time stub"); } + public void reset() { throw new RuntimeException("Compile-time stub"); } +} diff --git a/Ports/CLDC11/src/java/util/zip/CRC32.java b/Ports/CLDC11/src/java/util/zip/CRC32.java new file mode 100644 index 00000000000..9e7f6f40de4 --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/CRC32.java @@ -0,0 +1,11 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public class CRC32 implements java.util.zip.Checksum { + public CRC32() { throw new RuntimeException("Compile-time stub"); } + public void update(int arg0) { throw new RuntimeException("Compile-time stub"); } + public void update(byte[] arg0) { throw new RuntimeException("Compile-time stub"); } + public void update(byte[] arg0, int arg1, int arg2) { throw new RuntimeException("Compile-time stub"); } + public long getValue() { throw new RuntimeException("Compile-time stub"); } + public void reset() { throw new RuntimeException("Compile-time stub"); } +} diff --git a/Ports/CLDC11/src/java/util/zip/CheckedInputStream.java b/Ports/CLDC11/src/java/util/zip/CheckedInputStream.java new file mode 100644 index 00000000000..96d7d07e44b --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/CheckedInputStream.java @@ -0,0 +1,10 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public class CheckedInputStream extends java.io.FilterInputStream { + public CheckedInputStream(java.io.InputStream arg0, java.util.zip.Checksum arg1) { super(null); throw new RuntimeException("Compile-time stub"); } + public java.util.zip.Checksum getChecksum() { throw new RuntimeException("Compile-time stub"); } + public int read() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public int read(byte[] arg0, int arg1, int arg2) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public long skip(long arg0) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } +} diff --git a/Ports/CLDC11/src/java/util/zip/CheckedOutputStream.java b/Ports/CLDC11/src/java/util/zip/CheckedOutputStream.java new file mode 100644 index 00000000000..964700d61c8 --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/CheckedOutputStream.java @@ -0,0 +1,9 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public class CheckedOutputStream extends java.io.FilterOutputStream { + public CheckedOutputStream(java.io.OutputStream arg0, java.util.zip.Checksum arg1) { super(null); throw new RuntimeException("Compile-time stub"); } + public java.util.zip.Checksum getChecksum() { throw new RuntimeException("Compile-time stub"); } + public void write(int arg0) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void write(byte[] arg0, int arg1, int arg2) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } +} diff --git a/Ports/CLDC11/src/java/util/zip/Checksum.java b/Ports/CLDC11/src/java/util/zip/Checksum.java new file mode 100644 index 00000000000..5c8dc1c3cf0 --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/Checksum.java @@ -0,0 +1,9 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public interface Checksum { + public abstract void update(int arg0); + public abstract void update(byte[] arg0, int arg1, int arg2); + public abstract long getValue(); + public abstract void reset(); +} diff --git a/Ports/CLDC11/src/java/util/zip/DataFormatException.java b/Ports/CLDC11/src/java/util/zip/DataFormatException.java new file mode 100644 index 00000000000..c327b4facc8 --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/DataFormatException.java @@ -0,0 +1,7 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public class DataFormatException extends java.lang.Exception { + public DataFormatException() { throw new RuntimeException("Compile-time stub"); } + public DataFormatException(java.lang.String arg0) { throw new RuntimeException("Compile-time stub"); } +} diff --git a/Ports/CLDC11/src/java/util/zip/Deflater.java b/Ports/CLDC11/src/java/util/zip/Deflater.java new file mode 100644 index 00000000000..9a1edfab565 --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/Deflater.java @@ -0,0 +1,38 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public class Deflater { + public static final int DEFAULT_COMPRESSION = -1; + public static final int NO_COMPRESSION = 0; + public static final int BEST_SPEED = 1; + public static final int BEST_COMPRESSION = 9; + public static final int DEFAULT_STRATEGY = 0; + public static final int FILTERED = 1; + public static final int HUFFMAN_ONLY = 2; + public static final int DEFLATED = 8; + public static final int NO_FLUSH = 0; + public static final int SYNC_FLUSH = 2; + public static final int FULL_FLUSH = 3; + public Deflater() { throw new RuntimeException("Compile-time stub"); } + public Deflater(int arg0) { throw new RuntimeException("Compile-time stub"); } + public Deflater(int arg0, boolean arg1) { throw new RuntimeException("Compile-time stub"); } + public void setInput(byte[] arg0) { throw new RuntimeException("Compile-time stub"); } + public void setInput(byte[] arg0, int arg1, int arg2) { throw new RuntimeException("Compile-time stub"); } + public void setDictionary(byte[] arg0) { throw new RuntimeException("Compile-time stub"); } + public void setDictionary(byte[] arg0, int arg1, int arg2) { throw new RuntimeException("Compile-time stub"); } + public void setLevel(int arg0) { throw new RuntimeException("Compile-time stub"); } + public void setStrategy(int arg0) { throw new RuntimeException("Compile-time stub"); } + public boolean needsInput() { throw new RuntimeException("Compile-time stub"); } + public void finish() { throw new RuntimeException("Compile-time stub"); } + public boolean finished() { throw new RuntimeException("Compile-time stub"); } + public int deflate(byte[] arg0) { throw new RuntimeException("Compile-time stub"); } + public int deflate(byte[] arg0, int arg1, int arg2) { throw new RuntimeException("Compile-time stub"); } + public int deflate(byte[] arg0, int arg1, int arg2, int arg3) { throw new RuntimeException("Compile-time stub"); } + public int getAdler() { throw new RuntimeException("Compile-time stub"); } + public long getBytesRead() { throw new RuntimeException("Compile-time stub"); } + public long getBytesWritten() { throw new RuntimeException("Compile-time stub"); } + public int getTotalIn() { throw new RuntimeException("Compile-time stub"); } + public int getTotalOut() { throw new RuntimeException("Compile-time stub"); } + public void reset() { throw new RuntimeException("Compile-time stub"); } + public void end() { throw new RuntimeException("Compile-time stub"); } +} diff --git a/Ports/CLDC11/src/java/util/zip/DeflaterOutputStream.java b/Ports/CLDC11/src/java/util/zip/DeflaterOutputStream.java new file mode 100644 index 00000000000..a02fc5684a0 --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/DeflaterOutputStream.java @@ -0,0 +1,19 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public class DeflaterOutputStream extends java.io.FilterOutputStream { + protected java.util.zip.Deflater def; + protected byte[] buf; + public DeflaterOutputStream(java.io.OutputStream arg0) { super(null); throw new RuntimeException("Compile-time stub"); } + public DeflaterOutputStream(java.io.OutputStream arg0, boolean arg1) { super(null); throw new RuntimeException("Compile-time stub"); } + public DeflaterOutputStream(java.io.OutputStream arg0, java.util.zip.Deflater arg1) { super(null); throw new RuntimeException("Compile-time stub"); } + public DeflaterOutputStream(java.io.OutputStream arg0, java.util.zip.Deflater arg1, int arg2) { super(null); throw new RuntimeException("Compile-time stub"); } + public DeflaterOutputStream(java.io.OutputStream arg0, java.util.zip.Deflater arg1, boolean arg2) { super(null); throw new RuntimeException("Compile-time stub"); } + public DeflaterOutputStream(java.io.OutputStream arg0, java.util.zip.Deflater arg1, int arg2, boolean arg3) { super(null); throw new RuntimeException("Compile-time stub"); } + public void write(int arg0) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void write(byte[] arg0, int arg1, int arg2) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + protected void deflate() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void finish() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void flush() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void close() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } +} diff --git a/Ports/CLDC11/src/java/util/zip/GZIPInputStream.java b/Ports/CLDC11/src/java/util/zip/GZIPInputStream.java new file mode 100644 index 00000000000..fe2bc35f2d8 --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/GZIPInputStream.java @@ -0,0 +1,13 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public class GZIPInputStream extends java.util.zip.InflaterInputStream { + public static final int GZIP_MAGIC = 35615; + protected java.util.zip.CRC32 crc; + protected boolean eos; + public GZIPInputStream(java.io.InputStream arg0) throws java.io.IOException { super(null); throw new RuntimeException("Compile-time stub"); } + public GZIPInputStream(java.io.InputStream arg0, int arg1) throws java.io.IOException { super(null); throw new RuntimeException("Compile-time stub"); } + public int read(byte[] arg0, int arg1, int arg2) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public int available() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void close() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } +} diff --git a/Ports/CLDC11/src/java/util/zip/GZIPOutputStream.java b/Ports/CLDC11/src/java/util/zip/GZIPOutputStream.java new file mode 100644 index 00000000000..01e13bf3cf4 --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/GZIPOutputStream.java @@ -0,0 +1,13 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public class GZIPOutputStream extends java.util.zip.DeflaterOutputStream { + protected java.util.zip.CRC32 crc; + public GZIPOutputStream(java.io.OutputStream arg0) throws java.io.IOException { super(null); throw new RuntimeException("Compile-time stub"); } + public GZIPOutputStream(java.io.OutputStream arg0, int arg1) throws java.io.IOException { super(null); throw new RuntimeException("Compile-time stub"); } + public GZIPOutputStream(java.io.OutputStream arg0, boolean arg1) throws java.io.IOException { super(null); throw new RuntimeException("Compile-time stub"); } + public GZIPOutputStream(java.io.OutputStream arg0, int arg1, boolean arg2) throws java.io.IOException { super(null); throw new RuntimeException("Compile-time stub"); } + public void write(byte[] arg0, int arg1, int arg2) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void finish() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void close() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } +} diff --git a/Ports/CLDC11/src/java/util/zip/Inflater.java b/Ports/CLDC11/src/java/util/zip/Inflater.java new file mode 100644 index 00000000000..7af9e368eb9 --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/Inflater.java @@ -0,0 +1,24 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public class Inflater { + public Inflater() { throw new RuntimeException("Compile-time stub"); } + public Inflater(boolean arg0) { throw new RuntimeException("Compile-time stub"); } + public void setInput(byte[] arg0) { throw new RuntimeException("Compile-time stub"); } + public void setInput(byte[] arg0, int arg1, int arg2) { throw new RuntimeException("Compile-time stub"); } + public void setDictionary(byte[] arg0) { throw new RuntimeException("Compile-time stub"); } + public void setDictionary(byte[] arg0, int arg1, int arg2) { throw new RuntimeException("Compile-time stub"); } + public boolean needsInput() { throw new RuntimeException("Compile-time stub"); } + public boolean needsDictionary() { throw new RuntimeException("Compile-time stub"); } + public boolean finished() { throw new RuntimeException("Compile-time stub"); } + public int inflate(byte[] arg0) throws java.util.zip.DataFormatException { throw new RuntimeException("Compile-time stub"); } + public int inflate(byte[] arg0, int arg1, int arg2) throws java.util.zip.DataFormatException { throw new RuntimeException("Compile-time stub"); } + public int getRemaining() { throw new RuntimeException("Compile-time stub"); } + public int getAdler() { throw new RuntimeException("Compile-time stub"); } + public long getBytesRead() { throw new RuntimeException("Compile-time stub"); } + public long getBytesWritten() { throw new RuntimeException("Compile-time stub"); } + public int getTotalIn() { throw new RuntimeException("Compile-time stub"); } + public int getTotalOut() { throw new RuntimeException("Compile-time stub"); } + public void reset() { throw new RuntimeException("Compile-time stub"); } + public void end() { throw new RuntimeException("Compile-time stub"); } +} diff --git a/Ports/CLDC11/src/java/util/zip/InflaterInputStream.java b/Ports/CLDC11/src/java/util/zip/InflaterInputStream.java new file mode 100644 index 00000000000..61d50b8eb65 --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/InflaterInputStream.java @@ -0,0 +1,21 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public class InflaterInputStream extends java.io.FilterInputStream { + protected java.util.zip.Inflater inf; + protected byte[] buf; + protected int len; + public InflaterInputStream(java.io.InputStream arg0) { super(null); throw new RuntimeException("Compile-time stub"); } + public InflaterInputStream(java.io.InputStream arg0, java.util.zip.Inflater arg1) { super(null); throw new RuntimeException("Compile-time stub"); } + public InflaterInputStream(java.io.InputStream arg0, java.util.zip.Inflater arg1, int arg2) { super(null); throw new RuntimeException("Compile-time stub"); } + protected void ensureOpen() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public int read() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public int read(byte[] arg0, int arg1, int arg2) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + protected void fill() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public long skip(long arg0) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public int available() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public boolean markSupported() { throw new RuntimeException("Compile-time stub"); } + public void mark(int arg0) { throw new RuntimeException("Compile-time stub"); } + public void reset() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void close() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } +} diff --git a/Ports/CLDC11/src/java/util/zip/ZipEntry.java b/Ports/CLDC11/src/java/util/zip/ZipEntry.java new file mode 100644 index 00000000000..b83b2dad89e --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/ZipEntry.java @@ -0,0 +1,28 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public class ZipEntry implements java.lang.Cloneable { + public static final int STORED = 0; + public static final int DEFLATED = 8; + public ZipEntry(java.lang.String arg0) { throw new RuntimeException("Compile-time stub"); } + public ZipEntry(java.util.zip.ZipEntry arg0) { throw new RuntimeException("Compile-time stub"); } + public java.lang.String getName() { throw new RuntimeException("Compile-time stub"); } + public void setTime(long arg0) { throw new RuntimeException("Compile-time stub"); } + public long getTime() { throw new RuntimeException("Compile-time stub"); } + public void setSize(long arg0) { throw new RuntimeException("Compile-time stub"); } + public long getSize() { throw new RuntimeException("Compile-time stub"); } + public void setCompressedSize(long arg0) { throw new RuntimeException("Compile-time stub"); } + public long getCompressedSize() { throw new RuntimeException("Compile-time stub"); } + public void setCrc(long arg0) { throw new RuntimeException("Compile-time stub"); } + public long getCrc() { throw new RuntimeException("Compile-time stub"); } + public void setMethod(int arg0) { throw new RuntimeException("Compile-time stub"); } + public int getMethod() { throw new RuntimeException("Compile-time stub"); } + public void setExtra(byte[] arg0) { throw new RuntimeException("Compile-time stub"); } + public byte[] getExtra() { throw new RuntimeException("Compile-time stub"); } + public void setComment(java.lang.String arg0) { throw new RuntimeException("Compile-time stub"); } + public java.lang.String getComment() { throw new RuntimeException("Compile-time stub"); } + public boolean isDirectory() { throw new RuntimeException("Compile-time stub"); } + public java.lang.String toString() { throw new RuntimeException("Compile-time stub"); } + public int hashCode() { throw new RuntimeException("Compile-time stub"); } + public java.lang.Object clone() { throw new RuntimeException("Compile-time stub"); } +} diff --git a/Ports/CLDC11/src/java/util/zip/ZipException.java b/Ports/CLDC11/src/java/util/zip/ZipException.java new file mode 100644 index 00000000000..9f087e83562 --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/ZipException.java @@ -0,0 +1,7 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public class ZipException extends java.io.IOException { + public ZipException() { throw new RuntimeException("Compile-time stub"); } + public ZipException(java.lang.String arg0) { throw new RuntimeException("Compile-time stub"); } +} diff --git a/Ports/CLDC11/src/java/util/zip/ZipInputStream.java b/Ports/CLDC11/src/java/util/zip/ZipInputStream.java new file mode 100644 index 00000000000..fb27aedf2ec --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/ZipInputStream.java @@ -0,0 +1,13 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public class ZipInputStream extends java.util.zip.InflaterInputStream { + public ZipInputStream(java.io.InputStream arg0) { super(null); throw new RuntimeException("Compile-time stub"); } + public ZipInputStream(java.io.InputStream arg0, java.nio.charset.Charset arg1) { super(null); throw new RuntimeException("Compile-time stub"); } + public java.util.zip.ZipEntry getNextEntry() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + protected java.util.zip.ZipEntry createZipEntry(java.lang.String arg0) { throw new RuntimeException("Compile-time stub"); } + public int read(byte[] arg0, int arg1, int arg2) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void closeEntry() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public int available() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void close() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } +} diff --git a/Ports/CLDC11/src/java/util/zip/ZipOutputStream.java b/Ports/CLDC11/src/java/util/zip/ZipOutputStream.java new file mode 100644 index 00000000000..fef6f31a204 --- /dev/null +++ b/Ports/CLDC11/src/java/util/zip/ZipOutputStream.java @@ -0,0 +1,20 @@ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.util.zip; + +public class ZipOutputStream extends java.util.zip.DeflaterOutputStream { + public static final int STORED = 0; + public static final int DEFLATED = 8; + public ZipOutputStream(java.io.OutputStream arg0) { super(null); throw new RuntimeException("Compile-time stub"); } + public ZipOutputStream(java.io.OutputStream arg0, java.nio.charset.Charset arg1) { super(null); throw new RuntimeException("Compile-time stub"); } + public void setMethod(int arg0) { throw new RuntimeException("Compile-time stub"); } + public void setLevel(int arg0) { throw new RuntimeException("Compile-time stub"); } + public void setComment(java.lang.String arg0) { throw new RuntimeException("Compile-time stub"); } + public void putNextEntry(java.util.zip.ZipEntry arg0) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void write(int arg0) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void write(byte[] arg0, int arg1, int arg2) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + protected void deflate() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void closeEntry() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void finish() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void flush() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void close() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } +} diff --git a/docs/developer-guide/Backend-Operations.asciidoc b/docs/developer-guide/Backend-Operations.asciidoc index 04f018e5c53..705f7036fe4 100644 --- a/docs/developer-guide/Backend-Operations.asciidoc +++ b/docs/developer-guide/Backend-Operations.asciidoc @@ -187,3 +187,127 @@ no TLS, and what it does need is exactly what a translated binary is good at. The reasons to choose this are cold start, footprint, deployment shape, and one language across the app and its server. If none of those matter for the service in front of you, use a JVM framework. + +[[backend-zip]] +=== Streaming ZIP archives + +ParparVM provides the streaming subset of `java.util.zip` in its Java runtime. +The same API is declared in the mobile compile-time library. It includes +`ZipEntry`, `ZipInputStream`, `ZipOutputStream`, `Deflater`, `Inflater`, +`DeflaterOutputStream`, `InflaterInputStream`, `GZIPInputStream`, +`GZIPOutputStream`, `Checksum`, `CRC32`, `Adler32`, `CheckedInputStream`, +`CheckedOutputStream`, `ZipException`, and `DataFormatException`. +Compression uses the existing pure-Java JZlib engine. JVM and Android targets +use their platform implementation of the standard classes. + +This is a streaming API, not the entire Java SE ZIP package. Random-access +`ZipFile`, `DeflaterInputStream`, `InflaterOutputStream`, and the `FileTime` +methods on `ZipEntry` are not provided. Reading a ZIP entry verifies its CRC +and sizes when its end is reached; call `closeEntry()` to consume and verify +an entry you have only partly read. Closing the input stream does not drain +or verify unread entries. ZIP input does not extract files or interpret Unix +permissions. + +==== Executable files in a project download + +Java SE's `ZipEntry` has no setter for Unix external attributes. Use +`com.codename1.io.zip.ZipArchiveOutputStream` when an extracted launcher must +remain executable. It accepts standard `ZipEntry` objects and adds +`putNextEntry(entry, permissions)`. This helper runs with both JVM and ParparVM +compression and writes the Unix creator platform and external attributes in +the central directory. + +[source,java] +---- +try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(destination)) { + ZipEntry directory = new ZipEntry("project/"); + directory.setTime(0); + zip.putNextEntry(directory, 0755); + zip.closeEntry(); + + ZipEntry launcher = new ZipEntry("project/run.sh"); + launcher.setTime(0); + zip.putNextEntry(launcher, 0755); + try (InputStream source = openLauncher()) { + byte[] buffer = new byte[8192]; + int count; + while ((count = source.read(buffer)) != -1) { + zip.write(buffer, 0, count); + } + } + zip.closeEntry(); +} +---- + +The permission argument accepts `0000` through `0777`. The writer infers a +regular file or directory from the trailing slash; it does not create symlinks +or special files. Use `0644` for ordinary files. Names are archive paths with +`/` separators. The writer does not sanitize names for extraction, so choose +relative names without `..` components. + +The runnable example is `vm/backend/demo/zipdownload/com/demo/ZipDownload.java`. +It serves a small, fixed project at `http://127.0.0.1:8099/project.zip`: + +[source,bash] +---- +export CN1_BACKEND_DEMO=demo/zipdownload +export CN1_BACKEND_STANDALONE_DEMO=1 +export JDK_8_HOME=/path/to/jdk8 +export JAVA_HOME="$JDK_8_HOME" +export PATH="$JAVA_HOME/bin:$PATH" +cd vm/backend +./build.sh ZipDownload com.demo /tmp/zipdownload +/tmp/zipdownload +---- + +The same binary can generate a file without starting HTTP: +`/tmp/zipdownload --write /tmp/project.zip`. Extract it with `unzip`, then run +`project/run.sh`. The JVM version can be run with +`CN1_BACKEND_DEMO=demo/zipdownload ./run-javase.sh com.demo.ZipDownload`. + +The HTTP example buffers only its fixed literals. `HttpServer.Response` currently +accepts a byte array or an already-open file descriptor; it has no streaming +output callback. ZIP generation itself streams to any `OutputStream`, but this +change does not add HTTP streaming or cancellation notifications during archive +generation. For large downloads, generate to an application-owned file and use +the existing file response lifecycle; the application must manage deletion of +that file. Do not collect arbitrary project contents in a byte array. + +==== Format limits and ownership + +* The default name/comment encoding is UTF-8, with the ZIP UTF-8 flag set. + Charset constructors also accept encodings available in the runtime. +* Files may be empty. Directories are explicit entries ending in `/`. + Duplicate entry names and empty entry names are rejected. +* `DEFLATED` entries stream with data descriptors. `STORED` entries require a + size and CRC before `putNextEntry`; actual size and CRC are checked at close. +* ParparVM's writer and `ZipArchiveOutputStream` refuse ZIP64. Each entry size, + compressed size, and the complete archive must remain below `0xffffffff` + bytes. There may be at most 65,534 entries. Caller-supplied ZIP64 extra fields + are rejected. A limit discovered while streaming raises `ZipException` and + leaves an incomplete archive; discard that output. +* Payload memory does not grow with file size. Compression uses a fixed window + and output buffer. Central-directory records grow with entry count and encoded + names, extras, and comments; the writer caps their serialized total at 16 MiB. + Java objects and strings add overhead beyond that serialized limit. +* ZIP field lengths are limited to 65,535 encoded bytes. The writer reserves + nine extra-field bytes for an extended timestamp where representable. + It records UTC modification times at one-second precision for signed 32-bit + Unix seconds; dates outside that range use the DOS timestamp, with two-second + precision and a 1980--2107 range. Explicit `setTime(0)` gives stable metadata. +* For repeatable archives, fix entry order, names, comments, permissions, + timestamps, and compression level. JVM and ParparVM archives have equivalent + contents and metadata, but compressed bytes need not match between engines. +* `finish()` writes the directory and leaves the destination open. `close()` + releases compressor state and closes the destination, including after a write + failure. A write failure makes the writer unusable. Source streams belong to + the caller; use a separate try-with-resources block for each source. No + temporary files are created by the ZIP API. +* Reading supports STORED and DEFLATED ZIP entries, with or without descriptors + for DEFLATED entries. ZIP64, encryption, other compression methods, and STORED + descriptors are explicitly rejected. GZIP supports concatenated members and + validates header and trailer checksums. + +These explicit ZIP limits belong to the ParparVM implementation and the CN1 +helper. The platform's own `java.util.zip.ZipOutputStream` may have broader +limits, including ZIP64 support. diff --git a/scripts/generate-zip-api-stubs.py b/scripts/generate-zip-api-stubs.py new file mode 100644 index 00000000000..cbe3979be08 --- /dev/null +++ b/scripts/generate-zip-api-stubs.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Generate mobile compile-time ZIP API stubs from compiled ParparVM classes. +Run after building vm/JavaAPI, with JAVA_HOME pointing at JDK 8. +These stubs describe the common API; actual ports supply the implementation. +""" +import argparse +import os +from pathlib import Path +import re +import subprocess +ROOT = Path(__file__).resolve().parents[1] +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--check', action='store_true') +args = parser.parse_args() +java_home = Path(os.environ['JAVA_HOME']) +for source in sorted((ROOT / 'vm/JavaAPI/src/java/util/zip').glob('*.java')): + compiled = ROOT / 'vm/JavaAPI/target/classes/java/util/zip' / (source.stem + '.class') + api = subprocess.check_output([str(java_home / 'bin/javap'), '-protected', '-constants', str(compiled)], text=True) + lines = ['// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only.', 'package java.util.zip;', ''] + is_interface = False + for line in api.splitlines(): + if line.startswith('Compiled from '): + continue + if line.startswith('public '): + is_interface = ' interface ' in line + line = line.replace('java.util.zip.' + source.stem, source.stem, 1) + elif '(' in line: + line = line.replace('java.util.zip.' + source.stem + '(', source.stem + '(') + if not is_interface: + body = 'throw new RuntimeException("Compile-time stub");' + # The JDK filter base classes require an explicit constructor call. + if re.match(r'\s+public '+source.stem+r'\(', line) and source.stem in ('DeflaterOutputStream','InflaterInputStream','CheckedInputStream','CheckedOutputStream'): + body = 'super(null); ' + body + elif re.match(r'\s+public '+source.stem+r'\(', line) and source.stem in ('GZIPOutputStream','ZipOutputStream','GZIPInputStream','ZipInputStream'): + body = 'super(null); ' + body + # javap omits parameter names. + start, end = line.index('('), line.index(')') + types = line[start+1:end].split(', ') + params = ', '.join(t+' arg'+str(i) for i,t in enumerate(types) if t) + line = line[:start+1] + params + line[end:] + line = line.rstrip(';') + ' { ' + body + ' }' + else: + start, end = line.index('('), line.index(')') + params = ', '.join(t+' arg'+str(i) for i,t in enumerate(line[start+1:end].split(', ')) if t) + line = line[:start+1] + params + line[end:] + lines.append(line) + text = '\n'.join(lines) + '\n' + target = ROOT / 'Ports/CLDC11/src/java/util/zip' / source.name + if args.check: + if not target.exists() or target.read_text() != text: + raise SystemExit('Stale ZIP API stub: '+str(target)) + else: + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(text) diff --git a/scripts/sync-zip-runtime.py b/scripts/sync-zip-runtime.py new file mode 100644 index 00000000000..3c84288c9a0 --- /dev/null +++ b/scripts/sync-zip-runtime.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Generate the standalone ParparVM ZIP sources. Use --check to detect drift. + +JZlib's canonical sources remain in core. Relocation keeps JavaAPI standalone +and avoids dependencies on mobile core or duplicate runtime class names. +""" +import argparse +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +ENGINE = ('Adler32 CRC32 Checksum Deflate Deflater GZIPException GZIPHeader ' + 'InfBlocks InfCodes InfTree Inflate Inflater JZlib StaticTree Tree ZStream').split() + + +def generated(): + for name in ENGINE: + source = ROOT / 'CodenameOne/src/com/codename1/io/gzip' / (name + '.java') + text = source.read_text().replace('package com.codename1.io.gzip;', + 'package com.codename1.impl.zip;') + text = text.replace('@com.codename1.impl.SharedWithBackend\n', '') + yield ROOT / 'vm/JavaAPI/src/com/codename1/impl/zip' / source.name, text + source = ROOT / 'CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java' + text = source.read_text().replace('package com.codename1.io.zip;', 'package java.util.zip;') + text = text.replace('@com.codename1.impl.SharedWithBackend\n', '') + text = text.replace('ZipArchiveOutputStream', 'ZipOutputStream') + text = text.replace('Streaming ZIP writer with optional Unix permissions. Works on the JVM and\n * ParparVM.', + 'Streaming ZIP writer for ParparVM.') + # The permission overload belongs to the portable CN1 helper only. + start = text.index(' /**\n * Writes an entry with Unix') + end = text.index(' private void putEntry', start) + text = text[:start] + text[end:] + yield ROOT / 'vm/JavaAPI/src/java/util/zip/ZipOutputStream.java', text + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--check', action='store_true') + args = parser.parse_args() + stale = [] + for path, text in generated(): + text = '\n'.join(line.rstrip() for line in text.splitlines()) + '\n' + text = '// Generated by scripts/sync-zip-runtime.py; edit the canonical core source.\n' + text + if args.check: + if not path.exists() or path.read_text() != text: + stale.append(str(path.relative_to(ROOT))) + else: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text) + if stale: + raise SystemExit('Stale generated sources:\n' + '\n'.join(stale)) + + +if __name__ == '__main__': + main() diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/Adler32.java b/vm/JavaAPI/src/com/codename1/impl/zip/Adler32.java new file mode 100644 index 00000000000..e0aa41a295f --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/Adler32.java @@ -0,0 +1,154 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2000-2011 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +final public class Adler32 implements Checksum { + + // largest prime smaller than 65536 + static final private int BASE = 65521; + // NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 + static final private int NMAX = 5552; + + private long s1 = 1L; + private long s2 = 0L; + + // The following logic has come from zlib.1.2. + static long combine(long adler1, long adler2, long len2) { + long basel = BASE; + long sum1; + long sum2; + long rem; // unsigned int + + rem = len2 % basel; + sum1 = adler1 & 0xffffL; + sum2 = rem * sum1; + sum2 %= basel; // MOD(sum2); + sum1 += (adler2 & 0xffffL) + basel - 1; + sum2 += ((adler1 >> 16) & 0xffffL) + ((adler2 >> 16) & 0xffffL) + basel - rem; + if (sum1 >= basel) { + sum1 -= basel; + } + if (sum1 >= basel) { + sum1 -= basel; + } + if (sum2 >= (basel << 1)) { + sum2 -= (basel << 1); + } + if (sum2 >= basel) { + sum2 -= basel; + } + return sum1 | (sum2 << 16); + } + + @Override + public void reset(long init) { + s1 = init & 0xffff; + s2 = (init >> 16) & 0xffff; + } + + @Override + public void reset() { + s1 = 1L; + s2 = 0L; + } + + @Override + public long getValue() { + return ((s2 << 16) | s1); + } + + @Override + public void update(byte[] buf, int index, int len) { + + if (len == 1) { + s1 += buf[index++] & 0xff; + s2 += s1; + s1 %= BASE; + s2 %= BASE; + return; + } + + int len1 = len / NMAX; + int len2 = len % NMAX; + while (len1-- > 0) { + int k = NMAX; + while (k-- > 0) { + s1 += buf[index++] & 0xff; + s2 += s1; + } + s1 %= BASE; + s2 %= BASE; + } + + int k = len2; + while (k-- > 0) { + s1 += buf[index++] & 0xff; + s2 += s1; + } + s1 %= BASE; + s2 %= BASE; + } + + @Override + public Adler32 copy() { + Adler32 foo = new Adler32(); + foo.s1 = this.s1; + foo.s2 = this.s2; + return foo; + } + +/* + private java.util.zip.Adler32 adler=new java.util.zip.Adler32(); + public void update(byte[] buf, int index, int len){ + if(buf==null) {adler.reset();} + else{adler.update(buf, index, len);} + } + public void reset(){ + adler.reset(); + } + public void reset(long init){ + if(init==1L){ + adler.reset(); + } + else{ + System.err.println("unsupported operation"); + } + } + public long getValue(){ + return adler.getValue(); + } +*/ +} diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/CRC32.java b/vm/JavaAPI/src/com/codename1/impl/zip/CRC32.java new file mode 100644 index 00000000000..c1470fffab2 --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/CRC32.java @@ -0,0 +1,196 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +final public class CRC32 implements Checksum { + + // The following logic has come from zlib.1.2. + private static final int GF2_DIM = 32; + private static int[] crc_table = null; + + static { + crc_table = new int[256]; + for (int n = 0; n < 256; n++) { + int c = n; + + for (int k = 8; --k >= 0; ) { //NOPMD AssignmentInOperand + if ((c & 1) != 0) { + c = 0xedb88320 ^ (c >>> 1); + } else { + c = c >>> 1; + } + } + crc_table[n] = c; + } + } + + /* + * The following logic has come from RFC1952. + */ + private int v = 0; + + static long combine(long crc1, long crc2, long len2) { + long row; + long[] even = new long[GF2_DIM]; + long[] odd = new long[GF2_DIM]; + + // degenerate case (also disallow negative lengths) + if (len2 <= 0) { + return crc1; + } + + // put operator for one zero bit in odd + odd[0] = 0xedb88320L; // CRC-32 polynomial + row = 1; + for (int n = 1; n < GF2_DIM; n++) { + odd[n] = row; + row <<= 1; + } + + // put operator for two zero bits in even + gf2MatrixSquare(even, odd); + + // put operator for four zero bits in odd + gf2MatrixSquare(odd, even); + + // apply len2 zeros to crc1 (first square will put the operator for one + // zero byte, eight zero bits, in even) + do { + // apply zeros operator for this bit of len2 + gf2MatrixSquare(even, odd); + if ((len2 & 1) != 0) { + crc1 = gf2MatrixTimes(even, crc1); + } + len2 >>= 1; + + // if no more bits set, then done + if (len2 == 0) { + break; + } + + // another iteration of the loop with odd and even swapped + gf2MatrixSquare(odd, even); + if ((len2 & 1) != 0) { + crc1 = gf2MatrixTimes(odd, crc1); + } + len2 >>= 1; + + // if no more bits set, then done + } while (len2 != 0); + + /* return combined crc */ + crc1 ^= crc2; + return crc1; + } + + private static long gf2MatrixTimes(long[] mat, long vec) { + long sum = 0; + int index = 0; + while (vec != 0) { + if ((vec & 1) != 0) { + sum ^= mat[index]; + } + vec >>= 1; + index++; + } + return sum; + } + + static void gf2MatrixSquare(long[] square, long[] mat) { + for (int n = 0; n < GF2_DIM; n++) { + square[n] = gf2MatrixTimes(mat, mat[n]); + } + } + + public static int[] getCRC32Table() { + int[] tmp = new int[crc_table.length]; + System.arraycopy(crc_table, 0, tmp, 0, tmp.length); + return tmp; + } + + @Override + public void update(byte[] buf, int index, int len) { + int c = ~v; + while (--len >= 0) { + c = crc_table[(c ^ buf[index++]) & 0xff] ^ (c >>> 8); + } + v = ~c; + } + + @Override + public void reset() { + v = 0; + } + + @Override + public void reset(long vv) { + v = (int) (vv & 0xffffffffL); + } + + @Override + public long getValue() { + return v & 0xffffffffL; + } + + /* + private java.util.zip.CRC32 crc32 = new java.util.zip.CRC32(); + + public void update(byte[] buf, int index, int len){ + if(buf==null) {crc32.reset();} + else{crc32.update(buf, index, len);} + } + public void reset(){ + crc32.reset(); + } + public void reset(long init){ + if(init==0L){ + crc32.reset(); + } + else{ + System.err.println("unsupported operation"); + } + } + public long getValue(){ + return crc32.getValue(); + } + */ + @Override + public CRC32 copy() { + CRC32 foo = new CRC32(); + foo.v = this.v; + return foo; + } +} diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/Checksum.java b/vm/JavaAPI/src/com/codename1/impl/zip/Checksum.java new file mode 100644 index 00000000000..c67b6b660b1 --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/Checksum.java @@ -0,0 +1,48 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +interface Checksum { + void update(byte[] buf, int index, int len); + + void reset(); + + void reset(long init); + + long getValue(); + + Checksum copy(); +} diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/Deflate.java b/vm/JavaAPI/src/com/codename1/impl/zip/Deflate.java new file mode 100644 index 00000000000..a95b10578ee --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/Deflate.java @@ -0,0 +1,1777 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2000-2011 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +public +final class Deflate { + + static final private int MAX_MEM_LEVEL = 9; + + static final private int Z_DEFAULT_COMPRESSION = -1; + + static final private int MAX_WBITS = 15; // 32K LZ77 window + static final private int DEF_MEM_LEVEL = 8; + static final private int STORED = 0; + static final private int FAST = 1; + static final private int SLOW = 2; + static final private Config[] CONFIG_TABLE; + static final private String[] Z_ERRMSG = { + "need dictionary", // Z_NEED_DICT 2 + "stream end", // Z_STREAM_END 1 + "", // Z_OK 0 + "file error", // Z_ERRNO (-1) + "stream error", // Z_STREAM_ERROR (-2) + "data error", // Z_DATA_ERROR (-3) + "insufficient memory", // Z_MEM_ERROR (-4) + "buffer error", // Z_BUF_ERROR (-5) + "incompatible version", // Z_VERSION_ERROR (-6) + "" + }; + // block not completed, need more input or more output + static final private int NEED_MORE = 0; + // block flush performed + static final private int BLOCK_DONE = 1; + // finish started, need only more output at next deflate + static final private int FINISH_STARTED = 2; + // finish done, accept no more input or output + static final private int FINISH_DONE = 3; + // preset dictionary flag in zlib header + static final private int PRESET_DICT = 0x20; + static final private int Z_FILTERED = 1; + static final private int Z_HUFFMAN_ONLY = 2; + static final private int Z_DEFAULT_STRATEGY = 0; + static final private int Z_NO_FLUSH = 0; + static final private int Z_PARTIAL_FLUSH = 1; + static final private int Z_FULL_FLUSH = 3; + static final private int Z_FINISH = 4; + static final private int Z_OK = 0; + static final private int Z_STREAM_END = 1; + static final private int Z_NEED_DICT = 2; + static final private int Z_STREAM_ERROR = -2; + static final private int Z_DATA_ERROR = -3; + static final private int Z_BUF_ERROR = -5; + static final private int INIT_STATE = 42; + static final private int BUSY_STATE = 113; + static final private int FINISH_STATE = 666; + // The deflate compression method + static final private int Z_DEFLATED = 8; + static final private int STATIC_TREES = 1; + static final private int DYN_TREES = 2; + // The three kinds of block type + static final private int Z_BINARY = 0; + static final private int Z_ASCII = 1; + static final private int Z_UNKNOWN = 2; + static final private int BUF_SIZE = 8 * 2; + // repeat previous bit length 3-6 times (2 bits of repeat count) + static final private int REP_3_6 = 16; + // repeat a zero length 3-10 times (3 bits of repeat count) + static final private int REPZ_3_10 = 17; + // repeat a zero length 11-138 times (7 bits of repeat count) + static final private int REPZ_11_138 = 18; + static final private int MIN_MATCH = 3; + static final private int MAX_MATCH = 258; + static final private int MIN_LOOKAHEAD = (MAX_MATCH + MIN_MATCH + 1); + static final private int MAX_BITS = 15; + static final private int D_CODES = 30; + static final private int BL_CODES = 19; + static final private int LENGTH_CODES = 29; + static final private int LITERALS = 256; + static final private int L_CODES = (LITERALS + 1 + LENGTH_CODES); + static final private int HEAP_SIZE = (2 * L_CODES + 1); + static final private int END_BLOCK = 256; + + static { + CONFIG_TABLE = new Config[10]; + // good lazy nice chain + CONFIG_TABLE[0] = new Config(0, 0, 0, 0, STORED); + CONFIG_TABLE[1] = new Config(4, 4, 8, 4, FAST); + CONFIG_TABLE[2] = new Config(4, 5, 16, 8, FAST); + CONFIG_TABLE[3] = new Config(4, 6, 32, 32, FAST); + + CONFIG_TABLE[4] = new Config(4, 4, 16, 16, SLOW); + CONFIG_TABLE[5] = new Config(8, 16, 32, 32, SLOW); + CONFIG_TABLE[6] = new Config(8, 16, 128, 128, SLOW); + CONFIG_TABLE[7] = new Config(8, 32, 128, 256, SLOW); + CONFIG_TABLE[8] = new Config(32, 128, 258, 1024, SLOW); + CONFIG_TABLE[9] = new Config(32, 258, 258, 4096, SLOW); + } + + ZStream strm; // pointer back to this zlib stream + int status; // as the name implies + byte[] pendingBuf; // output still pending + int pendingBufSize; // size of pending_buf + int pendingOut; // next pending byte to output to the stream + int pending; // nb of bytes in the pending buffer + int wrap = 1; + byte dataType; // UNKNOWN, BINARY or ASCII + int lastFlush; // value of flush param for previous deflate call + int wSize; // LZ77 window size (32K by default) + int wBits; // log2(w_size) (8..16) + int wMask; // w_size - 1 + byte[] window; + int windowSize; + // Sliding window. Input bytes are read into the second half of the window, + // and move to the first half later to keep a dictionary of at least wSize + // bytes. With this organization, matches are limited to a distance of + // wSize-MAX_MATCH bytes, but this ensures that IO is always + // performed with a length multiple of the block size. Also, it limits + // the window size to 64K, which is quite useful on MSDOS. + // To do: use the user input buffer as sliding window. + short[] prev; + // Actual size of window: 2*wSize, except when the user input buffer + // is directly used as sliding window. + short[] head; // Heads of the hash chains or NIL. + // Link to older string with same hash index. To limit the size of this + // array to 64K, this link is maintained only for the last 32K strings. + // An index in this array is thus a window index modulo 32K. + int insH; // hash index of string to be inserted + int hashSize; // number of elements in hash table + int hashBits; // log2(hash_size) + int hashMask; // hash_size-1 + // Number of bits by which ins_h must be shifted at each input + // step. It must be such that after MIN_MATCH steps, the oldest + // byte no longer takes part in the hash key, that is: + // hash_shift * MIN_MATCH >= hash_bits + int hashShift; + int blockStart; + + // Window position at the beginning of the current output block. Gets + // negative when the window is moved backwards. + int matchLength; // length of best match + int prevMatch; // previous match + int matchAvailable; // set if previous match exists + int strStart; // start of string to insert + int matchStart; // start of matching string + int lookahead; // number of valid bytes ahead in window + // Length of the best match at previous step. Matches not greater than this + // are discarded. This is used in the lazy match evaluation. + int prevLength; + // To speed up deflation, hash chains are never searched beyond this + // length. A higher limit improves compression ratio but degrades the speed. + int maxChainLength; + // Attempt to find a better match only when the current match is strictly + // smaller than this value. This mechanism is used only for compression + // levels >= 4. + int maxLazyMatch; + int level; // compression level (1..9) + + // Insert new strings in the hash table only if the match length is not + // greater than this length. This saves time but degrades compression. + // max_insert_length is used only for compression levels <= 3. + int strategy; // favor or force Huffman coding + // Use a faster search when the previous match is longer than this + int goodMatch; + // Stop searching when current match exceeds this + int niceMatch; + short[] dynLtree; // literal and length tree + short[] dynDtree; // distance tree + short[] blTree; // Huffman tree for bit lengths + Tree lDesc = new Tree(); // desc for literal tree + Tree dDesc = new Tree(); // desc for distance tree + Tree blDesc = new Tree(); // desc for bit length tree + // number of codes at each bit length for an optimal tree + short[] blCount = new short[MAX_BITS + 1]; + // working area to be used in Tree#gen_codes() + short[] nextCode = new short[MAX_BITS + 1]; + // heap used to build the Huffman trees + int[] heap = new int[2 * L_CODES + 1]; + int heapLen; // number of elements in the heap + int heapMax; // element of largest frequency + // Depth of each subtree used as tie breaker for trees of equal frequency + byte[] depth = new byte[2 * L_CODES + 1]; + // The sons of heap[n] are heap[2*n] and heap[2*n+1]. heap[0] is not used. + // The same heap array is used to build all trees. + int lBuf; // index for literals or lengths */ + // Size of match buffer for literals/lengths. There are 4 reasons for + // limiting lit_bufsize to 64K: + // - frequencies can be kept in 16 bit counters + // - if compression is not successful for the first block, all input + // data is still in the window so we can still emit a stored block even + // when input comes from standard input. (This can also be done for + // all blocks if lit_bufsize is not greater than 32K.) + // - if compression is not successful for a file smaller than 64K, we can + // even emit a stored file instead of a stored block (saving 5 bytes). + // This is applicable only for zip (not gzip or zlib). + // - creating new Huffman trees less frequently may not provide fast + // adaptation to changes in the input data statistics. (Take for + // example a binary file with poorly compressible code followed by + // a highly compressible string table.) Smaller buffer sizes give + // fast adaptation but have of course the overhead of transmitting + // trees more frequently. + // - I can't count above 4 + int litBufSize; + int lastLit; // running index in l_buf + int dBuf; // index of pendig_buf + + // Buffer for distances. To simplify the code, d_buf and l_buf have + // the same number of elements. To use different lengths, an extra flag + // array would be necessary. + int optLen; // bit length of current block with optimal trees + int staticLen; // bit length of current block with static trees + int matches; // number of string matches in current block + int lastEobLen; // bit length of EOB code for last block + // Output buffer. bits are inserted starting at the bottom (least + // significant bits). + short biBuf; + // Number of valid bits in bi_buf. All bits above the last valid bit + // are always zero. + int biValid; + GZIPHeader gheader = null; + + Deflate(ZStream strm) { + this.strm = strm; + dynLtree = new short[HEAP_SIZE * 2]; + dynDtree = new short[(2 * D_CODES + 1) * 2]; // distance tree + blTree = new short[(2 * BL_CODES + 1) * 2]; // Huffman tree for bit lengths + } + + static boolean smaller(short[] tree, int n, int m, byte[] depth) { + short tn2 = tree[n * 2]; + short tm2 = tree[m * 2]; + return (tn2 < tm2 || + (tn2 == tm2 && depth[n] <= depth[m])); + } + + static int deflateCopy(ZStream dest, ZStream src) { + + if (src.dstate == null) { + return Z_STREAM_ERROR; + } + + if (src.nextIn != null) { + dest.nextIn = new byte[src.nextIn.length]; + System.arraycopy(src.nextIn, 0, dest.nextIn, 0, src.nextIn.length); + } + dest.nextInIndex = src.nextInIndex; + dest.availIn = src.availIn; + dest.totalIn = src.totalIn; + + if (src.nextOut != null) { + dest.nextOut = new byte[src.nextOut.length]; + System.arraycopy(src.nextOut, 0, dest.nextOut, 0, src.nextOut.length); + } + + dest.nextOutIndex = src.nextOutIndex; + dest.availOut = src.availOut; + dest.totalOut = src.totalOut; + + dest.msg = src.msg; + dest.dataType = src.dataType; + dest.adler = src.adler.copy(); + + dest.dstate = (Deflate) src.dstate.clone(); + dest.dstate.strm = dest; + return Z_OK; + } + + void lmInit() { + windowSize = 2 * wSize; + + head[hashSize - 1] = 0; + for (int i = 0; i < hashSize - 1; i++) { + head[i] = 0; + } + + // Set the default configuration parameters: + maxLazyMatch = Deflate.CONFIG_TABLE[level].maxLazy; + goodMatch = Deflate.CONFIG_TABLE[level].goodLength; + niceMatch = Deflate.CONFIG_TABLE[level].niceLength; + maxChainLength = Deflate.CONFIG_TABLE[level].maxChain; + + strStart = 0; + blockStart = 0; + lookahead = 0; + matchLength = prevLength = MIN_MATCH - 1; + matchAvailable = 0; + insH = 0; + } + + // Initialize the tree data structures for a new zlib stream. + void trInit() { + + lDesc.dynTree = dynLtree; + lDesc.statDesc = StaticTree.staticLDesc; + + dDesc.dynTree = dynDtree; + dDesc.statDesc = StaticTree.staticDDesc; + + blDesc.dynTree = blTree; + blDesc.statDesc = StaticTree.staticBlDesc; + + biBuf = 0; + biValid = 0; + lastEobLen = 8; // enough lookahead for inflate + + // Initialize the first block of the first file: + initBlock(); + } + + void initBlock() { + // Initialize the trees. + for (int i = 0; i < L_CODES; i++) { + dynLtree[i * 2] = 0; + } + for (int i = 0; i < D_CODES; i++) { + dynDtree[i * 2] = 0; + } + for (int i = 0; i < BL_CODES; i++) { + blTree[i * 2] = 0; + } + + dynLtree[END_BLOCK * 2] = 1; + optLen = staticLen = 0; + lastLit = matches = 0; + } + + // Restore the heap property by moving down the tree starting at node k, + // exchanging a node with the smallest of its two sons if necessary, stopping + // when the heap property is re-established (each father smaller than its + // two sons). + void pqdownheap(short[] tree, // the tree to restore + int k // node to move down + ) { + int v = heap[k]; + int j = k << 1; // left son of k + while (j <= heapLen) { + // Set j to the smallest of the two sons: + if (j < heapLen && + smaller(tree, heap[j + 1], heap[j], depth)) { + j++; + } + // Exit if v is smaller than both sons + if (smaller(tree, v, heap[j], depth)) { + break; + } + + // Exchange v with the smallest son + heap[k] = heap[j]; + k = j; + // And continue down the tree, setting j to the left son of k + j <<= 1; + } + heap[k] = v; + } + + // Scan a literal or distance tree to determine the frequencies of the codes + // in the bit length tree. + void scanTree(short[] tree, // the tree to be scanned + int maxCode // and its largest code of non-zero frequency + ) { + int n; // iterates over all tree elements + int prevlen = -1; // last emitted length + int curlen; // length of current code + int nextlen = tree[1]; // length of next code + int count = 0; // repeat count of the current code + int maxCount = 7; // max repeat count + int minCount = 4; // min repeat count + + if (nextlen == 0) { + maxCount = 138; + minCount = 3; + } + tree[(maxCode + 1) * 2 + 1] = (short) 0xffff; // guard + + for (n = 0; n <= maxCode; n++) { + curlen = nextlen; + nextlen = tree[(n + 1) * 2 + 1]; + if (++count < maxCount && curlen == nextlen) { //NOPMD AssignmentInOperand + continue; + } else if (count < minCount) { + blTree[curlen * 2] += count; + } else if (curlen != 0) { + if (curlen != prevlen) { + blTree[curlen * 2]++; + } + blTree[REP_3_6 * 2]++; + } else if (count <= 10) { + blTree[REPZ_3_10 * 2]++; + } else { + blTree[REPZ_11_138 * 2]++; + } + count = 0; + prevlen = curlen; + if (nextlen == 0) { + maxCount = 138; + minCount = 3; + } else if (curlen == nextlen) { + maxCount = 6; + minCount = 3; + } else { + maxCount = 7; + minCount = 4; + } + } + } + + // Construct the Huffman tree for the bit lengths and return the index in + // BL_ORDER of the last bit length code to send. + int buildBlTree() { + int maxBlindex; // index of last bit length code of non zero freq + + // Determine the bit length frequencies for literal and distance trees + scanTree(dynLtree, lDesc.maxCode); + scanTree(dynDtree, dDesc.maxCode); + + // Build the bit length tree: + blDesc.buildTree(this); + // opt_len now includes the length of the tree representations, except + // the lengths of the bit lengths codes and the 5+5+4 bits for the counts. + + // Determine the number of bit length codes to send. The pkzip format + // requires that at least 4 bit length codes be sent. (appnote.txt says + // 3 but the actual value used is 4.) + for (maxBlindex = BL_CODES - 1; maxBlindex >= 3; maxBlindex--) { + if (blTree[Tree.BL_ORDER[maxBlindex] * 2 + 1] != 0) { + break; + } + } + // Update opt_len to include the bit length tree and counts + optLen += 3 * (maxBlindex + 1) + 5 + 5 + 4; + + return maxBlindex; + } + + + // Send the header for a block using dynamic Huffman trees: the counts, the + // lengths of the bit length codes, the literal tree and the distance tree. + // IN assertion: lcodes >= 257, dcodes >= 1, blcodes >= 4. + void sendAllTrees(int lcodes, int dcodes, int blcodes) { + int rank; // index in bl_order + + sendBits(lcodes - 257, 5); // not +255 as stated in appnote.txt + sendBits(dcodes - 1, 5); + sendBits(blcodes - 4, 4); // not -3 as stated in appnote.txt + for (rank = 0; rank < blcodes; rank++) { + sendBits(blTree[Tree.BL_ORDER[rank] * 2 + 1], 3); + } + sendTree(dynLtree, lcodes - 1); // literal tree + sendTree(dynDtree, dcodes - 1); // distance tree + } + + // Send a literal or distance tree in compressed form, using the codes in + // bl_tree. + void sendTree(short[] tree, // the tree to be sent + int maxCode // and its largest code of non zero frequency + ) { + int n; // iterates over all tree elements + int prevlen = -1; // last emitted length + int curlen; // length of current code + int nextlen = tree[1]; // length of next code + int count = 0; // repeat count of the current code + int maxCount = 7; // max repeat count + int minCount = 4; // min repeat count + + if (nextlen == 0) { + maxCount = 138; + minCount = 3; + } + + for (n = 0; n <= maxCode; n++) { + curlen = nextlen; + nextlen = tree[(n + 1) * 2 + 1]; + if (++count < maxCount && curlen == nextlen) { //NOPMD AssignmentInOperand + continue; + } else if (count < minCount) { + do { + sendCode(curlen, blTree); + } while (--count != 0); + } else if (curlen != 0) { + if (curlen != prevlen) { + sendCode(curlen, blTree); + count--; + } + sendCode(REP_3_6, blTree); + sendBits(count - 3, 2); + } else if (count <= 10) { + sendCode(REPZ_3_10, blTree); + sendBits(count - 3, 3); + } else { + sendCode(REPZ_11_138, blTree); + sendBits(count - 11, 7); + } + count = 0; + prevlen = curlen; + if (nextlen == 0) { + maxCount = 138; + minCount = 3; + } else if (curlen == nextlen) { + maxCount = 6; + minCount = 3; + } else { + maxCount = 7; + minCount = 4; + } + } + } + + // Output a byte on the stream. + // IN assertion: there is enough room in pending_buf. + void putByte(byte[] p, int start, int len) { + System.arraycopy(p, start, pendingBuf, pending, len); + pending += len; + } + + void putByte(byte c) { + pendingBuf[pending++] = c; + } + + void putShort(int w) { + putByte((byte) (w/*&0xff*/)); + putByte((byte) (w >>> 8)); + } + + void putShortMSB(int b) { + putByte((byte) (b >> 8)); + putByte((byte) (b/*&0xff*/)); + } + + void sendCode(int c, short[] tree) { + int c2 = c * 2; + sendBits((tree[c2] & 0xffff), (tree[c2 + 1] & 0xffff)); + } + + void sendBits(int value, int length) { + int len = length; + if (biValid > BUF_SIZE - len) { + int val = value; +// bi_buf |= (val << bi_valid); + biBuf |= ((val << biValid) & 0xffff); + putShort(biBuf); + biBuf = (short) (val >>> (BUF_SIZE - biValid)); + biValid += len - BUF_SIZE; + } else { +// bi_buf |= (value) << bi_valid; + biBuf |= (((value) << biValid) & 0xffff); + biValid += len; + } + } + + // Send one empty static block to give enough lookahead for inflate. + // This takes 10 bits, of which 7 may remain in the bit buffer. + // The current inflate code requires 9 bits of lookahead. If the + // last two codes for the previous block (real code plus EOB) were coded + // on 5 bits or less, inflate may have only 5+3 bits of lookahead to decode + // the last real code. In this case we send two empty static blocks instead + // of one. (There are no problems if the previous block is stored or fixed.) + // To simplify the code, we assume the worst case of last real code encoded + // on one bit only. + void trAlign() { + sendBits(STATIC_TREES << 1, 3); + sendCode(END_BLOCK, StaticTree.STATIC_LTREE); + + biFlush(); + + // Of the 10 bits for the empty block, we have already sent + // (10 - bi_valid) bits. The lookahead for the last real code (before + // the EOB of the previous block) was thus at least one plus the length + // of the EOB plus what we have just sent of the empty static block. + if (1 + lastEobLen + 10 - biValid < 9) { + sendBits(STATIC_TREES << 1, 3); + sendCode(END_BLOCK, StaticTree.STATIC_LTREE); + biFlush(); + } + lastEobLen = 7; + } + + + // Save the match info and tally the frequency counts. Return true if + // the current block must be flushed. + boolean trTally(int dist, // distance of matched string + int lc // match length-MIN_MATCH or unmatched char (if dist==0) + ) { + + pendingBuf[dBuf + lastLit * 2] = (byte) (dist >>> 8); + pendingBuf[dBuf + lastLit * 2 + 1] = (byte) dist; + + pendingBuf[lBuf + lastLit] = (byte) lc; + lastLit++; + + if (dist == 0) { + // lc is the unmatched char + dynLtree[lc * 2]++; + } else { + matches++; + // Here, lc is the match length - MIN_MATCH + dist--; // dist = match distance - 1 + dynLtree[(Tree.LENGTH_CODE[lc] + LITERALS + 1) * 2]++; + dynDtree[Tree.dCode(dist) * 2]++; + } + + if ((lastLit & 0x1fff) == 0 && level > 2) { + // Compute an upper bound for the compressed length + int outLength = lastLit * 8; + int inLength = strStart - blockStart; + int dcode; + for (dcode = 0; dcode < D_CODES; dcode++) { + outLength += (int) dynDtree[dcode * 2] * + (5L + Tree.EXTRA_DBITS[dcode]); + } + outLength >>>= 3; + if ((matches < (lastLit / 2)) && outLength < inLength / 2) { + return true; + } + } + + return (lastLit == litBufSize - 1); + // We avoid equality with lit_bufsize because of wraparound at 64K + // on 16 bit machines and because stored blocks are restricted to + // 64K-1 bytes. + } + + // Send the block data compressed using the given Huffman trees + void compressBlock(short[] ltree, short[] dtree) { + int dist; // distance of matched string + int lc; // match length or unmatched char (if dist == 0) + int lx = 0; // running index in l_buf + int code; // the code to send + int extra; // number of extra bits to send + + if (lastLit != 0) { + do { + dist = ((pendingBuf[dBuf + lx * 2] << 8) & 0xff00) | + (pendingBuf[dBuf + lx * 2 + 1] & 0xff); + lc = (pendingBuf[lBuf + lx]) & 0xff; + lx++; + + if (dist == 0) { + sendCode(lc, ltree); // send a literal byte + } else { + // Here, lc is the match length - MIN_MATCH + code = Tree.LENGTH_CODE[lc]; + + sendCode(code + LITERALS + 1, ltree); // send the length code + extra = Tree.EXTRA_LBITS[code]; + if (extra != 0) { + lc -= Tree.BASE_LENGTH[code]; + sendBits(lc, extra); // send the extra length bits + } + dist--; // dist is now the match distance - 1 + code = Tree.dCode(dist); + + sendCode(code, dtree); // send the distance code + extra = Tree.EXTRA_DBITS[code]; + if (extra != 0) { + dist -= Tree.BASE_DIST[code]; + sendBits(dist, extra); // send the extra distance bits + } + } // literal or match pair ? + + // Check that the overlay between pending_buf and d_buf+l_buf is ok: + } + while (lx < lastLit); + } + + sendCode(END_BLOCK, ltree); + lastEobLen = ltree[END_BLOCK * 2 + 1]; + } + + // Set the data type to ASCII or BINARY, using a crude approximation: + // binary if more than 20% of the bytes are <= 6 or >= 128, ascii otherwise. + // IN assertion: the fields freq of dyn_ltree are set and the total of all + // frequencies does not exceed 64K (to fit in an int on 16 bit machines). + void setDataType() { + int n = 0; + int asciiFreq = 0; + int binFreq = 0; + while (n < 7) { + binFreq += dynLtree[n * 2]; + n++; + } + while (n < 128) { + asciiFreq += dynLtree[n * 2]; + n++; + } + while (n < LITERALS) { + binFreq += dynLtree[n * 2]; + n++; + } + dataType = (byte) (binFreq > (asciiFreq >>> 2) ? Z_BINARY : Z_ASCII); + } + + // Flush the bit buffer, keeping at most 7 bits in it. + void biFlush() { + if (biValid == 16) { + putShort(biBuf); + biBuf = 0; + biValid = 0; + } else if (biValid >= 8) { + putByte((byte) biBuf); + biBuf >>>= 8; + biValid -= 8; + } + } + + // Flush the bit buffer and align the output on a byte boundary + void biWindup() { + if (biValid > 8) { + putShort(biBuf); + } else if (biValid > 0) { + putByte((byte) biBuf); + } + biBuf = 0; + biValid = 0; + } + + // Copy a stored block, storing first the length and its + // one's complement if requested. + void copyBlock(int buf, // the input data + int len, // its length + boolean header // true if block header must be written + ) { + biWindup(); // align on byte boundary + lastEobLen = 8; // enough lookahead for inflate + + if (header) { + putShort((short) len); + putShort((short) ~len); + } + + // while(len--!=0) { + // put_byte(window[buf+index]); + // index++; + // } + putByte(window, buf, len); + } + + void flushBlockOnly(boolean eof) { + trFlushBlock(blockStart >= 0 ? blockStart : -1, + strStart - blockStart, + eof); + blockStart = strStart; + strm.flushPending(); + } + + // Copy without compression as much as possible from the input stream, return + // the current block state. + // This function does not insert new strings in the dictionary since + // uncompressible data is probably not useful. This function is used + // only for the level=0 compression option. + // NOTE: this function should be optimized to avoid extra copying from + // window to pending_buf. + int deflateStored(int flush) { + // Stored blocks are limited to 0xffff bytes, pending_buf is limited + // to pending_buf_size, and each stored block has a 5 byte header: + + int maxBlockSize = 0xffff; + int maxStart; + + if (maxBlockSize > pendingBufSize - 5) { + maxBlockSize = pendingBufSize - 5; + } + + // Copy as much as possible from input to output: + while (true) { + // Fill the window as much as possible: + if (lookahead <= 1) { + fillWindow(); + if (lookahead == 0 && flush == Z_NO_FLUSH) { + return NEED_MORE; + } + if (lookahead == 0) { + break; // flush the current block + } + } + + strStart += lookahead; + lookahead = 0; + + // Emit a stored block if pending_buf will be full: + maxStart = blockStart + maxBlockSize; + if (strStart == 0 || strStart >= maxStart) { + // strstart == 0 is possible when wraparound on 16-bit machine + lookahead = strStart - maxStart; + strStart = maxStart; + + flushBlockOnly(false); + if (strm.availOut == 0) { + return NEED_MORE; + } + + } + + // Flush if we may have to slide, otherwise block_start may become + // negative and the data will be gone: + if (strStart - blockStart >= wSize - MIN_LOOKAHEAD) { + flushBlockOnly(false); + if (strm.availOut == 0) { + return NEED_MORE; + } + } + } + + flushBlockOnly(flush == Z_FINISH); + if (strm.availOut == 0) { + return (flush == Z_FINISH) ? FINISH_STARTED : NEED_MORE; + } + + return flush == Z_FINISH ? FINISH_DONE : BLOCK_DONE; + } + + // Send a stored block + void trStoredBlock(int buf, // input block + int storedLen, // length of input block + boolean eof // true if this is the last block for a file + ) { + sendBits((eof ? 1 : 0), 3); // send block type + copyBlock(buf, storedLen, true); // with header + } + + // Determine the best encoding for the current block: dynamic trees, static + // trees or store, and output the encoded block to the zip file. + void trFlushBlock(int buf, // input block, or NULL if too old + int storedLen, // length of input block + boolean eof // true if this is the last block for a file + ) { + int optLenb; // opt_len in bytes + int staticLenb; // static_len in bytes + int maxBlIndex = 0; // index of last bit length code of non zero freq + + // Build the Huffman trees unless a stored block is forced + if (level > 0) { + // Check if the file is ascii or binary + if (dataType == Z_UNKNOWN) { + setDataType(); + } + + // Construct the literal and distance trees + lDesc.buildTree(this); + + dDesc.buildTree(this); + + // At this point, opt_len and static_len are the total bit lengths of + // the compressed block data, excluding the tree representations. + + // Build the bit length tree for the above two trees, and get the index + // in BL_ORDER of the last bit length code to send. + maxBlIndex = buildBlTree(); + + // Determine the best encoding. Compute first the block length in bytes + optLenb = (optLen + 3 + 7) >>> 3; + staticLenb = (staticLen + 3 + 7) >>> 3; + + if (staticLenb <= optLenb) { + optLenb = staticLenb; + } + } else { + optLenb = staticLenb = storedLen + 5; // force a stored block + } + + if (storedLen + 4 <= optLenb && buf != -1) { + // 4: two words for the lengths + // The test buf != NULL is only necessary if LIT_BUFSIZE > WSIZE. + // Otherwise we can't have processed more than WSIZE input bytes since + // the last block flush, because compression would have been + // successful. If LIT_BUFSIZE <= WSIZE, it is never too late to + // transform a block into a stored block. + trStoredBlock(buf, storedLen, eof); + } else if (staticLenb == optLenb) { + sendBits((STATIC_TREES << 1) + (eof ? 1 : 0), 3); + compressBlock(StaticTree.STATIC_LTREE, StaticTree.STATIC_DTREE); + } else { + sendBits((DYN_TREES << 1) + (eof ? 1 : 0), 3); + sendAllTrees(lDesc.maxCode + 1, dDesc.maxCode + 1, maxBlIndex + 1); + compressBlock(dynLtree, dynDtree); + } + + // The above check is made mod 2^32, for files larger than 512 MB + // and uLong implemented on 32 bits. + + initBlock(); + + if (eof) { + biWindup(); + } + } + + // Fill the window when the lookahead becomes insufficient. + // Updates strstart and lookahead. + // + // IN assertion: lookahead < MIN_LOOKAHEAD + // OUT assertions: strstart <= window_size-MIN_LOOKAHEAD + // At least one byte has been read, or avail_in == 0; reads are + // performed for at least two bytes (required for the zip translate_eol + // option -- not supported here). + void fillWindow() { + int n; + int m; + int p; + int more; // Amount of free space at the end of the window. + + do { + more = (windowSize - lookahead - strStart); + + // Deal with !@#$% 64K limit: + if (more == 0 && strStart == 0 && lookahead == 0) { + more = wSize; + } else if (more == -1) { + // Very unlikely, but possible on 16 bit machine if strstart == 0 + // and lookahead == 1 (input done one byte at time) + more--; + + // If the window is almost full and there is insufficient lookahead, + // move the upper half to the lower one to make room in the upper half. + } else if (strStart >= wSize + wSize - MIN_LOOKAHEAD) { + System.arraycopy(window, wSize, window, 0, wSize); + matchStart -= wSize; + strStart -= wSize; // we now have strstart >= MAX_DIST + blockStart -= wSize; + + // Slide the hash table (could be avoided with 32 bit values + // at the expense of memory usage). We slide even when level == 0 + // to keep the hash table consistent if we switch back to level > 0 + // later. (Using level 0 permanently is not an optimal usage of + // zlib, so we don't care about this pathological case.) + + n = hashSize; + p = n; + do { + m = (head[--p] & 0xffff); + head[p] = (m >= wSize ? (short) (m - wSize) : 0); + } + while (--n != 0); + + n = wSize; + p = n; + do { + m = (prev[--p] & 0xffff); + prev[p] = (m >= wSize ? (short) (m - wSize) : 0); + // If n is not on any hash chain, prev[n] is garbage but + // its value will never be used. + } + while (--n != 0); + more += wSize; + } + + if (strm.availIn == 0) { + return; + } + + // If there was no sliding: + // strstart <= WSIZE+MAX_DIST-1 && lookahead <= MIN_LOOKAHEAD - 1 && + // more == window_size - lookahead - strstart + // => more >= window_size - (MIN_LOOKAHEAD-1 + WSIZE + MAX_DIST-1) + // => more >= window_size - 2*WSIZE + 2 + // In the BIG_MEM or MMAP case (not yet supported), + // window_size == input_size + MIN_LOOKAHEAD && + // strstart + s->lookahead <= input_size => more >= MIN_LOOKAHEAD. + // Otherwise, window_size == 2*WSIZE so more >= 2. + // If there was sliding, more >= WSIZE. So in all cases, more >= 2. + + n = strm.readBuf(window, strStart + lookahead, more); + lookahead += n; + + // Initialize the hash value now that we have some input: + if (lookahead >= MIN_MATCH) { + insH = window[strStart] & 0xff; + insH = (((insH) << hashShift) ^ (window[strStart + 1] & 0xff)) & hashMask; + } + // If the whole input has less than MIN_MATCH bytes, ins_h is garbage, + // but this is not important since only literal bytes will be emitted. + } + while (lookahead < MIN_LOOKAHEAD && strm.availIn != 0); + } + + // Compress as much as possible from the input stream, return the current + // block state. + // This function does not perform lazy evaluation of matches and inserts + // new strings in the dictionary only for unmatched strings or for short + // matches. It is used only for the fast compression options. + int deflateFast(int flush) { +// short hashHead = 0; // head of the hash chain + int hashHead = 0; // head of the hash chain + boolean bflush; // set if current block must be flushed + + while (true) { + // Make sure that we always have enough lookahead, except + // at the end of the input file. We need MAX_MATCH bytes + // for the next match, plus MIN_MATCH bytes to insert the + // string following the next match. + if (lookahead < MIN_LOOKAHEAD) { + fillWindow(); + if (lookahead < MIN_LOOKAHEAD && flush == Z_NO_FLUSH) { + return NEED_MORE; + } + if (lookahead == 0) { + break; // flush the current block + } + } + + // Insert the string window[strstart .. strstart+2] in the + // dictionary, and set hashHead to the head of the hash chain: + if (lookahead >= MIN_MATCH) { + insH = (((insH) << hashShift) ^ (window[(strStart) + (MIN_MATCH - 1)] & 0xff)) & hashMask; + +// prev[strstart&w_mask]=hashHead=head[ins_h]; + hashHead = (head[insH] & 0xffff); + prev[strStart & wMask] = head[insH]; + head[insH] = (short) strStart; + } + + // Find the longest match, discarding those <= prev_length. + // At this point we have always match_length < MIN_MATCH + + if (hashHead != 0L && + ((strStart - hashHead) & 0xffff) <= wSize - MIN_LOOKAHEAD + ) { + // To simplify the code, we prevent matches with the string + // of window index 0 (in particular we have to avoid a match + // of the string with itself at the start of the input file). + if (strategy != Z_HUFFMAN_ONLY) { + matchLength = longestMatch(hashHead); + } + // longest_match() sets match_start + } + if (matchLength >= MIN_MATCH) { + // check_match(strstart, match_start, match_length); + + bflush = trTally(strStart - matchStart, matchLength - MIN_MATCH); + + lookahead -= matchLength; + + // Insert new strings in the hash table only if the match length + // is not too large. This saves time but degrades compression. + if (matchLength <= maxLazyMatch && + lookahead >= MIN_MATCH) { + matchLength--; // string at strstart already in hash table + do { + strStart++; + + insH = ((insH << hashShift) ^ (window[(strStart) + (MIN_MATCH - 1)] & 0xff)) & hashMask; +// prev[strstart&w_mask]=hashHead=head[ins_h]; + hashHead = (head[insH] & 0xffff); + prev[strStart & wMask] = head[insH]; + head[insH] = (short) strStart; + + // strstart never exceeds WSIZE-MAX_MATCH, so there are + // always MIN_MATCH bytes ahead. + } + while (--matchLength != 0); + strStart++; + } else { + strStart += matchLength; + matchLength = 0; + insH = window[strStart] & 0xff; + + insH = (((insH) << hashShift) ^ (window[strStart + 1] & 0xff)) & hashMask; + // If lookahead < MIN_MATCH, ins_h is garbage, but it does not + // matter since it will be recomputed at next deflate call. + } + } else { + // No match, output a literal byte + + bflush = trTally(0, window[strStart] & 0xff); + lookahead--; + strStart++; + } + if (bflush) { + + flushBlockOnly(false); + if (strm.availOut == 0) { + return NEED_MORE; + } + } + } + + flushBlockOnly(flush == Z_FINISH); + if (strm.availOut == 0) { + if (flush == Z_FINISH) { + return FINISH_STARTED; + } else { + return NEED_MORE; + } + } + return flush == Z_FINISH ? FINISH_DONE : BLOCK_DONE; + } + + // Same as above, but achieves better compression. We use a lazy + // evaluation for matches: a match is finally adopted only if there is + // no better match at the next window position. + int deflateSlow(int flush) { +// short hashHead = 0; // head of hash chain + int hashHead = 0; // head of hash chain + boolean bflush; // set if current block must be flushed + + // Process the input block. + while (true) { + // Make sure that we always have enough lookahead, except + // at the end of the input file. We need MAX_MATCH bytes + // for the next match, plus MIN_MATCH bytes to insert the + // string following the next match. + + if (lookahead < MIN_LOOKAHEAD) { + fillWindow(); + if (lookahead < MIN_LOOKAHEAD && flush == Z_NO_FLUSH) { + return NEED_MORE; + } + if (lookahead == 0) { + break; // flush the current block + } + } + + // Insert the string window[strstart .. strstart+2] in the + // dictionary, and set hashHead to the head of the hash chain: + + if (lookahead >= MIN_MATCH) { + insH = (((insH) << hashShift) ^ (window[(strStart) + (MIN_MATCH - 1)] & 0xff)) & hashMask; +// prev[strstart&w_mask]=hashHead=head[ins_h]; + hashHead = (head[insH] & 0xffff); + prev[strStart & wMask] = head[insH]; + head[insH] = (short) strStart; + } + + // Find the longest match, discarding those <= prev_length. + prevLength = matchLength; + prevMatch = matchStart; + matchLength = MIN_MATCH - 1; + + if (hashHead != 0 && prevLength < maxLazyMatch && + ((strStart - hashHead) & 0xffff) <= wSize - MIN_LOOKAHEAD + ) { + // To simplify the code, we prevent matches with the string + // of window index 0 (in particular we have to avoid a match + // of the string with itself at the start of the input file). + + if (strategy != Z_HUFFMAN_ONLY) { + matchLength = longestMatch(hashHead); + } + // longest_match() sets match_start + + if (matchLength <= 5 && (strategy == Z_FILTERED || + (matchLength == MIN_MATCH && + strStart - matchStart > 4096))) { + + // If prev_match is also MIN_MATCH, match_start is garbage + // but we will ignore the current match anyway. + matchLength = MIN_MATCH - 1; + } + } + + // If there was a match at the previous step and the current + // match is not better, output the previous match: + if (prevLength >= MIN_MATCH && matchLength <= prevLength) { + int maxInsert = strStart + lookahead - MIN_MATCH; + // Do not insert strings in hash table beyond this. + + // check_match(strstart-1, prev_match, prev_length); + + bflush = trTally(strStart - 1 - prevMatch, prevLength - MIN_MATCH); + + // Insert in hash table all strings up to the end of the match. + // strstart-1 and strstart are already inserted. If there is not + // enough lookahead, the last two strings are not inserted in + // the hash table. + lookahead -= prevLength - 1; + prevLength -= 2; + do { + if (++strStart <= maxInsert) { //NOPMD AssignmentInOperand + insH = (((insH) << hashShift) ^ (window[(strStart) + (MIN_MATCH - 1)] & 0xff)) & hashMask; + //prev[strstart&w_mask]=hashHead=head[ins_h]; + hashHead = (head[insH] & 0xffff); + prev[strStart & wMask] = head[insH]; + head[insH] = (short) strStart; + } + } + while (--prevLength != 0); + matchAvailable = 0; + matchLength = MIN_MATCH - 1; + strStart++; + + if (bflush) { + flushBlockOnly(false); + if (strm.availOut == 0) { + return NEED_MORE; + } + } + } else if (matchAvailable != 0) { + + // If there was no match at the previous position, output a + // single literal. If there was a match but the current match + // is longer, truncate the previous match to a single literal. + + bflush = trTally(0, window[strStart - 1] & 0xff); + + if (bflush) { + flushBlockOnly(false); + } + strStart++; + lookahead--; + if (strm.availOut == 0) { + return NEED_MORE; + } + } else { + // There is no previous match to compare with, wait for + // the next step to decide. + + matchAvailable = 1; + strStart++; + lookahead--; + } + } + + if (matchAvailable != 0) { + trTally(0, window[strStart - 1] & 0xff); + matchAvailable = 0; + } + flushBlockOnly(flush == Z_FINISH); + + if (strm.availOut == 0) { + if (flush == Z_FINISH) { + return FINISH_STARTED; + } else { + return NEED_MORE; + } + } + + return flush == Z_FINISH ? FINISH_DONE : BLOCK_DONE; + } + + int longestMatch(int curMatch) { + int chainLength = maxChainLength; // max hash chain length + int scan = strStart; // current string + int match; // matched string + int len; // length of current match + int bestLen = prevLength; // best match length so far + int limit = strStart > (wSize - MIN_LOOKAHEAD) ? + strStart - (wSize - MIN_LOOKAHEAD) : 0; + int niceMatch = this.niceMatch; + + // Stop when curMatch becomes <= limit. To simplify the code, + // we prevent matches with the string of window index 0. + + int wmask = wMask; + + int strend = strStart + MAX_MATCH; + byte scanEnd1 = window[scan + bestLen - 1]; + byte scanEnd = window[scan + bestLen]; + + // The code is optimized for HASH_BITS >= 8 and MAX_MATCH-2 multiple of 16. + // It is easy to get rid of this optimization if necessary. + + // Do not waste too much time if we already have a good match: + if (prevLength >= goodMatch) { + chainLength >>= 2; + } + + // Do not look for matches beyond the end of the input. This is necessary + // to make deflate deterministic. + if (niceMatch > lookahead) { + niceMatch = lookahead; + } + + do { + match = curMatch; + + // Skip to next match if the match length cannot increase + // or if the match length is less than 2: + if (window[match + bestLen] != scanEnd || //NOPMD AssignmentInOperand + window[match + bestLen - 1] != scanEnd1 || + window[match] != window[scan] || + window[++match] != window[scan + 1]) { + continue; + } + + // The check at bestLen-1 can be removed because it will be made + // again later. (This heuristic is not always a win.) + // It is not necessary to compare scan[2] and match[2] since they + // are always equal when the other bytes match, given that + // the hash keys are equal and that HASH_BITS >= 8. + scan += 2; + match++; + + // We check for insufficient lookahead only every 8th comparison; + // the 256th check will be made at strstart+258. + do { // NOPMD EmptyControlStatement - loop body intentionally empty; comparisons advance scan/match. + } while (window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + scan < strend); + + len = MAX_MATCH - (strend - scan); + scan = strend - MAX_MATCH; + + if (len > bestLen) { + matchStart = curMatch; + bestLen = len; + if (len >= niceMatch) { + break; + } + scanEnd1 = window[scan + bestLen - 1]; + scanEnd = window[scan + bestLen]; + } + + } while ((curMatch = (prev[curMatch & wmask] & 0xffff)) > limit + && --chainLength != 0); + + if (bestLen <= lookahead) { + return bestLen; + } + return lookahead; + } + + int deflateInit(int level, int bits, int memlevel) { + return deflateInit(level, Z_DEFLATED, bits, memlevel, + Z_DEFAULT_STRATEGY); + } + + int deflateInit(int level, int bits) { + return deflateInit(level, Z_DEFLATED, bits, DEF_MEM_LEVEL, + Z_DEFAULT_STRATEGY); + } + + int deflateInit(int level) { + return deflateInit(level, MAX_WBITS); + } + + private int deflateInit(int level, int method, int windowBits, + int memLevel, int strategy) { + int wrap = 1; + // byte[] my_version=ZLIB_VERSION; + + // + // if (version == null || version[0] != my_version[0] + // || stream_size != sizeof(z_stream)) { + // return Z_VERSION_ERROR; + // } + + strm.msg = null; + + if (level == Z_DEFAULT_COMPRESSION) { + level = 6; + } + + if (windowBits < 0) { // undocumented feature: suppress zlib header + wrap = 0; + windowBits = -windowBits; + } else if (windowBits > 15) { + wrap = 2; + windowBits -= 16; + strm.adler = new CRC32(); + } + + if (memLevel < 1 || memLevel > MAX_MEM_LEVEL || + method != Z_DEFLATED || + windowBits < 9 || windowBits > 15 || level < 0 || level > 9 || + strategy < 0 || strategy > Z_HUFFMAN_ONLY) { + return Z_STREAM_ERROR; + } + + strm.dstate = this; + + this.wrap = wrap; + wBits = windowBits; + wSize = 1 << wBits; + wMask = wSize - 1; + + hashBits = memLevel + 7; + hashSize = 1 << hashBits; + hashMask = hashSize - 1; + hashShift = ((hashBits + MIN_MATCH - 1) / MIN_MATCH); + + window = new byte[wSize * 2]; + prev = new short[wSize]; + head = new short[hashSize]; + + litBufSize = 1 << (memLevel + 6); // 16K elements by default + + // We overlay pending_buf and d_buf+l_buf. This works since the average + // output size for (length,distance) codes is <= 24 bits. + pendingBuf = new byte[litBufSize * 4]; + pendingBufSize = litBufSize * 4; + + dBuf = litBufSize / 2; + lBuf = (1 + 2) * litBufSize; + + this.level = level; + + this.strategy = strategy; + return deflateReset(); + } + + int deflateReset() { + strm.totalIn = strm.totalOut = 0; + strm.msg = null; // + strm.dataType = Z_UNKNOWN; + + pending = 0; + pendingOut = 0; + + if (wrap < 0) { + wrap = -wrap; + } + status = (wrap == 0) ? BUSY_STATE : INIT_STATE; + strm.adler.reset(); + + lastFlush = Z_NO_FLUSH; + + trInit(); + lmInit(); + return Z_OK; + } + + int deflateEnd() { + if (status != INIT_STATE && status != BUSY_STATE && status != FINISH_STATE) { + return Z_STREAM_ERROR; + } + // Deallocate in reverse order of allocations: + pendingBuf = null; + head = null; + prev = null; + window = null; + // free + // dstate=null; + return status == BUSY_STATE ? Z_DATA_ERROR : Z_OK; + } + + int deflateParams(int levelParam, int strategyParam) { + int err = Z_OK; + + if (levelParam == Z_DEFAULT_COMPRESSION) { + levelParam = 6; + } + if (levelParam < 0 || levelParam > 9 || + strategyParam < 0 || strategyParam > Z_HUFFMAN_ONLY) { + return Z_STREAM_ERROR; + } + + if (CONFIG_TABLE[level].func != CONFIG_TABLE[levelParam].func && + strm.totalIn != 0) { + // Flush the last buffer: + err = strm.deflate(Z_PARTIAL_FLUSH); + } + + if (level != levelParam) { + level = levelParam; + maxLazyMatch = CONFIG_TABLE[level].maxLazy; + goodMatch = CONFIG_TABLE[level].goodLength; + niceMatch = CONFIG_TABLE[level].niceLength; + maxChainLength = CONFIG_TABLE[level].maxChain; + } + strategy = strategyParam; + return err; + } + + int deflateSetDictionary(byte[] dictionary, int dictLength) { + int length = dictLength; + int index = 0; + + // Raw DEFLATE begins in BUSY_STATE because it has no zlib header. + // A preset dictionary is still valid before any input is compressed. + if (dictionary == null || (status != INIT_STATE && + !(wrap == 0 && status == BUSY_STATE && strm.totalIn == 0))) { + return Z_STREAM_ERROR; + } + + if (wrap != 0) { + strm.adler.update(dictionary, 0, dictLength); + } + + if (length < MIN_MATCH) { + return Z_OK; + } + if (length > wSize - MIN_LOOKAHEAD) { + length = wSize - MIN_LOOKAHEAD; + index = dictLength - length; // use the tail of the dictionary + } + System.arraycopy(dictionary, index, window, 0, length); + strStart = length; + blockStart = length; + + // Insert all strings in the hash table (except for the last two bytes). + // s->lookahead stays null, so s->ins_h will be recomputed at the next + // call of fill_window. + + insH = window[0] & 0xff; + insH = (((insH) << hashShift) ^ (window[1] & 0xff)) & hashMask; + + for (int n = 0; n <= length - MIN_MATCH; n++) { + insH = (((insH) << hashShift) ^ (window[(n) + (MIN_MATCH - 1)] & 0xff)) & hashMask; + prev[n & wMask] = head[insH]; + head[insH] = (short) n; + } + return Z_OK; + } + + int deflate(int flush) { + int oldFlush; + + if (flush > Z_FINISH || flush < 0) { + return Z_STREAM_ERROR; + } + + if (strm.nextOut == null || + (strm.nextIn == null && strm.availIn != 0) || + (status == FINISH_STATE && flush != Z_FINISH)) { + strm.msg = Z_ERRMSG[Z_NEED_DICT - (Z_STREAM_ERROR)]; + return Z_STREAM_ERROR; + } + if (strm.availOut == 0) { + strm.msg = Z_ERRMSG[Z_NEED_DICT - (Z_BUF_ERROR)]; + return Z_BUF_ERROR; + } + + oldFlush = lastFlush; + lastFlush = flush; + + // Write the zlib header + if (status == INIT_STATE) { + if (wrap == 2) { + getGZIPHeader().put(this); + status = BUSY_STATE; + strm.adler.reset(); + } else { + int header = (Z_DEFLATED + ((wBits - 8) << 4)) << 8; + int levelFlags = ((level - 1) & 0xff) >> 1; + + if (levelFlags > 3) { + levelFlags = 3; + } + header |= (levelFlags << 6); + if (strStart != 0) { + header |= PRESET_DICT; + } + header += 31 - (header % 31); + + status = BUSY_STATE; + putShortMSB(header); + + + // Save the adler32 of the preset dictionary: + if (strStart != 0) { + long adler = strm.adler.getValue(); + putShortMSB((int) (adler >>> 16)); + putShortMSB((int) (adler & 0xffff)); + } + strm.adler.reset(); + } + } + + // Flush as much pending output as possible + if (pending != 0) { + strm.flushPending(); + if (strm.availOut == 0) { + // Since avail_out is 0, deflate will be called again with + // more output space, but possibly with both pending and + // avail_in equal to zero. There won't be anything to do, + // but this is not an error situation so make sure we + // return OK instead of BUF_ERROR at next call of deflate: + lastFlush = -1; + return Z_OK; + } + + // Make sure there is something to do and avoid duplicate consecutive + // flushes. For repeated and useless calls with Z_FINISH, we keep + // returning Z_STREAM_END instead of Z_BUFF_ERROR. + } else if (strm.availIn == 0 && flush <= oldFlush && + flush != Z_FINISH) { + strm.msg = Z_ERRMSG[Z_NEED_DICT - (Z_BUF_ERROR)]; + return Z_BUF_ERROR; + } + + // User must not provide more input after the first FINISH: + if (status == FINISH_STATE && strm.availIn != 0) { + strm.msg = Z_ERRMSG[Z_NEED_DICT - (Z_BUF_ERROR)]; + return Z_BUF_ERROR; + } + + // Start a new block or continue the current one. + if (strm.availIn != 0 || lookahead != 0 || + (flush != Z_NO_FLUSH && status != FINISH_STATE)) { + int bstate = -1; + switch (CONFIG_TABLE[level].func) { + case STORED: + bstate = deflateStored(flush); + break; + case FAST: + bstate = deflateFast(flush); + break; + case SLOW: + bstate = deflateSlow(flush); + break; + default: + } + + if (bstate == FINISH_STARTED || bstate == FINISH_DONE) { + status = FINISH_STATE; + } + if (bstate == NEED_MORE || bstate == FINISH_STARTED) { + if (strm.availOut == 0) { + lastFlush = -1; // avoid BUF_ERROR next call, see above + } + return Z_OK; + // If flush != Z_NO_FLUSH && avail_out == 0, the next call + // of deflate should use the same flush parameter to make sure + // that the flush is complete. So we don't have to output an + // empty block here, this will be done at next call. This also + // ensures that for a very small output buffer, we emit at most + // one empty block. + } + + if (bstate == BLOCK_DONE) { + if (flush == Z_PARTIAL_FLUSH) { + trAlign(); + } else { // FULL_FLUSH or SYNC_FLUSH + trStoredBlock(0, 0, false); + // For a full flush, this empty block will be recognized + // as a special marker by inflate_sync(). + if (flush == Z_FULL_FLUSH) { + //state.head[s.hash_size-1]=0; + // forget history + for (int i = 0; i < hashSize/*-1*/; i++) { + head[i] = 0; + } + } + } + strm.flushPending(); + if (strm.availOut == 0) { + lastFlush = -1; // avoid BUF_ERROR at next call, see above + return Z_OK; + } + } + } + + if (flush != Z_FINISH) { + return Z_OK; + } + if (wrap <= 0) { + return Z_STREAM_END; + } + + if (wrap == 2) { + long adler = strm.adler.getValue(); + putByte((byte) (adler & 0xff)); + putByte((byte) ((adler >> 8) & 0xff)); + putByte((byte) ((adler >> 16) & 0xff)); + putByte((byte) ((adler >> 24) & 0xff)); + putByte((byte) (strm.totalIn & 0xff)); + putByte((byte) ((strm.totalIn >> 8) & 0xff)); + putByte((byte) ((strm.totalIn >> 16) & 0xff)); + putByte((byte) ((strm.totalIn >> 24) & 0xff)); + + getGZIPHeader().setCRC(adler); + } else { + // Write the zlib trailer (adler32) + long adler = strm.adler.getValue(); + putShortMSB((int) (adler >>> 16)); + putShortMSB((int) (adler & 0xffff)); + } + + strm.flushPending(); + + // If avail_out is zero, the application will call deflate again + // to flush the rest. + + if (wrap > 0) { + wrap = -wrap; // write the trailer only once! + } + return pending != 0 ? Z_OK : Z_STREAM_END; + } + + @Override + public Object clone() { + Deflate dest = new Deflate(strm); + + dest.pendingBuf = dup(dest.pendingBuf); + dest.window = dup(dest.window); + + dest.prev = dup(dest.prev); + dest.head = dup(dest.head); + dest.dynLtree = dup(dest.dynLtree); + dest.dynDtree = dup(dest.dynDtree); + dest.blTree = dup(dest.blTree); + + dest.blCount = dup(dest.blCount); + dest.nextCode = dup(dest.nextCode); + dest.heap = dup(dest.heap); + dest.depth = dup(dest.depth); + + dest.lDesc.dynTree = dest.dynLtree; + dest.dDesc.dynTree = dest.dynDtree; + dest.blDesc.dynTree = dest.blTree; + + if (dest.gheader != null) { + dest.gheader = (GZIPHeader) dest.gheader.clone(); + } + + return dest; + } + + private byte[] dup(byte[] buf) { + byte[] foo = new byte[buf.length]; + System.arraycopy(buf, 0, foo, 0, foo.length); + return foo; + } + + private short[] dup(short[] buf) { + short[] foo = new short[buf.length]; + System.arraycopy(buf, 0, foo, 0, foo.length); + return foo; + } + + private int[] dup(int[] buf) { + int[] foo = new int[buf.length]; + System.arraycopy(buf, 0, foo, 0, foo.length); + return foo; + } + + synchronized GZIPHeader getGZIPHeader() { + if (gheader == null) { + gheader = new GZIPHeader(); + } + return gheader; + } + + static class Config { + int goodLength; // reduce lazy search above this match length + int maxLazy; // do not perform lazy search above this match length + int niceLength; // quit search above this match length + int maxChain; + int func; + + Config(int goodLength, int maxLazy, + int niceLength, int maxChain, int func) { + this.goodLength = goodLength; + this.maxLazy = maxLazy; + this.niceLength = niceLength; + this.maxChain = maxChain; + this.func = func; + } + } +} diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/Deflater.java b/vm/JavaAPI/src/com/codename1/impl/zip/Deflater.java new file mode 100644 index 00000000000..a9f99b676a8 --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/Deflater.java @@ -0,0 +1,172 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. +/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */ +/* +Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +final public class Deflater extends ZStream { + + static final private int MAX_WBITS = 15; // 32K LZ77 window + static final private int Z_OK = 0; + static final private int Z_STREAM_END = 1; + static final private int Z_STREAM_ERROR = -2; + + private boolean finished = false; + + public Deflater() { + super(); + } + + public Deflater(int level) throws GZIPException { + this(level, MAX_WBITS); + } + + public Deflater(int level, boolean nowrap) throws GZIPException { + this(level, MAX_WBITS, nowrap); + } + + public Deflater(int level, int bits) throws GZIPException { + this(level, bits, false); + } + + public Deflater(int level, int bits, boolean nowrap) throws GZIPException { + super(); + int ret = init(level, bits, nowrap); + if (ret != Z_OK) { + throw new GZIPException(ret + ": " + msg); + } + } + + public Deflater(int level, int bits, int memlevel, JZlib.WrapperType wrapperType) throws GZIPException { + super(); + int ret = init(level, bits, memlevel, wrapperType); + if (ret != Z_OK) { + throw new GZIPException(ret + ": " + msg); + } + } + + public Deflater(int level, int bits, int memlevel) throws GZIPException { + super(); + int ret = init(level, bits, memlevel); + if (ret != Z_OK) { + throw new GZIPException(ret + ": " + msg); + } + } + + public int init(int level) { + return init(level, MAX_WBITS); + } + + public int init(int level, boolean nowrap) { + return init(level, MAX_WBITS, nowrap); + } + + public int init(int level, int bits) { + return init(level, bits, false); + } + + public int init(int level, int bits, int memlevel, JZlib.WrapperType wrapperType) { + if (bits < 9 || bits > 15) { + return Z_STREAM_ERROR; + } + if (wrapperType == JZlib.W_NONE) { //NOPMD CompareObjectsWithEquals + bits *= -1; + } else if (wrapperType == JZlib.W_GZIP) { //NOPMD CompareObjectsWithEquals + bits += 16; + } else if (wrapperType == JZlib.W_ANY) { //NOPMD CompareObjectsWithEquals + return Z_STREAM_ERROR; + } + return init(level, bits, memlevel); + } + + public int init(int level, int bits, int memlevel) { + finished = false; + dstate = new Deflate(this); + return dstate.deflateInit(level, bits, memlevel); + } + + public int init(int level, int bits, boolean nowrap) { + finished = false; + dstate = new Deflate(this); + return dstate.deflateInit(level, nowrap ? -bits : bits); + } + + @Override + public int deflate(int flush) { + if (dstate == null) { + return Z_STREAM_ERROR; + } + int ret = dstate.deflate(flush); + if (ret == Z_STREAM_END) { + finished = true; + } + return ret; + } + + @Override + public int end() { + finished = true; + if (dstate == null) { + return Z_STREAM_ERROR; + } + int ret = dstate.deflateEnd(); + dstate = null; + free(); + return ret; + } + + public int params(int level, int strategy) { + if (dstate == null) { + return Z_STREAM_ERROR; + } + return dstate.deflateParams(level, strategy); + } + + public int setDictionary(byte[] dictionary, int dictLength) { + if (dstate == null) { + return Z_STREAM_ERROR; + } + return dstate.deflateSetDictionary(dictionary, dictLength); + } + + @Override + public boolean finished() { + return finished; + } + + public int copy(Deflater src) { + this.finished = src.finished; + return Deflate.deflateCopy(this, src); + } +} diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/GZIPException.java b/vm/JavaAPI/src/com/codename1/impl/zip/GZIPException.java new file mode 100644 index 00000000000..987e373ae13 --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/GZIPException.java @@ -0,0 +1,46 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +public class GZIPException extends java.io.IOException { + public GZIPException() { + super(); + } + + public GZIPException(String s) { + super(s); + } +} diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/GZIPHeader.java b/vm/JavaAPI/src/com/codename1/impl/zip/GZIPHeader.java new file mode 100644 index 00000000000..0854366748a --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/GZIPHeader.java @@ -0,0 +1,211 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +import java.io.UnsupportedEncodingException; + +/// #### See also +/// +/// - "http://www.ietf.org/rfc/rfc1952.txt" +public class GZIPHeader { + + public static final byte OS_MSDOS = (byte) 0x00; + public static final byte OS_AMIGA = (byte) 0x01; + public static final byte OS_VMS = (byte) 0x02; + public static final byte OS_UNIX = (byte) 0x03; + public static final byte OS_ATARI = (byte) 0x05; + public static final byte OS_OS2 = (byte) 0x06; + public static final byte OS_MACOS = (byte) 0x07; + public static final byte OS_TOPS20 = (byte) 0x0a; + public static final byte OS_WIN32 = (byte) 0x0b; + public static final byte OS_VMCMS = (byte) 0x04; + public static final byte OS_ZSYSTEM = (byte) 0x08; + public static final byte OS_CPM = (byte) 0x09; + public static final byte OS_QDOS = (byte) 0x0c; + public static final byte OS_RISCOS = (byte) 0x0d; + public static final byte OS_UNKNOWN = (byte) 0xff; + + boolean text = false; + int os = 255; + byte[] extra; + byte[] name; + byte[] comment; + long crc; + long mtime = 0; + private boolean fhcrc = false; + + public long getModifiedTime() { + return mtime; + } + + public void setModifiedTime(long mtime) { + this.mtime = mtime; + } + + public int getOS() { + return os; + } + + public void setOS(int os) { + if ((0 <= os && os <= 13) || os == 255) { + this.os = os; + } else { + throw new IllegalArgumentException("os: " + os); + } + } + + public String getName() { + if (name == null) { + return ""; + } + try { + return new String(name, "ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e.toString(), e); + } + } + + public void setName(String name) { + try { + this.name = name.getBytes("ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("name must be in ISO-8859-1 " + name, e); + } + } + + public String getComment() { + if (comment == null) { + return ""; + } + try { + return new String(comment, "ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e.toString(), e); + } + } + + public void setComment(String comment) { + try { + this.comment = comment.getBytes("ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("comment must be in ISO-8859-1 " + comment, e); + } + } + + public long getCRC() { + return crc; + } + + public void setCRC(long crc) { + this.crc = crc; + } + + void put(Deflate d) { + int flag = 0; + if (text) { + flag |= 1; // FTEXT + } + if (fhcrc) { + flag |= 2; // FHCRC + } + if (extra != null) { + flag |= 4; // FEXTRA + } + if (name != null) { + flag |= 8; // FNAME + } + if (comment != null) { + flag |= 16; // FCOMMENT + } + int xfl = 0; + if (d.level == JZlib.Z_BEST_SPEED) { + xfl |= 4; + } else if (d.level == JZlib.Z_BEST_COMPRESSION) { + xfl |= 2; + } + + d.putShort((short) 0x8b1f); // ID1 ID2 + d.putByte((byte) 8); // CM(Compression Method) + d.putByte((byte) flag); + d.putByte((byte) mtime); + d.putByte((byte) (mtime >> 8)); + d.putByte((byte) (mtime >> 16)); + d.putByte((byte) (mtime >> 24)); + d.putByte((byte) xfl); + d.putByte((byte) os); + + if (extra != null) { + d.putByte((byte) extra.length); + d.putByte((byte) (extra.length >> 8)); + d.putByte(extra, 0, extra.length); + } + + if (name != null) { + d.putByte(name, 0, name.length); + d.putByte((byte) 0); + } + + if (comment != null) { + d.putByte(comment, 0, comment.length); + d.putByte((byte) 0); + } + } + + @Override + public Object clone() { + GZIPHeader gheader = new GZIPHeader(); + byte[] tmp; + if (gheader.extra != null) { + tmp = new byte[gheader.extra.length]; + System.arraycopy(gheader.extra, 0, tmp, 0, tmp.length); + gheader.extra = tmp; + } + + if (gheader.name != null) { + tmp = new byte[gheader.name.length]; + System.arraycopy(gheader.name, 0, tmp, 0, tmp.length); + gheader.name = tmp; + } + + if (gheader.comment != null) { + tmp = new byte[gheader.comment.length]; + System.arraycopy(gheader.comment, 0, tmp, 0, tmp.length); + gheader.comment = tmp; + } + + return gheader; + } +} diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/InfBlocks.java b/vm/JavaAPI/src/com/codename1/impl/zip/InfBlocks.java new file mode 100644 index 00000000000..25d9ed0e4dd --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/InfBlocks.java @@ -0,0 +1,682 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +final class InfBlocks { + // Table for deflate from PKZIP's appnote.txt. + static final int[] border = { // Order of the bit length code lengths + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; + static final private int MANY = 1440; + // And'ing with mask[n] masks the lower n bits + static final private int[] inflate_mask = {0x00000000, 0x00000001, 0x00000003, 0x00000007, 0x0000000f, 0x0000001f, 0x0000003f, 0x0000007f, 0x000000ff, 0x000001ff, 0x000003ff, 0x000007ff, 0x00000fff, 0x00001fff, 0x00003fff, 0x00007fff, 0x0000ffff}; + static final private int Z_OK = 0; + static final private int Z_STREAM_END = 1; + static final private int Z_STREAM_ERROR = -2; + static final private int Z_DATA_ERROR = -3; + static final private int Z_BUF_ERROR = -5; + + static final private int TYPE = 0; // get type bits (3, including end bit) + static final private int LENS = 1; // get lengths for stored + static final private int STORED = 2; // processing stored block + static final private int TABLE = 3; // get table lengths + static final private int BTREE = 4; // get bit lengths tree for a dynamic block + static final private int DTREE = 5; // get length, distance trees for a dynamic block + static final private int CODES = 6; // processing fixed or dynamic block + static final private int DRY = 7; // output remaining window bytes + static final private int DONE = 8; // finished last block, done + static final private int BAD = 9; // ot a data error--stuck here + private final InfCodes codes; // if CODES, current state + private final InfTree inftree = new InfTree(); + private final ZStream z; + private final boolean check; + int mode; // current inflate_block mode + int left; // if STORED, bytes left to copy + int table; // table lengths (14 bits) + int index; // index into blens (or border) + int[] blens; // bit lengths of codes + int[] bb = new int[1]; // bit length tree depth + int[] tb = new int[1]; // bit length decoding tree + int[] bl = new int[1]; + int[] bd = new int[1]; + int[][] tl = new int[1][]; + int[][] td = new int[1][]; + int[] tli = new int[1]; // tl_index + int[] tdi = new int[1]; // td_index + int last; // true if this block is the last block + // mode independent information + int bitk; // bits in bit buffer + int bitb; // bit buffer + int[] hufts; // single malloc for tree space + byte[] window; // sliding window + int end; // one byte after sliding window + int read; // window read pointer + int write; // window write pointer + + InfBlocks(ZStream z, int w) { + this.z = z; + this.codes = new InfCodes(this.z, this); + hufts = new int[MANY * 3]; + window = new byte[w]; + end = w; + this.check = z.istate.wrap != 0; + mode = TYPE; + reset(); + } + + void reset() { + if (mode == CODES) { + codes.free(z); + } + mode = TYPE; + bitk = 0; + bitb = 0; + read = write = 0; + if (check) { + z.adler.reset(); + } + } + + @SuppressWarnings("PMD.SwitchStmtsShouldHaveDefault") + int proc(int r) { + int t; // temporary storage + int b; // bit buffer + int k; // bits in bit buffer + int p; // input data pointer + int n; // bytes available there + int q; // output window write pointer + int m; // bytes to end of window or read pointer + + // copy input/output information to locals (UPDATE macro restores) + { + p = z.nextInIndex; + n = z.availIn; + b = bitb; + k = bitk; + } + { + q = write; + m = q < read ? read - q - 1 : end - q; + } + + // process input based on current state + while (true) { + switch (mode) { + case TYPE: + + while (k < (3)) { + if (n != 0) { + r = Z_OK; + } else { + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + } + n--; + b |= (z.nextIn[p++] & 0xff) << k; + k += 8; + } + t = b & 7; + last = t & 1; + + switch (t >>> 1) { + case 0: // stored + { + b >>>= (3); + k -= (3); + } + t = k & 7; // go to byte boundary + + { + b >>>= (t); + k -= (t); + } + mode = LENS; // get length of stored block + break; + case 1: // fixed + InfTree.inflateTreesFixed(bl, bd, tl, td, z); + codes.init(bl[0], bd[0], tl[0], 0, td[0], 0); + + { + b >>>= (3); + k -= (3); + } + + mode = CODES; + break; + case 2: // dynamic + + { + b >>>= (3); + k -= (3); + } + + mode = TABLE; + break; + case 3: // illegal + + { + b >>>= (3); + k -= (3); + } + mode = BAD; + z.msg = "invalid block type"; + r = Z_DATA_ERROR; + + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + } + break; + case LENS: + + while (k < (32)) { + if (n != 0) { + r = Z_OK; + } else { + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + } + n--; + b |= (z.nextIn[p++] & 0xff) << k; + k += 8; + } + + if ((((~b) >>> 16) & 0xffff) != (b & 0xffff)) { + mode = BAD; + z.msg = "invalid stored block lengths"; + r = Z_DATA_ERROR; + + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + } + left = (b & 0xffff); + b = k = 0; // dump bits + mode = left != 0 ? STORED : (last != 0 ? DRY : TYPE); + break; + case STORED: + if (n == 0) { + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + } + + if (m == 0) { + if (q == end && read != 0) { + q = 0; + m = q < read ? read - q - 1 : end - q; + } + if (m == 0) { + write = q; + r = inflateFlush(r); + q = write; + m = q < read ? read - q - 1 : end - q; + if (q == end && read != 0) { + q = 0; + m = q < read ? read - q - 1 : end - q; + } + if (m == 0) { + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + } + } + } + r = Z_OK; + + t = left; + if (t > n) { + t = n; + } + if (t > m) { + t = m; + } + System.arraycopy(z.nextIn, p, window, q, t); + p += t; + n -= t; + q += t; + m -= t; + if ((left -= t) != 0) { //NOPMD AssignmentInOperand + break; + } + mode = last != 0 ? DRY : TYPE; + break; + case TABLE: + + while (k < (14)) { + if (n != 0) { + r = Z_OK; + } else { + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + } + n--; + b |= (z.nextIn[p++] & 0xff) << k; + k += 8; + } + + table = t = (b & 0x3fff); + if ((t & 0x1f) > 29 || ((t >> 5) & 0x1f) > 29) { + mode = BAD; + z.msg = "too many length or distance symbols"; + r = Z_DATA_ERROR; + + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + } + t = 258 + (t & 0x1f) + ((t >> 5) & 0x1f); + if (blens == null || blens.length < t) { + blens = new int[t]; + } else { + for (int i = 0; i < t; i++) { + blens[i] = 0; + } + } + + { + b >>>= (14); + k -= (14); + } + + index = 0; + mode = BTREE; + case BTREE: + while (index < 4 + (table >>> 10)) { + while (k < (3)) { + if (n != 0) { + r = Z_OK; + } else { + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + } + n--; + b |= (z.nextIn[p++] & 0xff) << k; + k += 8; + } + + blens[border[index++]] = b & 7; + + { + b >>>= (3); + k -= (3); + } + } + + while (index < 19) { + blens[border[index++]] = 0; + } + + bb[0] = 7; + t = inftree.inflateTreesBits(blens, bb, tb, hufts, z); + if (t != Z_OK) { + r = t; + if (r == Z_DATA_ERROR) { + blens = null; + mode = BAD; + } + + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + } + + index = 0; + mode = DTREE; + case DTREE: + while (true) { + t = table; + if (index < 258 + (t & 0x1f) + ((t >> 5) & 0x1f)) { + int[] h; + int i; + int j; + int c; + + t = bb[0]; + + while (k < (t)) { + if (n != 0) { + r = Z_OK; + } else { + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + } + n--; + b |= (z.nextIn[p++] & 0xff) << k; + k += 8; + } + + t = hufts[(tb[0] + (b & inflate_mask[t])) * 3 + 1]; + c = hufts[(tb[0] + (b & inflate_mask[t])) * 3 + 2]; + + if (c < 16) { + b >>>= (t); + k -= (t); + blens[index++] = c; + } else { // c == 16..18 + i = c == 18 ? 7 : c - 14; + j = c == 18 ? 11 : 3; + + while (k < (t + i)) { + if (n != 0) { + r = Z_OK; + } else { + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + } + n--; + b |= (z.nextIn[p++] & 0xff) << k; + k += 8; + } + + b >>>= (t); + k -= (t); + + j += (b & inflate_mask[i]); + + b >>>= (i); + k -= (i); + + i = index; + t = table; + if (i + j > 258 + (t & 0x1f) + ((t >> 5) & 0x1f) || (c == 16 && i < 1)) { + blens = null; + mode = BAD; + z.msg = "invalid bit length repeat"; + r = Z_DATA_ERROR; + + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + } + + c = c == 16 ? blens[i - 1] : 0; + do { + blens[i++] = c; + } while (--j != 0); + index = i; + } + } else { + break; + } + } + + tb[0] = -1; + { + bl[0] = 9; // must be <= 9 for lookahead assumptions + bd[0] = 6; // must be <= 9 for lookahead assumptions + t = inftree.inflateTreesDynamic(257 + (t & 0x1f), 1 + ((t >> 5) & 0x1f), blens, bl, bd, tli, tdi, hufts, z); + + if (t != Z_OK) { + if (t == Z_DATA_ERROR) { + blens = null; + mode = BAD; + } + r = t; + + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + } + codes.init(bl[0], bd[0], hufts, tli[0], hufts, tdi[0]); + } + mode = CODES; + case CODES: + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + + if ((r = codes.proc(r)) != Z_STREAM_END) { //NOPMD AssignmentInOperand + return inflateFlush(r); + } + r = Z_OK; + codes.free(z); + + p = z.nextInIndex; + n = z.availIn; + b = bitb; + k = bitk; + q = write; + m = q < read ? read - q - 1 : end - q; + + if (last == 0) { + mode = TYPE; + break; + } + mode = DRY; + case DRY: + write = q; + r = inflateFlush(r); + q = write; + if (read != write) { + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + return inflateFlush(r); + } + mode = DONE; + case DONE: + r = Z_STREAM_END; + + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + case BAD: + r = Z_DATA_ERROR; + + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + + default: + r = Z_STREAM_ERROR; + + bitb = b; + bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + write = q; + return inflateFlush(r); + } + } + } + + void free() { + reset(); + window = null; + hufts = null; + //ZFREE(z, s); + } + + void setDictionary(byte[] d, int start, int n) { + System.arraycopy(d, start, window, 0, n); + read = write = n; + } + + // Returns true if inflate is currently at the end of a block generated + // by Z_SYNC_FLUSH or Z_FULL_FLUSH. + int syncPoint() { + return mode == LENS ? 1 : 0; + } + + // copy as much as possible from the sliding window to the output area + int inflateFlush(int r) { + int n; + int p; + int q; + + // local copies of source and destination pointers + p = z.nextOutIndex; + q = read; + + // compute number of bytes to copy as far as end of window + n = (q <= write ? write : end) - q; + if (n > z.availOut) { + n = z.availOut; + } + if (n != 0 && r == Z_BUF_ERROR) { + r = Z_OK; + } + + // update counters + z.availOut -= n; + z.totalOut += n; + + // update check information + if (check && n > 0) { + z.adler.update(window, q, n); + } + + // copy as far as end of window + System.arraycopy(window, q, z.nextOut, p, n); + p += n; + q += n; + + // see if more to copy at beginning of window + if (q == end) { + // wrap pointers + q = 0; + if (write == end) { + write = 0; + } + + // compute bytes to copy + n = write - q; + if (n > z.availOut) { + n = z.availOut; + } + if (n != 0 && r == Z_BUF_ERROR) { + r = Z_OK; + } + + // update counters + z.availOut -= n; + z.totalOut += n; + + // update check information + if (check && n > 0) { + z.adler.update(window, q, n); + } + + // copy + System.arraycopy(window, q, z.nextOut, p, n); + p += n; + q += n; + } + + // update pointers + z.nextOutIndex = p; + read = q; + + // done + return r; + } +} diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/InfCodes.java b/vm/JavaAPI/src/com/codename1/impl/zip/InfCodes.java new file mode 100644 index 00000000000..cb7901f9201 --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/InfCodes.java @@ -0,0 +1,716 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +final class InfCodes { + + static final private int[] inflate_mask = { + 0x00000000, 0x00000001, 0x00000003, 0x00000007, 0x0000000f, + 0x0000001f, 0x0000003f, 0x0000007f, 0x000000ff, 0x000001ff, + 0x000003ff, 0x000007ff, 0x00000fff, 0x00001fff, 0x00003fff, + 0x00007fff, 0x0000ffff + }; + + static final private int Z_OK = 0; + static final private int Z_STREAM_END = 1; + static final private int Z_STREAM_ERROR = -2; + static final private int Z_DATA_ERROR = -3; + + // waiting for "i:"=input, + // "o:"=output, + // "x:"=nothing + static final private int START = 0; // x: set up for LEN + static final private int LEN = 1; // i: get length/literal/eob next + static final private int LENEXT = 2; // i: getting length extra (have base) + static final private int DIST = 3; // i: get distance next + static final private int DISTEXT = 4; // i: getting distance extra + static final private int COPY = 5; // o: copying bytes in window, waiting for space + static final private int LIT = 6; // o: got literal, waiting for output space + static final private int WASH = 7; // o: got eob, possibly still output waiting + static final private int END = 8; // x: got eob and all data flushed + static final private int BADCODE = 9; // x: got error + private final ZStream z; + private final InfBlocks s; + int mode; // current inflate_codes mode + // mode dependent information + int len; + int[] tree; // pointer into tree + int tree_index = 0; + int need; // bits needed + int lit; + // if EXT or COPY, where and how much + int get; // bits to get for extra + int dist; // distance back to copy from + byte lbits; // ltree bits decoded per branch + byte dbits; // dtree bits decoder per branch + int[] ltree; // literal/length/eob tree + int ltree_index; // literal/length/eob tree + int[] dtree; // distance tree + int dtree_index; // distance tree + + InfCodes(ZStream z, InfBlocks s) { + this.z = z; + this.s = s; + } + + void init(int bl, int bd, + int[] tl, int tlIndex, + int[] td, int tdIndex) { + mode = START; + lbits = (byte) bl; + dbits = (byte) bd; + ltree = tl; + ltree_index = tlIndex; + dtree = td; + dtree_index = tdIndex; + tree = null; + } + + int proc(int r) { + int j; // temporary storage + int[] t; // temporary pointer + int tindex; // temporary pointer + int e; // extra bits or operation + int b = 0; // bit buffer + int k = 0; // bits in bit buffer + int p = 0; // input data pointer + int n; // bytes available there + int q; // output window write pointer + int m; // bytes to end of window or read pointer + int f; // pointer to copy strings from + + // copy input/output information to locals (UPDATE macro restores) + p = z.nextInIndex; + n = z.availIn; + b = s.bitb; + k = s.bitk; + q = s.write; + m = q < s.read ? s.read - q - 1 : s.end - q; + + // process input and output based on current state + while (true) { + switch (mode) { + // waiting for "i:"=input, "o:"=output, "x:"=nothing + case START: // x: set up for LEN + if (m >= 258 && n >= 10) { + + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + r = inflateFast(lbits, dbits, + ltree, ltree_index, + dtree, dtree_index, + s, z); + + p = z.nextInIndex; + n = z.availIn; + b = s.bitb; + k = s.bitk; + q = s.write; + m = q < s.read ? s.read - q - 1 : s.end - q; + + if (r != Z_OK) { + mode = r == Z_STREAM_END ? WASH : BADCODE; + break; + } + } + need = lbits; + tree = ltree; + tree_index = ltree_index; + + mode = LEN; + case LEN: // i: get length/literal/eob next + j = need; + + while (k < (j)) { + if (n != 0) { + r = Z_OK; + } else { + + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + return s.inflateFlush(r); + } + n--; + b |= (z.nextIn[p++] & 0xff) << k; + k += 8; + } + + tindex = (tree_index + (b & inflate_mask[j])) * 3; + + b >>>= (tree[tindex + 1]); + k -= (tree[tindex + 1]); + + e = tree[tindex]; + + if (e == 0) { // literal + lit = tree[tindex + 2]; + mode = LIT; + break; + } + if ((e & 16) != 0) { // length + get = e & 15; + len = tree[tindex + 2]; + mode = LENEXT; + break; + } + if ((e & 64) == 0) { // next table + need = e; + tree_index = tindex / 3 + tree[tindex + 2]; + break; + } + if ((e & 32) != 0) { // end of block + mode = WASH; + break; + } + mode = BADCODE; // invalid code + z.msg = "invalid literal/length code"; + r = Z_DATA_ERROR; + + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + return s.inflateFlush(r); + + case LENEXT: // i: getting length extra (have base) + j = get; + + while (k < (j)) { + if (n != 0) { + r = Z_OK; + } else { + + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + return s.inflateFlush(r); + } + n--; + b |= (z.nextIn[p++] & 0xff) << k; + k += 8; + } + + len += (b & inflate_mask[j]); + + b >>= j; + k -= j; + + need = dbits; + tree = dtree; + tree_index = dtree_index; + mode = DIST; + case DIST: // i: get distance next + j = need; + + while (k < (j)) { + if (n != 0) { + r = Z_OK; + } else { + + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + return s.inflateFlush(r); + } + n--; + b |= (z.nextIn[p++] & 0xff) << k; + k += 8; + } + + tindex = (tree_index + (b & inflate_mask[j])) * 3; + + b >>= tree[tindex + 1]; + k -= tree[tindex + 1]; + + e = (tree[tindex]); + if ((e & 16) != 0) { // distance + get = e & 15; + dist = tree[tindex + 2]; + mode = DISTEXT; + break; + } + if ((e & 64) == 0) { // next table + need = e; + tree_index = tindex / 3 + tree[tindex + 2]; + break; + } + mode = BADCODE; // invalid code + z.msg = "invalid distance code"; + r = Z_DATA_ERROR; + + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + return s.inflateFlush(r); + + case DISTEXT: // i: getting distance extra + j = get; + + while (k < (j)) { + if (n != 0) { + r = Z_OK; + } else { + + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + return s.inflateFlush(r); + } + n--; + b |= (z.nextIn[p++] & 0xff) << k; + k += 8; + } + + dist += (b & inflate_mask[j]); + + b >>= j; + k -= j; + + mode = COPY; + case COPY: // o: copying bytes in window, waiting for space + f = q - dist; + while (f < 0) { // modulo window size-"while" instead + f += s.end; // of "if" handles invalid distances + } + while (len != 0) { + + if (m == 0) { + if (q == s.end && s.read != 0) { + q = 0; + m = q < s.read ? s.read - q - 1 : s.end - q; + } + if (m == 0) { + s.write = q; + r = s.inflateFlush(r); + q = s.write; + m = q < s.read ? s.read - q - 1 : s.end - q; + + if (q == s.end && s.read != 0) { + q = 0; + m = q < s.read ? s.read - q - 1 : s.end - q; + } + + if (m == 0) { + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + return s.inflateFlush(r); + } + } + } + + s.window[q++] = s.window[f++]; + m--; + + if (f == s.end) { + f = 0; + } + len--; + } + mode = START; + break; + case LIT: // o: got literal, waiting for output space + if (m == 0) { + if (q == s.end && s.read != 0) { + q = 0; + m = q < s.read ? s.read - q - 1 : s.end - q; + } + if (m == 0) { + s.write = q; + r = s.inflateFlush(r); + q = s.write; + m = q < s.read ? s.read - q - 1 : s.end - q; + + if (q == s.end && s.read != 0) { + q = 0; + m = q < s.read ? s.read - q - 1 : s.end - q; + } + if (m == 0) { + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + return s.inflateFlush(r); + } + } + } + r = Z_OK; + + s.window[q++] = (byte) lit; + m--; + + mode = START; + break; + case WASH: // o: got eob, possibly more output + if (k > 7) { // return unused byte, if any + k -= 8; + n++; + p--; // can always return one + } + + s.write = q; + r = s.inflateFlush(r); + q = s.write; + if (s.read != s.write) { + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + return s.inflateFlush(r); + } + mode = END; + case END: + r = Z_STREAM_END; + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + return s.inflateFlush(r); + + case BADCODE: // x: got error + + r = Z_DATA_ERROR; + + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + return s.inflateFlush(r); + + default: + r = Z_STREAM_ERROR; + + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + return s.inflateFlush(r); + } + } + } + + void free(ZStream z) { + // ZFREE(z, c); + } + + // Called with number of bytes left to write in window at least 258 + // (the maximum string length) and number of input bytes available + // at least ten. The ten bytes are six bytes for the longest length/ + // distance pair plus four bytes for overloading the bit buffer. + + int inflateFast(int bl, int bd, + int[] tl, int tlIndex, + int[] td, int tdIndex, + InfBlocks s, ZStream z) { + int t; // temporary pointer + int[] tp; // temporary pointer + int tpIndex; // temporary pointer + int e; // extra bits or operation + int b; // bit buffer + int k; // bits in bit buffer + int p; // input data pointer + int n; // bytes available there + int q; // output window write pointer + int m; // bytes to end of window or read pointer + int ml; // mask for literal/length tree + int md; // mask for distance tree + int c; // bytes to copy + int d; // distance back to copy from + int r; // copy source pointer + + int tpIndexT3; // (tpIndex+t)*3 + + // load input, output, bit values + p = z.nextInIndex; + n = z.availIn; + b = s.bitb; + k = s.bitk; + q = s.write; + m = q < s.read ? s.read - q - 1 : s.end - q; + + // initialize masks + ml = inflate_mask[bl]; + md = inflate_mask[bd]; + + // do until not enough input or output space for fast loop + do { // assume called with m >= 258 && n >= 10 + // get literal/length code + while (k < (20)) { // max bits for literal/length code + n--; + b |= (z.nextIn[p++] & 0xff) << k; + k += 8; + } + + t = b & ml; + tp = tl; + tpIndex = tlIndex; + tpIndexT3 = (tpIndex + t) * 3; + if ((e = tp[tpIndexT3]) == 0) { //NOPMD AssignmentInOperand + b >>= (tp[tpIndexT3 + 1]); + k -= (tp[tpIndexT3 + 1]); + + s.window[q++] = (byte) tp[tpIndexT3 + 2]; + m--; + continue; + } + do { + + b >>= (tp[tpIndexT3 + 1]); + k -= (tp[tpIndexT3 + 1]); + + if ((e & 16) != 0) { + e &= 15; + c = tp[tpIndexT3 + 2] + (b & inflate_mask[e]); + + b >>= e; + k -= e; + + // decode distance base of block to copy + while (k < (15)) { // max bits for distance code + n--; + b |= (z.nextIn[p++] & 0xff) << k; + k += 8; + } + + t = b & md; + tp = td; + tpIndex = tdIndex; + tpIndexT3 = (tpIndex + t) * 3; + e = tp[tpIndexT3]; + + do { + + b >>= (tp[tpIndexT3 + 1]); + k -= (tp[tpIndexT3 + 1]); + + if ((e & 16) != 0) { + // get extra bits to add to distance base + e &= 15; + while (k < (e)) { // get extra bits (up to 13) + n--; + b |= (z.nextIn[p++] & 0xff) << k; + k += 8; + } + + d = tp[tpIndexT3 + 2] + (b & inflate_mask[e]); + + b >>= (e); + k -= (e); + + // do the copy + m -= c; + if (q >= d) { // offset before dest + // just copy + r = q - d; + if (q - r > 0 && 2 > (q - r)) { + s.window[q++] = s.window[r++]; // minimum count is three, + s.window[q++] = s.window[r++]; // so unroll loop a little + c -= 2; + } else { + System.arraycopy(s.window, r, s.window, q, 2); + q += 2; + r += 2; + c -= 2; + } + } else { // else offset after destination + r = q - d; + do { + r += s.end; // force pointer in window + } while (r < 0); // covers invalid distances + e = s.end - r; + if (c > e) { // if source crosses, + c -= e; // wrapped copy + if (q - r > 0 && e > (q - r)) { + do { + s.window[q++] = s.window[r++]; + } + while (--e != 0); + } else { + System.arraycopy(s.window, r, s.window, q, e); + q += e; + r += e; + e = 0; + } + r = 0; // copy rest from start of window + } + + } + + // copy all or what's left + if (q - r > 0 && c > (q - r)) { + do { + s.window[q++] = s.window[r++]; + } + while (--c != 0); + } else { + System.arraycopy(s.window, r, s.window, q, c); + q += c; + r += c; + c = 0; + } + break; + } else if ((e & 64) == 0) { + t += tp[tpIndexT3 + 2]; + t += (b & inflate_mask[e]); + tpIndexT3 = (tpIndex + t) * 3; + e = tp[tpIndexT3]; + } else { + z.msg = "invalid distance code"; + + c = z.availIn - n; + c = (k >> 3) < c ? k >> 3 : c; + n += c; + p -= c; + k -= c << 3; + + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + + return Z_DATA_ERROR; + } + } + while (true); + break; + } + + if ((e & 64) == 0) { + t += tp[tpIndexT3 + 2]; + t += (b & inflate_mask[e]); + tpIndexT3 = (tpIndex + t) * 3; + if ((e = tp[tpIndexT3]) == 0) { //NOPMD AssignmentInOperand + + b >>= (tp[tpIndexT3 + 1]); + k -= (tp[tpIndexT3 + 1]); + + s.window[q++] = (byte) tp[tpIndexT3 + 2]; + m--; + break; + } + } else if ((e & 32) != 0) { + + c = z.availIn - n; + c = (k >> 3) < c ? k >> 3 : c; + n += c; + p -= c; + k -= c << 3; + + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + + return Z_STREAM_END; + } else { + z.msg = "invalid literal/length code"; + + c = z.availIn - n; + c = (k >> 3) < c ? k >> 3 : c; + n += c; + p -= c; + k -= c << 3; + + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + + return Z_DATA_ERROR; + } + } + while (true); + } + while (m >= 258 && n >= 10); + + // not enough input or output--restore pointers and return + c = z.availIn - n; + c = (k >> 3) < c ? k >> 3 : c; + n += c; + p -= c; + k -= c << 3; + + s.bitb = b; + s.bitk = k; + z.availIn = n; + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + s.write = q; + + return Z_OK; + } +} diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/InfTree.java b/vm/JavaAPI/src/com/codename1/impl/zip/InfTree.java new file mode 100644 index 00000000000..c995582e132 --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/InfTree.java @@ -0,0 +1,517 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. +/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */ +/* +Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +final class InfTree { + + static final int fixed_bl = 9; + static final int fixed_bd = 5; + static final int[] fixed_tl = { + 96, 7, 256, 0, 8, 80, 0, 8, 16, 84, 8, 115, + 82, 7, 31, 0, 8, 112, 0, 8, 48, 0, 9, 192, + 80, 7, 10, 0, 8, 96, 0, 8, 32, 0, 9, 160, + 0, 8, 0, 0, 8, 128, 0, 8, 64, 0, 9, 224, + 80, 7, 6, 0, 8, 88, 0, 8, 24, 0, 9, 144, + 83, 7, 59, 0, 8, 120, 0, 8, 56, 0, 9, 208, + 81, 7, 17, 0, 8, 104, 0, 8, 40, 0, 9, 176, + 0, 8, 8, 0, 8, 136, 0, 8, 72, 0, 9, 240, + 80, 7, 4, 0, 8, 84, 0, 8, 20, 85, 8, 227, + 83, 7, 43, 0, 8, 116, 0, 8, 52, 0, 9, 200, + 81, 7, 13, 0, 8, 100, 0, 8, 36, 0, 9, 168, + 0, 8, 4, 0, 8, 132, 0, 8, 68, 0, 9, 232, + 80, 7, 8, 0, 8, 92, 0, 8, 28, 0, 9, 152, + 84, 7, 83, 0, 8, 124, 0, 8, 60, 0, 9, 216, + 82, 7, 23, 0, 8, 108, 0, 8, 44, 0, 9, 184, + 0, 8, 12, 0, 8, 140, 0, 8, 76, 0, 9, 248, + 80, 7, 3, 0, 8, 82, 0, 8, 18, 85, 8, 163, + 83, 7, 35, 0, 8, 114, 0, 8, 50, 0, 9, 196, + 81, 7, 11, 0, 8, 98, 0, 8, 34, 0, 9, 164, + 0, 8, 2, 0, 8, 130, 0, 8, 66, 0, 9, 228, + 80, 7, 7, 0, 8, 90, 0, 8, 26, 0, 9, 148, + 84, 7, 67, 0, 8, 122, 0, 8, 58, 0, 9, 212, + 82, 7, 19, 0, 8, 106, 0, 8, 42, 0, 9, 180, + 0, 8, 10, 0, 8, 138, 0, 8, 74, 0, 9, 244, + 80, 7, 5, 0, 8, 86, 0, 8, 22, 192, 8, 0, + 83, 7, 51, 0, 8, 118, 0, 8, 54, 0, 9, 204, + 81, 7, 15, 0, 8, 102, 0, 8, 38, 0, 9, 172, + 0, 8, 6, 0, 8, 134, 0, 8, 70, 0, 9, 236, + 80, 7, 9, 0, 8, 94, 0, 8, 30, 0, 9, 156, + 84, 7, 99, 0, 8, 126, 0, 8, 62, 0, 9, 220, + 82, 7, 27, 0, 8, 110, 0, 8, 46, 0, 9, 188, + 0, 8, 14, 0, 8, 142, 0, 8, 78, 0, 9, 252, + 96, 7, 256, 0, 8, 81, 0, 8, 17, 85, 8, 131, + 82, 7, 31, 0, 8, 113, 0, 8, 49, 0, 9, 194, + 80, 7, 10, 0, 8, 97, 0, 8, 33, 0, 9, 162, + 0, 8, 1, 0, 8, 129, 0, 8, 65, 0, 9, 226, + 80, 7, 6, 0, 8, 89, 0, 8, 25, 0, 9, 146, + 83, 7, 59, 0, 8, 121, 0, 8, 57, 0, 9, 210, + 81, 7, 17, 0, 8, 105, 0, 8, 41, 0, 9, 178, + 0, 8, 9, 0, 8, 137, 0, 8, 73, 0, 9, 242, + 80, 7, 4, 0, 8, 85, 0, 8, 21, 80, 8, 258, + 83, 7, 43, 0, 8, 117, 0, 8, 53, 0, 9, 202, + 81, 7, 13, 0, 8, 101, 0, 8, 37, 0, 9, 170, + 0, 8, 5, 0, 8, 133, 0, 8, 69, 0, 9, 234, + 80, 7, 8, 0, 8, 93, 0, 8, 29, 0, 9, 154, + 84, 7, 83, 0, 8, 125, 0, 8, 61, 0, 9, 218, + 82, 7, 23, 0, 8, 109, 0, 8, 45, 0, 9, 186, + 0, 8, 13, 0, 8, 141, 0, 8, 77, 0, 9, 250, + 80, 7, 3, 0, 8, 83, 0, 8, 19, 85, 8, 195, + 83, 7, 35, 0, 8, 115, 0, 8, 51, 0, 9, 198, + 81, 7, 11, 0, 8, 99, 0, 8, 35, 0, 9, 166, + 0, 8, 3, 0, 8, 131, 0, 8, 67, 0, 9, 230, + 80, 7, 7, 0, 8, 91, 0, 8, 27, 0, 9, 150, + 84, 7, 67, 0, 8, 123, 0, 8, 59, 0, 9, 214, + 82, 7, 19, 0, 8, 107, 0, 8, 43, 0, 9, 182, + 0, 8, 11, 0, 8, 139, 0, 8, 75, 0, 9, 246, + 80, 7, 5, 0, 8, 87, 0, 8, 23, 192, 8, 0, + 83, 7, 51, 0, 8, 119, 0, 8, 55, 0, 9, 206, + 81, 7, 15, 0, 8, 103, 0, 8, 39, 0, 9, 174, + 0, 8, 7, 0, 8, 135, 0, 8, 71, 0, 9, 238, + 80, 7, 9, 0, 8, 95, 0, 8, 31, 0, 9, 158, + 84, 7, 99, 0, 8, 127, 0, 8, 63, 0, 9, 222, + 82, 7, 27, 0, 8, 111, 0, 8, 47, 0, 9, 190, + 0, 8, 15, 0, 8, 143, 0, 8, 79, 0, 9, 254, + 96, 7, 256, 0, 8, 80, 0, 8, 16, 84, 8, 115, + 82, 7, 31, 0, 8, 112, 0, 8, 48, 0, 9, 193, + + 80, 7, 10, 0, 8, 96, 0, 8, 32, 0, 9, 161, + 0, 8, 0, 0, 8, 128, 0, 8, 64, 0, 9, 225, + 80, 7, 6, 0, 8, 88, 0, 8, 24, 0, 9, 145, + 83, 7, 59, 0, 8, 120, 0, 8, 56, 0, 9, 209, + 81, 7, 17, 0, 8, 104, 0, 8, 40, 0, 9, 177, + 0, 8, 8, 0, 8, 136, 0, 8, 72, 0, 9, 241, + 80, 7, 4, 0, 8, 84, 0, 8, 20, 85, 8, 227, + 83, 7, 43, 0, 8, 116, 0, 8, 52, 0, 9, 201, + 81, 7, 13, 0, 8, 100, 0, 8, 36, 0, 9, 169, + 0, 8, 4, 0, 8, 132, 0, 8, 68, 0, 9, 233, + 80, 7, 8, 0, 8, 92, 0, 8, 28, 0, 9, 153, + 84, 7, 83, 0, 8, 124, 0, 8, 60, 0, 9, 217, + 82, 7, 23, 0, 8, 108, 0, 8, 44, 0, 9, 185, + 0, 8, 12, 0, 8, 140, 0, 8, 76, 0, 9, 249, + 80, 7, 3, 0, 8, 82, 0, 8, 18, 85, 8, 163, + 83, 7, 35, 0, 8, 114, 0, 8, 50, 0, 9, 197, + 81, 7, 11, 0, 8, 98, 0, 8, 34, 0, 9, 165, + 0, 8, 2, 0, 8, 130, 0, 8, 66, 0, 9, 229, + 80, 7, 7, 0, 8, 90, 0, 8, 26, 0, 9, 149, + 84, 7, 67, 0, 8, 122, 0, 8, 58, 0, 9, 213, + 82, 7, 19, 0, 8, 106, 0, 8, 42, 0, 9, 181, + 0, 8, 10, 0, 8, 138, 0, 8, 74, 0, 9, 245, + 80, 7, 5, 0, 8, 86, 0, 8, 22, 192, 8, 0, + 83, 7, 51, 0, 8, 118, 0, 8, 54, 0, 9, 205, + 81, 7, 15, 0, 8, 102, 0, 8, 38, 0, 9, 173, + 0, 8, 6, 0, 8, 134, 0, 8, 70, 0, 9, 237, + 80, 7, 9, 0, 8, 94, 0, 8, 30, 0, 9, 157, + 84, 7, 99, 0, 8, 126, 0, 8, 62, 0, 9, 221, + 82, 7, 27, 0, 8, 110, 0, 8, 46, 0, 9, 189, + 0, 8, 14, 0, 8, 142, 0, 8, 78, 0, 9, 253, + 96, 7, 256, 0, 8, 81, 0, 8, 17, 85, 8, 131, + 82, 7, 31, 0, 8, 113, 0, 8, 49, 0, 9, 195, + 80, 7, 10, 0, 8, 97, 0, 8, 33, 0, 9, 163, + 0, 8, 1, 0, 8, 129, 0, 8, 65, 0, 9, 227, + 80, 7, 6, 0, 8, 89, 0, 8, 25, 0, 9, 147, + 83, 7, 59, 0, 8, 121, 0, 8, 57, 0, 9, 211, + 81, 7, 17, 0, 8, 105, 0, 8, 41, 0, 9, 179, + 0, 8, 9, 0, 8, 137, 0, 8, 73, 0, 9, 243, + 80, 7, 4, 0, 8, 85, 0, 8, 21, 80, 8, 258, + 83, 7, 43, 0, 8, 117, 0, 8, 53, 0, 9, 203, + 81, 7, 13, 0, 8, 101, 0, 8, 37, 0, 9, 171, + 0, 8, 5, 0, 8, 133, 0, 8, 69, 0, 9, 235, + 80, 7, 8, 0, 8, 93, 0, 8, 29, 0, 9, 155, + 84, 7, 83, 0, 8, 125, 0, 8, 61, 0, 9, 219, + 82, 7, 23, 0, 8, 109, 0, 8, 45, 0, 9, 187, + 0, 8, 13, 0, 8, 141, 0, 8, 77, 0, 9, 251, + 80, 7, 3, 0, 8, 83, 0, 8, 19, 85, 8, 195, + 83, 7, 35, 0, 8, 115, 0, 8, 51, 0, 9, 199, + 81, 7, 11, 0, 8, 99, 0, 8, 35, 0, 9, 167, + 0, 8, 3, 0, 8, 131, 0, 8, 67, 0, 9, 231, + 80, 7, 7, 0, 8, 91, 0, 8, 27, 0, 9, 151, + 84, 7, 67, 0, 8, 123, 0, 8, 59, 0, 9, 215, + 82, 7, 19, 0, 8, 107, 0, 8, 43, 0, 9, 183, + 0, 8, 11, 0, 8, 139, 0, 8, 75, 0, 9, 247, + 80, 7, 5, 0, 8, 87, 0, 8, 23, 192, 8, 0, + 83, 7, 51, 0, 8, 119, 0, 8, 55, 0, 9, 207, + 81, 7, 15, 0, 8, 103, 0, 8, 39, 0, 9, 175, + 0, 8, 7, 0, 8, 135, 0, 8, 71, 0, 9, 239, + 80, 7, 9, 0, 8, 95, 0, 8, 31, 0, 9, 159, + 84, 7, 99, 0, 8, 127, 0, 8, 63, 0, 9, 223, + 82, 7, 27, 0, 8, 111, 0, 8, 47, 0, 9, 191, + 0, 8, 15, 0, 8, 143, 0, 8, 79, 0, 9, 255 + }; + static final int[] fixed_td = { + 80, 5, 1, 87, 5, 257, 83, 5, 17, 91, 5, 4097, + 81, 5, 5, 89, 5, 1025, 85, 5, 65, 93, 5, 16385, + 80, 5, 3, 88, 5, 513, 84, 5, 33, 92, 5, 8193, + 82, 5, 9, 90, 5, 2049, 86, 5, 129, 192, 5, 24577, + 80, 5, 2, 87, 5, 385, 83, 5, 25, 91, 5, 6145, + 81, 5, 7, 89, 5, 1537, 85, 5, 97, 93, 5, 24577, + 80, 5, 4, 88, 5, 769, 84, 5, 49, 92, 5, 12289, + 82, 5, 13, 90, 5, 3073, 86, 5, 193, 192, 5, 24577 + }; + // Tables for deflate from PKZIP's appnote.txt. + static final int[] cplens = { // Copy lengths for literal codes 257..285 + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 + }; + // see note #13 above about 258 + static final int[] cplext = { // Extra bits for literal codes 257..285 + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 112, 112 // 112==invalid + }; + static final int[] cpdist = { // Copy offsets for distance codes 0..29 + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577 + }; + static final int[] cpdext = { // Extra bits for distance codes + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, + 12, 12, 13, 13}; + // If BMAX needs to be larger than 16, then h and x[] should be uLong. + static final int BMAX = 15; // maximum bit length of any code + static final private int MANY = 1440; + static final private int Z_OK = 0; + static final private int Z_DATA_ERROR = -3; + static final private int Z_MEM_ERROR = -4; + static final private int Z_BUF_ERROR = -5; + int[] hn = null; // hufts used in space + int[] v = null; // work area for huftBuild + int[] c = null; // bit length count table + int[] r = null; // table entry for structure assignment + int[] u = null; // table stack + int[] x = null; // bit offsets, then code stack + + static int inflateTreesFixed(int[] bl, //literal desired/actual bit depth + int[] bd, //distance desired/actual bit depth + int[][] tl, //literal/length tree result + int[][] td, //distance tree result + ZStream z //for memory allocation + ) { + bl[0] = fixed_bl; + bd[0] = fixed_bd; + tl[0] = fixed_tl; + td[0] = fixed_td; + return Z_OK; + } + + private int huftBuild(int[] b, // code lengths in bits (all assumed <= BMAX) + int bindex, + int n, // number of codes (assumed <= 288) + int s, // number of simple-valued codes (0..s-1) + int[] d, // list of base values for non-simple codes + int[] e, // list of extra bits for non-simple codes + int[] t, // result: starting table + int[] m, // maximum lookup bits, returns actual + int[] hp, // space for trees + int[] hn, // hufts used in space + int[] v // working area: values in order of bit length + ) { + // Given a list of code lengths and a maximum table size, make a set of + // tables to decode that set of codes. Return Z_OK on success, Z_BUF_ERROR + // if the given code set is incomplete (the tables are still built in this + // case), Z_DATA_ERROR if the input is invalid (an over-subscribed set of + // lengths), or Z_MEM_ERROR if not enough memory. + + int a; // counter for codes of length k + int f; // i repeats in table every f entries + int g; // maximum code length + int h; // table level + int i; // counter, current code + int j; // counter + int k; // number of bits in current code + int l; // bits per table (returned in m) + int mask; // (1 << w) - 1, to avoid cc -O bug on HP + int p; // pointer into c[], b[], or v[] + int q; // points to current table + int w; // bits before this table == (l * h) + int xp; // pointer into x + int y; // number of dummy codes added + int z; // number of entries in current table + + // Generate counts for each bit length + + p = 0; + i = n; + do { + c[b[bindex + p]]++; + p++; + i--; // assume all entries <= BMAX + } while (i != 0); + + if (c[0] == n) { // null input--all zero length codes + t[0] = -1; + m[0] = 0; + return Z_OK; + } + + // Find minimum and maximum length, bound *m by those + l = m[0]; + for (j = 1; j <= BMAX; j++) { + if (c[j] != 0) { + break; + } + } + k = j; // minimum code length + if (l < j) { + l = j; + } + for (i = BMAX; i != 0; i--) { + if (c[i] != 0) { + break; + } + } + g = i; // maximum code length + if (l > i) { + l = i; + } + m[0] = l; + + // Adjust last length count to fill out codes, if needed + for (y = 1 << j; j < i; j++, y <<= 1) { + if ((y -= c[j]) < 0) { //NOPMD AssignmentInOperand + return Z_DATA_ERROR; + } + } + if ((y -= c[i]) < 0) { //NOPMD AssignmentInOperand + return Z_DATA_ERROR; + } + c[i] += y; + + // Generate starting offsets into the value table for each length + x[1] = j = 0; + p = 1; + xp = 2; + while (--i != 0) { // note that i == g from above + x[xp] = (j += c[p]); + xp++; + p++; + } + + // Make a table of values in order of bit lengths + i = 0; + p = 0; + do { + if ((j = b[bindex + p]) != 0) { //NOPMD AssignmentInOperand + v[x[j]++] = i; + } + p++; + } + while (++i < n); + n = x[g]; // set n to length of v + + // Generate the Huffman codes and for each, make the table entries + x[0] = i = 0; // first Huffman code is zero + p = 0; // grab values in bit order + h = -1; // no tables yet--level -1 + w = -l; // bits decoded == (l * h) + u[0] = 0; // just to keep compilers happy + q = 0; // ditto + z = 0; // ditto + + // go through the bit lengths (k already is bits in shortest code) + for (; k <= g; k++) { + a = c[k]; + while (a-- != 0) { + // here i is the Huffman code of length k bits for value *p + // make tables up to required level + while (k > w + l) { + h++; + w += l; // previous table always l bits + // compute minimum size table less than or equal to l bits + z = g - w; + z = (z > l) ? l : z; // table size upper limit + if ((f = 1 << (j = k - w)) > a + 1) { // try a k-w bit table //NOPMD AssignmentInOperand + // too few codes for k-w bit table + f -= a + 1; // deduct codes from patterns left + xp = k; + if (j < z) { + while (++j < z) { // try smaller tables up to z bits + if ((f <<= 1) <= c[++xp]) { //NOPMD AssignmentInOperand + break; // enough codes to use up j bits + } + f -= c[xp]; // else deduct codes from patterns + } + } + } + z = 1 << j; // table entries for j-bit table + + // allocate new table + if (hn[0] + z > MANY) { // (note: doesn't matter for fixed) + return Z_DATA_ERROR; // overflow of MANY + } + u[h] = q = /*hp+*/ hn[0]; // DEBUG + hn[0] += z; + + // connect to last table, if there is one + if (h != 0) { + x[h] = i; // save pattern for backing up + r[0] = (byte) j; // bits in this table + r[1] = (byte) l; // bits to dump before this table + j = i >>> (w - l); + r[2] = q - u[h - 1] - j; // offset to this table + System.arraycopy(r, 0, hp, (u[h - 1] + j) * 3, 3); // connect to last table + } else { + t[0] = q; // first table is returned result + } + } + + // set up table entry in r + r[1] = (byte) (k - w); + if (p >= n) { + r[0] = 128 + 64; // out of values--invalid code + } else if (v[p] < s) { + r[0] = (byte) (v[p] < 256 ? 0 : 32 + 64); // 256 is end-of-block + r[2] = v[p++]; // simple code is just the value + } else { + r[0] = (byte) (e[v[p] - s] + 16 + 64); // non-simple--look up in lists + r[2] = d[v[p++] - s]; + } + + // fill code-like entries with r + f = 1 << (k - w); + for (j = i >>> w; j < z; j += f) { + System.arraycopy(r, 0, hp, (q + j) * 3, 3); + } + + // backwards increment the k-bit code i + for (j = 1 << (k - 1); (i & j) != 0; j >>>= 1) { + i ^= j; + } + i ^= j; + + // backup over finished tables + mask = (1 << w) - 1; // needed on HP, cc -O bug + while ((i & mask) != x[h]) { + h--; // don't need to update q + w -= l; + mask = (1 << w) - 1; + } + } + } + // Return Z_BUF_ERROR if we were given an incomplete table + return y != 0 && g != 1 ? Z_BUF_ERROR : Z_OK; + } + + int inflateTreesBits(int[] c, // 19 code lengths + int[] bb, // bits tree desired/actual depth + int[] tb, // bits tree result + int[] hp, // space for trees + ZStream z // for messages + ) { + int result; + initWorkArea(19); + hn[0] = 0; + result = huftBuild(c, 0, 19, 19, null, null, tb, bb, hp, hn, v); + + if (result == Z_DATA_ERROR) { + z.msg = "oversubscribed dynamic bit lengths tree"; + } else if (result == Z_BUF_ERROR || bb[0] == 0) { + z.msg = "incomplete dynamic bit lengths tree"; + result = Z_DATA_ERROR; + } + return result; + } + + int inflateTreesDynamic(int nl, // number of literal/length codes + int nd, // number of distance codes + int[] c, // that many (total) code lengths + int[] bl, // literal desired/actual bit depth + int[] bd, // distance desired/actual bit depth + int[] tl, // literal/length tree result + int[] td, // distance tree result + int[] hp, // space for trees + ZStream z // for messages + ) { + int result; + + // build literal/length tree + initWorkArea(288); + hn[0] = 0; + result = huftBuild(c, 0, nl, 257, cplens, cplext, tl, bl, hp, hn, v); + if (result != Z_OK || bl[0] == 0) { + if (result == Z_DATA_ERROR) { + z.msg = "oversubscribed literal/length tree"; + } else if (result != Z_MEM_ERROR) { + z.msg = "incomplete literal/length tree"; + result = Z_DATA_ERROR; + } + return result; + } + + // build distance tree + initWorkArea(288); + result = huftBuild(c, nl, nd, 0, cpdist, cpdext, td, bd, hp, hn, v); + + if (result != Z_OK || (bd[0] == 0 && nl > 257)) { + if (result == Z_DATA_ERROR) { + z.msg = "oversubscribed distance tree"; + } else if (result == Z_BUF_ERROR) { + z.msg = "incomplete distance tree"; + result = Z_DATA_ERROR; + } else if (result != Z_MEM_ERROR) { + z.msg = "empty distance tree with lengths"; + result = Z_DATA_ERROR; + } + return result; + } + + return Z_OK; + } + + private void initWorkArea(int vsize) { + if (hn == null) { + hn = new int[1]; + v = new int[vsize]; + c = new int[BMAX + 1]; + r = new int[3]; + u = new int[BMAX]; + x = new int[BMAX + 1]; + } + if (v.length < vsize) { + v = new int[vsize]; + } + for (int i = 0; i < vsize; i++) { + v[i] = 0; + } + for (int i = 0; i < BMAX + 1; i++) { + c[i] = 0; + } + for (int i = 0; i < 3; i++) { + r[i] = 0; + } + System.arraycopy(c, 0, u, 0, BMAX); + System.arraycopy(c, 0, x, 0, BMAX + 1); + } +} diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/Inflate.java b/vm/JavaAPI/src/com/codename1/impl/zip/Inflate.java new file mode 100644 index 00000000000..8e843c649e4 --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/Inflate.java @@ -0,0 +1,795 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2000-2011 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +final class Inflate { + + static final int Z_NO_FLUSH = 0; + static final int Z_PARTIAL_FLUSH = 1; + static final int Z_SYNC_FLUSH = 2; + static final int Z_FULL_FLUSH = 3; + static final int Z_FINISH = 4; + static final int INFLATE_ANY = 0x40000000; + // preset dictionary flag in zlib header + static final private int PRESET_DICT = 0x20; + static final private int Z_DEFLATED = 8; + static final private int Z_OK = 0; + static final private int Z_STREAM_END = 1; + static final private int Z_NEED_DICT = 2; + static final private int Z_STREAM_ERROR = -2; + static final private int Z_DATA_ERROR = -3; + static final private int Z_BUF_ERROR = -5; + static final private int DICT4 = 2; // four dictionary check bytes to go + static final private int DICT3 = 3; // three dictionary check bytes to go + static final private int DICT2 = 4; // two dictionary check bytes to go + static final private int DICT1 = 5; // one dictionary check byte to go + static final private int DICT0 = 6; // waiting for inflateSetDictionary + static final private int BLOCKS = 7; // decompressing blocks + static final private int CHECK4 = 8; // four check bytes to go + static final private int CHECK3 = 9; // three check bytes to go + static final private int CHECK2 = 10; // two check bytes to go + static final private int CHECK1 = 11; // one check byte to go + static final private int DONE = 12; // finished check, done + static final private int BAD = 13; // got an error--stay here + static final private int HEAD = 14; + static final private int LENGTH = 15; + static final private int TIME = 16; + static final private int OS = 17; + static final private int EXLEN = 18; + static final private int EXTRA = 19; + static final private int NAME = 20; + static final private int COMMENT = 21; + static final private int HCRC = 22; + static final private int FLAGS = 23; + static private final byte[] mark = {(byte) 0, (byte) 0, (byte) 0xff, (byte) 0xff}; + private final ZStream z; + + @SuppressWarnings("PMD.SingularField") + private final byte[] crcbuf = new byte[4]; + int mode; // current inflate mode + // mode dependent information + int method; // if FLAGS, method byte + // if CHECK, check values to compare + long was = -1; // computed check value + long need; // stream check value + // 0: no wrapper + // 1: zlib header + // 2: gzip header + // 4: auto detection + // if BAD, inflateSync's marker bytes count + int marker; + // mode independent information + int wrap; // flag for no wrapper + int wbits; // log2(window size) (8..15, defaults to 15) + InfBlocks blocks; // current inflate_blocks state + GZIPHeader gheader = null; + + @SuppressWarnings("PMD.SingularField") + private int flags; + private int need_bytes = -1; + private java.io.ByteArrayOutputStream tmp_string = null; + + Inflate(ZStream z) { + this.z = z; + } + + int inflateReset() { + if (z == null) { + return Z_STREAM_ERROR; + } + + z.totalIn = z.totalOut = 0; + z.msg = null; + this.mode = HEAD; + this.need_bytes = -1; + if (this.blocks != null) { + this.blocks.reset(); + } + return Z_OK; + } + + int inflateEnd() { + if (blocks != null) { + blocks.free(); + } + return Z_OK; + } + + int inflateInit(int w) { + z.msg = null; + blocks = null; + + // handle undocumented wrap option (no zlib header or check) + wrap = 0; + if (w < 0) { + w = -w; + } else if ((w & INFLATE_ANY) != 0) { + wrap = 4; + w &= ~INFLATE_ANY; + if (w < 48) { + w &= 15; + } + } else if ((w & ~31) != 0) { // for example, DEF_WBITS + 32 + wrap = 4; // zlib and gzip wrapped data should be accepted. + w &= 15; + } else { + wrap = (w >> 4) + 1; + if (w < 48) { + w &= 15; + } + } + + if (w < 8 || w > 15) { + inflateEnd(); + return Z_STREAM_ERROR; + } + + // set window size + wbits = w; + + this.blocks = new InfBlocks(z, 1 << w); + + // reset state + inflateReset(); + + return Z_OK; + } + + @SuppressWarnings("PMD.UnnecessaryLocalBeforeReturn") + int inflate(int f) { + int r; + int b; + + if (z == null || z.nextIn == null) { + if (f == Z_FINISH && this.mode == HEAD) { + return Z_OK; + } + return Z_STREAM_ERROR; + } + + f = f == Z_FINISH ? Z_BUF_ERROR : Z_OK; + r = Z_BUF_ERROR; + while (true) { + + switch (this.mode) { + case HEAD: + if (wrap == 0) { + this.mode = BLOCKS; + break; + } + + try { + r = readBytes(2, r, f); + } catch (Return e) { + return e.r; + } + + if ((wrap == 4 || (wrap & 2) != 0) && + this.need == 0x8b1fL) { // gzip header + if (wrap == 4) { + wrap = 2; + } + z.adler = new CRC32(); + checksum(2, this.need); + + if (gheader == null) { + gheader = new GZIPHeader(); + } + + this.mode = FLAGS; + break; + } + + if ((wrap & 2) != 0) { + this.mode = BAD; + z.msg = "incorrect header check"; + break; + } + + flags = 0; + + this.method = ((int) this.need) & 0xff; + b = ((int) (this.need >> 8)) & 0xff; + + if (((wrap & 1) == 0 || // check if zlib header allowed + (((this.method << 8) + b) % 31) != 0) && + (this.method & 0xf) != Z_DEFLATED) { + if (wrap == 4) { + z.nextInIndex -= 2; + z.availIn += 2; + z.totalIn -= 2; + wrap = 0; + this.mode = BLOCKS; + break; + } + this.mode = BAD; + z.msg = "incorrect header check"; + // since zlib 1.2, it is allowted to inflateSync for this case. + /* + this.marker = 5; // can't try inflateSync + */ + break; + } + + if ((this.method & 0xf) != Z_DEFLATED) { + this.mode = BAD; + z.msg = "unknown compression method"; + // since zlib 1.2, it is allowted to inflateSync for this case. + /* + this.marker = 5; // can't try inflateSync + */ + break; + } + + if (wrap == 4) { + wrap = 1; + } + + if ((this.method >> 4) + 8 > this.wbits) { + this.mode = BAD; + z.msg = "invalid window size"; + // since zlib 1.2, it is allowted to inflateSync for this case. + /* + this.marker = 5; // can't try inflateSync + */ + break; + } + + z.adler = new Adler32(); + + if ((b & PRESET_DICT) == 0) { + this.mode = BLOCKS; + break; + } + this.mode = DICT4; + case DICT4: + + if (z.availIn == 0) { + return r; + } + z.availIn--; + z.totalIn++; + this.need = ((long) (z.nextIn[z.nextInIndex++] & 0xff) << 24) & 0xff000000L; + this.mode = DICT3; + case DICT3: + + if (z.availIn == 0) { + return r; + } + r = f; + + z.availIn--; + z.totalIn++; + this.need += ((z.nextIn[z.nextInIndex++] & 0xff) << 16) & 0xff0000L; + this.mode = DICT2; + case DICT2: + + if (z.availIn == 0) { + return r; + } + r = f; + + z.availIn--; + z.totalIn++; + this.need += ((z.nextIn[z.nextInIndex++] & 0xff) << 8) & 0xff00L; + this.mode = DICT1; + case DICT1: + + if (z.availIn == 0) { + return r; + } + + z.availIn--; + z.totalIn++; + this.need += (z.nextIn[z.nextInIndex++] & 0xffL); + z.adler.reset(this.need); + this.mode = DICT0; + return Z_NEED_DICT; + case DICT0: + this.mode = BAD; + z.msg = "need dictionary"; + this.marker = 0; // can try inflateSync + return Z_STREAM_ERROR; + case BLOCKS: + if (this.blocks != null) { + r = this.blocks.proc(r); + } + if (r == Z_DATA_ERROR) { + this.mode = BAD; + this.marker = 0; // can try inflateSync + break; + } + if (r == Z_OK) { + r = f; + } + if (r != Z_STREAM_END) { + return r; + } + r = f; + this.was = z.adler.getValue(); + this.blocks.reset(); + if (this.wrap == 0) { + this.mode = DONE; + break; + } + this.mode = CHECK4; + case CHECK4: + + if (z.availIn == 0) { + return r; + } + r = f; + + z.availIn--; + z.totalIn++; + this.need = ((long) (z.nextIn[z.nextInIndex++] & 0xff) << 24) & 0xff000000L; + this.mode = CHECK3; + case CHECK3: + + if (z.availIn == 0) { + return r; + } + r = f; + + z.availIn--; + z.totalIn++; + this.need += ((z.nextIn[z.nextInIndex++] & 0xff) << 16) & 0xff0000L; + this.mode = CHECK2; + case CHECK2: + + if (z.availIn == 0) { + return r; + } + r = f; + + z.availIn--; + z.totalIn++; + this.need += ((z.nextIn[z.nextInIndex++] & 0xff) << 8) & 0xff00L; + this.mode = CHECK1; + case CHECK1: + + if (z.availIn == 0) { + return r; + } + r = f; + + z.availIn--; + z.totalIn++; + this.need += (z.nextIn[z.nextInIndex++] & 0xffL); + + if (flags != 0) { // gzip + this.need = ((this.need & 0xff000000) >> 24 | + (this.need & 0x00ff0000) >> 8 | + (this.need & 0x0000ff00) << 8 | + (this.need & 0x0000ffff) << 24) & 0xffffffffL; + } + + if (((int) (this.was)) != ((int) (this.need))) { + z.msg = "incorrect data check"; + // chack is delayed + /* + this.mode = BAD; + this.marker = 5; // can't try inflateSync + break; + */ + } else if (flags != 0 && gheader != null) { + gheader.crc = this.need; + } + + this.mode = LENGTH; + case LENGTH: + if (wrap != 0 && flags != 0) { + + try { + r = readBytes(4, r, f); + } catch (Return e) { + return e.r; + } + + if (z.msg != null && "incorrect data check".equals(z.msg)) { + this.mode = BAD; + this.marker = 5; // can't try inflateSync + break; + } + + if (this.need != (z.totalOut & 0xffffffffL)) { + z.msg = "incorrect length check"; + this.mode = BAD; + break; + } + z.msg = null; + } else { + if (z.msg != null && "incorrect data check".equals(z.msg)) { + this.mode = BAD; + this.marker = 5; // can't try inflateSync + break; + } + } + + this.mode = DONE; + case DONE: + return Z_STREAM_END; + case BAD: + return Z_DATA_ERROR; + + case FLAGS: + + try { + r = readBytes(2, r, f); + } catch (Return e) { + return e.r; + } + + flags = ((int) this.need) & 0xffff; + + if ((flags & 0xff) != Z_DEFLATED) { + z.msg = "unknown compression method"; + this.mode = BAD; + break; + } + if ((flags & 0xe000) != 0) { + z.msg = "unknown header flags set"; + this.mode = BAD; + break; + } + + if ((flags & 0x0200) != 0) { + checksum(2, this.need); + } + + this.mode = TIME; + + case TIME: + try { + r = readBytes(4, r, f); + } catch (Return e) { + return e.r; + } + if (gheader != null) { + gheader.setModifiedTime(this.need); + } + if ((flags & 0x0200) != 0) { + checksum(4, this.need); + } + this.mode = OS; + case OS: + try { + r = readBytes(2, r, f); + } catch (Return e) { + return e.r; + } + if (gheader != null) { + gheader.os = (((int) this.need) >> 8) & 0xff; + } + if ((flags & 0x0200) != 0) { + checksum(2, this.need); + } + this.mode = EXLEN; + case EXLEN: + if ((flags & 0x0400) != 0) { + try { + r = readBytes(2, r, f); + } catch (Return e) { + return e.r; + } + if (gheader != null) { + gheader.extra = new byte[((int) this.need) & 0xffff]; + } + if ((flags & 0x0200) != 0) { + checksum(2, this.need); + } + } else if (gheader != null) { + gheader.extra = null; + } + this.mode = EXTRA; + + case EXTRA: + if ((flags & 0x0400) != 0) { + try { + r = readBytes(r, f); + if (gheader != null) { + byte[] foo = tmp_string.toByteArray(); + tmp_string = null; + if (foo.length == gheader.extra.length) { + System.arraycopy(foo, 0, gheader.extra, 0, foo.length); + } else { + z.msg = "bad extra field length"; + this.mode = BAD; + break; + } + } + } catch (Return e) { + return e.r; + } + } else if (gheader != null) { + gheader.extra = null; + } + this.mode = NAME; + case NAME: + if ((flags & 0x0800) != 0) { + try { + r = readString(r, f); + if (gheader != null) { + gheader.name = tmp_string.toByteArray(); + } + tmp_string = null; + } catch (Return e) { + return e.r; + } + } else if (gheader != null) { + gheader.name = null; + } + this.mode = COMMENT; + case COMMENT: + if ((flags & 0x1000) != 0) { + try { + r = readString(r, f); + if (gheader != null) { + gheader.comment = tmp_string.toByteArray(); + } + tmp_string = null; + } catch (Return e) { + return e.r; + } + } else if (gheader != null) { + gheader.comment = null; + } + this.mode = HCRC; + case HCRC: + if ((flags & 0x0200) != 0) { + try { + r = readBytes(2, r, f); + } catch (Return e) { + return e.r; + } + if (this.need != (z.adler.getValue() & 0xffffL)) { + this.mode = BAD; + z.msg = "header crc mismatch"; + this.marker = 5; // can't try inflateSync + break; + } + } + z.adler = new CRC32(); + + this.mode = BLOCKS; + break; + default: + return Z_STREAM_ERROR; + } + } + } + + int inflateSetDictionary(byte[] dictionary, int dictLength) { + if (z == null || (this.mode != DICT0 && this.wrap != 0)) { + return Z_STREAM_ERROR; + } + + int index = 0; + int length = dictLength; + + if (this.mode == DICT0) { + long adlerNeed = z.adler.getValue(); + z.adler.reset(); + z.adler.update(dictionary, 0, dictLength); + if (z.adler.getValue() != adlerNeed) { + return Z_DATA_ERROR; + } + } + + z.adler.reset(); + + if (length >= (1 << this.wbits)) { + length = (1 << this.wbits) - 1; + index = dictLength - length; + } + if (this.blocks != null) { + this.blocks.setDictionary(dictionary, index, length); + } + this.mode = BLOCKS; + return Z_OK; + } + + int inflateSync() { + int n; // number of bytes to look at + int p; // pointer to bytes + long r; // temporaries to save total_in and total_out + long w; + + // set up + if (z == null) { + return Z_STREAM_ERROR; + } + if (this.mode != BAD) { + this.mode = BAD; + this.marker = 0; + } + if ((n = z.availIn) == 0) { //NOPMD AssignmentInOperand + return Z_BUF_ERROR; + } + + p = z.nextInIndex; + // search + while (n != 0 && this.marker < 4) { + if (z.nextIn[p] == mark[this.marker]) { + this.marker++; + } else if (z.nextIn[p] != 0) { + this.marker = 0; + } else { + this.marker = 4 - this.marker; + } + p++; + n--; + } + + // restore + z.totalIn += p - z.nextInIndex; + z.nextInIndex = p; + z.availIn = n; + + // return no joy or set up to restart on a new block + if (this.marker != 4) { + return Z_DATA_ERROR; + } + r = z.totalIn; + w = z.totalOut; + inflateReset(); + z.totalIn = r; + z.totalOut = w; + this.mode = BLOCKS; + + return Z_OK; + } + + // Returns true if inflate is currently at the end of a block generated + // by Z_SYNC_FLUSH or Z_FULL_FLUSH. This function is used by one PPP + // implementation to provide an additional safety check. PPP uses Z_SYNC_FLUSH + // but removes the length bytes of the resulting empty stored block. When + // decompressing, PPP checks that at the end of input packet, inflate is + // waiting for these length bytes. + int inflateSyncPoint() { + if (z == null || this.blocks == null) { + return Z_STREAM_ERROR; + } + return this.blocks.syncPoint(); + } + + private int readBytes(int n, int r, int f) throws Return { + if (need_bytes == -1) { + need_bytes = n; + this.need = 0; + } + while (need_bytes > 0) { + if (z.availIn == 0) { + throw new Return(r); + } + r = f; + z.availIn--; + z.totalIn++; + this.need = this.need | + ((long) (z.nextIn[z.nextInIndex++] & 0xff) << ((n - need_bytes) * 8)); + need_bytes--; + } + if (n == 2) { + this.need &= 0xffffL; + } else if (n == 4) { + this.need &= 0xffffffffL; + } + need_bytes = -1; + return r; + } + + private int readString(int r, int f) throws Return { + if (tmp_string == null) { + tmp_string = new java.io.ByteArrayOutputStream(); + } + int b = 0; + do { + if (z.availIn == 0) { + throw new Return(r); + } + r = f; + z.availIn--; + z.totalIn++; + b = z.nextIn[z.nextInIndex]; + if (b != 0) { + tmp_string.write(z.nextIn, z.nextInIndex, 1); + } + z.adler.update(z.nextIn, z.nextInIndex, 1); + z.nextInIndex++; + } while (b != 0); + return r; + } + + private int readBytes(int r, int f) throws Return { + if (tmp_string == null) { + tmp_string = new java.io.ByteArrayOutputStream(); + } + while (this.need > 0) { + if (z.availIn == 0) { + throw new Return(r); + } + r = f; + z.availIn--; + z.totalIn++; + tmp_string.write(z.nextIn, z.nextInIndex, 1); + z.adler.update(z.nextIn, z.nextInIndex, 1); + z.nextInIndex++; + this.need--; + } + return r; + } + + private void checksum(int n, long v) { + for (int i = 0; i < n; i++) { + crcbuf[i] = (byte) (v & 0xff); + v >>= 8; + } + z.adler.update(crcbuf, 0, n); + } + + public GZIPHeader getGZIPHeader() { + return gheader; + } + + boolean inParsingHeader() { + switch (mode) { + case HEAD: + case DICT4: + case DICT3: + case DICT2: + case DICT1: + case FLAGS: + case TIME: + case OS: + case EXLEN: + case EXTRA: + case NAME: + case COMMENT: + case HCRC: + return true; + default: + return false; + } + } + + static class Return extends Exception { + int r; + + Return(int r) { + this.r = r; + } + } +} diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/Inflater.java b/vm/JavaAPI/src/com/codename1/impl/zip/Inflater.java new file mode 100644 index 00000000000..12cf6921afd --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/Inflater.java @@ -0,0 +1,163 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. + +/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */ +/* +Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +final public class Inflater extends ZStream { + + static final private int MAX_WBITS = 15; // 32K LZ77 window + static final private int DEF_WBITS = MAX_WBITS; + + static final private int Z_OK = 0; + static final private int Z_STREAM_END = 1; + static final private int Z_STREAM_ERROR = -2; + private boolean finished = false; + + public Inflater() { + super(); + init(); + } + + public Inflater(JZlib.WrapperType wrapperType) throws GZIPException { + this(DEF_WBITS, wrapperType); + } + + public Inflater(int w, JZlib.WrapperType wrapperType) throws GZIPException { + super(); + int ret = init(w, wrapperType); + if (ret != Z_OK) { + throw new GZIPException(ret + ": " + msg); + } + } + + public Inflater(int w) throws GZIPException { + this(w, false); + } + + public Inflater(boolean nowrap) throws GZIPException { + this(DEF_WBITS, nowrap); + } + + public Inflater(int w, boolean nowrap) throws GZIPException { + super(); + int ret = init(w, nowrap); + if (ret != Z_OK) { + throw new GZIPException(ret + ": " + msg); + } + } + + public int init() { + return init(DEF_WBITS); + } + + public int init(JZlib.WrapperType wrapperType) { + return init(DEF_WBITS, wrapperType); + } + + public int init(int w, JZlib.WrapperType wrapperType) { + boolean nowrap = false; + if (wrapperType == JZlib.W_NONE) { //NOPMD CompareObjectsWithEquals + nowrap = true; + } else if (wrapperType == JZlib.W_GZIP) { //NOPMD CompareObjectsWithEquals + w += 16; + } else if (wrapperType == JZlib.W_ANY) { //NOPMD CompareObjectsWithEquals + w |= Inflate.INFLATE_ANY; + } + return init(w, nowrap); + } + + public int init(boolean nowrap) { + return init(DEF_WBITS, nowrap); + } + + public int init(int w) { + return init(w, false); + } + + public int init(int w, boolean nowrap) { + finished = false; + istate = new Inflate(this); + return istate.inflateInit(nowrap ? -w : w); + } + + @Override + public int inflate(int f) { + if (istate == null) { + return Z_STREAM_ERROR; + } + int ret = istate.inflate(f); + if (ret == Z_STREAM_END) { + finished = true; + } + return ret; + } + + @Override + public int end() { + finished = true; + if (istate == null) { + return Z_STREAM_ERROR; + } +// istate = null; + return istate.inflateEnd(); + } + + public int sync() { + if (istate == null) { + return Z_STREAM_ERROR; + } + return istate.inflateSync(); + } + + public int syncPoint() { + if (istate == null) { + return Z_STREAM_ERROR; + } + return istate.inflateSyncPoint(); + } + + public int setDictionary(byte[] dictionary, int dictLength) { + if (istate == null) { + return Z_STREAM_ERROR; + } + return istate.inflateSetDictionary(dictionary, dictLength); + } + + @Override + public boolean finished() { + return finished; + } +} diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/JZlib.java b/vm/JavaAPI/src/com/codename1/impl/zip/JZlib.java new file mode 100644 index 00000000000..ae39eea7cc9 --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/JZlib.java @@ -0,0 +1,120 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +final public class JZlib { + static final public int MAX_WBITS = 15; // 32K LZ77 window + static final public int DEF_WBITS = MAX_WBITS; + public static final WrapperType W_NONE = new NONE(); + public static final WrapperType W_ZLIB = new ZLIB(); + public static final WrapperType W_GZIP = new GZIP(); + public static final WrapperType W_ANY = new ANY(); + // compression levels + static final public int Z_NO_COMPRESSION = 0; + static final public int Z_BEST_SPEED = 1; + static final public int Z_BEST_COMPRESSION = 9; + static final public int Z_DEFAULT_COMPRESSION = (-1); + // compression strategy + static final public int Z_FILTERED = 1; + static final public int Z_HUFFMAN_ONLY = 2; + static final public int Z_DEFAULT_STRATEGY = 0; + static final public int Z_NO_FLUSH = 0; + static final public int Z_PARTIAL_FLUSH = 1; + static final public int Z_SYNC_FLUSH = 2; + static final public int Z_FULL_FLUSH = 3; + static final public int Z_FINISH = 4; + static final public int Z_OK = 0; + static final public int Z_STREAM_END = 1; + static final public int Z_NEED_DICT = 2; + static final public int Z_ERRNO = -1; + static final public int Z_STREAM_ERROR = -2; + static final public int Z_DATA_ERROR = -3; + static final public int Z_MEM_ERROR = -4; + static final public int Z_BUF_ERROR = -5; + static final public int Z_VERSION_ERROR = -6; + // The three kinds of block type + static final public byte Z_BINARY = 0; + static final public byte Z_ASCII = 1; + static final public byte Z_UNKNOWN = 2; + private static final String version = "1.1.0"; + + public static String version() { + return version; + } + + private JZlib() {} + + /// #### Deprecated + /// + /// Use `long, long)`. + @Deprecated + @SuppressWarnings("PMD.MethodNamingConventions") + public static long adler32_combine(long adler1, long adler2, long len2) { + return adler32Combine(adler1, adler2, len2); + } + + public static long adler32Combine(long adler1, long adler2, long len2) { + return Adler32.combine(adler1, adler2, len2); + } + + /// #### Deprecated + /// + /// Use `long, long)`. + @Deprecated + @SuppressWarnings("PMD.MethodNamingConventions") + public static long crc32_combine(long crc1, long crc2, long len2) { + return crc32Combine(crc1, crc2, len2); + } + + public static long crc32Combine(long crc1, long crc2, long len2) { + return CRC32.combine(crc1, crc2, len2); + } + + static class WrapperType { + } + + static class NONE extends WrapperType { + } + + static class ZLIB extends WrapperType { + } + + static class GZIP extends WrapperType { + } + + static class ANY extends WrapperType { + } +} diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/StaticTree.java b/vm/JavaAPI/src/com/codename1/impl/zip/StaticTree.java new file mode 100644 index 00000000000..96ce76ce230 --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/StaticTree.java @@ -0,0 +1,144 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +final class StaticTree { + // Bit length codes must not exceed MAX_BL_BITS bits + static final int MAX_BL_BITS = 7; + static final short[] STATIC_LTREE = { + 12, 8, 140, 8, 76, 8, 204, 8, 44, 8, + 172, 8, 108, 8, 236, 8, 28, 8, 156, 8, + 92, 8, 220, 8, 60, 8, 188, 8, 124, 8, + 252, 8, 2, 8, 130, 8, 66, 8, 194, 8, + 34, 8, 162, 8, 98, 8, 226, 8, 18, 8, + 146, 8, 82, 8, 210, 8, 50, 8, 178, 8, + 114, 8, 242, 8, 10, 8, 138, 8, 74, 8, + 202, 8, 42, 8, 170, 8, 106, 8, 234, 8, + 26, 8, 154, 8, 90, 8, 218, 8, 58, 8, + 186, 8, 122, 8, 250, 8, 6, 8, 134, 8, + 70, 8, 198, 8, 38, 8, 166, 8, 102, 8, + 230, 8, 22, 8, 150, 8, 86, 8, 214, 8, + 54, 8, 182, 8, 118, 8, 246, 8, 14, 8, + 142, 8, 78, 8, 206, 8, 46, 8, 174, 8, + 110, 8, 238, 8, 30, 8, 158, 8, 94, 8, + 222, 8, 62, 8, 190, 8, 126, 8, 254, 8, + 1, 8, 129, 8, 65, 8, 193, 8, 33, 8, + 161, 8, 97, 8, 225, 8, 17, 8, 145, 8, + 81, 8, 209, 8, 49, 8, 177, 8, 113, 8, + 241, 8, 9, 8, 137, 8, 73, 8, 201, 8, + 41, 8, 169, 8, 105, 8, 233, 8, 25, 8, + 153, 8, 89, 8, 217, 8, 57, 8, 185, 8, + 121, 8, 249, 8, 5, 8, 133, 8, 69, 8, + 197, 8, 37, 8, 165, 8, 101, 8, 229, 8, + 21, 8, 149, 8, 85, 8, 213, 8, 53, 8, + 181, 8, 117, 8, 245, 8, 13, 8, 141, 8, + 77, 8, 205, 8, 45, 8, 173, 8, 109, 8, + 237, 8, 29, 8, 157, 8, 93, 8, 221, 8, + 61, 8, 189, 8, 125, 8, 253, 8, 19, 9, + 275, 9, 147, 9, 403, 9, 83, 9, 339, 9, + 211, 9, 467, 9, 51, 9, 307, 9, 179, 9, + 435, 9, 115, 9, 371, 9, 243, 9, 499, 9, + 11, 9, 267, 9, 139, 9, 395, 9, 75, 9, + 331, 9, 203, 9, 459, 9, 43, 9, 299, 9, + 171, 9, 427, 9, 107, 9, 363, 9, 235, 9, + 491, 9, 27, 9, 283, 9, 155, 9, 411, 9, + 91, 9, 347, 9, 219, 9, 475, 9, 59, 9, + 315, 9, 187, 9, 443, 9, 123, 9, 379, 9, + 251, 9, 507, 9, 7, 9, 263, 9, 135, 9, + 391, 9, 71, 9, 327, 9, 199, 9, 455, 9, + 39, 9, 295, 9, 167, 9, 423, 9, 103, 9, + 359, 9, 231, 9, 487, 9, 23, 9, 279, 9, + 151, 9, 407, 9, 87, 9, 343, 9, 215, 9, + 471, 9, 55, 9, 311, 9, 183, 9, 439, 9, + 119, 9, 375, 9, 247, 9, 503, 9, 15, 9, + 271, 9, 143, 9, 399, 9, 79, 9, 335, 9, + 207, 9, 463, 9, 47, 9, 303, 9, 175, 9, + 431, 9, 111, 9, 367, 9, 239, 9, 495, 9, + 31, 9, 287, 9, 159, 9, 415, 9, 95, 9, + 351, 9, 223, 9, 479, 9, 63, 9, 319, 9, + 191, 9, 447, 9, 127, 9, 383, 9, 255, 9, + 511, 9, 0, 7, 64, 7, 32, 7, 96, 7, + 16, 7, 80, 7, 48, 7, 112, 7, 8, 7, + 72, 7, 40, 7, 104, 7, 24, 7, 88, 7, + 56, 7, 120, 7, 4, 7, 68, 7, 36, 7, + 100, 7, 20, 7, 84, 7, 52, 7, 116, 7, + 3, 8, 131, 8, 67, 8, 195, 8, 35, 8, + 163, 8, 99, 8, 227, 8 + }; + static final short[] STATIC_DTREE = { + 0, 5, 16, 5, 8, 5, 24, 5, 4, 5, + 20, 5, 12, 5, 28, 5, 2, 5, 18, 5, + 10, 5, 26, 5, 6, 5, 22, 5, 14, 5, + 30, 5, 1, 5, 17, 5, 9, 5, 25, 5, + 5, 5, 21, 5, 13, 5, 29, 5, 3, 5, + 19, 5, 11, 5, 27, 5, 7, 5, 23, 5 + }; + static final private int MAX_BITS = 15; + static final private int BL_CODES = 19; + static final private int D_CODES = 30; + static final private int LITERALS = 256; + static final private int LENGTH_CODES = 29; + static final private int L_CODES = (LITERALS + 1 + LENGTH_CODES); + static StaticTree staticLDesc = + new StaticTree(STATIC_LTREE, Tree.EXTRA_LBITS, + LITERALS + 1, L_CODES, MAX_BITS); + + static StaticTree staticDDesc = + new StaticTree(STATIC_DTREE, Tree.EXTRA_DBITS, + 0, D_CODES, MAX_BITS); + + static StaticTree staticBlDesc = + new StaticTree(null, Tree.EXTRA_BLBITS, + 0, BL_CODES, MAX_BL_BITS); + + short[] staticTree; // static tree or null + int[] extraBits; // extra bits for each code or null + int extraBase; // base index for extra_bits + int elems; // max number of elements in the tree + int maxLength; // max bit length for the codes + + private StaticTree(short[] staticTree, + int[] extraBits, + int extraBase, + int elems, + int maxLength) { + this.staticTree = staticTree; + this.extraBits = extraBits; + this.extraBase = extraBase; + this.elems = elems; + this.maxLength = maxLength; + } +} diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/Tree.java b/vm/JavaAPI/src/com/codename1/impl/zip/Tree.java new file mode 100644 index 00000000000..8362a5a350d --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/Tree.java @@ -0,0 +1,374 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. +/* -*-mode:java; c-basic-offset:2; -*- */ +/* +Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +final class Tree { + // Bit length codes must not exceed MAX_BL_BITS bits + static final int MAX_BL_BITS = 7; + // end of block literal code + static final int END_BLOCK = 256; + // repeat previous bit length 3-6 times (2 bits of repeat count) + static final int REP_3_6 = 16; + // repeat a zero length 3-10 times (3 bits of repeat count) + static final int REPZ_3_10 = 17; + // repeat a zero length 11-138 times (7 bits of repeat count) + static final int REPZ_11_138 = 18; + // extra bits for each length code + static final int[] EXTRA_LBITS = { + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 + }; + // extra bits for each distance code + static final int[] EXTRA_DBITS = { + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 + }; + // extra bits for each bit length code + static final int[] EXTRA_BLBITS = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 7 + }; + static final byte[] BL_ORDER = { + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; + static final int Buf_size = 8 * 2; + // see definition of array dist_code below + static final int DIST_CODE_LEN = 512; + static final byte[] DIST_CODE = { + 0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, + 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 16, 17, + 18, 18, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, + 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, + 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, + 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, + 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29 + }; + static final byte[] LENGTH_CODE = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 12, 12, + 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, + 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, + 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28 + }; + static final int[] BASE_LENGTH = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, + 64, 80, 96, 112, 128, 160, 192, 224, 0 + }; + static final int[] BASE_DIST = { + 0, 1, 2, 3, 4, 6, 8, 12, 16, 24, + 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, + 1024, 1536, 2048, 3072, 4096, 6144, 8192, 12288, 16384, 24576 + }; + static final private int MAX_BITS = 15; + + + // The lengths of the bit length codes are sent in order of decreasing + // probability, to avoid transmitting the lengths for unused bit + // length codes. + static final private int LITERALS = 256; + static final private int LENGTH_CODES = 29; + static final private int L_CODES = (LITERALS + 1 + LENGTH_CODES); + static final private int HEAP_SIZE = (2 * L_CODES + 1); + short[] dynTree; // the dynamic tree + int maxCode; // largest code with non zero frequency + StaticTree statDesc; // the corresponding static tree + + // Mapping from a distance to a distance code. dist is the distance - 1 and + // must not have side effects. _dist_code[256] and _dist_code[257] are never + // used. + static int dCode(int dist) { + return ((dist) < 256 ? DIST_CODE[dist] : DIST_CODE[256 + ((dist) >>> 7)]); + } + + // Generate the codes for a given tree and bit counts (which need not be + // optimal). + // IN assertion: the array blCount contains the bit length statistics for + // the given tree and the field len is set for all tree elements. + // OUT assertion: the field code is set for all tree elements of non + // zero code length. + private static void genCodes( + short[] tree, // the tree to decorate + int maxCode, // largest code with non zero frequency + short[] blCount, // number of codes at each bit length + short[] nextCode) { + short code = 0; // running code value + int bits; // bit index + int n; // code index + + // The distribution counts are first used to generate the code values + // without bit reversal. + nextCode[0] = 0; + for (bits = 1; bits <= MAX_BITS; bits++) { + nextCode[bits] = code = (short) ((code + blCount[bits - 1]) << 1); + } + + // Check that the bit counts in blCount are consistent. The last code + // must be all ones. + //Assert (code + blCount[MAX_BITS]-1 == (1<>>= 1; + if (i + 1 < len) { + res <<= 1; + } + } + return res; + } + + // Compute the optimal bit lengths for a tree and update the total bit length + // for the current block. + // IN assertion: the fields freq and dad are set, heap[heap_max] and + // above are the tree nodes sorted by increasing frequency. + // OUT assertions: the field len is set to the optimal bit length, the + // array bl_count contains the frequencies for each bit length. + // The length opt_len is updated; static_len is also updated if stree is + // not null. + void genBitLen(Deflate s) { + short[] tree = dynTree; + short[] stree = statDesc != null ? statDesc.staticTree : null; + int[] extra = statDesc != null ? statDesc.extraBits : null; + int base = statDesc != null ? statDesc.extraBase : 0; + int maxLength = statDesc != null ? statDesc.maxLength : 0; + int h; // heap index + int n; // iterate over the tree elements + int m; + int bits; // bit length + int xbits; // extra bits + short f; // frequency + int overflow = 0; // number of elements with bit length too large + + for (bits = 0; bits <= MAX_BITS; bits++) { + s.blCount[bits] = 0; + } + + // In a first pass, compute the optimal bit lengths (which may + // overflow in the case of the bit length tree). + tree[s.heap[s.heapMax] * 2 + 1] = 0; // root of the heap + + for (h = s.heapMax + 1; h < HEAP_SIZE; h++) { + n = s.heap[h]; + bits = tree[tree[n * 2 + 1] * 2 + 1] + 1; + if (bits > maxLength) { + bits = maxLength; + overflow++; + } + tree[n * 2 + 1] = (short) bits; + // We overwrite tree[n*2+1] which is no longer needed + + if (n > maxCode) { + continue; // not a leaf node + } + + s.blCount[bits]++; + xbits = 0; + if (n >= base) { + xbits = extra[n - base]; + } + f = tree[n * 2]; + s.optLen += f * (bits + xbits); + if (stree != null) { + s.staticLen += f * (stree[n * 2 + 1] + xbits); + } + } + if (overflow == 0) { + return; + } + + // This happens for example on obj2 and pic of the Calgary corpus + // Find the first bit length which could increase: + do { + bits = maxLength - 1; + while (s.blCount[bits] == 0) { + bits--; + } + s.blCount[bits]--; // move one leaf down the tree + s.blCount[bits + 1] += 2; // move one overflow item as its brother + s.blCount[maxLength]--; + // The brother of the overflow item also moves one step up, + // but this does not affect bl_count[maxLength] + overflow -= 2; + } + while (overflow > 0); + + for (bits = maxLength; bits != 0; bits--) { + n = s.blCount[bits]; + while (n != 0) { + m = s.heap[--h]; + if (m > maxCode) { + continue; + } + if (tree[m * 2 + 1] != bits) { + s.optLen += ((long) bits - (long) tree[m * 2 + 1]) * (long) tree[m * 2]; + tree[m * 2 + 1] = (short) bits; + } + n--; + } + } + } + + // Construct one Huffman tree and assigns the code bit strings and lengths. + // Update the total bit length for the current block. + // IN assertion: the field freq is set for all tree elements. + // OUT assertions: the fields len and code are set to the optimal bit length + // and corresponding code. The length opt_len is updated; static_len is + // also updated if stree is not null. The field max_code is set. + void buildTree(Deflate s) { + short[] tree = dynTree; + short[] stree = statDesc != null ? statDesc.staticTree : null; + int elems = statDesc != null ? statDesc.elems : 0; + int n; // iterate over heap elements + int m; + int maxCode = -1; // largest code with non zero frequency + int node; // new node being created + + // Construct the initial heap, with least frequent element in + // heap[1]. The sons of heap[n] are heap[2*n] and heap[2*n+1]. + // heap[0] is not used. + s.heapLen = 0; + s.heapMax = HEAP_SIZE; + + for (n = 0; n < elems; n++) { + if (tree[n * 2] != 0) { + s.heap[++s.heapLen] = maxCode = n; + s.depth[n] = 0; + } else { + tree[n * 2 + 1] = 0; + } + } + + // The pkzip format requires that at least one distance code exists, + // and that at least one bit should be sent even if there is only one + // possible code. So to avoid special checks later on we force at least + // two codes of non zero frequency. + while (s.heapLen < 2) { + node = s.heap[++s.heapLen] = (maxCode < 2 ? ++maxCode : 0); + tree[node * 2] = 1; + s.depth[node] = 0; + s.optLen--; + if (stree != null) { + s.staticLen -= stree[node * 2 + 1]; + } + // node is 0 or 1 so it does not have extra bits + } + this.maxCode = maxCode; + + // The elements heap[heap_len/2+1 .. heap_len] are leaves of the tree, + // establish sub-heaps of increasing lengths: + + for (n = s.heapLen / 2; n >= 1; n--) { + s.pqdownheap(tree, n); + } + + // Construct the Huffman tree by repeatedly combining the least two + // frequent nodes. + + node = elems; // next internal node of the tree + do { + // n = node of least frequency + n = s.heap[1]; + s.heap[1] = s.heap[s.heapLen--]; + s.pqdownheap(tree, 1); + m = s.heap[1]; // m = node of next least frequency + + s.heap[--s.heapMax] = n; // keep the nodes sorted by frequency + s.heap[--s.heapMax] = m; + + // Create a new node father of n and m + tree[node * 2] = (short) (tree[n * 2] + tree[m * 2]); + s.depth[node] = (byte) (Math.max(s.depth[n], s.depth[m]) + 1); + tree[n * 2 + 1] = tree[m * 2 + 1] = (short) node; + + // and insert the new node in the heap + s.heap[1] = node++; + s.pqdownheap(tree, 1); + } + while (s.heapLen >= 2); + + s.heap[--s.heapMax] = s.heap[1]; + + // At this point, the fields freq and dad are set. We can now + // generate the bit lengths. + + genBitLen(s); + + // The field len is now set, we can generate the bit codes + genCodes(tree, maxCode, s.blCount, s.nextCode); + } +} diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/ZStream.java b/vm/JavaAPI/src/com/codename1/impl/zip/ZStream.java new file mode 100644 index 00000000000..e660fc517b0 --- /dev/null +++ b/vm/JavaAPI/src/com/codename1/impl/zip/ZStream.java @@ -0,0 +1,404 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. + +/* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */ +/* +Copyright (c) 2000-2011 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This program is based on zlib-1.1.3, so all credit should go authors + * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) + * and contributors of zlib. + */ + +package com.codename1.impl.zip; + +/// ZStream +/// +/// #### Deprecated +/// +/// Not for public use in the future. +@Deprecated +public class ZStream { + + static final private int MAX_WBITS = 15; // 32K LZ77 window + static final private int DEF_WBITS = MAX_WBITS; + + static final private int Z_NO_FLUSH = 0; + static final private int Z_PARTIAL_FLUSH = 1; + static final private int Z_SYNC_FLUSH = 2; + static final private int Z_FULL_FLUSH = 3; + static final private int Z_FINISH = 4; + + static final private int MAX_MEM_LEVEL = 9; + + static final private int Z_OK = 0; + static final private int Z_STREAM_END = 1; + static final private int Z_NEED_DICT = 2; + static final private int Z_ERRNO = -1; + static final private int Z_STREAM_ERROR = -2; + static final private int Z_DATA_ERROR = -3; + static final private int Z_MEM_ERROR = -4; + static final private int Z_BUF_ERROR = -5; + static final private int Z_VERSION_ERROR = -6; + + public byte[] nextIn; // next input byte + public int nextInIndex; + public int availIn; // number of bytes available at next_in + public long totalIn; // total nb of input bytes read so far + + public byte[] nextOut; // next output byte should be put there + public int nextOutIndex; + public int availOut; // remaining free space at next_out + public long totalOut; // total nb of bytes output so far + + public String msg; + + Deflate dstate; + Inflate istate; + + int dataType; // best guess about the data type: ascii or binary + + Checksum adler; + + public ZStream() { + this(new Adler32()); + } + + public ZStream(Checksum adler) { + this.adler = adler; + } + + public int inflateInit() { + return inflateInit(DEF_WBITS); + } + + public int inflateInit(boolean nowrap) { + return inflateInit(DEF_WBITS, nowrap); + } + + public int inflateInit(int w) { + return inflateInit(w, false); + } + + public int inflateInit(JZlib.WrapperType wrapperType) { + return inflateInit(DEF_WBITS, wrapperType); + } + + public int inflateInit(int w, JZlib.WrapperType wrapperType) { + boolean nowrap = false; + if (wrapperType == JZlib.W_NONE) { //NOPMD CompareObjectsWithEquals + nowrap = true; + } else if (wrapperType == JZlib.W_GZIP) { //NOPMD CompareObjectsWithEquals + w += 16; + } else if (wrapperType == JZlib.W_ANY) { //NOPMD CompareObjectsWithEquals + w |= Inflate.INFLATE_ANY; + } + return inflateInit(w, nowrap); + } + + public int inflateInit(int w, boolean nowrap) { + istate = new Inflate(this); + return istate.inflateInit(nowrap ? -w : w); + } + + public int inflate(int f) { + if (istate == null) { + return Z_STREAM_ERROR; + } + return istate.inflate(f); + } + + public int inflateEnd() { + if (istate == null) { + return Z_STREAM_ERROR; + } +// istate = null; + return istate.inflateEnd(); + } + + public int inflateSync() { + if (istate == null) { + return Z_STREAM_ERROR; + } + return istate.inflateSync(); + } + + public int inflateSyncPoint() { + if (istate == null) { + return Z_STREAM_ERROR; + } + return istate.inflateSyncPoint(); + } + + public int inflateSetDictionary(byte[] dictionary, int dictLength) { + if (istate == null) { + return Z_STREAM_ERROR; + } + return istate.inflateSetDictionary(dictionary, dictLength); + } + + public boolean inflateFinished() { + return istate.mode == 12 /*DONE*/; + } + + public int deflateInit(int level) { + return deflateInit(level, MAX_WBITS); + } + + public int deflateInit(int level, boolean nowrap) { + return deflateInit(level, MAX_WBITS, nowrap); + } + + public int deflateInit(int level, int bits) { + return deflateInit(level, bits, false); + } + + public int deflateInit(int level, int bits, int memlevel, JZlib.WrapperType wrapperType) { + if (bits < 9 || bits > 15) { + return Z_STREAM_ERROR; + } + if (wrapperType == JZlib.W_NONE) { //NOPMD CompareObjectsWithEquals + bits *= -1; + } else if (wrapperType == JZlib.W_GZIP) { //NOPMD CompareObjectsWithEquals + bits += 16; + } else if (wrapperType == JZlib.W_ANY) { //NOPMD CompareObjectsWithEquals + return Z_STREAM_ERROR; + } + return this.deflateInit(level, bits, memlevel); + } + + public int deflateInit(int level, int bits, int memlevel) { + dstate = new Deflate(this); + return dstate.deflateInit(level, bits, memlevel); + } + + public int deflateInit(int level, int bits, boolean nowrap) { + dstate = new Deflate(this); + return dstate.deflateInit(level, nowrap ? -bits : bits); + } + + public int deflate(int flush) { + if (dstate == null) { + return Z_STREAM_ERROR; + } + return dstate.deflate(flush); + } + + public int deflateEnd() { + if (dstate == null) { + return Z_STREAM_ERROR; + } + int ret = dstate.deflateEnd(); + dstate = null; + return ret; + } + + public int deflateParams(int level, int strategy) { + if (dstate == null) { + return Z_STREAM_ERROR; + } + return dstate.deflateParams(level, strategy); + } + + public int deflateSetDictionary(byte[] dictionary, int dictLength) { + if (dstate == null) { + return Z_STREAM_ERROR; + } + return dstate.deflateSetDictionary(dictionary, dictLength); + } + + // Flush as much pending output as possible. All deflate() output goes + // through this function so some applications may wish to modify it + // to avoid allocating a large strm->next_out buffer and copying into it. + // (See also readBuf()). + void flushPending() { + int len = dstate.pending; + + if (len > availOut) { + len = availOut; + } + if (len == 0) { + return; + } + + System.arraycopy(dstate.pendingBuf, dstate.pendingOut, + nextOut, nextOutIndex, len); + + nextOutIndex += len; + dstate.pendingOut += len; + totalOut += len; + availOut -= len; + dstate.pending -= len; + if (dstate.pending == 0) { + dstate.pendingOut = 0; + } + } + + // Read a new buffer from the current input stream, update the adler32 + // and total number of bytes read. All deflate() input goes through + // this function so some applications may wish to modify it to avoid + // allocating a large strm->next_in buffer and copying from it. + // (See also flush_pending()). + int readBuf(byte[] buf, int start, int size) { + int len = availIn; + + if (len > size) { + len = size; + } + if (len == 0) { + return 0; + } + + availIn -= len; + + if (dstate.wrap != 0) { + adler.update(nextIn, nextInIndex, len); + } + System.arraycopy(nextIn, nextInIndex, buf, start, len); + nextInIndex += len; + totalIn += len; + return len; + } + + public long getAdler() { + return adler.getValue(); + } + + public void free() { + nextIn = null; + nextOut = null; + msg = null; + } + + public void setOutput(byte[] buf) { + setOutput(buf, 0, buf.length); + } + + public void setOutput(byte[] buf, int off, int len) { + nextOut = buf; + nextOutIndex = off; + availOut = len; + } + + public void setInput(byte[] buf) { + setInput(buf, 0, buf.length, false); + } + + public void setInput(byte[] buf, boolean append) { + setInput(buf, 0, buf.length, append); + } + + public void setInput(byte[] buf, int off, int len, boolean append) { + if (len <= 0 && append && nextIn != null) { + return; + } + + if (availIn > 0 && append) { + byte[] tmp = new byte[availIn + len]; + System.arraycopy(nextIn, nextInIndex, tmp, 0, availIn); + System.arraycopy(buf, off, tmp, availIn, len); + nextIn = tmp; + nextInIndex = 0; + availIn += len; + } else { + nextIn = buf; + nextInIndex = off; + availIn = len; + } + } + + public byte[] getNextIn() { + return nextIn; + } + + public void setNextIn(byte[] nextIn) { + this.nextIn = nextIn; + } + + public int getNextInIndex() { + return nextInIndex; + } + + public void setNextInIndex(int nextInIndex) { + this.nextInIndex = nextInIndex; + } + + public int getAvailIn() { + return availIn; + } + + public void setAvailIn(int availIn) { + this.availIn = availIn; + } + + public byte[] getNextOut() { + return nextOut; + } + + public void setNextOut(byte[] nextOut) { + this.nextOut = nextOut; + } + + public int getNextOutIndex() { + return nextOutIndex; + } + + public void setNextOutIndex(int nextOutIndex) { + this.nextOutIndex = nextOutIndex; + } + + public int getAvailOut() { + return availOut; + + } + + public void setAvailOut(int availOut) { + this.availOut = availOut; + } + + public long getTotalOut() { + return totalOut; + } + + public long getTotalIn() { + return totalIn; + } + + public String getMessage() { + return msg; + } + + /// Those methods are expected to be override by Inflater and Deflater. + /// In the future, they will become abstract methods. + public int end() { + return Z_OK; + } + + public boolean finished() { + return false; + } +} diff --git a/vm/JavaAPI/src/java/util/zip/Adler32.java b/vm/JavaAPI/src/java/util/zip/Adler32.java new file mode 100644 index 00000000000..4fa5fa7c773 --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/Adler32.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * This code is licensed under GPLv2 with the Classpath exception. + */ +package java.util.zip; + +/** Standard Adler32 checksum. */ +public class Adler32 implements Checksum { + private final com.codename1.impl.zip.Adler32 delegate = new com.codename1.impl.zip.Adler32(); + public void update(int value) { + update(new byte[] {(byte) value}, 0, 1); + } + public void update(byte[] data) { + update(data, 0, data.length); + } + public void update(byte[] data, int offset, int length) { + if (data == null) + throw new NullPointerException(); + if (offset < 0 || length < 0 || offset > data.length - length) + throw new ArrayIndexOutOfBoundsException(); + delegate.update(data, offset, length); + } + public long getValue() { + return delegate.getValue(); + } + public void reset() { + delegate.reset(); + } +} diff --git a/vm/JavaAPI/src/java/util/zip/CRC32.java b/vm/JavaAPI/src/java/util/zip/CRC32.java new file mode 100644 index 00000000000..ff8895213ca --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/CRC32.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * This code is licensed under GPLv2 with the Classpath exception. + */ +package java.util.zip; + +/** Standard CRC32 checksum. */ +public class CRC32 implements Checksum { + private final com.codename1.impl.zip.CRC32 delegate = new com.codename1.impl.zip.CRC32(); + public void update(int value) { + update(new byte[] {(byte) value}, 0, 1); + } + public void update(byte[] data) { + update(data, 0, data.length); + } + public void update(byte[] data, int offset, int length) { + if (data == null) + throw new NullPointerException(); + if (offset < 0 || length < 0 || offset > data.length - length) + throw new ArrayIndexOutOfBoundsException(); + delegate.update(data, offset, length); + } + public long getValue() { + return delegate.getValue(); + } + public void reset() { + delegate.reset(); + } +} diff --git a/vm/JavaAPI/src/java/util/zip/CheckedInputStream.java b/vm/JavaAPI/src/java/util/zip/CheckedInputStream.java new file mode 100644 index 00000000000..72d6f69f02e --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/CheckedInputStream.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * This code is licensed under GPLv2 with the Classpath exception. + */ +package java.util.zip; + +import java.io.*; +/** A stream which updates the supplied checksum. */ +public class CheckedInputStream extends FilterInputStream { + private final Checksum checksum; + public CheckedInputStream(InputStream stream, Checksum checksum) { + super(stream); + if (checksum == null) + throw new NullPointerException(); + this.checksum = checksum; + } + public Checksum getChecksum() { + return checksum; + } + public int read() throws IOException { + int b = in.read(); + if (b >= 0) + checksum.update(b); + return b; + } + public int read(byte[] b, int off, int len) throws IOException { + int n = in.read(b, off, len); + if (n > 0) + checksum.update(b, off, n); + return n; + } + public long skip(long n) throws IOException { + byte[] b = new byte[512]; + long total = 0; + while (total < n) { + int count = read(b, 0, (int) Math.min(n - total, b.length)); + if (count < 0) + break; + total += count; + } + return total; + } +} diff --git a/vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java b/vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java new file mode 100644 index 00000000000..8e0af2d09ec --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * This code is licensed under GPLv2 with the Classpath exception. + */ +package java.util.zip; + +import java.io.*; +/** A stream which updates the supplied checksum. */ +public class CheckedOutputStream extends FilterOutputStream { + private final Checksum checksum; + public CheckedOutputStream(OutputStream stream, Checksum checksum) { + super(stream); + if (checksum == null) + throw new NullPointerException(); + this.checksum = checksum; + } + public Checksum getChecksum() { + return checksum; + } + public void write(int b) throws IOException { + out.write(b); + checksum.update(b); + } + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + checksum.update(b, off, len); + } +} diff --git a/vm/JavaAPI/src/java/util/zip/Checksum.java b/vm/JavaAPI/src/java/util/zip/Checksum.java new file mode 100644 index 00000000000..697a23fe7cc --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/Checksum.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * This code is licensed under GPLv2 with the Classpath exception. + */ +package java.util.zip; + +/** A running checksum of bytes. */ +public interface Checksum { + void update(int value); + void update(byte[] data, int offset, int length); + long getValue(); + void reset(); +} diff --git a/vm/JavaAPI/src/java/util/zip/DataFormatException.java b/vm/JavaAPI/src/java/util/zip/DataFormatException.java new file mode 100644 index 00000000000..69a40ea4f92 --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/DataFormatException.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * This code is licensed under GPLv2 with the Classpath exception. + */ +package java.util.zip; + +/** Reports invalid compressed data. */ +public class DataFormatException extends Exception { + public DataFormatException() { + } + public DataFormatException(String message) { + super(message); + } +} diff --git a/vm/JavaAPI/src/java/util/zip/Deflater.java b/vm/JavaAPI/src/java/util/zip/Deflater.java new file mode 100644 index 00000000000..77be6db8d70 --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/Deflater.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * This code is licensed under GPLv2 with the Classpath exception. + */ +package java.util.zip; + +/** Pure Java DEFLATE compressor, with zlib framing unless nowrap is true. */ +public class Deflater { + public static final int DEFAULT_COMPRESSION = -1, NO_COMPRESSION = 0, BEST_SPEED = 1, BEST_COMPRESSION = 9; + public static final int DEFAULT_STRATEGY = 0, FILTERED = 1, HUFFMAN_ONLY = 2, DEFLATED = 8; + public static final int NO_FLUSH = 0, SYNC_FLUSH = 2, FULL_FLUSH = 3; + private com.codename1.impl.zip.Deflater engine; + private final boolean nowrap; + private int level, strategy; + private boolean finishing, done, paramsChanged; + public Deflater() { + this(DEFAULT_COMPRESSION); + } + public Deflater(int level) { + this(level, false); + } + public Deflater(int level, boolean nowrap) { + checkLevel(level); + this.level = level; + this.nowrap = nowrap; + init(); + } + private static void checkLevel(int level) { + if (level < -1 || level > 9) + throw new IllegalArgumentException("Invalid compression level"); + } + private void init() { + engine = new com.codename1.impl.zip.Deflater(); + if (engine.init(level, nowrap) != 0) + throw new IllegalArgumentException("Cannot initialize deflater"); + if (strategy != 0) + engine.params(level, strategy); + finishing = false; + done = false; + paramsChanged = false; + } + private void open() { + if (engine == null) + throw new NullPointerException("Deflater ended"); + } + static void bounds(byte[] b, int off, int len) { + if (b == null) + throw new NullPointerException(); + if (off < 0 || len < 0 || off > b.length - len) + throw new ArrayIndexOutOfBoundsException(); + } + public void setInput(byte[] b) { + setInput(b, 0, b.length); + } + public void setInput(byte[] b, int off, int len) { + bounds(b, off, len); + open(); + engine.setInput(b, off, len, false); + } + public void setDictionary(byte[] b) { + setDictionary(b, 0, b.length); + } + public void setDictionary(byte[] b, int off, int len) { + bounds(b, off, len); + open(); + byte[] copy = new byte[len]; + System.arraycopy(b, off, copy, 0, len); + if (engine.setDictionary(copy, len) != 0) + throw new IllegalArgumentException("Invalid dictionary state"); + } + public void setLevel(int level) { + checkLevel(level); + this.level = level; + paramsChanged = true; + } + public void setStrategy(int strategy) { + if (strategy < 0 || strategy > 2) + throw new IllegalArgumentException("Invalid strategy"); + this.strategy = strategy; + paramsChanged = true; + } + public boolean needsInput() { + open(); + return engine.availIn == 0; + } + public void finish() { + finishing = true; + } + public boolean finished() { + return done; + } + public int deflate(byte[] b) { + return deflate(b, 0, b.length); + } + public int deflate(byte[] b, int off, int len) { + return deflate(b, off, len, NO_FLUSH); + } + public int deflate(byte[] b, int off, int len, int flush) { + bounds(b, off, len); + open(); + if (flush != NO_FLUSH && flush != SYNC_FLUSH && flush != FULL_FLUSH) + throw new IllegalArgumentException("Invalid flush"); + if (len == 0 || done) + return 0; + engine.setOutput(b, off, len); + if (paramsChanged) { + int rc = engine.params(level, strategy); + if (rc == 0) + paramsChanged = false; + else if (rc != -5) + throw new IllegalStateException(engine.msg); + // deflateParams may itself produce output, including a partial flush. + return len - engine.availOut; + } + int rc = engine.deflate(finishing ? 4 : flush); + if (rc == 1) + done = true; + else if (rc != 0 && rc != -5) + throw new IllegalStateException(engine.msg); + return len - engine.availOut; + } + public int getAdler() { + open(); + return (int) engine.getAdler(); + } + public long getBytesRead() { + open(); + return engine.totalIn; + } + public long getBytesWritten() { + open(); + return engine.totalOut; + } + public int getTotalIn() { + return (int) getBytesRead(); + } + public int getTotalOut() { + return (int) getBytesWritten(); + } + public void reset() { + open(); + engine.end(); + init(); + } + public void end() { + if (engine != null) { + engine.end(); + engine = null; + } + } +} diff --git a/vm/JavaAPI/src/java/util/zip/DeflaterOutputStream.java b/vm/JavaAPI/src/java/util/zip/DeflaterOutputStream.java new file mode 100644 index 00000000000..e41d2b843d0 --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/DeflaterOutputStream.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * This code is licensed under GPLv2 with the Classpath exception. + */ +package java.util.zip; + +import java.io.*; + +/** A streaming DEFLATE encoder. finish() leaves the destination open. */ +public class DeflaterOutputStream extends FilterOutputStream { + protected Deflater def; + protected byte[] buf; + private boolean own, closed, syncFlush; + public DeflaterOutputStream(OutputStream out) { + this(out, new Deflater(), 512, false); + own = true; + } + public DeflaterOutputStream(OutputStream out, boolean syncFlush) { + this(out, new Deflater(), 512, syncFlush); + own = true; + } + public DeflaterOutputStream(OutputStream out, Deflater def) { + this(out, def, 512, false); + } + public DeflaterOutputStream(OutputStream out, Deflater def, int size) { + this(out, def, size, false); + } + public DeflaterOutputStream(OutputStream out, Deflater def, boolean syncFlush) { + this(out, def, 512, syncFlush); + } + public DeflaterOutputStream(OutputStream out, Deflater def, int size, boolean syncFlush) { + super(out); + if (out == null || def == null) + throw new NullPointerException(); + if (size <= 0) + throw new IllegalArgumentException("Buffer size must be positive"); + this.def = def; + buf = new byte[size]; + this.syncFlush = syncFlush; + } + public void write(int b) throws IOException { + write(new byte[] {(byte) b}, 0, 1); + } + public void write(byte[] b, int off, int len) throws IOException { + if (closed) + throw new IOException("Stream closed"); + Deflater.bounds(b, off, len); + if (def.finished()) + throw new IOException("Stream finished"); + if (len == 0) + return; + def.setInput(b, off, len); + while (!def.needsInput()) deflate(); + } + protected void deflate() throws IOException { + int n = def.deflate(buf, 0, buf.length); + if (n > 0) + out.write(buf, 0, n); + } + public void finish() throws IOException { + if (closed) + throw new IOException("Stream closed"); + if (!def.finished()) { + def.finish(); + while (!def.finished()) deflate(); + } + } + public void flush() throws IOException { + if (closed) + throw new IOException("Stream closed"); + if (syncFlush && !def.finished()) { + int n; + do { + n = def.deflate(buf, 0, buf.length, Deflater.SYNC_FLUSH); + if (n > 0) + out.write(buf, 0, n); + } while (n == buf.length); + } + out.flush(); + } + public void close() throws IOException { + if (closed) + return; + try { + finish(); + } finally { + closed = true; + if (own) + def.end(); + out.close(); + } + } +} diff --git a/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java b/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java new file mode 100644 index 00000000000..c68d1cb3286 --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java @@ -0,0 +1,103 @@ +/* Copyright (c) 2026 Codename One. GPLv2 with the Classpath exception. */ +package java.util.zip; +import java.io.*; + +/** RFC 1952 decoder, including concatenated members and header/trailer CRC checks. */ +public class GZIPInputStream extends InflaterInputStream { + public static final int GZIP_MAGIC = 0x8b1f; + protected CRC32 crc = new CRC32(); + protected boolean eos; + public GZIPInputStream(InputStream in) throws IOException { + this(in, 512); + } + public GZIPInputStream(InputStream in, int size) throws IOException { + super(new PushbackInputStream(in, Math.max(size, 2)), new Inflater(true), size); + try { + header(); + } catch (IOException ex) { + inf.end(); + throw ex; + } + } + private int byteValue() throws IOException { + int b = in.read(); + if (b < 0) + throw new EOFException("Truncated GZIP"); + return b; + } + private int headerByte() throws IOException { + int b = byteValue(); + crc.update(b); + return b; + } + private void header() throws IOException { + crc.reset(); + if (headerByte() != 31 || headerByte() != 139 || headerByte() != 8) + throw new ZipException("Invalid GZIP header"); + int flags = headerByte(); + if ((flags & 224) != 0) + throw new ZipException("Reserved GZIP flags"); + for (int i = 0; i < 6; i++) headerByte(); + if ((flags & 4) != 0) { + int n = headerByte() | (headerByte() << 8); + while (n-- > 0) headerByte(); + } + if ((flags & 8) != 0) + while (headerByte() != 0) { + } + if ((flags & 16) != 0) + while (headerByte() != 0) { + } + if ((flags & 2) != 0) { + int expected = (int) crc.getValue() & 65535; + if ((byteValue() | (byteValue() << 8)) != expected) + throw new ZipException("Invalid GZIP header CRC"); + } + crc.reset(); + } + private long word() throws IOException { + long result = 0; + for (int i = 0; i < 4; i++) result |= (long) byteValue() << (8 * i); + return result; + } + public int read(byte[] b, int off, int length) throws IOException { + ensureOpen(); + Deflater.bounds(b, off, length); + if (length == 0) + return 0; + while (!eos) { + int n = super.read(b, off, length); + if (n > 0) { + crc.update(b, off, n); + return n; + } + if (!inf.finished()) + throw new ZipException("GZIP dictionary unsupported"); + int left = inf.getRemaining(); + if (left > 0) + ((PushbackInputStream) in).unread(buf, len - left, left); + if (word() != crc.getValue() || word() != (inf.getBytesWritten() & 0xffffffffL)) + throw new ZipException("Invalid GZIP trailer"); + int first = in.read(); + if (first < 0) { + eos = true; + break; + } + ((PushbackInputStream) in).unread(first); + header(); + inf.reset(); + } + return -1; + } + public int available() throws IOException { + ensureOpen(); + return eos ? 0 : 1; + } + public void close() throws IOException { + try { + super.close(); + } finally { + inf.end(); + } + } +} diff --git a/vm/JavaAPI/src/java/util/zip/GZIPOutputStream.java b/vm/JavaAPI/src/java/util/zip/GZIPOutputStream.java new file mode 100644 index 00000000000..2fa684e0f04 --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/GZIPOutputStream.java @@ -0,0 +1,51 @@ +/* Copyright (c) 2026 Codename One. GPLv2 with the Classpath exception. */ +package java.util.zip; +import java.io.*; + +/** RFC 1952 encoder with deterministic (zero) header timestamp. */ +public class GZIPOutputStream extends DeflaterOutputStream { + protected CRC32 crc = new CRC32(); + private boolean trailer; + public GZIPOutputStream(OutputStream out) throws IOException { + this(out, 512, false); + } + public GZIPOutputStream(OutputStream out, int size) throws IOException { + this(out, size, false); + } + public GZIPOutputStream(OutputStream out, boolean syncFlush) throws IOException { + this(out, 512, syncFlush); + } + public GZIPOutputStream(OutputStream out, int size, boolean syncFlush) throws IOException { + super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true), size, syncFlush); + try { + out.write(new byte[] {31, (byte) 139, 8, 0, 0, 0, 0, 0, 0, (byte) 255}); + } catch (IOException ex) { + def.end(); + throw ex; + } + } + public void write(byte[] b, int off, int len) throws IOException { + super.write(b, off, len); + crc.update(b, off, len); + } + public void finish() throws IOException { + if (trailer) + return; + super.finish(); + byte[] b = new byte[8]; + long checksum = crc.getValue(), size = def.getBytesRead(); + for (int i = 0; i < 4; i++) { + b[i] = (byte) (checksum >>> (8 * i)); + b[4 + i] = (byte) (size >>> (8 * i)); + } + out.write(b); + trailer = true; + } + public void close() throws IOException { + try { + super.close(); + } finally { + def.end(); + } + } +} diff --git a/vm/JavaAPI/src/java/util/zip/Inflater.java b/vm/JavaAPI/src/java/util/zip/Inflater.java new file mode 100644 index 00000000000..ad5f44f36ee --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/Inflater.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * This code is licensed under GPLv2 with the Classpath exception. + */ +package java.util.zip; + +/** Pure Java DEFLATE decompressor. */ +public class Inflater { + private com.codename1.impl.zip.Inflater engine; + private final boolean nowrap; + private boolean done, dictionary; + public Inflater() { + this(false); + } + public Inflater(boolean nowrap) { + this.nowrap = nowrap; + init(); + } + private void init() { + engine = new com.codename1.impl.zip.Inflater(); + engine.init(nowrap); + engine.setInput(new byte[0], 0, 0, false); + done = false; + dictionary = false; + } + private void open() { + if (engine == null) + throw new NullPointerException("Inflater ended"); + } + public void setInput(byte[] b) { + setInput(b, 0, b.length); + } + public void setInput(byte[] b, int off, int len) { + Deflater.bounds(b, off, len); + open(); + engine.setInput(b, off, len, false); + } + public void setDictionary(byte[] b) { + setDictionary(b, 0, b.length); + } + public void setDictionary(byte[] b, int off, int len) { + Deflater.bounds(b, off, len); + open(); + byte[] copy = new byte[len]; + System.arraycopy(b, off, copy, 0, len); + if (engine.setDictionary(copy, len) != 0) + throw new IllegalArgumentException("Invalid dictionary"); + dictionary = false; + } + public boolean needsInput() { + return getRemaining() == 0; + } + public boolean needsDictionary() { + return dictionary; + } + public boolean finished() { + return done; + } + public int inflate(byte[] b) throws DataFormatException { + return inflate(b, 0, b.length); + } + public int inflate(byte[] b, int off, int len) throws DataFormatException { + Deflater.bounds(b, off, len); + open(); + if (len == 0 || done) + return 0; + engine.setOutput(b, off, len); + int rc = engine.inflate(0); + if (rc == 1) + done = true; + else if (rc == 2) + dictionary = true; + else if (rc != 0 && rc != -5) + throw new DataFormatException(engine.msg); + return len - engine.availOut; + } + public int getRemaining() { + open(); + return engine.availIn; + } + public int getAdler() { + open(); + return (int) engine.getAdler(); + } + public long getBytesRead() { + open(); + return engine.totalIn; + } + public long getBytesWritten() { + open(); + return engine.totalOut; + } + public int getTotalIn() { + return (int) getBytesRead(); + } + public int getTotalOut() { + return (int) getBytesWritten(); + } + public void reset() { + open(); + engine.end(); + init(); + } + public void end() { + if (engine != null) { + engine.end(); + engine = null; + } + } +} diff --git a/vm/JavaAPI/src/java/util/zip/InflaterInputStream.java b/vm/JavaAPI/src/java/util/zip/InflaterInputStream.java new file mode 100644 index 00000000000..b0aa51685d9 --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/InflaterInputStream.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * This code is licensed under GPLv2 with the Classpath exception. + */ +package java.util.zip; + +import java.io.*; + +/** A streaming DEFLATE decoder; closing it closes its source. */ +public class InflaterInputStream extends FilterInputStream { + protected Inflater inf; + protected byte[] buf; + protected int len; + private boolean own, closed, eof; + public InflaterInputStream(InputStream in) { + this(in, new Inflater(), 512); + own = true; + } + public InflaterInputStream(InputStream in, Inflater inf) { + this(in, inf, 512); + } + public InflaterInputStream(InputStream in, Inflater inf, int size) { + super(in); + if (in == null || inf == null) + throw new NullPointerException(); + if (size <= 0) + throw new IllegalArgumentException("Buffer size must be positive"); + this.inf = inf; + buf = new byte[size]; + } + protected void ensureOpen() throws IOException { + if (closed) + throw new IOException("Stream closed"); + } + public int read() throws IOException { + byte[] b = new byte[1]; + return read(b, 0, 1) < 0 ? -1 : b[0] & 255; + } + public int read(byte[] b, int off, int length) throws IOException { + ensureOpen(); + Deflater.bounds(b, off, length); + if (length == 0) + return 0; + try { + while (true) { + int n = inf.inflate(b, off, length); + if (n > 0) + return n; + if (inf.finished() || inf.needsDictionary()) { + eof = true; + return -1; + } + if (inf.needsInput()) + fill(); + else + throw new ZipException("Inflater made no progress"); + } + } catch (DataFormatException ex) { + throw new ZipException(ex.getMessage()); + } + } + protected void fill() throws IOException { + ensureOpen(); + len = in.read(buf, 0, buf.length); + if (len < 0) + throw new EOFException("Truncated compressed stream"); + if (len == 0) { + int b = in.read(); + if (b < 0) + throw new EOFException(); + buf[0] = (byte) b; + len = 1; + } + inf.setInput(buf, 0, len); + } + public long skip(long n) throws IOException { + if (n < 0) + throw new IllegalArgumentException(); + byte[] b = new byte[512]; + long total = 0; + while (total < n) { + int count = read(b, 0, (int) Math.min(n - total, b.length)); + if (count < 0) + break; + total += count; + } + return total; + } + public int available() throws IOException { + ensureOpen(); + return eof ? 0 : 1; + } + public boolean markSupported() { + return false; + } + public void mark(int limit) { + } + public void reset() throws IOException { + throw new IOException("Mark/reset unsupported"); + } + public void close() throws IOException { + if (!closed) { + closed = true; + if (own) + inf.end(); + in.close(); + } + } +} diff --git a/vm/JavaAPI/src/java/util/zip/ZipEntry.java b/vm/JavaAPI/src/java/util/zip/ZipEntry.java new file mode 100644 index 00000000000..a378512ba46 --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/ZipEntry.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * This code is licensed under GPLv2 with the Classpath exception. + */ +package java.util.zip; + +/** Metadata for a ZIP entry. Directory names end in '/'. */ +public class ZipEntry implements Cloneable { + public static final int STORED = 0, DEFLATED = 8; + private String name, comment; + private long time = -1, size = -1, compressedSize = -1, crc = -1; + private int method = -1; + private byte[] extra; + public ZipEntry(String name) { + if (name == null) + throw new NullPointerException(); + if (name.length() > 65535) + throw new IllegalArgumentException("Entry name too long"); + this.name = name; + } + public ZipEntry(ZipEntry e) { + name = e.name; + comment = e.comment; + time = e.time; + size = e.size; + compressedSize = e.compressedSize; + crc = e.crc; + method = e.method; + extra = e.extra == null ? null : e.extra.clone(); + } + public String getName() { + return name; + } + public void setTime(long time) { + this.time = time; + } + public long getTime() { + return time; + } + public void setSize(long size) { + if (size < 0) + throw new IllegalArgumentException(); + this.size = size; + } + public long getSize() { + return size; + } + public void setCompressedSize(long size) { + compressedSize = size; + } + public long getCompressedSize() { + return compressedSize; + } + public void setCrc(long crc) { + if (crc < 0 || crc > 0xffffffffL) + throw new IllegalArgumentException(); + this.crc = crc; + } + public long getCrc() { + return crc; + } + public void setMethod(int method) { + if (method != STORED && method != DEFLATED) + throw new IllegalArgumentException(); + this.method = method; + } + public int getMethod() { + return method; + } + public void setExtra(byte[] extra) { + if (extra != null && extra.length > 65535) + throw new IllegalArgumentException("Extra too long"); + this.extra = extra; + if (extra != null) + for (int at = 0; at + 4 <= extra.length;) { + int id = (extra[at] & 255) | ((extra[at + 1] & 255) << 8); + int n = (extra[at + 2] & 255) | ((extra[at + 3] & 255) << 8); + if (n > extra.length - at - 4) + break; + if (id == 0x5455 && n >= 5 && (extra[at + 4] & 1) != 0) { + int seconds = (extra[at + 5] & 255) | ((extra[at + 6] & 255) << 8) | ((extra[at + 7] & 255) << 16) + | (extra[at + 8] << 24); + time = seconds * 1000L; + } + at += 4 + n; + } + } + public byte[] getExtra() { + return extra; + } + public void setComment(String comment) { + this.comment = comment; + } + public String getComment() { + return comment; + } + public boolean isDirectory() { + return name.endsWith("/"); + } + public String toString() { + return name; + } + public int hashCode() { + return name.hashCode(); + } + public Object clone() { + return new ZipEntry(this); + } +} diff --git a/vm/JavaAPI/src/java/util/zip/ZipException.java b/vm/JavaAPI/src/java/util/zip/ZipException.java new file mode 100644 index 00000000000..b3dcc91e8a3 --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/ZipException.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * This code is licensed under GPLv2 with the Classpath exception. + */ +package java.util.zip; + +/** Reports invalid compressed data. */ +public class ZipException extends java.io.IOException { + public ZipException() { + } + public ZipException(String message) { + super(message); + } +} diff --git a/vm/JavaAPI/src/java/util/zip/ZipInputStream.java b/vm/JavaAPI/src/java/util/zip/ZipInputStream.java new file mode 100644 index 00000000000..f2ae18f2f81 --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/ZipInputStream.java @@ -0,0 +1,185 @@ +/* Copyright (c) 2026 Codename One. GPLv2 with the Classpath exception. */ +package java.util.zip; + +import java.io.*; +import java.nio.charset.Charset; +import java.util.Calendar; + +/** Sequential ZIP reader for STORED and DEFLATED entries. ZIP64 and encryption are refused. */ +public class ZipInputStream extends InflaterInputStream { + private final CRC32 crc = new CRC32(); + private final String encoding; + private ZipEntry entry; + private long remaining, count; + private int flags; + private boolean end; + public ZipInputStream(InputStream in) { + this(in, Charset.forName("UTF-8")); + } + public ZipInputStream(InputStream in, Charset charset) { + super(new PushbackInputStream(in, 8192), new Inflater(true), 8192); + if (charset == null) + throw new NullPointerException(); + encoding = charset.displayName(); + } + public ZipEntry getNextEntry() throws IOException { + ensureOpen(); + closeEntry(); + if (end) + return null; + int first = in.read(); + if (first < 0) { + end = true; + return null; + } + long signature = first | (word(3) << 8); + if (signature == 0x02014b50L || signature == 0x06054b50L) { + end = true; + return null; + } + if (signature != 0x04034b50L) + throw new ZipException("Invalid ZIP local header"); + word(2); + flags = (int) word(2); + int method = (int) word(2); + long time = word(4); + long expectedCrc = word(4), compressed = word(4), size = word(4); + int nameLength = (int) word(2), extraLength = (int) word(2); + if ((flags & (1 | 0x40 | 0x2000)) != 0) + throw new ZipException("Encrypted ZIP unsupported"); + if (method != ZipEntry.STORED && method != ZipEntry.DEFLATED) + throw new ZipException("Unsupported ZIP method"); + if (method == ZipEntry.STORED && (flags & 8) != 0) + throw new ZipException("STORED descriptor unsupported"); + if (size == 0xffffffffL || compressed == 0xffffffffL) + throw new ZipException("ZIP64 unsupported"); + byte[] name = bytes(nameLength), extra = bytes(extraLength); + int at = 0; + while (at < extra.length) { + if (extra.length - at < 4) + throw new ZipException("Malformed ZIP extra"); + int id = (extra[at] & 255) | ((extra[at + 1] & 255) << 8); + int n = (extra[at + 2] & 255) | ((extra[at + 3] & 255) << 8); + if (id == 1) + throw new ZipException("ZIP64 unsupported"); + if (n > extra.length - at - 4) + throw new ZipException("Malformed ZIP extra"); + at += 4 + n; + } + entry = createZipEntry(new String(name, (flags & 0x800) != 0 ? "UTF-8" : encoding)); + entry.setMethod(method); + Calendar c = Calendar.getInstance(); + c.set(Calendar.YEAR, 1980 + (int) (time >>> 25)); + c.set(Calendar.MONTH, (int) ((time >>> 21) & 15) - 1); + c.set(Calendar.DAY_OF_MONTH, (int) ((time >>> 16) & 31)); + c.set(Calendar.HOUR_OF_DAY, (int) ((time >>> 11) & 31)); + c.set(Calendar.MINUTE, (int) ((time >>> 5) & 63)); + c.set(Calendar.SECOND, (int) ((time & 31) * 2)); + c.set(Calendar.MILLISECOND, 0); + entry.setTime(c.getTime().getTime()); + entry.setExtra(extra); + if ((flags & 8) == 0) { + entry.setCrc(expectedCrc); + entry.setSize(size); + entry.setCompressedSize(compressed); + } + if (method == ZipEntry.STORED && size != compressed) + throw new ZipException("Invalid STORED sizes"); + remaining = size; + count = 0; + crc.reset(); + inf.reset(); + return entry; + } + protected ZipEntry createZipEntry(String name) { + return new ZipEntry(name); + } + public int read(byte[] b, int off, int length) throws IOException { + ensureOpen(); + Deflater.bounds(b, off, length); + if (length == 0) + return 0; + if (entry == null) + return -1; + int n; + if (entry.getMethod() == ZipEntry.STORED) { + n = remaining == 0 ? -1 : in.read(b, off, (int) Math.min(remaining, length)); + if (n < 0 && remaining != 0) + throw new EOFException("Truncated STORED entry"); + if (n > 0) + remaining -= n; + } else + n = super.read(b, off, length); + if (n > 0) { + crc.update(b, off, n); + count += n; + if (count >= 0xffffffffL) + throw new ZipException("ZIP64 unsupported"); + return n; + } + if (n == 0) + return 0; + long compressed = count; + if (entry.getMethod() == ZipEntry.DEFLATED) { + if (!inf.finished()) + throw new ZipException("ZIP entry requires a dictionary"); + compressed = inf.getBytesRead(); + int left = inf.getRemaining(); + if (left > 0) + ((PushbackInputStream) in).unread(buf, len - left, left); + } + if ((flags & 8) != 0) { + long value = word(4); + if (value == 0x08074b50L) + value = word(4); + entry.setCrc(value); + entry.setCompressedSize(word(4)); + entry.setSize(word(4)); + } + if (entry.getCrc() != crc.getValue() || entry.getSize() != count || entry.getCompressedSize() != compressed) + throw new ZipException("ZIP entry CRC or size mismatch"); + entry = null; + return -1; + } + public void closeEntry() throws IOException { + ensureOpen(); + byte[] b = new byte[8192]; + while (entry != null && read(b, 0, b.length) >= 0) { + } + } + public int available() throws IOException { + ensureOpen(); + return entry == null ? 0 : 1; + } + public void close() throws IOException { + try { + super.close(); + } finally { + inf.end(); + } + } + private long word(int n) throws IOException { + long value = 0; + for (int i = 0; i < n; i++) { + int b = in.read(); + if (b < 0) + throw new EOFException("Truncated ZIP"); + value |= (long) b << (8 * i); + } + return value; + } + private byte[] bytes(int n) throws IOException { + byte[] b = new byte[n]; + int off = 0; + while (off < n) { + int read = in.read(b, off, n - off); + if (read < 0) + throw new EOFException("Truncated ZIP"); + if (read == 0) { + b[off++] = (byte) word(1); + } else + off += read; + } + return b; + } +} diff --git a/vm/JavaAPI/src/java/util/zip/ZipOutputStream.java b/vm/JavaAPI/src/java/util/zip/ZipOutputStream.java new file mode 100644 index 00000000000..11116c6e0e4 --- /dev/null +++ b/vm/JavaAPI/src/java/util/zip/ZipOutputStream.java @@ -0,0 +1,405 @@ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * This code is licensed under GPLv2 with the Classpath exception. + */ +package java.util.zip; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashSet; +import java.util.zip.CRC32; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; + +/** + * Streaming ZIP writer for ParparVM. Payloads are never buffered in full. Central-directory metadata is + * limited to 16 MiB and 65534 entries; ZIP64 is explicitly refused. + * finish() leaves the destination open; close() closes it even after a failure. + * Input streams are owned by the caller. This stream is not thread safe. + */ +public class ZipOutputStream extends DeflaterOutputStream { + public static final int STORED = 0, DEFLATED = 8; + private static final long LIMIT = 0xffffffffL; + private static final int METADATA_LIMIT = 16 * 1024 * 1024; + private final ArrayList entries = new ArrayList(); + private final HashSet names = new HashSet(); + private final CRC32 crc = new CRC32(); + private final String encoding; + private final int encodingFlag; + private Record current; + private ZipEntry original; + private int method = DEFLATED, metadataSize; + private long written, entryStart, entrySize; + private boolean finished, closed; + private IOException failure; + private byte[] comment = new byte[0]; + + private static final class Record { + ZipEntry entry; + byte[] name, extra, comment; + long offset, dosTime, attributes; + int flags, platform; + } + + public ZipOutputStream(OutputStream out) { + this(out, Charset.forName("UTF-8")); + } + public ZipOutputStream(OutputStream out, Charset charset) { + super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true), 8192); + if (charset == null) + throw new NullPointerException(); + encoding = charset.displayName(); + encodingFlag = "UTF-8".equalsIgnoreCase(encoding) ? 0x800 : 0; + } + + public void setMethod(int method) { + if (method != STORED && method != DEFLATED) + throw new IllegalArgumentException("Invalid method"); + this.method = method; + } + public void setLevel(int level) { + def.setLevel(level); + } + public void setComment(String comment) { + try { + this.comment = encode(comment == null ? "" : comment); + } catch (IOException ex) { + throw new IllegalArgumentException(ex.getMessage()); + } + } + private byte[] encode(String value) throws IOException { + byte[] bytes = value.getBytes(encoding); + if (bytes.length > 65535) + throw new ZipException("Encoded ZIP field exceeds 65535 bytes"); + return bytes; + } + private void writable() throws IOException { + if (closed) + throw new IOException("ZIP stream closed"); + if (failure != null) + throw failure; + if (finished) + throw new IOException("ZIP stream finished"); + } + /** Writes an entry without Unix attributes, like java.util.zip.ZipOutputStream. */ + public void putNextEntry(ZipEntry entry) throws IOException { + putEntry(entry, 0, 0); + } + + private void putEntry(ZipEntry entry, int platform, long attributes) throws IOException { + writable(); + if (entry == null) + throw new NullPointerException(); + closeEntry(); + if (entries.size() >= 65534) + throw new ZipException("ZIP64 entry count unsupported"); + if (names.contains(entry.getName())) + throw new ZipException("Duplicate entry: " + entry.getName()); + Record r = new Record(); + r.entry = new ZipEntry(entry); + if (r.entry.getMethod() == -1) + r.entry.setMethod(method); + if (r.entry.getTime() == -1) + r.entry.setTime(System.currentTimeMillis()); + r.name = encode(r.entry.getName()); + r.comment = encode(r.entry.getComment() == null ? "" : r.entry.getComment()); + byte[] extra = r.entry.getExtra(); + r.extra = extra == null ? new byte[0] : extra.clone(); + validateExtra(r.extra); + r.extra = timestampExtra(r.extra, r.entry.getTime()); + if (r.name.length == 0) + throw new ZipException("Empty entry name"); + checkSize(r.entry.getSize()); + checkSize(r.entry.getCompressedSize()); + if (r.entry.getMethod() == STORED) { + if (r.entry.getSize() < 0 && r.entry.getCompressedSize() >= 0) + r.entry.setSize(r.entry.getCompressedSize()); + if (r.entry.getCompressedSize() < 0) + r.entry.setCompressedSize(r.entry.getSize()); + if (r.entry.getSize() < 0 || r.entry.getCrc() < 0 || r.entry.getSize() != r.entry.getCompressedSize()) + throw new ZipException("STORED entries require size and CRC before writing"); + } + int metadata = 46 + r.name.length + r.extra.length + r.comment.length; + if (metadataSize > METADATA_LIMIT - metadata) + throw new ZipException("ZIP central directory exceeds 16 MiB"); + r.flags = encodingFlag | (r.entry.getMethod() == DEFLATED ? 8 : 0); + r.platform = platform; + r.attributes = attributes; + r.offset = written; + r.dosTime = dosTime(r.entry.getTime()); + current = r; + original = entry; + entrySize = 0; + crc.reset(); + if (r.entry.getMethod() == DEFLATED) + def.reset(); + try { + word(0x04034b50L, 4); + word(20, 2); + word(r.flags, 2); + word(r.entry.getMethod(), 2); + word(r.dosTime, 4); + word(r.entry.getMethod() == STORED ? r.entry.getCrc() : 0, 4); + word(r.entry.getMethod() == STORED ? r.entry.getSize() : 0, 4); + word(r.entry.getMethod() == STORED ? r.entry.getSize() : 0, 4); + word(r.name.length, 2); + word(r.extra.length, 2); + raw(r.name); + raw(r.extra); + entryStart = written; + entries.add(r); + names.add(r.entry.getName()); + metadataSize += metadata; + } catch (IOException ex) { + failure = ex; + throw ex; + } + } + private static void validateExtra(byte[] extra) throws ZipException { + int at = 0; + while (at < extra.length) { + if (extra.length - at < 4) + throw new ZipException("Malformed ZIP extra field"); + int id = (extra[at] & 255) | ((extra[at + 1] & 255) << 8); + int size = (extra[at + 2] & 255) | ((extra[at + 3] & 255) << 8); + if (id == 1) + throw new ZipException("ZIP64 extra field unsupported"); + if (size > extra.length - at - 4) + throw new ZipException("Malformed ZIP extra field"); + at += 4 + size; + } + } + private static void checkSize(long size) throws ZipException { + if (size >= LIMIT) + throw new ZipException("ZIP64 size unsupported"); + } + private static byte[] timestampExtra(byte[] extra, long millis) throws ZipException { + long seconds = millis / 1000; + if (millis < 0 && millis % 1000 != 0) + seconds--; + if (seconds < Integer.MIN_VALUE || seconds > Integer.MAX_VALUE) + return extra; + // Replace any caller-provided extended timestamp with the entry's time. + int kept = 0; + for (int at = 0; at < extra.length;) { + int size = 4 + (extra[at + 2] & 255) + ((extra[at + 3] & 255) << 8); + if ((extra[at] & 255) != 0x55 || (extra[at + 1] & 255) != 0x54) + kept += size; + at += size; + } + if (kept > 65535 - 9) + throw new ZipException("No room for ZIP timestamp extra field"); + byte[] result = new byte[kept + 9]; + int pos = 0; + for (int at = 0; at < extra.length;) { + int size = 4 + (extra[at + 2] & 255) + ((extra[at + 3] & 255) << 8); + if ((extra[at] & 255) != 0x55 || (extra[at + 1] & 255) != 0x54) { + System.arraycopy(extra, at, result, pos, size); + pos += size; + } + at += size; + } + result[pos] = 0x55; + result[pos + 1] = 0x54; + result[pos + 2] = 5; + result[pos + 4] = 1; + for (int i = 0; i < 4; i++) result[pos + 5 + i] = (byte) (seconds >>> (8 * i)); + return result; + } + private static long dosTime(long millis) { + // The deterministic epoch timestamp predates the DOS range in every + // timezone. Avoid calendar/timezone lookups for each such entry. + if (millis == 0) + return 0x00210000L; + Calendar c = Calendar.getInstance(); + c.setTime(new java.util.Date(millis)); + int year = c.get(Calendar.YEAR); + if (year < 1980) + return 0x00210000L; + if (year > 2107) + return 0xff9fbf7dL; + return ((long) (year - 1980) << 25) | ((c.get(Calendar.MONTH) + 1) << 21) | (c.get(Calendar.DAY_OF_MONTH) << 16) + | (c.get(Calendar.HOUR_OF_DAY) << 11) | (c.get(Calendar.MINUTE) << 5) | (c.get(Calendar.SECOND) >> 1); + } + public void write(int value) throws IOException { + write(new byte[] {(byte) value}, 0, 1); + } + public void write(byte[] data, int off, int len) throws IOException { + writable(); + if (data == null) + throw new NullPointerException(); + if (off < 0 || len < 0 || off > data.length - len) + throw new IndexOutOfBoundsException(); + if (current == null) + throw new ZipException("No current ZIP entry"); + try { + checkSize(entrySize + len); + if (current.entry.getMethod() == STORED && entrySize + len > current.entry.getSize()) + throw new ZipException("STORED entry exceeds declared size"); + crc.update(data, off, len); + entrySize += len; + if (current.entry.getMethod() == STORED) + raw(data, off, len); + else { + def.setInput(data, off, len); + while (!def.needsInput()) deflate(); + } + } catch (IOException ex) { + failure = ex; + throw ex; + } + } + protected void deflate() throws IOException { + int n = def.deflate(buf, 0, buf.length); + if (n > 0) + raw(buf, 0, n); + } + public void closeEntry() throws IOException { + writable(); + if (current == null) + return; + try { + ZipEntry e = current.entry; + if (e.getMethod() == DEFLATED) { + def.finish(); + while (!def.finished()) deflate(); + } + long compressed = written - entryStart; + checkSize(compressed); + if ((e.getSize() >= 0 && e.getSize() != entrySize) + || (e.getCompressedSize() >= 0 && e.getCompressedSize() != compressed) + || (e.getCrc() >= 0 && e.getCrc() != crc.getValue())) + throw new ZipException("ZIP entry size or CRC mismatch"); + e.setSize(entrySize); + e.setCompressedSize(compressed); + e.setCrc(crc.getValue()); + original.setSize(entrySize); + original.setCompressedSize(compressed); + original.setCrc(crc.getValue()); + if (e.getMethod() == DEFLATED) { + word(0x08074b50L, 4); + word(e.getCrc(), 4); + word(compressed, 4); + word(entrySize, 4); + } + current = null; + original = null; + } catch (IOException ex) { + failure = ex; + throw ex; + } + } + public void finish() throws IOException { + if (closed) + throw new IOException("ZIP stream closed"); + if (failure != null) + throw failure; + if (finished) + return; + closeEntry(); + try { + long start = written; + for (Record r : entries) { + ZipEntry e = r.entry; + word(0x02014b50L, 4); + word((r.platform << 8) | 20, 2); + word(20, 2); + word(r.flags, 2); + word(e.getMethod(), 2); + word(r.dosTime, 4); + word(e.getCrc(), 4); + word(e.getCompressedSize(), 4); + word(e.getSize(), 4); + word(r.name.length, 2); + word(r.extra.length, 2); + word(r.comment.length, 2); + word(0, 2); + word(0, 2); + word(r.attributes, 4); + word(r.offset, 4); + raw(r.name); + raw(r.extra); + raw(r.comment); + } + long length = written - start; + word(0x06054b50L, 4); + word(0, 2); + word(0, 2); + word(entries.size(), 2); + word(entries.size(), 2); + word(length, 4); + word(start, 4); + word(comment.length, 2); + raw(comment); + finished = true; + entries.clear(); + names.clear(); + } catch (IOException ex) { + failure = ex; + throw ex; + } + } + public void flush() throws IOException { + if (closed) + throw new IOException("ZIP stream closed"); + if (failure != null) + throw failure; + try { + out.flush(); + } catch (IOException ex) { + failure = ex; + throw ex; + } + } + public void close() throws IOException { + if (closed) + return; + // An earlier write already delivered failure to the caller. Throwing + // that same object again from close breaks try-with-resources through + // Throwable's self-suppression guard. + IOException error = null; + try { + if (failure == null) + finish(); + } catch (IOException ex) { + error = ex; + } finally { + closed = true; + def.end(); + entries.clear(); + names.clear(); + current = null; + original = null; + try { + out.close(); + } catch (IOException ex) { + if (error == null) + error = ex; + } + } + if (error != null) + throw error; + } + private void word(long value, int bytes) throws IOException { + byte[] b = new byte[bytes]; + for (int i = 0; i < bytes; i++) { + b[i] = (byte) value; + value >>>= 8; + } + raw(b); + } + private void raw(byte[] data) throws IOException { + raw(data, 0, data.length); + } + private void raw(byte[] data, int off, int len) throws IOException { + checkSize(written + len); + out.write(data, off, len); + written += len; + } +} diff --git a/vm/README.md b/vm/README.md index 2e69ec1fb84..58ec08e0211 100644 --- a/vm/README.md +++ b/vm/README.md @@ -119,3 +119,24 @@ Parpar is a butterfly in Hebrew. Initially when we worked at Sun and produced LWUIT we wanted to call it Morpho (butterfly I think in Spanish) but marketing shot it down. Unfortunately MorphoVM is already taken so we decided to go with ParparVM instead. + +### Streaming ZIP support + +`JavaAPI` includes the streaming `java.util.zip` API, using the existing JZlib +engine. `com.codename1.io.zip.ZipArchiveOutputStream` adds portable Unix file +permissions for JVM and native applications. See the **Streaming ZIP archives** +section in `docs/developer-guide/Backend-Operations.asciidoc` for the supported +classes, ZIP64 refusal, metadata limits, and stream ownership. + +The compression sources under `JavaAPI/src/com/codename1/impl/zip` and the +standard `ZipOutputStream` are generated from canonical core sources. After +editing the engine or `ZipArchiveOutputStream`, run +`python3 scripts/sync-zip-runtime.py` from the repository root. Use `--check` to +check for drift. After compiling `vm/JavaAPI` with JDK 8, regenerate the mobile +API declarations with `JAVA_HOME=/path/to/jdk8 python3 scripts/generate-zip-api-stubs.py`. +The latter also supports `--check`. + +`ZipApiIntegrationTest` runs the same archive program against the JDK, the actual +JavaAPI ZIP classes on HotSpot, and a locally translated ParparVM executable. +It uses the JDK ZIP reader and system `unzip` as independent readers, checks Unix +mode bits after extraction, and compares contents and metadata across runtimes. diff --git a/vm/backend/demo/zipdownload/com/demo/ZipDownload.java b/vm/backend/demo/zipdownload/com/demo/ZipDownload.java new file mode 100644 index 00000000000..d0aea2777ba --- /dev/null +++ b/vm/backend/demo/zipdownload/com/demo/ZipDownload.java @@ -0,0 +1,65 @@ +/* Copyright (c) 2026 Codename One. GPLv2 with the Classpath exception. */ +package com.demo; + +import com.codename1.backend.Backend; +import com.codename1.backend.HttpServer; +import com.codename1.io.zip.ZipArchiveOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.ZipEntry; + +/** A small, fixed-size downloadable project. No temporary files are created. */ +public final class ZipDownload { + private static void archive(OutputStream destination) throws IOException { + // Closing the ZIP owns destination. For a caller-owned stream use + // finish(), then release the writer with a non-closing destination wrapper. + try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(destination)) { + ZipEntry dir = new ZipEntry("project/"); + dir.setTime(0); + zip.putNextEntry(dir, 0755); + zip.closeEntry(); + ZipEntry launcher = new ZipEntry("project/run.sh"); + launcher.setTime(0); + zip.putNextEntry(launcher, 0755); + zip.write("#!/bin/sh\nprintf 'Hello from the archive\\n'\n".getBytes("UTF-8")); + zip.closeEntry(); + ZipEntry readme = new ZipEntry("project/README.txt"); + readme.setTime(0); + zip.putNextEntry(readme, 0644); + zip.write("Run ./run.sh after extraction.\n".getBytes("UTF-8")); + zip.closeEntry(); + } + } + public static void main(String[] args) throws Exception { + if (args.length == 2 && "--write".equals(args[0])) { + archive(new FileOutputStream(args[1])); + System.out.println("ZIP_DOWNLOAD_OK"); + return; + } + // This demo contains only the fixed literals above. Do not accumulate + // arbitrary user files in memory: write those to an owned output stream. + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + archive(bytes); + final byte[] download = bytes.toByteArray(); + Backend server = + Backend.builder() + .host("127.0.0.1") + .port(8099) + .handler(new HttpServer.Handler() { + public HttpServer.Response handle(HttpServer.Request request) { + if (!"GET".equals(request.getMethod()) || !"/project.zip".equals(request.getTarget())) + return HttpServer.Response.text(404, "Not found"); + return new HttpServer.Response(200, "application/zip", download) + .header("Content-Disposition", "attachment; filename=project.zip"); + } + }) + .start(); + try { + server.awaitTermination(); + } finally { + server.stop(); + } + } +} diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java new file mode 100644 index 00000000000..f73da19ce98 --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java @@ -0,0 +1,124 @@ +package com.codename1.tools.translator; + +import org.junit.jupiter.api.Test; +import java.io.*; +import java.nio.file.*; +import java.util.*; +import java.util.zip.*; +import static org.junit.jupiter.api.Assertions.*; + +/** Tests the real JavaAPI classes, not accidentally the host JDK's java.util.zip. */ +class ZipApiIntegrationTest { + @Test + void streamingArchivesInteroperateOnJvmAndTranslatedRuntime() throws Exception { + Parser.cleanup(); + Path repo = Paths.get("..").toAbsolutePath().normalize(); + if (!Files.isDirectory(repo.resolve("vm"))) + repo = repo.getParent(); + Path work = Files.createTempDirectory("zip-api-integration-"); + CompilerHelper.CompilerConfig config = CompilerHelper.getAvailableCompilers("1.8").get(0); + Path api = work.resolve("api"), app = work.resolve("app"), patch = work.resolve("patch"); + Files.createDirectories(api); + Files.createDirectories(app); + CompilerHelper.compileJavaAPI(api, config); + Path source = work.resolve("ZipApiApp.java"); + try (InputStream in = getClass().getResourceAsStream("/com/codename1/tools/translator/ZipApiApp.java")) { + assertNotNull(in); + Files.copy(in, source); + } + List args = Arrays.asList("-source", "1.8", "-target", "1.8", "-encoding", "UTF-8", "-bootclasspath", + api.toString(), "-d", app.toString(), source.toString(), + repo.resolve("CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java").toString(), + repo.resolve("CodenameOne/src/com/codename1/impl/SharedWithBackend.java").toString()); + assertEquals(0, CompilerHelper.compile(config.jdkHome, args), CompilerHelper.getLastErrorLog()); + String java = config.jdkHome.resolve("bin/java").toString(); + Path jdk = work.resolve("jdk"), patched = work.resolve("patched"), nativePrefix = work.resolve("native"); + assertTrue(run(work, java, "-cp", app.toString(), "ZipApiApp", jdk.toString()).contains("ZIP_API_OK")); + // Patch only ZIP and its engine. Replacing Object/String on HotSpot is invalid. + CompilerHelper.copyDirectory(api.resolve("java/util/zip"), patch.resolve("java/util/zip")); + CompilerHelper.copyDirectory(api.resolve("com/codename1/impl/zip"), patch.resolve("com/codename1/impl/zip")); + assertTrue(run(work, java, "-Xbootclasspath/p:" + patch, "-cp", app.toString(), "ZipApiApp", patched.toString(), + jdk + "-standard.zip") + .contains("ZIP_API_OK")); + verifyArchive(patched, jdk); + CompilerHelper.copyDirectory(api, app); + Path output = work.resolve("translated"); + Files.createDirectories(output); + CleanTargetIntegrationTest.runTranslator(app, output, "ZipApiApp"); + Path dist = output.resolve("dist"), build = dist.resolve("build"); + CleanTargetIntegrationTest.replaceLibraryWithExecutableTarget(dist.resolve("CMakeLists.txt"), "ZipApiApp-src"); + run(dist, "cmake", "-S", dist.toString(), "-B", build.toString(), "-DCMAKE_C_COMPILER=clang", + "-DCMAKE_OBJC_COMPILER=clang"); + run(dist, "cmake", "--build", build.toString(), "-j", "4"); + assertTrue(run(work, build.resolve("ZipApiApp").toString(), nativePrefix.toString(), jdk + "-standard.zip") + .contains("ZIP_API_OK")); + verifyArchive(nativePrefix, jdk); + System.out.println("ZIP artifacts verified in " + work); + } + private static String run(Path dir, String... command) throws Exception { + Path log = Files.createTempFile(dir, "zip-command-", ".log"); + Process process = new ProcessBuilder(command).directory(dir.toFile()) + .redirectErrorStream(true).redirectOutput(log.toFile()).start(); + if (!process.waitFor(180, java.util.concurrent.TimeUnit.SECONDS)) { + process.destroyForcibly(); + fail("ZIP validation command timed out: " + Arrays.toString(command)); + } + String output = new String(Files.readAllBytes(log), java.nio.charset.StandardCharsets.UTF_8); + assertEquals(0, process.exitValue(), Arrays.toString(command) + "\n" + output); + return output; + } + private static void verifyArchive(Path prefix, Path reference) throws Exception { + for (String suffix : Arrays.asList(".zip", "-standard.zip")) { + try (ZipFile actual = new ZipFile(prefix + suffix); ZipFile expected = new ZipFile(reference + suffix)) { + assertEquals(expected.size(), actual.size()); + Enumeration entries = expected.entries(); + while (entries.hasMoreElements()) { + ZipEntry e = entries.nextElement(), a = actual.getEntry(e.getName()); + assertNotNull(a); + assertEquals(e.getMethod(), a.getMethod()); + assertEquals(e.getSize(), a.getSize()); + assertEquals(e.getCrc(), a.getCrc()); + assertEquals(e.getTime(), a.getTime()); + assertEquals(e.getComment(), a.getComment()); + try (InputStream x = expected.getInputStream(e); InputStream y = actual.getInputStream(a)) { + assertArrayEquals(bytes(x), bytes(y)); + } + } + } + } + byte[] archive = Files.readAllBytes(Paths.get(prefix + ".zip")); + // Central-directory Unix metadata is not exposed by the JDK ZipFile API. + int end = archive.length - 22 - "archive comment".length(); + int at = (int) u32(archive, end + 16); + int[] modes = {040755, 0100755, 0100644, 0100644}; + for (int mode : modes) { + assertEquals(0x02014b50L, u32(archive, at)); + assertEquals(3, archive[at + 5]); + assertEquals(mode, u32(archive, at + 38) >>> 16); + assertEquals(0x800, u16(archive, at + 8) & 0x800); + at += 46 + u16(archive, at + 28) + u16(archive, at + 30) + u16(archive, at + 32); + } + try (InputStream in = new GZIPInputStream(new FileInputStream(prefix + ".gz")); + InputStream expected = new GZIPInputStream(new FileInputStream(reference + ".gz"))) { + assertArrayEquals(bytes(expected), bytes(in)); + } + run(prefix.getParent(), "unzip", "-t", prefix + ".zip"); + Path extracted = Files.createTempDirectory(prefix.getParent(), "unzip-"); + run(prefix.getParent(), "unzip", "-q", prefix + ".zip", "-d", extracted.toString()); + assertTrue( + Files.isExecutable(extracted.resolve("project/run.sh")), "Unzip must retain launcher executable bits"); + } + private static byte[] bytes(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] b = new byte[8192]; + int n; + while ((n = in.read(b)) != -1) out.write(b, 0, n); + return out.toByteArray(); + } + private static int u16(byte[] b, int at) { + return (b[at] & 255) | ((b[at + 1] & 255) << 8); + } + private static long u32(byte[] b, int at) { + return u16(b, at) | ((long) u16(b, at + 2) << 16); + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java new file mode 100644 index 00000000000..f7da1234f29 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java @@ -0,0 +1,363 @@ +import java.io.*; +import java.util.*; +import java.util.zip.*; +import com.codename1.io.zip.ZipArchiveOutputStream; + +public class ZipApiApp { + static void require(boolean value, String message) { + if (!value) + throw new RuntimeException(message); + } + static byte[] readAll(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] b = new byte[7919]; + int n; + while ((n = in.read(b)) != -1) out.write(b, 0, n); + return out.toByteArray(); + } + static ZipEntry entry(String name) { + ZipEntry e = new ZipEntry(name); + e.setTime(0); + return e; + } + static void checksums() throws Exception { + byte[] data = "123456789".getBytes("UTF-8"); + CRC32 crc = new CRC32(); + crc.update(data); + require(crc.getValue() == 0xcbf43926L, "CRC32"); + Adler32 adler = new Adler32(); + adler.update(data); + require(adler.getValue() == 0x091e01deL, "Adler32"); + crc.reset(); + CheckedOutputStream out = new CheckedOutputStream(new ByteArrayOutputStream(), crc); + out.write(data); + require(crc.getValue() == 0xcbf43926L, "checked output"); + adler.reset(); + CheckedInputStream in = new CheckedInputStream(new ByteArrayInputStream(data), adler); + require(in.skip(3) == 3, "checked skip"); + readAll(in); + require(adler.getValue() == 0x091e01deL, "checked input"); + } + static byte[] payload() { + byte[] data = new byte[180000]; + Random r = new Random(42); + for (int i = 0; i < data.length; i++) data[i] = i % 5 == 0 ? (byte) r.nextInt() : (byte) (i % 31); + return data; + } + static void compression(byte[] data) throws Exception { + for (int raw = 0; raw < 2; raw++) + for (int level = 0; level <= 9; level += 3) { + Deflater def = new Deflater(level, raw == 1); + Inflater inf = new Inflater(raw == 1); + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DeflaterOutputStream out = new DeflaterOutputStream(bytes, def, 137); + for (int at = 0; at < data.length; at += 101) out.write(data, at, Math.min(101, data.length - at)); + out.finish(); + require(Arrays.equals(data, + readAll(new InflaterInputStream( + new ByteArrayInputStream(bytes.toByteArray()), inf, 113))), + "deflate round trip"); + require(def.getBytesRead() == data.length && inf.getBytesWritten() == data.length, "byte counts"); + def.reset(); + inf.reset(); + require(def.getBytesRead() == 0 && inf.getBytesWritten() == 0, "reset"); + } finally { + def.end(); + inf.end(); + } + } + byte[] dictionary = "shared-dictionary-shared-dictionary".getBytes("UTF-8"); + byte[] dictionaryData = new byte[10000]; + for (int n = 0; n < dictionaryData.length; n++) dictionaryData[n] = dictionary[n % dictionary.length]; + for (int raw = 0; raw < 2; raw++) { + Deflater d = new Deflater(6, raw == 1); + Inflater i = new Inflater(raw == 1); + try { + d.setDictionary(dictionary); + d.setInput(dictionaryData); + d.finish(); + ByteArrayOutputStream compressed = new ByteArrayOutputStream(); + byte[] buf = new byte[97]; + while (!d.finished()) { + int n = d.deflate(buf); + compressed.write(buf, 0, n); + } + if (raw == 1) i.setDictionary(dictionary); + i.setInput(compressed.toByteArray()); + ByteArrayOutputStream decoded = new ByteArrayOutputStream(); + while (!i.finished()) { + int n = i.inflate(buf); + decoded.write(buf, 0, n); + if (i.needsDictionary()) i.setDictionary(dictionary); + } + require(Arrays.equals(dictionaryData, decoded.toByteArray()), "dictionary raw=" + raw); + } finally { + d.end(); + i.end(); + } + } + } + + static void gzip(byte[] data, String prefix) throws Exception { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + GZIPOutputStream g = new GZIPOutputStream(b, 123, true); + g.write(data, 0, 37); + g.flush(); + g.write(data, 37, data.length - 37); + g.finish(); + g.close(); + byte[] encoded = b.toByteArray(); + try (FileOutputStream out = new FileOutputStream(prefix + ".gz")) { + out.write(encoded); + } + require(Arrays.equals(data, readAll(new GZIPInputStream(new ByteArrayInputStream(encoded), 71))), "gzip"); + ByteArrayOutputStream twice = new ByteArrayOutputStream(); + twice.write(encoded); + twice.write(encoded); + require(readAll(new GZIPInputStream(new ByteArrayInputStream(twice.toByteArray()), 71)).length + == data.length * 2, + "concatenated gzip"); + encoded[encoded.length - 8] ^= 1; + boolean corrupt = false; + try { + readAll(new GZIPInputStream(new ByteArrayInputStream(encoded))); + } catch (ZipException ex) { + corrupt = true; + } + require(corrupt, "gzip crc failure"); + } + static void archive(byte[] data, String prefix) throws Exception { + try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(prefix + "-standard.zip"))) { + out.putNextEntry(entry("standard.txt")); + out.write(data); + out.closeEntry(); + out.putNextEntry(entry("empty")); + out.closeEntry(); + out.finish(); + out.finish(); + } + try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(new FileOutputStream(prefix + ".zip"))) { + out.setComment("archive comment"); + out.putNextEntry(entry("project/"), 0755); + out.closeEntry(); + ZipEntry launcher = entry("project/run.sh"); + launcher.setComment("launcher"); + out.putNextEntry(launcher, 0755); + out.write("#!/bin/sh\necho works\n".getBytes("UTF-8")); + out.closeEntry(); + ZipEntry stored = entry("project/zero"); + stored.setMethod(ZipEntry.STORED); + stored.setSize(0); + stored.setCrc(0); + out.putNextEntry(stored, 0644); + out.closeEntry(); + out.putNextEntry(entry("project/caf\u00e9-\u05e9.txt"), 0644); + for (int at = 0; at < data.length; at += 4093) out.write(data, at, Math.min(4093, data.length - at)); + out.closeEntry(); + boolean duplicate = false; + try { + out.putNextEntry(entry("project/run.sh")); + } catch (ZipException ex) { + duplicate = true; + } + require(duplicate, "duplicates refused"); + ZipEntry large = entry("large"); + large.setSize(0xffffffffL); + boolean limit = false; + try { + out.putNextEntry(large); + } catch (ZipException ex) { + limit = true; + } + require(limit, "ZIP64 refused"); + } + try (ZipInputStream in = new ZipInputStream(new FileInputStream(prefix + ".zip"))) { + int entries = 0; + ZipEntry e; + while ((e = in.getNextEntry()) != null) { + byte[] bytes = readAll(in); + entries++; + if (e.getName().endsWith(".txt")) + require(Arrays.equals(bytes, data), "ZIP payload"); + } + require(entries == 4, "ZIP entry count"); + } + } + static class Sink extends OutputStream { + int remaining = 80; + boolean closed; + public void write(int b) throws IOException { + if (--remaining < 0) + throw new IOException("cancelled"); + } + public void close() { + closed = true; + } + } + static class Source extends ByteArrayInputStream { + boolean closed; + Source(byte[] data) { + super(data); + } + public void close() { + closed = true; + } + } + static void cancellation(byte[] data) throws Exception { + Sink sink = new Sink(); + boolean failed = false; + Source source = new Source(data); + try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(sink)) { + out.putNextEntry(entry("payload")); + try (InputStream input = source) { + byte[] buffer = new byte[8192]; + int n; + while ((n = input.read(buffer)) != -1) out.write(buffer, 0, n); + } + out.finish(); + } catch (IOException ex) { + failed = true; + } + require(failed && sink.closed && source.closed, "cancelled source and destination closed"); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ZipArchiveOutputStream out = new ZipArchiveOutputStream(bytes); + out.putNextEntry(entry("first")); + out.closeEntry(); + out.finish(); + bytes.write(42); + require(bytes.toByteArray()[bytes.size() - 1] == 42, "finish leaves destination open"); + out.close(); + } + static class Discard extends OutputStream { + public void write(int value) { + } + public void write(byte[] b, int off, int len) { + } + } + static ZipEntry emptyStored(String name) { + ZipEntry e = entry(name); + e.setMethod(ZipEntry.STORED); + e.setSize(0); + e.setCrc(0); + return e; + } + static void limits() throws Exception { + try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(new Discard())) { + for (int i = 0; i < 65534; i++) { + out.putNextEntry(emptyStored("f" + i)); + out.closeEntry(); + } + boolean refused = false; + try { + out.putNextEntry(emptyStored("overflow")); + } catch (ZipException ex) { + refused = true; + } + require(refused, "entry count limit"); + } + char[] chars = new char[65535]; + Arrays.fill(chars, 'x'); + String comment = new String(chars); + try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(new Discard())) { + boolean refused = false; + for (int i = 0; i < 300; i++) { + ZipEntry e = emptyStored("f" + i); + e.setComment(comment); + try { + out.putNextEntry(e); + out.closeEntry(); + } catch (ZipException ex) { + refused = true; + break; + } + } + require(refused, "metadata memory limit"); + } + try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(new Discard())) { + ZipEntry e = entry("bad-extra"); + e.setExtra(new byte[] {1, 0, 0, 0}); + boolean refused = false; + try { + out.putNextEntry(e); + } catch (ZipException ex) { + refused = true; + } + require(refused, "ZIP64 extra refused"); + e.setExtra(new byte[] {2, 0, 9, 0}); + refused = false; + try { + out.putNextEntry(e); + } catch (ZipException ex) { + refused = true; + } + require(refused, "malformed extra refused"); + } + boolean mismatch = false; + try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(new Discard())) { + ZipEntry e = entry("stored"); + e.setMethod(ZipEntry.STORED); + e.setSize(1); + e.setCrc(0); + out.putNextEntry(e); + out.write(42); + out.closeEntry(); + } catch (ZipException ex) { + mismatch = true; + } + require(mismatch, "STORED CRC checked"); + } + static void badZip() throws Exception { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (ZipOutputStream out = new ZipOutputStream(bytes)) { + ZipEntry e = entry("stored"); + e.setMethod(ZipEntry.STORED); + e.setSize(1); + CRC32 crc = new CRC32(); + crc.update(42); + e.setCrc(crc.getValue()); + out.putNextEntry(e); + out.write(42); + out.closeEntry(); + } + byte[] archive = bytes.toByteArray(); + int payloadAt = 30 + (archive[26] & 255) + ((archive[27] & 255) << 8) + (archive[28] & 255) + + ((archive[29] & 255) << 8); + archive[payloadAt] ^= 1; + boolean corrupt = false; + try (ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(archive))) { + in.getNextEntry(); + readAll(in); + } catch (ZipException ex) { + corrupt = true; + } + require(corrupt, "ZIP CRC checked"); + boolean truncated = false; + try (ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(archive, 0, payloadAt - 1))) { + in.getNextEntry(); + } catch (EOFException ex) { + truncated = true; + } + require(truncated, "truncated ZIP header"); + } + public static void main(String[] args) throws Exception { + byte[] data = payload(); + checksums(); + compression(data); + gzip(data, args[0]); + archive(data, args[0]); + cancellation(data); + limits(); + badZip(); + if (args.length > 1) { + try (ZipInputStream in = new ZipInputStream(new FileInputStream(args[1]))) { + require(in.getNextEntry() != null, "JDK fixture entry"); + require(Arrays.equals(readAll(in), data), "JDK fixture payload"); + require(in.getNextEntry() != null, "JDK empty fixture entry"); + require(readAll(in).length == 0, "JDK empty fixture"); + require(in.getNextEntry() == null, "JDK fixture end"); + } + } + System.out.println("ZIP_API_OK"); + } +} From 5372e0b93a5d4f013b6d5eccbb24e4cfc9c4e045 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:34:14 +0300 Subject: [PATCH 02/16] Complete backend ZIP streaming and address review findings --- .../src/com/codename1/io/gzip/Deflate.java | 5 +- .../src/com/codename1/io/gzip/Inflate.java | 6 +- .../src/com/codename1/io/gzip/Tree.java | 7 +- .../io/zip/ZipArchiveOutputStream.java | 239 ++++++++++++------ .../com/codename1/io/zip/package-info.java | 24 ++ Ports/CLDC11/src/java/util/zip/Adler32.java | 22 ++ Ports/CLDC11/src/java/util/zip/CRC32.java | 22 ++ .../src/java/util/zip/CheckedInputStream.java | 22 ++ .../java/util/zip/CheckedOutputStream.java | 22 ++ Ports/CLDC11/src/java/util/zip/Checksum.java | 22 ++ .../java/util/zip/DataFormatException.java | 22 ++ Ports/CLDC11/src/java/util/zip/Deflater.java | 22 ++ .../java/util/zip/DeflaterOutputStream.java | 22 ++ .../src/java/util/zip/GZIPInputStream.java | 22 ++ .../src/java/util/zip/GZIPOutputStream.java | 22 ++ Ports/CLDC11/src/java/util/zip/Inflater.java | 22 ++ .../java/util/zip/InflaterInputStream.java | 22 ++ Ports/CLDC11/src/java/util/zip/ZipEntry.java | 22 ++ .../src/java/util/zip/ZipException.java | 22 ++ .../src/java/util/zip/ZipInputStream.java | 22 ++ .../src/java/util/zip/ZipOutputStream.java | 22 ++ .../developerguide/backend/ZipDownload.java | 64 +++++ .../developer-guide/backend-zip-download.sh | 10 + .../Backend-Operations.asciidoc | 58 ++--- scripts/copyright-header-exclusions.txt | 18 ++ scripts/generate-zip-api-stubs.py | 3 +- scripts/sync-zip-runtime.py | 7 +- .../src/com/codename1/impl/zip/Adler32.java | 2 +- .../src/com/codename1/impl/zip/CRC32.java | 2 +- .../src/com/codename1/impl/zip/Checksum.java | 2 +- .../src/com/codename1/impl/zip/Deflate.java | 7 +- .../src/com/codename1/impl/zip/Deflater.java | 2 +- .../com/codename1/impl/zip/GZIPException.java | 2 +- .../com/codename1/impl/zip/GZIPHeader.java | 2 +- .../src/com/codename1/impl/zip/InfBlocks.java | 2 +- .../src/com/codename1/impl/zip/InfCodes.java | 2 +- .../src/com/codename1/impl/zip/InfTree.java | 2 +- .../src/com/codename1/impl/zip/Inflate.java | 8 +- .../src/com/codename1/impl/zip/Inflater.java | 2 +- .../src/com/codename1/impl/zip/JZlib.java | 2 +- .../com/codename1/impl/zip/StaticTree.java | 2 +- .../src/com/codename1/impl/zip/Tree.java | 9 +- .../src/com/codename1/impl/zip/ZStream.java | 2 +- vm/JavaAPI/src/java/util/zip/Adler32.java | 20 +- vm/JavaAPI/src/java/util/zip/CRC32.java | 20 +- .../src/java/util/zip/CheckedInputStream.java | 20 +- .../java/util/zip/CheckedOutputStream.java | 20 +- vm/JavaAPI/src/java/util/zip/Checksum.java | 20 +- .../java/util/zip/DataFormatException.java | 20 +- vm/JavaAPI/src/java/util/zip/Deflater.java | 20 +- .../java/util/zip/DeflaterOutputStream.java | 29 ++- .../src/java/util/zip/GZIPInputStream.java | 32 ++- .../src/java/util/zip/GZIPOutputStream.java | 23 +- vm/JavaAPI/src/java/util/zip/Inflater.java | 20 +- .../java/util/zip/InflaterInputStream.java | 20 +- vm/JavaAPI/src/java/util/zip/ZipEntry.java | 20 +- .../src/java/util/zip/ZipException.java | 20 +- .../src/java/util/zip/ZipInputStream.java | 23 +- .../src/java/util/zip/ZipOutputStream.java | 226 ++++++++++++----- vm/README.md | 18 ++ .../zipdownload/com/demo/ZipDownload.java | 52 +++- .../javase/com/codename1/backend/Http2.java | 16 ++ .../parparvm/com/codename1/backend/Http2.java | 30 +++ vm/backend/native/cn1_backend_http2.c | 84 +++++- .../src/com/codename1/backend/HttpServer.java | 219 +++++++++++++++- vm/backend/tests/zip-streaming.py | 227 +++++++++++++++++ .../translator/BackendZipStreamingTest.java | 92 +++++++ .../translator/ZipApiIntegrationTest.java | 22 ++ .../codename1/tools/translator/ZipApiApp.java | 115 ++++++++- .../zipserver/com/demo/ZipTestServer.java | 103 ++++++++ 70 files changed, 2118 insertions(+), 256 deletions(-) create mode 100644 CodenameOne/src/com/codename1/io/zip/package-info.java create mode 100644 docs/demos/backend/src/main/java/com/codenameone/developerguide/backend/ZipDownload.java create mode 100644 docs/demos/common/src/main/snippets/developer-guide/backend-zip-download.sh create mode 100644 vm/backend/tests/zip-streaming.py create mode 100644 vm/tests/src/test/java/com/codename1/tools/translator/BackendZipStreamingTest.java create mode 100644 vm/tests/src/test/resources/com/codename1/tools/translator/zipserver/com/demo/ZipTestServer.java diff --git a/CodenameOne/src/com/codename1/io/gzip/Deflate.java b/CodenameOne/src/com/codename1/io/gzip/Deflate.java index 6e9508f2efc..fd418ed7647 100644 --- a/CodenameOne/src/com/codename1/io/gzip/Deflate.java +++ b/CodenameOne/src/com/codename1/io/gzip/Deflate.java @@ -410,7 +410,8 @@ void scanTree(short[] tree, // the tree to be scanned if (++count < maxCount && curlen == nextlen) { //NOPMD AssignmentInOperand continue; } else if (count < minCount) { - blTree[curlen * 2] += count; + // At most HEAP_SIZE code lengths are counted, well below Short.MAX_VALUE. + blTree[curlen * 2] = (short) (blTree[curlen * 2] + count); } else if (curlen != 0) { if (curlen != prevlen) { blTree[curlen * 2]++; @@ -632,7 +633,7 @@ boolean trTally(int dist, // distance of matched string if ((lastLit & 0x1fff) == 0 && level > 2) { // Compute an upper bound for the compressed length - int outLength = lastLit * 8; + long outLength = (long) lastLit * 8; int inLength = strStart - blockStart; int dcode; for (dcode = 0; dcode < D_CODES; dcode++) { diff --git a/CodenameOne/src/com/codename1/io/gzip/Inflate.java b/CodenameOne/src/com/codename1/io/gzip/Inflate.java index 240618df3f1..915ff2f3611 100644 --- a/CodenameOne/src/com/codename1/io/gzip/Inflate.java +++ b/CodenameOne/src/com/codename1/io/gzip/Inflate.java @@ -602,9 +602,9 @@ int inflateSetDictionary(byte[] dictionary, int dictLength) { if (this.mode == DICT0) { long adlerNeed = z.adler.getValue(); - z.adler.reset(); - z.adler.update(dictionary, 0, dictLength); - if (z.adler.getValue() != adlerNeed) { + Adler32 candidate = new Adler32(); + candidate.update(dictionary, 0, dictLength); + if (candidate.getValue() != adlerNeed) { return Z_DATA_ERROR; } } diff --git a/CodenameOne/src/com/codename1/io/gzip/Tree.java b/CodenameOne/src/com/codename1/io/gzip/Tree.java index 27192979e3b..5456d2c7b5e 100644 --- a/CodenameOne/src/com/codename1/io/gzip/Tree.java +++ b/CodenameOne/src/com/codename1/io/gzip/Tree.java @@ -259,7 +259,8 @@ void genBitLen(Deflate s) { bits--; } s.blCount[bits]--; // move one leaf down the tree - s.blCount[bits + 1] += 2; // move one overflow item as its brother + // Counts are bounded by the number of tree leaves (at most L_CODES). + s.blCount[bits + 1] = (short) (s.blCount[bits + 1] + 2); // move its brother s.blCount[maxLength]--; // The brother of the overflow item also moves one step up, // but this does not affect bl_count[maxLength] @@ -275,7 +276,9 @@ void genBitLen(Deflate s) { continue; } if (tree[m * 2 + 1] != bits) { - s.optLen += ((long) bits - (long) tree[m * 2 + 1]) * (long) tree[m * 2]; + // Code lengths are at most 15 and frequencies at most 32767; + // this block cost correction fits in an int. + s.optLen += (bits - tree[m * 2 + 1]) * tree[m * 2]; tree[m * 2 + 1] = (short) bits; } n--; diff --git a/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java b/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java index 68425a7d000..ac855536715 100644 --- a/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java +++ b/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java @@ -1,12 +1,31 @@ /* * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. - * This code is licensed under GPLv2 with the Classpath exception. + * 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. */ package com.codename1.io.zip; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Calendar; import java.util.HashSet; @@ -16,16 +35,15 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipException; -/** - * Streaming ZIP writer with optional Unix permissions. Works on the JVM and - * ParparVM. Payloads are never buffered in full. Central-directory metadata is - * limited to 16 MiB and 65534 entries; ZIP64 is explicitly refused. - * finish() leaves the destination open; close() closes it even after a failure. - * Input streams are owned by the caller. This stream is not thread safe. - */ +/// Streaming ZIP writer with optional Unix permissions. Works on the JVM and +/// ParparVM. Payloads are never buffered in full. Central-directory metadata is +/// limited to 16 MiB and 65534 entries; ZIP64 is explicitly refused. +/// finish() leaves the destination open; close() closes it even after a failure. +/// Input streams are owned by the caller. This stream is not thread safe. @com.codename1.impl.SharedWithBackend public class ZipArchiveOutputStream extends DeflaterOutputStream { - public static final int STORED = 0, DEFLATED = 8; + public static final int STORED = 0; + public static final int DEFLATED = 8; private static final long LIMIT = 0xffffffffL; private static final int METADATA_LIMIT = 16 * 1024 * 1024; private final ArrayList entries = new ArrayList(); @@ -35,33 +53,44 @@ public class ZipArchiveOutputStream extends DeflaterOutputStream { private final int encodingFlag; private Record current; private ZipEntry original; - private int method = DEFLATED, metadataSize; - private long written, entryStart, entrySize; - private boolean finished, closed; + private int method = DEFLATED; + private int metadataSize; + private long written; + private long entryStart; + private long entrySize; + private boolean finished; + private boolean closed; private IOException failure; private byte[] comment = new byte[0]; private static final class Record { ZipEntry entry; - byte[] name, extra, comment; - long offset, dosTime, attributes; - int flags, platform; + byte[] name; + byte[] extra; + byte[] comment; + long offset; + long dosTime; + long attributes; + int flags; + int platform; } public ZipArchiveOutputStream(OutputStream out) { - this(out, Charset.forName("UTF-8")); + this(out, StandardCharsets.UTF_8); } public ZipArchiveOutputStream(OutputStream out, Charset charset) { super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true), 8192); - if (charset == null) + if (charset == null) { throw new NullPointerException(); + } encoding = charset.displayName(); encodingFlag = "UTF-8".equalsIgnoreCase(encoding) ? 0x800 : 0; } public void setMethod(int method) { - if (method != STORED && method != DEFLATED) + if (method != STORED && method != DEFLATED) { throw new IllegalArgumentException("Invalid method"); + } this.method = method; } public void setLevel(int level) { @@ -71,75 +100,91 @@ public void setComment(String comment) { try { this.comment = encode(comment == null ? "" : comment); } catch (IOException ex) { - throw new IllegalArgumentException(ex.getMessage()); + throw new IllegalArgumentException(ex.getMessage(), ex); } } private byte[] encode(String value) throws IOException { byte[] bytes = value.getBytes(encoding); - if (bytes.length > 65535) + if (!value.equals(new String(bytes, encoding))) { + throw new ZipException("ZIP field cannot be represented in " + encoding); + } + if (bytes.length > 65535) { throw new ZipException("Encoded ZIP field exceeds 65535 bytes"); + } return bytes; } private void writable() throws IOException { - if (closed) + if (closed) { throw new IOException("ZIP stream closed"); - if (failure != null) + } + if (failure != null) { throw failure; - if (finished) + } + if (finished) { throw new IOException("ZIP stream finished"); + } } - /** Writes an entry without Unix attributes, like java.util.zip.ZipOutputStream. */ + /// Writes an entry without Unix attributes, like java.util.zip.ZipOutputStream. public void putNextEntry(ZipEntry entry) throws IOException { putEntry(entry, 0, 0); } - /** - * Writes an entry with Unix permission bits (for example 0755 or 0644). - * File/directory type bits are inferred from the trailing slash. Symlinks - * and special files are not created. Values outside 0000..0777 are refused. - */ + /// Writes an entry with Unix permission bits (for example 0755 or 0644). + /// File/directory type bits are inferred from the trailing slash. Symlinks + /// and special files are not created. Values outside 0000..0777 are refused. public void putNextEntry(ZipEntry entry, int permissions) throws IOException { - if (permissions < 0 || permissions > 0777) + if (permissions < 0 || permissions > 0x1ff) { throw new IllegalArgumentException("Invalid permissions"); - long mode = (entry.isDirectory() ? 0040000 : 0100000) | permissions; + } + long mode = (entry.isDirectory() ? 0x4000 : 0x8000) | permissions; putEntry(entry, 3, (mode << 16) | (entry.isDirectory() ? 0x10 : 0)); } private void putEntry(ZipEntry entry, int platform, long attributes) throws IOException { writable(); - if (entry == null) + if (entry == null) { throw new NullPointerException(); + } closeEntry(); - if (entries.size() >= 65534) + if (entries.size() >= 65534) { throw new ZipException("ZIP64 entry count unsupported"); - if (names.contains(entry.getName())) + } + if (names.contains(entry.getName())) { throw new ZipException("Duplicate entry: " + entry.getName()); + } Record r = new Record(); r.entry = new ZipEntry(entry); - if (r.entry.getMethod() == -1) + if (r.entry.getMethod() == -1) { r.entry.setMethod(method); - if (r.entry.getTime() == -1) + } + if (r.entry.getTime() == -1) { r.entry.setTime(System.currentTimeMillis()); + } r.name = encode(r.entry.getName()); r.comment = encode(r.entry.getComment() == null ? "" : r.entry.getComment()); byte[] extra = r.entry.getExtra(); r.extra = extra == null ? new byte[0] : extra.clone(); validateExtra(r.extra); r.extra = timestampExtra(r.extra, r.entry.getTime()); - if (r.name.length == 0) + if (r.name.length == 0) { throw new ZipException("Empty entry name"); + } checkSize(r.entry.getSize()); checkSize(r.entry.getCompressedSize()); if (r.entry.getMethod() == STORED) { - if (r.entry.getSize() < 0 && r.entry.getCompressedSize() >= 0) + if (r.entry.getSize() < 0 && r.entry.getCompressedSize() >= 0) { r.entry.setSize(r.entry.getCompressedSize()); - if (r.entry.getCompressedSize() < 0) + } + if (r.entry.getCompressedSize() < 0) { r.entry.setCompressedSize(r.entry.getSize()); - if (r.entry.getSize() < 0 || r.entry.getCrc() < 0 || r.entry.getSize() != r.entry.getCompressedSize()) + } + if (r.entry.getSize() < 0 || r.entry.getCrc() < 0 || r.entry.getSize() != r.entry.getCompressedSize()) { throw new ZipException("STORED entries require size and CRC before writing"); + } } int metadata = 46 + r.name.length + r.extra.length + r.comment.length; - if (metadataSize > METADATA_LIMIT - metadata) + if (metadataSize > METADATA_LIMIT - metadata) { throw new ZipException("ZIP central directory exceeds 16 MiB"); + } r.flags = encodingFlag | (r.entry.getMethod() == DEFLATED ? 8 : 0); r.platform = platform; r.attributes = attributes; @@ -149,8 +194,9 @@ private void putEntry(ZipEntry entry, int platform, long attributes) throws IOEx original = entry; entrySize = 0; crc.reset(); - if (r.entry.getMethod() == DEFLATED) + if (r.entry.getMethod() == DEFLATED) { def.reset(); + } try { word(0x04034b50L, 4); word(20, 2); @@ -176,38 +222,45 @@ private void putEntry(ZipEntry entry, int platform, long attributes) throws IOEx private static void validateExtra(byte[] extra) throws ZipException { int at = 0; while (at < extra.length) { - if (extra.length - at < 4) + if (extra.length - at < 4) { throw new ZipException("Malformed ZIP extra field"); + } int id = (extra[at] & 255) | ((extra[at + 1] & 255) << 8); int size = (extra[at + 2] & 255) | ((extra[at + 3] & 255) << 8); - if (id == 1) + if (id == 1) { throw new ZipException("ZIP64 extra field unsupported"); - if (size > extra.length - at - 4) + } + if (size > extra.length - at - 4) { throw new ZipException("Malformed ZIP extra field"); + } at += 4 + size; } } private static void checkSize(long size) throws ZipException { - if (size >= LIMIT) + if (size >= LIMIT) { throw new ZipException("ZIP64 size unsupported"); + } } private static byte[] timestampExtra(byte[] extra, long millis) throws ZipException { long seconds = millis / 1000; - if (millis < 0 && millis % 1000 != 0) + if (millis < 0 && millis % 1000 != 0) { seconds--; - if (seconds < Integer.MIN_VALUE || seconds > Integer.MAX_VALUE) - return extra; + } + boolean timestamp = seconds >= Integer.MIN_VALUE && seconds <= Integer.MAX_VALUE; + int added = timestamp ? 9 : 0; // Replace any caller-provided extended timestamp with the entry's time. int kept = 0; for (int at = 0; at < extra.length;) { int size = 4 + (extra[at + 2] & 255) + ((extra[at + 3] & 255) << 8); - if ((extra[at] & 255) != 0x55 || (extra[at + 1] & 255) != 0x54) + if ((extra[at] & 255) != 0x55 || (extra[at + 1] & 255) != 0x54) { kept += size; + } at += size; } - if (kept > 65535 - 9) + if (kept > 65535 - added) { throw new ZipException("No room for ZIP timestamp extra field"); - byte[] result = new byte[kept + 9]; + } + byte[] result = new byte[kept + added]; int pos = 0; for (int at = 0; at < extra.length;) { int size = 4 + (extra[at + 2] & 255) + ((extra[at + 3] & 255) << 8); @@ -217,77 +270,99 @@ private static byte[] timestampExtra(byte[] extra, long millis) throws ZipExcept } at += size; } + if (!timestamp) { + return result; + } result[pos] = 0x55; result[pos + 1] = 0x54; result[pos + 2] = 5; result[pos + 4] = 1; - for (int i = 0; i < 4; i++) result[pos + 5 + i] = (byte) (seconds >>> (8 * i)); + for (int i = 0; i < 4; i++) { + result[pos + 5 + i] = (byte) (seconds >>> (8 * i)); + } return result; } private static long dosTime(long millis) { // The deterministic epoch timestamp predates the DOS range in every // timezone. Avoid calendar/timezone lookups for each such entry. - if (millis == 0) + if (millis == 0) { return 0x00210000L; + } Calendar c = Calendar.getInstance(); c.setTime(new java.util.Date(millis)); int year = c.get(Calendar.YEAR); - if (year < 1980) + if (year < 1980) { return 0x00210000L; - if (year > 2107) + } + if (year > 2107) { return 0xff9fbf7dL; + } return ((long) (year - 1980) << 25) | ((c.get(Calendar.MONTH) + 1) << 21) | (c.get(Calendar.DAY_OF_MONTH) << 16) | (c.get(Calendar.HOUR_OF_DAY) << 11) | (c.get(Calendar.MINUTE) << 5) | (c.get(Calendar.SECOND) >> 1); } + @Override public void write(int value) throws IOException { write(new byte[] {(byte) value}, 0, 1); } + @Override public void write(byte[] data, int off, int len) throws IOException { writable(); - if (data == null) + if (data == null) { throw new NullPointerException(); - if (off < 0 || len < 0 || off > data.length - len) + } + if (off < 0 || len < 0 || off > data.length - len) { throw new IndexOutOfBoundsException(); - if (current == null) + } + if (current == null) { throw new ZipException("No current ZIP entry"); + } try { checkSize(entrySize + len); - if (current.entry.getMethod() == STORED && entrySize + len > current.entry.getSize()) + if (current.entry.getMethod() == STORED && entrySize + len > current.entry.getSize()) { throw new ZipException("STORED entry exceeds declared size"); + } crc.update(data, off, len); entrySize += len; - if (current.entry.getMethod() == STORED) + if (current.entry.getMethod() == STORED) { raw(data, off, len); - else { + } else { def.setInput(data, off, len); - while (!def.needsInput()) deflate(); + while (!def.needsInput()) { + deflate(); + } } } catch (IOException ex) { failure = ex; throw ex; } } + @Override protected void deflate() throws IOException { int n = def.deflate(buf, 0, buf.length); - if (n > 0) + if (n > 0) { raw(buf, 0, n); + } } public void closeEntry() throws IOException { writable(); - if (current == null) + if (current == null) { return; + } try { ZipEntry e = current.entry; if (e.getMethod() == DEFLATED) { def.finish(); - while (!def.finished()) deflate(); + while (!def.finished()) { + deflate(); + } } long compressed = written - entryStart; checkSize(compressed); if ((e.getSize() >= 0 && e.getSize() != entrySize) || (e.getCompressedSize() >= 0 && e.getCompressedSize() != compressed) - || (e.getCrc() >= 0 && e.getCrc() != crc.getValue())) + || (e.getCrc() >= 0 && e.getCrc() != crc.getValue())) { throw new ZipException("ZIP entry size or CRC mismatch"); + } e.setSize(entrySize); e.setCompressedSize(compressed); e.setCrc(crc.getValue()); @@ -307,13 +382,17 @@ public void closeEntry() throws IOException { throw ex; } } + @Override public void finish() throws IOException { - if (closed) + if (closed) { throw new IOException("ZIP stream closed"); - if (failure != null) + } + if (failure != null) { throw failure; - if (finished) + } + if (finished) { return; + } closeEntry(); try { long start = written; @@ -357,11 +436,14 @@ public void finish() throws IOException { throw ex; } } + @Override public void flush() throws IOException { - if (closed) + if (closed) { throw new IOException("ZIP stream closed"); - if (failure != null) + } + if (failure != null) { throw failure; + } try { out.flush(); } catch (IOException ex) { @@ -369,16 +451,19 @@ public void flush() throws IOException { throw ex; } } + @Override public void close() throws IOException { - if (closed) + if (closed) { return; + } // An earlier write already delivered failure to the caller. Throwing // that same object again from close breaks try-with-resources through // Throwable's self-suppression guard. IOException error = null; try { - if (failure == null) + if (failure == null) { finish(); + } } catch (IOException ex) { error = ex; } finally { @@ -391,12 +476,14 @@ public void close() throws IOException { try { out.close(); } catch (IOException ex) { - if (error == null) + if (error == null) { error = ex; + } } } - if (error != null) + if (error != null) { throw error; + } } private void word(long value, int bytes) throws IOException { byte[] b = new byte[bytes]; diff --git a/CodenameOne/src/com/codename1/io/zip/package-info.java b/CodenameOne/src/com/codename1/io/zip/package-info.java new file mode 100644 index 00000000000..47e551a3a5d --- /dev/null +++ b/CodenameOne/src/com/codename1/io/zip/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// Streaming ZIP archive creation with portable Unix file metadata. +package com.codename1.io.zip; diff --git a/Ports/CLDC11/src/java/util/zip/Adler32.java b/Ports/CLDC11/src/java/util/zip/Adler32.java index 9e4cf619d1f..9bff28678c0 100644 --- a/Ports/CLDC11/src/java/util/zip/Adler32.java +++ b/Ports/CLDC11/src/java/util/zip/Adler32.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/Ports/CLDC11/src/java/util/zip/CRC32.java b/Ports/CLDC11/src/java/util/zip/CRC32.java index 9e7f6f40de4..2ac2db2b41b 100644 --- a/Ports/CLDC11/src/java/util/zip/CRC32.java +++ b/Ports/CLDC11/src/java/util/zip/CRC32.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/Ports/CLDC11/src/java/util/zip/CheckedInputStream.java b/Ports/CLDC11/src/java/util/zip/CheckedInputStream.java index 96d7d07e44b..a3d3c6fccd2 100644 --- a/Ports/CLDC11/src/java/util/zip/CheckedInputStream.java +++ b/Ports/CLDC11/src/java/util/zip/CheckedInputStream.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/Ports/CLDC11/src/java/util/zip/CheckedOutputStream.java b/Ports/CLDC11/src/java/util/zip/CheckedOutputStream.java index 964700d61c8..d8066d398da 100644 --- a/Ports/CLDC11/src/java/util/zip/CheckedOutputStream.java +++ b/Ports/CLDC11/src/java/util/zip/CheckedOutputStream.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/Ports/CLDC11/src/java/util/zip/Checksum.java b/Ports/CLDC11/src/java/util/zip/Checksum.java index 5c8dc1c3cf0..7eeed66e33a 100644 --- a/Ports/CLDC11/src/java/util/zip/Checksum.java +++ b/Ports/CLDC11/src/java/util/zip/Checksum.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/Ports/CLDC11/src/java/util/zip/DataFormatException.java b/Ports/CLDC11/src/java/util/zip/DataFormatException.java index c327b4facc8..c9b69e8bb2d 100644 --- a/Ports/CLDC11/src/java/util/zip/DataFormatException.java +++ b/Ports/CLDC11/src/java/util/zip/DataFormatException.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/Ports/CLDC11/src/java/util/zip/Deflater.java b/Ports/CLDC11/src/java/util/zip/Deflater.java index 9a1edfab565..c78fb925869 100644 --- a/Ports/CLDC11/src/java/util/zip/Deflater.java +++ b/Ports/CLDC11/src/java/util/zip/Deflater.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/Ports/CLDC11/src/java/util/zip/DeflaterOutputStream.java b/Ports/CLDC11/src/java/util/zip/DeflaterOutputStream.java index a02fc5684a0..18d3c6f9f51 100644 --- a/Ports/CLDC11/src/java/util/zip/DeflaterOutputStream.java +++ b/Ports/CLDC11/src/java/util/zip/DeflaterOutputStream.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/Ports/CLDC11/src/java/util/zip/GZIPInputStream.java b/Ports/CLDC11/src/java/util/zip/GZIPInputStream.java index fe2bc35f2d8..83d8aa76168 100644 --- a/Ports/CLDC11/src/java/util/zip/GZIPInputStream.java +++ b/Ports/CLDC11/src/java/util/zip/GZIPInputStream.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/Ports/CLDC11/src/java/util/zip/GZIPOutputStream.java b/Ports/CLDC11/src/java/util/zip/GZIPOutputStream.java index 01e13bf3cf4..d8ca897f497 100644 --- a/Ports/CLDC11/src/java/util/zip/GZIPOutputStream.java +++ b/Ports/CLDC11/src/java/util/zip/GZIPOutputStream.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/Ports/CLDC11/src/java/util/zip/Inflater.java b/Ports/CLDC11/src/java/util/zip/Inflater.java index 7af9e368eb9..c0a094ead7f 100644 --- a/Ports/CLDC11/src/java/util/zip/Inflater.java +++ b/Ports/CLDC11/src/java/util/zip/Inflater.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/Ports/CLDC11/src/java/util/zip/InflaterInputStream.java b/Ports/CLDC11/src/java/util/zip/InflaterInputStream.java index 61d50b8eb65..9217580d4ba 100644 --- a/Ports/CLDC11/src/java/util/zip/InflaterInputStream.java +++ b/Ports/CLDC11/src/java/util/zip/InflaterInputStream.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/Ports/CLDC11/src/java/util/zip/ZipEntry.java b/Ports/CLDC11/src/java/util/zip/ZipEntry.java index b83b2dad89e..95178b05e2a 100644 --- a/Ports/CLDC11/src/java/util/zip/ZipEntry.java +++ b/Ports/CLDC11/src/java/util/zip/ZipEntry.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/Ports/CLDC11/src/java/util/zip/ZipException.java b/Ports/CLDC11/src/java/util/zip/ZipException.java index 9f087e83562..b5451422063 100644 --- a/Ports/CLDC11/src/java/util/zip/ZipException.java +++ b/Ports/CLDC11/src/java/util/zip/ZipException.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/Ports/CLDC11/src/java/util/zip/ZipInputStream.java b/Ports/CLDC11/src/java/util/zip/ZipInputStream.java index fb27aedf2ec..9faa4f25a85 100644 --- a/Ports/CLDC11/src/java/util/zip/ZipInputStream.java +++ b/Ports/CLDC11/src/java/util/zip/ZipInputStream.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/Ports/CLDC11/src/java/util/zip/ZipOutputStream.java b/Ports/CLDC11/src/java/util/zip/ZipOutputStream.java index fef6f31a204..f7fc522d099 100644 --- a/Ports/CLDC11/src/java/util/zip/ZipOutputStream.java +++ b/Ports/CLDC11/src/java/util/zip/ZipOutputStream.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ // Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. package java.util.zip; diff --git a/docs/demos/backend/src/main/java/com/codenameone/developerguide/backend/ZipDownload.java b/docs/demos/backend/src/main/java/com/codenameone/developerguide/backend/ZipDownload.java new file mode 100644 index 00000000000..c5a1e6215be --- /dev/null +++ b/docs/demos/backend/src/main/java/com/codenameone/developerguide/backend/ZipDownload.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.backend; + +import com.codename1.backend.HttpServer; +import com.codename1.io.zip.ZipArchiveOutputStream; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; +import java.util.zip.ZipEntry; + +public final class ZipDownload { + private static InputStream openLauncher() throws IOException { + return new ByteArrayInputStream("#!/bin/sh\necho Hello\n".getBytes("UTF-8")); + } + + // tag::backend-zip-download[] + public static HttpServer.Response projectDownload() { + return HttpServer.Response.stream(200, "application/zip", new HttpServer.BodyWriter() { + public void writeTo(OutputStream destination) throws IOException { + try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(destination)) { + ZipEntry directory = new ZipEntry("project/"); + directory.setTime(0); + zip.putNextEntry(directory, 0755); + zip.closeEntry(); + + ZipEntry launcher = new ZipEntry("project/run.sh"); + launcher.setTime(0); + zip.putNextEntry(launcher, 0755); + try (InputStream source = openLauncher()) { + byte[] buffer = new byte[8192]; + int count; + while ((count = source.read(buffer)) != -1) { + zip.write(buffer, 0, count); + } + } + zip.closeEntry(); + } + } + }).header("Content-Disposition", "attachment; filename=project.zip"); + } + // end::backend-zip-download[] +} diff --git a/docs/demos/common/src/main/snippets/developer-guide/backend-zip-download.sh b/docs/demos/common/src/main/snippets/developer-guide/backend-zip-download.sh new file mode 100644 index 00000000000..c6757098f02 --- /dev/null +++ b/docs/demos/common/src/main/snippets/developer-guide/backend-zip-download.sh @@ -0,0 +1,10 @@ +# tag::backend-zip-download[] +export CN1_BACKEND_DEMO=demo/zipdownload +export CN1_BACKEND_STANDALONE_DEMO=1 +export JDK_8_HOME=/path/to/jdk8 +export JAVA_HOME="$JDK_8_HOME" +export PATH="$JAVA_HOME/bin:$PATH" +cd vm/backend +./build.sh ZipDownload com.demo /tmp/zipdownload +/tmp/zipdownload +# end::backend-zip-download[] diff --git a/docs/developer-guide/Backend-Operations.asciidoc b/docs/developer-guide/Backend-Operations.asciidoc index 705f7036fe4..e1c03d39cdf 100644 --- a/docs/developer-guide/Backend-Operations.asciidoc +++ b/docs/developer-guide/Backend-Operations.asciidoc @@ -219,24 +219,7 @@ the central directory. [source,java] ---- -try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(destination)) { - ZipEntry directory = new ZipEntry("project/"); - directory.setTime(0); - zip.putNextEntry(directory, 0755); - zip.closeEntry(); - - ZipEntry launcher = new ZipEntry("project/run.sh"); - launcher.setTime(0); - zip.putNextEntry(launcher, 0755); - try (InputStream source = openLauncher()) { - byte[] buffer = new byte[8192]; - int count; - while ((count = source.read(buffer)) != -1) { - zip.write(buffer, 0, count); - } - } - zip.closeEntry(); -} +include::../demos/backend/src/main/java/com/codenameone/developerguide/backend/ZipDownload.java[tag=backend-zip-download,indent=0] ---- The permission argument accepts `0000` through `0777`. The writer infers a @@ -250,14 +233,7 @@ It serves a small, fixed project at `http://127.0.0.1:8099/project.zip`: [source,bash] ---- -export CN1_BACKEND_DEMO=demo/zipdownload -export CN1_BACKEND_STANDALONE_DEMO=1 -export JDK_8_HOME=/path/to/jdk8 -export JAVA_HOME="$JDK_8_HOME" -export PATH="$JAVA_HOME/bin:$PATH" -cd vm/backend -./build.sh ZipDownload com.demo /tmp/zipdownload -/tmp/zipdownload +include::../demos/common/src/main/snippets/developer-guide/backend-zip-download.sh[tag=backend-zip-download,indent=0] ---- The same binary can generate a file without starting HTTP: @@ -265,18 +241,32 @@ The same binary can generate a file without starting HTTP: `project/run.sh`. The JVM version can be run with `CN1_BACKEND_DEMO=demo/zipdownload ./run-javase.sh com.demo.ZipDownload`. -The HTTP example buffers only its fixed literals. `HttpServer.Response` currently -accepts a byte array or an already-open file descriptor; it has no streaming -output callback. ZIP generation itself streams to any `OutputStream`, but this -change does not add HTTP streaming or cancellation notifications during archive -generation. For large downloads, generate to an application-owned file and use -the existing file response lifecycle; the application must manage deletion of -that file. Do not collect arbitrary project contents in a byte array. +`HttpServer.Response.stream` invokes the writer when it sends the response. Open +sources inside this callback and close them with try-with-resources. HEAD and +statuses without a body do not invoke it, so they do not open sources. Closing +the supplied output flushes it without closing the connection. The server ends +the response only after the callback returns successfully. + +Generation uses the serving worker and fixed transport buffers: 16 KiB for +HTTP/1, plus a 16 KiB receive buffer and a 16 KiB native chunk for HTTP/2. It +creates no archive-sized byte array, temporary file, or producer thread. +HTTP/1.1 uses chunked framing, HTTP/1.0 closes the connection after the body, +and native HTTP/2 respects the client's flow-control window. A slow client +holds the serving worker until it grants credit or the socket deadline expires; +other streams on that HTTP/2 connection wait while the callback runs. + +A disconnected client, HTTP/2 reset, or socket timeout makes output writes throw +`IOException`. Let that exception propagate so source cleanup runs and archive +generation stops. Cancellation is detected on output I/O; a callback doing +lengthy computation or waiting on its own source must bound that work itself. +A callback failure aborts the response rather than emitting a success terminator. +The local JVM transport supports HTTP/1; test HTTP/2 with the packaged backend. ==== Format limits and ownership * The default name/comment encoding is UTF-8, with the ZIP UTF-8 flag set. - Charset constructors also accept encodings available in the runtime. + Charset constructors also accept encodings available in the runtime. Names and + comments that cannot be encoded without replacement are rejected. * Files may be empty. Directories are explicit entries ending in `/`. Duplicate entry names and empty entry names are rejected. * `DEFLATED` entries stream with data descriptors. `STORED` entries require a diff --git a/scripts/copyright-header-exclusions.txt b/scripts/copyright-header-exclusions.txt index e5024951365..f404927ffd9 100644 --- a/scripts/copyright-header-exclusions.txt +++ b/scripts/copyright-header-exclusions.txt @@ -367,3 +367,21 @@ vm/ByteCodeTranslator/src/com/codename1/tools/translator/classfile/tree/TryCatch vm/ByteCodeTranslator/src/com/codename1/tools/translator/classfile/tree/TypeInsnNode.java | Rewrite of ASM org.objectweb.asm.tree.TypeInsnNode for ParparVM, retaining ASM's BSD-3-Clause notice (INRIA, France Telecom) vm/ByteCodeTranslator/src/com/codename1/tools/translator/classfile/tree/VarInsnNode.java | Rewrite of ASM org.objectweb.asm.tree.VarInsnNode for ParparVM, retaining ASM's BSD-3-Clause notice (INRIA, France Telecom) scripts/backend-mvc/src/main/resources/static/htmx-2.0.11.min.js | Unmodified htmx 2.0.11 distribution, Zero-Clause BSD license in scripts/backend-mvc/HTMX-LICENSE.txt + +# Relocated JZlib retains its original BSD license. +vm/JavaAPI/src/com/codename1/impl/zip/Adler32.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice +vm/JavaAPI/src/com/codename1/impl/zip/CRC32.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice +vm/JavaAPI/src/com/codename1/impl/zip/Checksum.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice +vm/JavaAPI/src/com/codename1/impl/zip/Deflate.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice +vm/JavaAPI/src/com/codename1/impl/zip/Deflater.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice +vm/JavaAPI/src/com/codename1/impl/zip/GZIPException.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice +vm/JavaAPI/src/com/codename1/impl/zip/GZIPHeader.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice +vm/JavaAPI/src/com/codename1/impl/zip/InfBlocks.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice +vm/JavaAPI/src/com/codename1/impl/zip/InfCodes.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice +vm/JavaAPI/src/com/codename1/impl/zip/InfTree.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice +vm/JavaAPI/src/com/codename1/impl/zip/Inflate.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice +vm/JavaAPI/src/com/codename1/impl/zip/Inflater.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice +vm/JavaAPI/src/com/codename1/impl/zip/JZlib.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice +vm/JavaAPI/src/com/codename1/impl/zip/StaticTree.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice +vm/JavaAPI/src/com/codename1/impl/zip/Tree.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice +vm/JavaAPI/src/com/codename1/impl/zip/ZStream.java | JZlib, Copyright ymnk, JCraft Inc., BSD license; generated relocation retains original notice diff --git a/scripts/generate-zip-api-stubs.py b/scripts/generate-zip-api-stubs.py index cbe3979be08..f3f97099021 100644 --- a/scripts/generate-zip-api-stubs.py +++ b/scripts/generate-zip-api-stubs.py @@ -44,7 +44,8 @@ params = ', '.join(t+' arg'+str(i) for i,t in enumerate(line[start+1:end].split(', ')) if t) line = line[:start+1] + params + line[end:] lines.append(line) - text = '\n'.join(lines) + '\n' + header = (ROOT / 'CodenameOne/src/com/codename1/impl/SharedWithBackend.java').read_text().split('*/', 1)[0] + '*/\n' + text = header + '\n'.join(lines) + '\n' target = ROOT / 'Ports/CLDC11/src/java/util/zip' / source.name if args.check: if not target.exists() or target.read_text() != text: diff --git a/scripts/sync-zip-runtime.py b/scripts/sync-zip-runtime.py index 3c84288c9a0..a9b0c6e2b7b 100644 --- a/scripts/sync-zip-runtime.py +++ b/scripts/sync-zip-runtime.py @@ -23,10 +23,10 @@ def generated(): text = source.read_text().replace('package com.codename1.io.zip;', 'package java.util.zip;') text = text.replace('@com.codename1.impl.SharedWithBackend\n', '') text = text.replace('ZipArchiveOutputStream', 'ZipOutputStream') - text = text.replace('Streaming ZIP writer with optional Unix permissions. Works on the JVM and\n * ParparVM.', + text = text.replace('Streaming ZIP writer with optional Unix permissions. Works on the JVM and\n/// ParparVM.', 'Streaming ZIP writer for ParparVM.') # The permission overload belongs to the portable CN1 helper only. - start = text.index(' /**\n * Writes an entry with Unix') + start = text.index(' /// Writes an entry with Unix') end = text.index(' private void putEntry', start) text = text[:start] + text[end:] yield ROOT / 'vm/JavaAPI/src/java/util/zip/ZipOutputStream.java', text @@ -39,7 +39,8 @@ def main(): stale = [] for path, text in generated(): text = '\n'.join(line.rstrip() for line in text.splitlines()) + '\n' - text = '// Generated by scripts/sync-zip-runtime.py; edit the canonical core source.\n' + text + notice_end = text.index('*/') + 2 + text = text[:notice_end] + '\n// Generated by scripts/sync-zip-runtime.py; edit the canonical core source.' + text[notice_end:] if args.check: if not path.exists() or path.read_text() != text: stale.append(str(path.relative_to(ROOT))) diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/Adler32.java b/vm/JavaAPI/src/com/codename1/impl/zip/Adler32.java index e0aa41a295f..556f1b6f134 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/Adler32.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/Adler32.java @@ -1,5 +1,5 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2000-2011 ymnk, JCraft,Inc. All rights reserved. diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/CRC32.java b/vm/JavaAPI/src/com/codename1/impl/zip/CRC32.java index c1470fffab2..ee53f2c4c59 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/CRC32.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/CRC32.java @@ -1,5 +1,5 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/Checksum.java b/vm/JavaAPI/src/com/codename1/impl/zip/Checksum.java index c67b6b660b1..300adc9d2f3 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/Checksum.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/Checksum.java @@ -1,5 +1,5 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/Deflate.java b/vm/JavaAPI/src/com/codename1/impl/zip/Deflate.java index a95b10578ee..03bd26c1e6a 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/Deflate.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/Deflate.java @@ -1,5 +1,5 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2000-2011 ymnk, JCraft,Inc. All rights reserved. @@ -410,7 +410,8 @@ void scanTree(short[] tree, // the tree to be scanned if (++count < maxCount && curlen == nextlen) { //NOPMD AssignmentInOperand continue; } else if (count < minCount) { - blTree[curlen * 2] += count; + // At most HEAP_SIZE code lengths are counted, well below Short.MAX_VALUE. + blTree[curlen * 2] = (short) (blTree[curlen * 2] + count); } else if (curlen != 0) { if (curlen != prevlen) { blTree[curlen * 2]++; @@ -632,7 +633,7 @@ boolean trTally(int dist, // distance of matched string if ((lastLit & 0x1fff) == 0 && level > 2) { // Compute an upper bound for the compressed length - int outLength = lastLit * 8; + long outLength = (long) lastLit * 8; int inLength = strStart - blockStart; int dcode; for (dcode = 0; dcode < D_CODES; dcode++) { diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/Deflater.java b/vm/JavaAPI/src/com/codename1/impl/zip/Deflater.java index a9f99b676a8..ce5c69a21a8 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/Deflater.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/Deflater.java @@ -1,5 +1,5 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/GZIPException.java b/vm/JavaAPI/src/com/codename1/impl/zip/GZIPException.java index 987e373ae13..4fe73b827a9 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/GZIPException.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/GZIPException.java @@ -1,5 +1,5 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/GZIPHeader.java b/vm/JavaAPI/src/com/codename1/impl/zip/GZIPHeader.java index 0854366748a..ba8255f6ac4 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/GZIPHeader.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/GZIPHeader.java @@ -1,5 +1,5 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/InfBlocks.java b/vm/JavaAPI/src/com/codename1/impl/zip/InfBlocks.java index 25d9ed0e4dd..722e5c9d389 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/InfBlocks.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/InfBlocks.java @@ -1,5 +1,5 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/InfCodes.java b/vm/JavaAPI/src/com/codename1/impl/zip/InfCodes.java index cb7901f9201..a4d5ee02175 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/InfCodes.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/InfCodes.java @@ -1,5 +1,5 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/InfTree.java b/vm/JavaAPI/src/com/codename1/impl/zip/InfTree.java index c995582e132..383922688f1 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/InfTree.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/InfTree.java @@ -1,5 +1,5 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/Inflate.java b/vm/JavaAPI/src/com/codename1/impl/zip/Inflate.java index 8e843c649e4..4c58ed976a5 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/Inflate.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/Inflate.java @@ -1,5 +1,5 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2000-2011 ymnk, JCraft,Inc. All rights reserved. @@ -602,9 +602,9 @@ int inflateSetDictionary(byte[] dictionary, int dictLength) { if (this.mode == DICT0) { long adlerNeed = z.adler.getValue(); - z.adler.reset(); - z.adler.update(dictionary, 0, dictLength); - if (z.adler.getValue() != adlerNeed) { + Adler32 candidate = new Adler32(); + candidate.update(dictionary, 0, dictLength); + if (candidate.getValue() != adlerNeed) { return Z_DATA_ERROR; } } diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/Inflater.java b/vm/JavaAPI/src/com/codename1/impl/zip/Inflater.java index 12cf6921afd..0a01fed7861 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/Inflater.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/Inflater.java @@ -1,6 +1,6 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/JZlib.java b/vm/JavaAPI/src/com/codename1/impl/zip/JZlib.java index ae39eea7cc9..8c44407734f 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/JZlib.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/JZlib.java @@ -1,5 +1,5 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2011 ymnk, JCraft,Inc. All rights reserved. diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/StaticTree.java b/vm/JavaAPI/src/com/codename1/impl/zip/StaticTree.java index 96ce76ce230..af1126898ae 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/StaticTree.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/StaticTree.java @@ -1,5 +1,5 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/Tree.java b/vm/JavaAPI/src/com/codename1/impl/zip/Tree.java index 8362a5a350d..ed6aa6a5510 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/Tree.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/Tree.java @@ -1,5 +1,5 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. @@ -259,7 +259,8 @@ void genBitLen(Deflate s) { bits--; } s.blCount[bits]--; // move one leaf down the tree - s.blCount[bits + 1] += 2; // move one overflow item as its brother + // Counts are bounded by the number of tree leaves (at most L_CODES). + s.blCount[bits + 1] = (short) (s.blCount[bits + 1] + 2); // move its brother s.blCount[maxLength]--; // The brother of the overflow item also moves one step up, // but this does not affect bl_count[maxLength] @@ -275,7 +276,9 @@ void genBitLen(Deflate s) { continue; } if (tree[m * 2 + 1] != bits) { - s.optLen += ((long) bits - (long) tree[m * 2 + 1]) * (long) tree[m * 2]; + // Code lengths are at most 15 and frequencies at most 32767; + // this block cost correction fits in an int. + s.optLen += (bits - tree[m * 2 + 1]) * tree[m * 2]; tree[m * 2 + 1] = (short) bits; } n--; diff --git a/vm/JavaAPI/src/com/codename1/impl/zip/ZStream.java b/vm/JavaAPI/src/com/codename1/impl/zip/ZStream.java index e660fc517b0..69578f589f3 100644 --- a/vm/JavaAPI/src/com/codename1/impl/zip/ZStream.java +++ b/vm/JavaAPI/src/com/codename1/impl/zip/ZStream.java @@ -1,6 +1,6 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* -*-mode:java; c-basic-offset:2; indent-tabs-mode:nil -*- */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* Copyright (c) 2000-2011 ymnk, JCraft,Inc. All rights reserved. diff --git a/vm/JavaAPI/src/java/util/zip/Adler32.java b/vm/JavaAPI/src/java/util/zip/Adler32.java index 4fa5fa7c773..60b5e63ed42 100644 --- a/vm/JavaAPI/src/java/util/zip/Adler32.java +++ b/vm/JavaAPI/src/java/util/zip/Adler32.java @@ -1,6 +1,24 @@ /* * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. - * This code is licensed under GPLv2 with the Classpath exception. + * 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. */ package java.util.zip; diff --git a/vm/JavaAPI/src/java/util/zip/CRC32.java b/vm/JavaAPI/src/java/util/zip/CRC32.java index ff8895213ca..e02f9456eda 100644 --- a/vm/JavaAPI/src/java/util/zip/CRC32.java +++ b/vm/JavaAPI/src/java/util/zip/CRC32.java @@ -1,6 +1,24 @@ /* * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. - * This code is licensed under GPLv2 with the Classpath exception. + * 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. */ package java.util.zip; diff --git a/vm/JavaAPI/src/java/util/zip/CheckedInputStream.java b/vm/JavaAPI/src/java/util/zip/CheckedInputStream.java index 72d6f69f02e..4fa18e2de83 100644 --- a/vm/JavaAPI/src/java/util/zip/CheckedInputStream.java +++ b/vm/JavaAPI/src/java/util/zip/CheckedInputStream.java @@ -1,6 +1,24 @@ /* * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. - * This code is licensed under GPLv2 with the Classpath exception. + * 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. */ package java.util.zip; diff --git a/vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java b/vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java index 8e0af2d09ec..5dc39b3b6d2 100644 --- a/vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java +++ b/vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java @@ -1,6 +1,24 @@ /* * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. - * This code is licensed under GPLv2 with the Classpath exception. + * 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. */ package java.util.zip; diff --git a/vm/JavaAPI/src/java/util/zip/Checksum.java b/vm/JavaAPI/src/java/util/zip/Checksum.java index 697a23fe7cc..aa977074b77 100644 --- a/vm/JavaAPI/src/java/util/zip/Checksum.java +++ b/vm/JavaAPI/src/java/util/zip/Checksum.java @@ -1,6 +1,24 @@ /* * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. - * This code is licensed under GPLv2 with the Classpath exception. + * 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. */ package java.util.zip; diff --git a/vm/JavaAPI/src/java/util/zip/DataFormatException.java b/vm/JavaAPI/src/java/util/zip/DataFormatException.java index 69a40ea4f92..3a9ef308778 100644 --- a/vm/JavaAPI/src/java/util/zip/DataFormatException.java +++ b/vm/JavaAPI/src/java/util/zip/DataFormatException.java @@ -1,6 +1,24 @@ /* * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. - * This code is licensed under GPLv2 with the Classpath exception. + * 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. */ package java.util.zip; diff --git a/vm/JavaAPI/src/java/util/zip/Deflater.java b/vm/JavaAPI/src/java/util/zip/Deflater.java index 77be6db8d70..50bd9ef1e94 100644 --- a/vm/JavaAPI/src/java/util/zip/Deflater.java +++ b/vm/JavaAPI/src/java/util/zip/Deflater.java @@ -1,6 +1,24 @@ /* * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. - * This code is licensed under GPLv2 with the Classpath exception. + * 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. */ package java.util.zip; diff --git a/vm/JavaAPI/src/java/util/zip/DeflaterOutputStream.java b/vm/JavaAPI/src/java/util/zip/DeflaterOutputStream.java index e41d2b843d0..8201f6f021f 100644 --- a/vm/JavaAPI/src/java/util/zip/DeflaterOutputStream.java +++ b/vm/JavaAPI/src/java/util/zip/DeflaterOutputStream.java @@ -1,6 +1,24 @@ /* * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. - * This code is licensed under GPLv2 with the Classpath exception. + * 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. */ package java.util.zip; @@ -69,12 +87,15 @@ public void flush() throws IOException { if (closed) throw new IOException("Stream closed"); if (syncFlush && !def.finished()) { + // A flush marker needs room to complete in one call. Tiny buffers + // can otherwise cause another marker on every continuation call. + byte[] flushBuffer = buf.length < 512 ? new byte[512] : buf; int n; do { - n = def.deflate(buf, 0, buf.length, Deflater.SYNC_FLUSH); + n = def.deflate(flushBuffer, 0, flushBuffer.length, Deflater.SYNC_FLUSH); if (n > 0) - out.write(buf, 0, n); - } while (n == buf.length); + out.write(flushBuffer, 0, n); + } while (n == flushBuffer.length); } out.flush(); } diff --git a/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java b/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java index c68d1cb3286..e7f09af1fb8 100644 --- a/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java +++ b/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java @@ -1,4 +1,25 @@ -/* Copyright (c) 2026 Codename One. GPLv2 with the Classpath exception. */ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.util.zip; import java.io.*; @@ -84,7 +105,14 @@ public int read(byte[] b, int off, int length) throws IOException { break; } ((PushbackInputStream) in).unread(first); - header(); + try { + header(); + } catch (IOException trailingData) { + // Like the JDK, ignore bytes that are not another complete header. + // Payload and trailer errors after a valid header remain errors. + eos = true; + break; + } inf.reset(); } return -1; diff --git a/vm/JavaAPI/src/java/util/zip/GZIPOutputStream.java b/vm/JavaAPI/src/java/util/zip/GZIPOutputStream.java index 2fa684e0f04..778ad5823fa 100644 --- a/vm/JavaAPI/src/java/util/zip/GZIPOutputStream.java +++ b/vm/JavaAPI/src/java/util/zip/GZIPOutputStream.java @@ -1,4 +1,25 @@ -/* Copyright (c) 2026 Codename One. GPLv2 with the Classpath exception. */ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.util.zip; import java.io.*; diff --git a/vm/JavaAPI/src/java/util/zip/Inflater.java b/vm/JavaAPI/src/java/util/zip/Inflater.java index ad5f44f36ee..df032ebeb76 100644 --- a/vm/JavaAPI/src/java/util/zip/Inflater.java +++ b/vm/JavaAPI/src/java/util/zip/Inflater.java @@ -1,6 +1,24 @@ /* * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. - * This code is licensed under GPLv2 with the Classpath exception. + * 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. */ package java.util.zip; diff --git a/vm/JavaAPI/src/java/util/zip/InflaterInputStream.java b/vm/JavaAPI/src/java/util/zip/InflaterInputStream.java index b0aa51685d9..4b40aac8f7f 100644 --- a/vm/JavaAPI/src/java/util/zip/InflaterInputStream.java +++ b/vm/JavaAPI/src/java/util/zip/InflaterInputStream.java @@ -1,6 +1,24 @@ /* * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. - * This code is licensed under GPLv2 with the Classpath exception. + * 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. */ package java.util.zip; diff --git a/vm/JavaAPI/src/java/util/zip/ZipEntry.java b/vm/JavaAPI/src/java/util/zip/ZipEntry.java index a378512ba46..e7a76bdb8de 100644 --- a/vm/JavaAPI/src/java/util/zip/ZipEntry.java +++ b/vm/JavaAPI/src/java/util/zip/ZipEntry.java @@ -1,6 +1,24 @@ /* * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. - * This code is licensed under GPLv2 with the Classpath exception. + * 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. */ package java.util.zip; diff --git a/vm/JavaAPI/src/java/util/zip/ZipException.java b/vm/JavaAPI/src/java/util/zip/ZipException.java index b3dcc91e8a3..21a11db1355 100644 --- a/vm/JavaAPI/src/java/util/zip/ZipException.java +++ b/vm/JavaAPI/src/java/util/zip/ZipException.java @@ -1,6 +1,24 @@ /* * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. - * This code is licensed under GPLv2 with the Classpath exception. + * 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. */ package java.util.zip; diff --git a/vm/JavaAPI/src/java/util/zip/ZipInputStream.java b/vm/JavaAPI/src/java/util/zip/ZipInputStream.java index f2ae18f2f81..d4e3f64aa2c 100644 --- a/vm/JavaAPI/src/java/util/zip/ZipInputStream.java +++ b/vm/JavaAPI/src/java/util/zip/ZipInputStream.java @@ -1,4 +1,25 @@ -/* Copyright (c) 2026 Codename One. GPLv2 with the Classpath exception. */ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package java.util.zip; import java.io.*; diff --git a/vm/JavaAPI/src/java/util/zip/ZipOutputStream.java b/vm/JavaAPI/src/java/util/zip/ZipOutputStream.java index 11116c6e0e4..4b2775fb17b 100644 --- a/vm/JavaAPI/src/java/util/zip/ZipOutputStream.java +++ b/vm/JavaAPI/src/java/util/zip/ZipOutputStream.java @@ -1,13 +1,32 @@ -// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. /* * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. - * This code is licensed under GPLv2 with the Classpath exception. + * 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. */ +// Generated by scripts/sync-zip-runtime.py; edit the canonical core source. package java.util.zip; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Calendar; import java.util.HashSet; @@ -17,14 +36,13 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipException; -/** - * Streaming ZIP writer for ParparVM. Payloads are never buffered in full. Central-directory metadata is - * limited to 16 MiB and 65534 entries; ZIP64 is explicitly refused. - * finish() leaves the destination open; close() closes it even after a failure. - * Input streams are owned by the caller. This stream is not thread safe. - */ +/// Streaming ZIP writer for ParparVM. Payloads are never buffered in full. Central-directory metadata is +/// limited to 16 MiB and 65534 entries; ZIP64 is explicitly refused. +/// finish() leaves the destination open; close() closes it even after a failure. +/// Input streams are owned by the caller. This stream is not thread safe. public class ZipOutputStream extends DeflaterOutputStream { - public static final int STORED = 0, DEFLATED = 8; + public static final int STORED = 0; + public static final int DEFLATED = 8; private static final long LIMIT = 0xffffffffL; private static final int METADATA_LIMIT = 16 * 1024 * 1024; private final ArrayList entries = new ArrayList(); @@ -34,33 +52,44 @@ public class ZipOutputStream extends DeflaterOutputStream { private final int encodingFlag; private Record current; private ZipEntry original; - private int method = DEFLATED, metadataSize; - private long written, entryStart, entrySize; - private boolean finished, closed; + private int method = DEFLATED; + private int metadataSize; + private long written; + private long entryStart; + private long entrySize; + private boolean finished; + private boolean closed; private IOException failure; private byte[] comment = new byte[0]; private static final class Record { ZipEntry entry; - byte[] name, extra, comment; - long offset, dosTime, attributes; - int flags, platform; + byte[] name; + byte[] extra; + byte[] comment; + long offset; + long dosTime; + long attributes; + int flags; + int platform; } public ZipOutputStream(OutputStream out) { - this(out, Charset.forName("UTF-8")); + this(out, StandardCharsets.UTF_8); } public ZipOutputStream(OutputStream out, Charset charset) { super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true), 8192); - if (charset == null) + if (charset == null) { throw new NullPointerException(); + } encoding = charset.displayName(); encodingFlag = "UTF-8".equalsIgnoreCase(encoding) ? 0x800 : 0; } public void setMethod(int method) { - if (method != STORED && method != DEFLATED) + if (method != STORED && method != DEFLATED) { throw new IllegalArgumentException("Invalid method"); + } this.method = method; } public void setLevel(int level) { @@ -70,64 +99,81 @@ public void setComment(String comment) { try { this.comment = encode(comment == null ? "" : comment); } catch (IOException ex) { - throw new IllegalArgumentException(ex.getMessage()); + throw new IllegalArgumentException(ex.getMessage(), ex); } } private byte[] encode(String value) throws IOException { byte[] bytes = value.getBytes(encoding); - if (bytes.length > 65535) + if (!value.equals(new String(bytes, encoding))) { + throw new ZipException("ZIP field cannot be represented in " + encoding); + } + if (bytes.length > 65535) { throw new ZipException("Encoded ZIP field exceeds 65535 bytes"); + } return bytes; } private void writable() throws IOException { - if (closed) + if (closed) { throw new IOException("ZIP stream closed"); - if (failure != null) + } + if (failure != null) { throw failure; - if (finished) + } + if (finished) { throw new IOException("ZIP stream finished"); + } } - /** Writes an entry without Unix attributes, like java.util.zip.ZipOutputStream. */ + /// Writes an entry without Unix attributes, like java.util.zip.ZipOutputStream. public void putNextEntry(ZipEntry entry) throws IOException { putEntry(entry, 0, 0); } private void putEntry(ZipEntry entry, int platform, long attributes) throws IOException { writable(); - if (entry == null) + if (entry == null) { throw new NullPointerException(); + } closeEntry(); - if (entries.size() >= 65534) + if (entries.size() >= 65534) { throw new ZipException("ZIP64 entry count unsupported"); - if (names.contains(entry.getName())) + } + if (names.contains(entry.getName())) { throw new ZipException("Duplicate entry: " + entry.getName()); + } Record r = new Record(); r.entry = new ZipEntry(entry); - if (r.entry.getMethod() == -1) + if (r.entry.getMethod() == -1) { r.entry.setMethod(method); - if (r.entry.getTime() == -1) + } + if (r.entry.getTime() == -1) { r.entry.setTime(System.currentTimeMillis()); + } r.name = encode(r.entry.getName()); r.comment = encode(r.entry.getComment() == null ? "" : r.entry.getComment()); byte[] extra = r.entry.getExtra(); r.extra = extra == null ? new byte[0] : extra.clone(); validateExtra(r.extra); r.extra = timestampExtra(r.extra, r.entry.getTime()); - if (r.name.length == 0) + if (r.name.length == 0) { throw new ZipException("Empty entry name"); + } checkSize(r.entry.getSize()); checkSize(r.entry.getCompressedSize()); if (r.entry.getMethod() == STORED) { - if (r.entry.getSize() < 0 && r.entry.getCompressedSize() >= 0) + if (r.entry.getSize() < 0 && r.entry.getCompressedSize() >= 0) { r.entry.setSize(r.entry.getCompressedSize()); - if (r.entry.getCompressedSize() < 0) + } + if (r.entry.getCompressedSize() < 0) { r.entry.setCompressedSize(r.entry.getSize()); - if (r.entry.getSize() < 0 || r.entry.getCrc() < 0 || r.entry.getSize() != r.entry.getCompressedSize()) + } + if (r.entry.getSize() < 0 || r.entry.getCrc() < 0 || r.entry.getSize() != r.entry.getCompressedSize()) { throw new ZipException("STORED entries require size and CRC before writing"); + } } int metadata = 46 + r.name.length + r.extra.length + r.comment.length; - if (metadataSize > METADATA_LIMIT - metadata) + if (metadataSize > METADATA_LIMIT - metadata) { throw new ZipException("ZIP central directory exceeds 16 MiB"); + } r.flags = encodingFlag | (r.entry.getMethod() == DEFLATED ? 8 : 0); r.platform = platform; r.attributes = attributes; @@ -137,8 +183,9 @@ private void putEntry(ZipEntry entry, int platform, long attributes) throws IOEx original = entry; entrySize = 0; crc.reset(); - if (r.entry.getMethod() == DEFLATED) + if (r.entry.getMethod() == DEFLATED) { def.reset(); + } try { word(0x04034b50L, 4); word(20, 2); @@ -164,38 +211,45 @@ private void putEntry(ZipEntry entry, int platform, long attributes) throws IOEx private static void validateExtra(byte[] extra) throws ZipException { int at = 0; while (at < extra.length) { - if (extra.length - at < 4) + if (extra.length - at < 4) { throw new ZipException("Malformed ZIP extra field"); + } int id = (extra[at] & 255) | ((extra[at + 1] & 255) << 8); int size = (extra[at + 2] & 255) | ((extra[at + 3] & 255) << 8); - if (id == 1) + if (id == 1) { throw new ZipException("ZIP64 extra field unsupported"); - if (size > extra.length - at - 4) + } + if (size > extra.length - at - 4) { throw new ZipException("Malformed ZIP extra field"); + } at += 4 + size; } } private static void checkSize(long size) throws ZipException { - if (size >= LIMIT) + if (size >= LIMIT) { throw new ZipException("ZIP64 size unsupported"); + } } private static byte[] timestampExtra(byte[] extra, long millis) throws ZipException { long seconds = millis / 1000; - if (millis < 0 && millis % 1000 != 0) + if (millis < 0 && millis % 1000 != 0) { seconds--; - if (seconds < Integer.MIN_VALUE || seconds > Integer.MAX_VALUE) - return extra; + } + boolean timestamp = seconds >= Integer.MIN_VALUE && seconds <= Integer.MAX_VALUE; + int added = timestamp ? 9 : 0; // Replace any caller-provided extended timestamp with the entry's time. int kept = 0; for (int at = 0; at < extra.length;) { int size = 4 + (extra[at + 2] & 255) + ((extra[at + 3] & 255) << 8); - if ((extra[at] & 255) != 0x55 || (extra[at + 1] & 255) != 0x54) + if ((extra[at] & 255) != 0x55 || (extra[at + 1] & 255) != 0x54) { kept += size; + } at += size; } - if (kept > 65535 - 9) + if (kept > 65535 - added) { throw new ZipException("No room for ZIP timestamp extra field"); - byte[] result = new byte[kept + 9]; + } + byte[] result = new byte[kept + added]; int pos = 0; for (int at = 0; at < extra.length;) { int size = 4 + (extra[at + 2] & 255) + ((extra[at + 3] & 255) << 8); @@ -205,77 +259,99 @@ private static byte[] timestampExtra(byte[] extra, long millis) throws ZipExcept } at += size; } + if (!timestamp) { + return result; + } result[pos] = 0x55; result[pos + 1] = 0x54; result[pos + 2] = 5; result[pos + 4] = 1; - for (int i = 0; i < 4; i++) result[pos + 5 + i] = (byte) (seconds >>> (8 * i)); + for (int i = 0; i < 4; i++) { + result[pos + 5 + i] = (byte) (seconds >>> (8 * i)); + } return result; } private static long dosTime(long millis) { // The deterministic epoch timestamp predates the DOS range in every // timezone. Avoid calendar/timezone lookups for each such entry. - if (millis == 0) + if (millis == 0) { return 0x00210000L; + } Calendar c = Calendar.getInstance(); c.setTime(new java.util.Date(millis)); int year = c.get(Calendar.YEAR); - if (year < 1980) + if (year < 1980) { return 0x00210000L; - if (year > 2107) + } + if (year > 2107) { return 0xff9fbf7dL; + } return ((long) (year - 1980) << 25) | ((c.get(Calendar.MONTH) + 1) << 21) | (c.get(Calendar.DAY_OF_MONTH) << 16) | (c.get(Calendar.HOUR_OF_DAY) << 11) | (c.get(Calendar.MINUTE) << 5) | (c.get(Calendar.SECOND) >> 1); } + @Override public void write(int value) throws IOException { write(new byte[] {(byte) value}, 0, 1); } + @Override public void write(byte[] data, int off, int len) throws IOException { writable(); - if (data == null) + if (data == null) { throw new NullPointerException(); - if (off < 0 || len < 0 || off > data.length - len) + } + if (off < 0 || len < 0 || off > data.length - len) { throw new IndexOutOfBoundsException(); - if (current == null) + } + if (current == null) { throw new ZipException("No current ZIP entry"); + } try { checkSize(entrySize + len); - if (current.entry.getMethod() == STORED && entrySize + len > current.entry.getSize()) + if (current.entry.getMethod() == STORED && entrySize + len > current.entry.getSize()) { throw new ZipException("STORED entry exceeds declared size"); + } crc.update(data, off, len); entrySize += len; - if (current.entry.getMethod() == STORED) + if (current.entry.getMethod() == STORED) { raw(data, off, len); - else { + } else { def.setInput(data, off, len); - while (!def.needsInput()) deflate(); + while (!def.needsInput()) { + deflate(); + } } } catch (IOException ex) { failure = ex; throw ex; } } + @Override protected void deflate() throws IOException { int n = def.deflate(buf, 0, buf.length); - if (n > 0) + if (n > 0) { raw(buf, 0, n); + } } public void closeEntry() throws IOException { writable(); - if (current == null) + if (current == null) { return; + } try { ZipEntry e = current.entry; if (e.getMethod() == DEFLATED) { def.finish(); - while (!def.finished()) deflate(); + while (!def.finished()) { + deflate(); + } } long compressed = written - entryStart; checkSize(compressed); if ((e.getSize() >= 0 && e.getSize() != entrySize) || (e.getCompressedSize() >= 0 && e.getCompressedSize() != compressed) - || (e.getCrc() >= 0 && e.getCrc() != crc.getValue())) + || (e.getCrc() >= 0 && e.getCrc() != crc.getValue())) { throw new ZipException("ZIP entry size or CRC mismatch"); + } e.setSize(entrySize); e.setCompressedSize(compressed); e.setCrc(crc.getValue()); @@ -295,13 +371,17 @@ public void closeEntry() throws IOException { throw ex; } } + @Override public void finish() throws IOException { - if (closed) + if (closed) { throw new IOException("ZIP stream closed"); - if (failure != null) + } + if (failure != null) { throw failure; - if (finished) + } + if (finished) { return; + } closeEntry(); try { long start = written; @@ -345,11 +425,14 @@ public void finish() throws IOException { throw ex; } } + @Override public void flush() throws IOException { - if (closed) + if (closed) { throw new IOException("ZIP stream closed"); - if (failure != null) + } + if (failure != null) { throw failure; + } try { out.flush(); } catch (IOException ex) { @@ -357,16 +440,19 @@ public void flush() throws IOException { throw ex; } } + @Override public void close() throws IOException { - if (closed) + if (closed) { return; + } // An earlier write already delivered failure to the caller. Throwing // that same object again from close breaks try-with-resources through // Throwable's self-suppression guard. IOException error = null; try { - if (failure == null) + if (failure == null) { finish(); + } } catch (IOException ex) { error = ex; } finally { @@ -379,12 +465,14 @@ public void close() throws IOException { try { out.close(); } catch (IOException ex) { - if (error == null) + if (error == null) { error = ex; + } } } - if (error != null) + if (error != null) { throw error; + } } private void word(long value, int bytes) throws IOException { byte[] b = new byte[bytes]; diff --git a/vm/README.md b/vm/README.md index 58ec08e0211..476a94f7b84 100644 --- a/vm/README.md +++ b/vm/README.md @@ -140,3 +140,21 @@ The latter also supports `--check`. JavaAPI ZIP classes on HotSpot, and a locally translated ParparVM executable. It uses the JDK ZIP reader and system `unzip` as independent readers, checks Unix mode bits after extraction, and compares contents and metadata across runtimes. + +`HttpServer.Response.stream` connects ZIP generation directly to HTTP/1 and +native HTTP/2 without buffering the archive. `BackendZipStreamingTest` downloads +64 MiB with a 32 MiB JVM heap, verifies framing and source cleanup, and cancels a +1 GiB producer before it finishes. The socket harness also exercises native +HTTP/2 flow control and stream resets: + +```sh +# Run the ZipTestServer fixture on a chosen local port, then: +python3 vm/backend/tests/zip-streaming.py 18199 /tmp/zip-artifacts --http2 +``` + +The fixture source directory is +`vm/tests/src/test/resources/com/codename1/tools/translator/zipserver`; set +`CN1_BACKEND_DEMO` to its absolute path and `CN1_BACKEND_STANDALONE_DEMO=1` +when invoking `build.sh ZipTestServer com.demo /tmp/zip-test-server` or +`package.sh ZipTestServer com.demo glibc-arm64`. Pass the listening port as the +binary's first argument. Native packaging and GraalVM validation are local only. diff --git a/vm/backend/demo/zipdownload/com/demo/ZipDownload.java b/vm/backend/demo/zipdownload/com/demo/ZipDownload.java index d0aea2777ba..87ce37e3aaf 100644 --- a/vm/backend/demo/zipdownload/com/demo/ZipDownload.java +++ b/vm/backend/demo/zipdownload/com/demo/ZipDownload.java @@ -1,16 +1,38 @@ -/* Copyright (c) 2026 Codename One. GPLv2 with the Classpath exception. */ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package com.demo; import com.codename1.backend.Backend; import com.codename1.backend.HttpServer; import com.codename1.io.zip.ZipArchiveOutputStream; -import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.zip.ZipEntry; -/** A small, fixed-size downloadable project. No temporary files are created. */ +/** A streaming project download. No archive buffer or temporary files are created. */ public final class ZipDownload { private static void archive(OutputStream destination) throws IOException { // Closing the ZIP owns destination. For a caller-owned stream use @@ -23,7 +45,15 @@ private static void archive(OutputStream destination) throws IOException { ZipEntry launcher = new ZipEntry("project/run.sh"); launcher.setTime(0); zip.putNextEntry(launcher, 0755); - zip.write("#!/bin/sh\nprintf 'Hello from the archive\\n'\n".getBytes("UTF-8")); + // Open sources only inside the callback and close even when the client cancels. + try (InputStream source = new ByteArrayInputStream( + "#!/bin/sh\nprintf 'Hello from the archive\\n'\n".getBytes("UTF-8"))) { + byte[] buffer = new byte[8192]; + int count; + while ((count = source.read(buffer)) != -1) { + zip.write(buffer, 0, count); + } + } zip.closeEntry(); ZipEntry readme = new ZipEntry("project/README.txt"); readme.setTime(0); @@ -38,20 +68,20 @@ public static void main(String[] args) throws Exception { System.out.println("ZIP_DOWNLOAD_OK"); return; } - // This demo contains only the fixed literals above. Do not accumulate - // arbitrary user files in memory: write those to an owned output stream. - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - archive(bytes); - final byte[] download = bytes.toByteArray(); Backend server = Backend.builder() .host("127.0.0.1") .port(8099) .handler(new HttpServer.Handler() { public HttpServer.Response handle(HttpServer.Request request) { - if (!"GET".equals(request.getMethod()) || !"/project.zip".equals(request.getTarget())) + if ((!"GET".equals(request.getMethod()) && !"HEAD".equals(request.getMethod())) + || !"/project.zip".equals(request.getTarget())) return HttpServer.Response.text(404, "Not found"); - return new HttpServer.Response(200, "application/zip", download) + return HttpServer.Response.stream(200, "application/zip", new HttpServer.BodyWriter() { + public void writeTo(OutputStream destination) throws IOException { + archive(destination); + } + }) .header("Content-Disposition", "attachment; filename=project.zip"); } }) diff --git a/vm/backend/impl/javase/com/codename1/backend/Http2.java b/vm/backend/impl/javase/com/codename1/backend/Http2.java index e00c94a8a6c..f4cc5c01946 100644 --- a/vm/backend/impl/javase/com/codename1/backend/Http2.java +++ b/vm/backend/impl/javase/com/codename1/backend/Http2.java @@ -130,6 +130,22 @@ public boolean respond(int streamId, int status, String contentType, List extraH public static void setMaxBodyBytes(long limit) { } + /// Starts a response with one bounded chunk supplied at a time by the server. + public boolean respondStream(int streamId, int status, String contentType, List headers) + throws IOException { + throw new IOException(UNSUPPORTED); + } + + /// Supplies the next chunk after the previous one has drained. + public void streamChunk(int streamId, byte[] bytes, int length, boolean end) throws IOException { + throw new IOException(UNSUPPORTED); + } + + /// Unsent bytes, or -1 if the stream has closed or been reset. + public int streamPending(int streamId) { + return -1; + } + public byte[] drain() throws IOException { throw new IOException(UNSUPPORTED); } diff --git a/vm/backend/impl/parparvm/com/codename1/backend/Http2.java b/vm/backend/impl/parparvm/com/codename1/backend/Http2.java index ea9c829bea2..c1e6e0a7ba7 100644 --- a/vm/backend/impl/parparvm/com/codename1/backend/Http2.java +++ b/vm/backend/impl/parparvm/com/codename1/backend/Http2.java @@ -228,6 +228,32 @@ public boolean respondFile(int streamId, int status, String contentType, List ex return true; } + /// Starts a response with one bounded chunk supplied at a time by the server. + public boolean respondStream(int streamId, int status, String contentType, List headers) + throws IOException { + int rc = respondStreamImpl(session, streamId, String.valueOf(status), headerBytes(contentType, headers)); + if (rc == OVER_BODY_BUDGET) { + return false; + } + if (rc != 0) { + throw new IOException("Could not start HTTP/2 streamed response"); + } + return true; + } + + /// Supplies the next chunk after the previous one has drained. + public void streamChunk(int streamId, byte[] bytes, int length, boolean end) throws IOException { + checkRange(bytes, 0, length); + if (length > 16384 || streamChunkImpl(session, streamId, bytes, length, end) != 0) { + throw new IOException("HTTP/2 streamed response closed or not drained"); + } + } + + /// Unsent bytes, or -1 if the stream has closed or been reset. + public int streamPending(int streamId) { + return streamPendingImpl(session, streamId); + } + /// Runs the session's output side and returns the bytes to put on the wire. /// Empty when there is nothing pending. public byte[] drain() throws IOException { @@ -354,6 +380,10 @@ private static native int respondFileImpl(long session, int streamId, String sta private static native void setMaxFileBodiesImpl(int limit); + private static native int respondStreamImpl(long session, int streamId, String status, byte[] headers); + private static native int streamChunkImpl(long session, int streamId, byte[] bytes, int length, boolean end); + private static native int streamPendingImpl(long session, int streamId); + private static native int respondImpl(long session, int streamId, String status, byte[] headerLines, byte[] body); private static native boolean wantsMoreImpl(long session); diff --git a/vm/backend/native/cn1_backend_http2.c b/vm/backend/native/cn1_backend_http2.c index 3dea286eaf1..3ea4431784e 100644 --- a/vm/backend/native/cn1_backend_http2.c +++ b/vm/backend/native/cn1_backend_http2.c @@ -111,6 +111,8 @@ typedef struct CN1H2Body { int64_t fileOffset; size_t length; size_t offset; + int streaming; + int finished; struct CN1H2Body* next; } CN1H2Body; @@ -280,7 +282,7 @@ static void cn1H2FreeBody(CN1H2Body* body) { file body holds no buffer and was never charged. */ if(body->data != NULL) { atomic_fetch_sub_explicit(&cn1H2PendingBodyBytes, - (long)body->length, memory_order_relaxed); + (long)(body->streaming ? 16384 : body->length), memory_order_relaxed); } if(body->fd >= 0) { atomic_fetch_sub_explicit(&cn1H2OpenFileBodies, 1, memory_order_relaxed); @@ -1236,6 +1238,9 @@ static ssize_t cn1H2ReadBody(nghttp2_session* session, int32_t streamId, uint8_t *dataFlags |= NGHTTP2_DATA_FLAG_EOF; return 0; } + if(body->streaming && body->offset == body->length && !body->finished) { + return NGHTTP2_ERR_DEFERRED; + } remaining = body->length - body->offset; if(remaining > length) { remaining = length; @@ -1279,7 +1284,7 @@ static ssize_t cn1H2ReadBody(nghttp2_session* session, int32_t streamId, uint8_t admitted the next one. The whole allocation is released in cn1H2FreeBody, which is the moment the memory actually goes away. */ } - if(body->offset >= body->length) { + if(body->offset >= body->length && (!body->streaming || body->finished)) { *dataFlags |= NGHTTP2_DATA_FLAG_EOF; cn1H2ReleaseBody(s, body); } @@ -1467,7 +1472,7 @@ JAVA_INT com_codename1_backend_Http2_respondImpl___long_int_java_lang_String_byt free(headerCopy); return -2; } - pending = (CN1H2Body*)malloc(sizeof(CN1H2Body)); + pending = (CN1H2Body*)calloc(1, sizeof(CN1H2Body)); if(pending != NULL) { pending->data = (unsigned char*)malloc((size_t)arr->length); if(pending->data == NULL) { @@ -1543,7 +1548,7 @@ JAVA_INT com_codename1_backend_Http2_respondFileImpl___long_int_java_lang_String } cn1H2ReleaseBodyForStream(s, streamId); - pending = (CN1H2Body*)malloc(sizeof(CN1H2Body)); + pending = (CN1H2Body*)calloc(1, sizeof(CN1H2Body)); if(pending == NULL) { close(fd); free(statusCopy); @@ -1579,6 +1584,77 @@ JAVA_INT com_codename1_backend_Http2_respondFileImpl___long_int_java_lang_String return rc == 0 ? 0 : -1; } +/* One reusable chunk per generated response. No callback enters Java from nghttp2. */ +JAVA_INT com_codename1_backend_Http2_respondStreamImpl___long_int_java_lang_String_byte_1ARRAY_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle, JAVA_INT streamId, JAVA_OBJECT status, JAVA_OBJECT headers) { + CN1H2Session* s = (CN1H2Session*)(intptr_t)handle; + CN1H2Body* body; + nghttp2_nv nva[CN1_H2_MAX_HEADERS + 1]; + nghttp2_data_provider provider; + char* statusCopy = NULL; + char* headerCopy = NULL; + long count; + int rc; + if(s == NULL) return -1; + count = cn1H2BuildHeaders(threadStateData, status, headers, nva, &statusCopy, &headerCopy); + if(count < 0) return -1; + if(!cn1H2ReserveBodyBytes(16384)) { + free(statusCopy); + free(headerCopy); + return -2; + } + body = (CN1H2Body*)calloc(1, sizeof(CN1H2Body)); + if(body != NULL) body->data = (unsigned char*)malloc(16384); + if(body == NULL || body->data == NULL) { + free(body); + atomic_fetch_sub_explicit(&cn1H2PendingBodyBytes, 16384, memory_order_relaxed); + free(statusCopy); + free(headerCopy); + return -1; + } + body->streamId = streamId; + body->fd = -1; + body->streaming = 1; + body->next = s->bodies; + s->bodies = body; + provider.source.ptr = body; + provider.read_callback = cn1H2ReadBody; + rc = nghttp2_submit_response(s->session, streamId, nva, (size_t)count, &provider); + if(rc != 0) cn1H2ReleaseBody(s, body); + free(statusCopy); + free(headerCopy); + return rc == 0 ? 0 : -1; +} + +static CN1H2Body* cn1H2StreamingBody(CN1H2Session* s, int32_t streamId) { + CN1H2Body* body; + if(s == NULL) return NULL; + for(body = s->bodies; body != NULL; body = body->next) { + if(body->streamId == streamId && body->streaming) return body; + } + return NULL; +} + +JAVA_INT com_codename1_backend_Http2_streamChunkImpl___long_int_byte_1ARRAY_int_boolean_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle, JAVA_INT streamId, JAVA_OBJECT bytes, JAVA_INT length, JAVA_BOOLEAN end) { + CN1H2Session* s = (CN1H2Session*)(intptr_t)handle; + CN1H2Body* body = cn1H2StreamingBody(s, streamId); + if(body == NULL || body->finished || body->offset != body->length + || bytes == JAVA_NULL || length < 0 || length > 16384 + || length > ((JAVA_ARRAY)bytes)->length) return -1; + if(length > 0) memcpy(body->data, CN1_ARRAY_DATA(bytes), (size_t)length); + body->length = (size_t)length; + body->offset = 0; + body->finished = end != JAVA_FALSE; + /* A consumed chunk may have exactly exhausted the peer window before the + provider was called again to defer; resume is only needed when deferred. */ + nghttp2_session_resume_data(s->session, streamId); + return 0; +} + +JAVA_INT com_codename1_backend_Http2_streamPendingImpl___long_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle, JAVA_INT streamId) { + CN1H2Body* body = cn1H2StreamingBody((CN1H2Session*)(intptr_t)handle, streamId); + return body == NULL ? -1 : (JAVA_INT)(body->length - body->offset); +} + JAVA_BOOLEAN com_codename1_backend_Http2_wantsMoreImpl___long_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle) { CN1H2Session* s = (CN1H2Session*)(intptr_t)handle; if(s == NULL) { diff --git a/vm/backend/src/com/codename1/backend/HttpServer.java b/vm/backend/src/com/codename1/backend/HttpServer.java index f85cc4ececd..4f50a355180 100644 --- a/vm/backend/src/com/codename1/backend/HttpServer.java +++ b/vm/backend/src/com/codename1/backend/HttpServer.java @@ -23,6 +23,7 @@ package com.codename1.backend; import java.io.IOException; +import java.io.OutputStream; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -1230,6 +1231,17 @@ public String getText() { } } + /// Generates a response on the serving worker, with bounded transport buffering. + /// Open sources inside writeTo and close them with try-with-resources. The server + /// invokes this only for responses that carry a body, never for HEAD or a bodiless + /// status. Capture request values in the handler; do not retain the Request or output. + /// A disconnect, reset or socket timeout makes writes throw IOException; let it + /// propagate so generation stops and source cleanup runs. No temporary files or + /// producer threads are created. Closing output flushes it, but not the connection. + public interface BodyWriter { + void writeTo(OutputStream output) throws IOException; + } + /// What a handler returns. public static final class Response { // Not final because Request.respond hands back ONE Response per connection, @@ -1248,6 +1260,7 @@ public static final class Response { /// Serialised into the connection buffer at write time; see jsonValue. Object deferredJson; boolean hasDeferredJson; + BodyWriter bodyWriter; /// Drops what this response pointed at, once it has been written. /// @@ -1258,6 +1271,7 @@ public static final class Response { void releaseRetained() { this.deferredJson = null; this.hasDeferredJson = false; + this.bodyWriter = null; this.body = EMPTY_BODY; this.extraHeaders = null; } @@ -1297,6 +1311,7 @@ void reset(int status, String contentType, byte[] body, int fileFd, this.extraHeaders = extraHeaders; this.deferredJson = null; this.hasDeferredJson = false; + this.bodyWriter = null; } public Response(int status, String contentType, byte[] body) { @@ -1337,9 +1352,23 @@ Response withHeaders(Map headers) { fileLength, headers); copy.deferredJson = deferredJson; copy.hasDeferredJson = hasDeferredJson; + copy.bodyWriter = bodyWriter; return copy; } + /// A response generated as it is downloaded. Its length is unknown: HTTP/1.1 + /// uses chunked framing, HTTP/1.0 closes after the body, and HTTP/2 uses DATA + /// frames with flow control. A generation failure aborts the response; headers + /// have already been sent, so it cannot be replaced with a successful error body. + public static Response stream(int status, String contentType, BodyWriter writer) { + if (writer == null) { + throw new NullPointerException("writer"); + } + Response response = new Response(status, contentType, EMPTY_BODY); + response.bodyWriter = writer; + return response; + } + /// A response whose body is a file. The server sends it with sendfile where /// the platform has it, so the bytes never enter user space, and CLOSES the /// descriptor when it is done -- a handler that returned one must not. @@ -6060,7 +6089,11 @@ private void serveOneRelease(int fd) { // Read before the write: writing releases what the Response held. int status = response.status; try { - writeResponse(conn, fd, session, response, keepAlive, headOnly); + boolean http11 = !"HTTP/1.0".equals(request.getVersion()); + if (response.bodyWriter != null && !http11) { + keepAlive = false; + } + writeResponse(conn, fd, session, response, keepAlive, headOnly, http11); sentStatus = status; if (conn.stripe >= 0) { servedStripes[conn.stripe]++; // single writer: this host @@ -6514,7 +6547,22 @@ private void serveHttp2(int fd, long session, byte[] pending, int pendingLength) // The same rule as HTTP/1: a 204, 304 or 1xx carries no body, so // a DATA frame must not follow the headers here either. boolean noBody = headOnly || statusForbidsBody(response.status); - if (response.fileFd >= 0 && !noBody + if (response.bodyWriter != null && !noBody) { + if (!h2.respondStream(stream.getId(), response.status, contentType, extra)) { + h2.respond(stream.getId(), 503, "text/plain", refusalHeaders(), null); + fallbackStatus = 503; + } else { + flushHttp2(fd, session, h2); + ResponseOutput output = new ResponseOutput(fd, session, false, h2, stream.getId()); //NOPMD CloseResource - complete flushes on success; failure must abort and the connection owns the socket. + try { + response.bodyWriter.writeTo(output); + output.complete(); + } catch (StreamCancelledException cancelled) { + // RST_STREAM cancels this producer, not other streams on the connection. + handlerError = cancelled; + } + } + } else if (response.fileFd >= 0 && !noBody && Http2.pendingBodyFiles() >= MAX_OPEN_H2_FILES) { // BEFORE submitting, not after. The turn check below stops // this session, but every other session wakes on a control @@ -6583,7 +6631,7 @@ private void serveHttp2(int fd, long session, byte[] pending, int pendingLength) // close is worse than the leak it was meant to fix, because // the number is reusable the instant the first close returns // and the second one then lands on whatever took it. - if (headOnly && !statusForbidsLength(response.status)) { + if (headOnly && response.bodyWriter == null && !statusForbidsLength(response.status)) { long described; if (response.fileFd >= 0) { described = response.fileLength; @@ -7441,7 +7489,7 @@ private void writeRefusedUpgrade(Conn conn, int fd, long session, Response refus conn.bodySink.reset(); Json.write(refused.deferredJson, conn.bodySink); } - writeResponse(conn, fd, session, refused, false, false); + writeResponse(conn, fd, session, refused, false, false, true); conn.writtenStatus = status; } catch (IOException err) { // The peer is already gone; there is nowhere to report this. @@ -8528,9 +8576,9 @@ private static void refreshHttpDate() { /// deferred JSON body and the two early returns that write a small body in one /// syscall are all in front of it. private void writeResponse(Conn conn, int fd, long session, Response response, - boolean keepAlive, boolean headOnly) throws IOException { + boolean keepAlive, boolean headOnly, boolean http11) throws IOException { try { - writeHeadAndBody(conn, fd, session, response, keepAlive, headOnly); + writeHeadAndBody(conn, fd, session, response, keepAlive, headOnly, http11); } finally { if (response.fileFd >= 0) { StaticFiles.closeFile(response.fileFd); @@ -8539,7 +8587,7 @@ private void writeResponse(Conn conn, int fd, long session, Response response, } private void writeHeadAndBody(Conn conn, int fd, long session, Response response, - boolean keepAlive, boolean headOnly) throws IOException { + boolean keepAlive, boolean headOnly, boolean http11) throws IOException { // A deferred JSON body was rendered before this was called -- see the // caller -- into a second reusable buffer rather than the head's, because // Content-Length has to be written before it and the head is not built yet. @@ -8553,7 +8601,9 @@ private void writeHeadAndBody(Conn conn, int fd, long session, Response response : (deferred != null ? deferredLength : response.body.length); // HEAD is not the only thing that suppresses a body; see statusForbidsBody. boolean noBody = headOnly || statusForbidsBody(response.status); - boolean noLength = statusForbidsLength(response.status); + boolean streamed = response.bodyWriter != null; + boolean chunked = streamed && http11 && !noBody; + boolean noLength = statusForbidsLength(response.status) || (streamed && (headOnly || !noBody)); // A HEAD and a bodiless STATUS are suppressed for different reasons and // must advertise different lengths. HEAD describes the representation it // is not sending, so it keeps the real figure. A 205 has no @@ -8628,6 +8678,9 @@ private void writeHeadAndBody(Conn conn, int fd, long session, Response response } conn.put(keepAlive ? "\r\nConnection: keep-alive" : "\r\nConnection: close"); } + if (chunked) { + conn.put("\r\nTransfer-Encoding: chunked"); + } if (response.extraHeaders != null) { java.util.Iterator it = response.extraHeaders.keySet().iterator(); while (it.hasNext()) { @@ -8671,6 +8724,16 @@ private void writeHeadAndBody(Conn conn, int fd, long session, Response response conn.put("\r\n\r\n"); } + if (streamed) { + writeTo(fd, session, conn.out, 0, conn.outLength); + if (!noBody) { + ResponseOutput output = new ResponseOutput(fd, session, chunked, null, 0); //NOPMD CloseResource - complete flushes on success; failure must abort and the connection owns the socket. + response.bodyWriter.writeTo(output); + output.complete(); + } + return; + } + // Head and body in ONE write when the body is small and already in memory. // Two writes are two syscalls and, on a fresh connection, two segments: the // client sees the headers, acknowledges, and only then gets the body. @@ -8720,6 +8783,146 @@ private void writeHeadAndBody(Conn conn, int fd, long session, Response response } } + private static final class StreamCancelledException extends IOException { + StreamCancelledException() { + super("HTTP/2 download cancelled"); + } + } + + private final class ResponseOutput extends OutputStream { + private final int fd; + private final int streamId; + private final long session; + private final boolean chunked; + private final Http2 h2; + private final byte[] buffer = new byte[16384]; + private final byte[] incoming; + private int count; + private boolean closed; + private boolean failed; + + ResponseOutput(int fd, long session, boolean chunked, Http2 h2, int streamId) { + this.fd = fd; + this.session = session; + this.chunked = chunked; + this.h2 = h2; + this.streamId = streamId; + incoming = h2 == null ? null : new byte[16384]; + } + + private void checkOpen() throws IOException { + if (closed || failed) { + throw new IOException("Response output closed or failed"); + } + } + + @Override + public void write(int value) throws IOException { + checkOpen(); + buffer[count++] = (byte) value; + if (count == buffer.length) { + flush(); + } + } + + @Override + public void write(byte[] data, int offset, int length) throws IOException { + checkOpen(); + if (data == null) { + throw new NullPointerException(); + } + if (offset < 0 || length < 0 || offset > data.length - length) { + throw new IndexOutOfBoundsException(); + } + while (length > 0) { + int n = Math.min(length, buffer.length - count); + System.arraycopy(data, offset, buffer, count, n); + count += n; + offset += n; + length -= n; + if (count == buffer.length) { + flush(); + } + } + } + + @Override + public void flush() throws IOException { + checkOpen(); + try { + if (count > 0) { + if (h2 != null) { + h2.streamChunk(streamId, buffer, count, false); + drainChunk(); + } else { + if (chunked) { + byte[] size = asciiBytes(Integer.toHexString(count) + "\r\n"); + HttpServer.this.writeTo(fd, session, size, 0, size.length); + } + HttpServer.this.writeTo(fd, session, buffer, 0, count); + if (chunked) { + byte[] end = asciiBytes("\r\n"); + HttpServer.this.writeTo(fd, session, end, 0, end.length); + } + } + count = 0; + } + } catch (IOException error) { + failed = true; + throw error; + } + } + + private void drainChunk() throws IOException { + for (;;) { + flushHttp2(fd, session, h2); + int pending = h2.streamPending(streamId); + if (pending < 0) { + throw new StreamCancelledException(); + } + if (pending == 0) { + return; + } + // The peer controls the window. Read its WINDOW_UPDATE or reset; + // socket deadlines also bound a peer that stops granting credit. + int n = readFrom(fd, session, incoming, 0, incoming.length); + if (n <= 0) { + throw new IOException("HTTP/2 connection closed"); + } + h2.receive(incoming, 0, n); + } + } + + @Override + public void close() throws IOException { + if (!closed) { + try { + if (!failed) { + flush(); + } + } finally { + closed = true; + } + } + } + + void complete() throws IOException { + if (failed) { + throw new IOException("Response output failed"); + } + if (!closed) { + close(); + } + if (h2 != null) { + h2.streamChunk(streamId, buffer, 0, true); + flushHttp2(fd, session, h2); + } else if (chunked) { + byte[] end = asciiBytes("0\r\n\r\n"); + HttpServer.this.writeTo(fd, session, end, 0, end.length); + } + } + } + /// Pre-encoded: this goes out on the body path of every expecting client. private static final byte[] CONTINUE_100 = asciiBytes("HTTP/1.1 100 Continue\r\n\r\n"); diff --git a/vm/backend/tests/zip-streaming.py b/vm/backend/tests/zip-streaming.py new file mode 100644 index 00000000000..485c6d85fe5 --- /dev/null +++ b/vm/backend/tests/zip-streaming.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +"""Exercise ZipTestServer over real sockets; --http2 also tests the native transport. + +The fixture lives in vm/tests/src/test/resources/com/codename1/tools/translator/zipserver. +Run it on the JVM or package that directory with CN1_BACKEND_DEMO. No third-party +Python modules are required. Downloads go to the supplied artifact directory. +""" +import argparse +import http.client +import socket +import ssl +import struct +import subprocess +import time +import zipfile +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('port', type=int) + parser.add_argument('artifacts', type=Path) + parser.add_argument('--http2', action='store_true') + parser.add_argument('--ca', type=Path, help='Trust the TLS fixture certificate at this path') + args = parser.parse_args() + args.artifacts.mkdir(parents=True, exist_ok=True) + + def connect(protocol="http/1.1"): + sock = socket.create_connection(('127.0.0.1', args.port), timeout=20) + if args.ca: + context = ssl.create_default_context(cafile=str(args.ca)) + context.set_alpn_protocols([protocol]) + sock = context.wrap_socket(sock, server_hostname='localhost') + return sock + + def request(method, path): + if args.ca: + conn = http.client.HTTPSConnection('localhost', args.port, timeout=30, + context=ssl.create_default_context(cafile=str(args.ca))) + else: + conn = http.client.HTTPConnection('127.0.0.1', args.port, timeout=30) + conn.request(method, path) + return conn, conn.getresponse() + + def metrics(): + conn, response = request('GET', '/metrics') + try: + assert response.status == 200 + return tuple(map(int, response.read().split())) + finally: + conn.close() + + def cleaned(before, cancelled=False): + deadline = time.monotonic() + 15 + while time.monotonic() < deadline: + current = metrics() + if current[0] > before[0] and current[0] == current[1]: + if cancelled: + assert current[2] - before[2] < 131072, 'Cancellation consumed the entire 1 GiB source' + return + time.sleep(.05) + raise AssertionError(('Source leaked', before, current)) + + def verify(path, size): + with zipfile.ZipFile(path) as archive: + assert archive.namelist() == ['project/', 'project/run.sh', 'project/empty', 'project/caf\u00e9.bin'] + for entry, mode in zip(archive.infolist(), [0o40755, 0o100755, 0o100644, 0o100644]): + assert entry.create_system == 3 and entry.external_attr >> 16 == mode + assert entry.flag_bits & 0x800 + assert entry.extra == b'UT\x05\x00\x01\x00\x00\x00\x00' + assert archive.getinfo('project/caf\u00e9.bin').file_size == size + assert archive.getinfo('project/empty').file_size == 0 + # Read with bounded memory and verify CRC all the way to EOF. + for entry in archive.infolist(): + with archive.open(entry) as source: + while source.read(65536): + pass + subprocess.run(['unzip', '-tq', str(path)], check=True) + extracted = path.parent / (path.stem + '-extracted') + subprocess.run(['unzip', '-oq', str(path), '-d', str(extracted)], check=True) + result = subprocess.check_output([str(extracted / 'project/run.sh')]) + assert result.strip() == b'ZIP_STREAM_OK' + + deadline = time.monotonic() + 60 + while True: + try: + metrics() + break + except OSError: + if time.monotonic() > deadline: + raise + time.sleep(.1) + + before = metrics() + # Reuse a connection: HEAD/204 must not start a source or corrupt the next response. + conn, response = request('HEAD', '/project.zip') + assert response.status == 200 and response.getheader('Content-Length') is None + assert response.read() == b'' + conn.request('GET', '/no-body') + response = conn.getresponse() + assert response.status == 204 and response.read() == b'' + conn.request('GET', '/metrics') + response = conn.getresponse() + assert tuple(map(int, response.read().split())) == before + conn.close() + + for name, size in [('project', 1024), ('large', 64 * 1024 * 1024)]: + path = args.artifacts / (name + '.zip') + conn, response = request('GET', '/' + name + '.zip') + assert response.status == 200 and response.chunked + assert response.getheader('Content-Disposition') == 'attachment; filename=project.zip' + with path.open('wb') as output: + while True: + data = response.read(65536) + if not data: + break + output.write(data) + conn.close() + verify(path, size) + before = metrics() + conn, response = request('GET', '/cancel.zip') + assert len(response.read(65536)) == 65536 + # Close both response and socket so the server sees cancellation promptly. + response.close() + conn.close() + cleaned(before, True) + + conn, response = request('GET', '/fail') + try: + response.read() + raise AssertionError('Failed callback emitted a successful response terminator') + except http.client.IncompleteRead: + pass + finally: + conn.close() + + # HTTP/1.0 cannot use chunked framing and must close even when keep-alive is requested. + with connect() as sock: + sock.sendall(b'GET /project.zip HTTP/1.0\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n') + response = http.client.HTTPResponse(sock) + response.begin() + assert not response.chunked and response.getheader('Content-Length') is None + assert response.getheader('Connection') == 'close' + assert response.read().startswith(b'PK') + + if args.http2: + def frame(sock, kind, flags, stream, payload=b''): + sock.sendall(len(payload).to_bytes(3, 'big') + bytes([kind, flags]) + + struct.pack('>I', stream) + payload) + + def receive(sock): + def exact(size): + data = b'' + while len(data) < size: + part = sock.recv(size - len(data)) + if not part: + raise EOFError('HTTP/2 connection closed') + data += part + return data + head = exact(9) + return head[3], head[4], int.from_bytes(head[5:], 'big'), exact(int.from_bytes(head[:3], 'big')) + + def h2start(path, window=65535, method=b"\x82"): + sock = connect("h2") + sock.sendall(b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n') + frame(sock, 4, 0, 0, struct.pack('>HI', 4, window)) + # HPACK: indexed GET/http, literal :path and :authority, without Huffman encoding. + encoded = path.encode('ascii') + frame(sock, 1, 5, 1, method + b'\x86\x04' + bytes([len(encoded)]) + encoded + + b'\x01\x09localhost') + return sock + + before = metrics() + for route, method in [('/project.zip', b'\x02\x04HEAD'), ('/no-body', b'\x82')]: + with h2start(route, method=method) as sock: + while True: + kind, flags, stream, data = receive(sock) + if kind == 4 and not flags & 1: + frame(sock, 4, 1, 0) + assert kind != 0, 'HEAD/204 emitted HTTP/2 DATA' + if stream == 1 and flags & 1: + break + assert metrics() == before + path = args.artifacts / 'http2.zip' + with h2start('/large.zip', 1024) as sock, path.open('wb') as output: + total = 0 + while True: + kind, flags, stream, data = receive(sock) + if kind == 4 and not flags & 1: + frame(sock, 4, 1, 0) + if kind == 0 and stream == 1: + output.write(data) + total += len(data) + if total == len(data): + # Hold the window shut briefly: no producer-sized native buffer may accumulate. + time.sleep(.1) + if data: + frame(sock, 8, 0, 0, struct.pack('>I', len(data))) + frame(sock, 8, 0, 1, struct.pack('>I', len(data))) + if flags & 1: + break + assert kind not in (3, 7), ('Unexpected reset/GOAWAY', kind, data) + verify(path, 64 * 1024 * 1024) + before = metrics() + with h2start('/cancel.zip', 1024) as sock: + while True: + kind, flags, stream, data = receive(sock) + if kind == 4 and not flags & 1: + frame(sock, 4, 1, 0) + if kind == 0 and stream == 1: + frame(sock, 3, 0, 1, struct.pack('>I', 8)) # CANCEL, without closing the socket first. + cleaned(before, True) + # A stream reset must leave the connection usable for another request. + frame(sock, 1, 5, 3, b'\x82\x86\x04\x08/metrics\x01\x09localhost') + while True: + kind, flags, stream, data = receive(sock) + assert kind != 7, 'Stream cancellation closed the whole connection' + if stream == 3 and flags & 1: + break + break + print('HTTP/2 flow control and RST_STREAM cleanup passed') + assert metrics()[0] == metrics()[1] + print('ZIP_STREAMING_OK', args.artifacts) + + +if __name__ == '__main__': + main() diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/BackendZipStreamingTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/BackendZipStreamingTest.java new file mode 100644 index 00000000000..61d8b01acb4 --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/BackendZipStreamingTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.tools.translator; + +import org.junit.jupiter.api.Test; +import java.io.IOException; +import java.net.ServerSocket; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +/** Runs the shared backend's real socket path with less heap than the ZIP payload. */ +class BackendZipStreamingTest { + @Test + void boundedDownloadsAndCancellationCloseSources() throws Exception { + assumeFalse(CompilerHelper.isWindows(), "POSIX backend fixture"); + Path backend = Paths.get("..", "backend").toAbsolutePath().normalize(); + Path work = Files.createTempDirectory("backend-zip-streaming-"); + Path classes = Files.createDirectories(work.resolve("classes")); + Path jdk = CompilerHelper.getAvailableCompilers("1.8").get(0).jdkHome; + List compile = new ArrayList(Arrays.asList(jdk.resolve("bin/javac").toString(), + "-encoding", "UTF-8", "-d", classes.toString())); + for (Path root : Arrays.asList(backend.resolve("src"), backend.resolve("impl/javase"), + backend.resolve("../tests/src/test/resources/com/codename1/tools/translator/zipserver"))) { + try (Stream paths = Files.walk(root)) { + paths.filter(p -> p.toString().endsWith(".java")).forEach(p -> compile.add(p.toString())); + } + } + String shared = run(backend, work, Arrays.asList("./shared-sources.sh")); + for (String file : shared.split("\\r?\\n")) { + if (!file.isEmpty()) compile.add(file); + } + run(backend, work, compile); + int port; + try (ServerSocket socket = new ServerSocket(0)) { port = socket.getLocalPort(); } + Path serverLog = work.resolve("server.log"); + Process server = new ProcessBuilder(jdk.resolve("bin/java").toString(), "-Xmx32m", "-cp", + classes.toString(), "com.demo.ZipTestServer", String.valueOf(port)) + .redirectErrorStream(true).redirectOutput(serverLog.toFile()).start(); + try { + String output = run(backend, work, Arrays.asList("python3", "tests/zip-streaming.py", + String.valueOf(port), work.resolve("artifacts").toString())); + assertTrue(output.contains("ZIP_STREAMING_OK"), output); + assertTrue(server.isAlive(), new String(Files.readAllBytes(serverLog), StandardCharsets.UTF_8)); + } finally { + server.destroy(); + if (!server.waitFor(5, TimeUnit.SECONDS)) server.destroyForcibly(); + } + } + + private static String run(Path directory, Path work, List command) throws Exception { + Path log = Files.createTempFile(work, "command-", ".log"); + Process process = new ProcessBuilder(command).directory(directory.toFile()) + .redirectErrorStream(true).redirectOutput(log.toFile()).start(); + try { + assertTrue(process.waitFor(180, TimeUnit.SECONDS), "Timed out: " + command); + String output = new String(Files.readAllBytes(log), StandardCharsets.UTF_8); + assertEquals(0, process.exitValue(), output); + return output; + } finally { + if (process.isAlive()) process.destroyForcibly(); + } + } +} diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java index f73da19ce98..459bf036417 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package com.codename1.tools.translator; import org.junit.jupiter.api.Test; diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java index f7da1234f29..c1a30528625 100644 --- a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ import java.io.*; import java.util.*; import java.util.zip.*; @@ -89,7 +111,18 @@ static void compression(byte[] data) throws Exception { while (!i.finished()) { int n = i.inflate(buf); decoded.write(buf, 0, n); - if (i.needsDictionary()) i.setDictionary(dictionary); + if (i.needsDictionary()) { + int expected = i.getAdler(); + boolean rejected = false; + try { + i.setDictionary(new byte[] {1, 2, 3}); + } catch (IllegalArgumentException wrong) { + rejected = true; + } + require(rejected && i.needsDictionary() && i.getAdler() == expected, + "wrong dictionary preserves retry state"); + i.setDictionary(dictionary); + } } require(Arrays.equals(dictionaryData, decoded.toByteArray()), "dictionary raw=" + raw); } finally { @@ -340,9 +373,89 @@ static void badZip() throws Exception { } require(truncated, "truncated ZIP header"); } + static void compatibilityRegressions(boolean customRuntime) throws Exception { + byte[] data = "flushed payload".getBytes("UTF-8"); + // JDK 8 itself loops indefinitely with buffers smaller than the flush marker. + for (int size = customRuntime ? 1 : 8; size <= 8; size++) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + Deflater def = new Deflater(); + try (DeflaterOutputStream out = new DeflaterOutputStream(bytes, def, size, true)) { + out.flush(); + out.write(data); + out.flush(); + out.flush(); + out.write(data); + } finally { + def.end(); + } + require(bytes.size() < 200, "tiny flush buffer stays bounded"); + byte[] twice = new byte[data.length * 2]; + System.arraycopy(data, 0, twice, 0, data.length); + System.arraycopy(data, 0, twice, data.length, data.length); + require(Arrays.equals(twice, readAll(new InflaterInputStream( + new ByteArrayInputStream(bytes.toByteArray())))), "tiny buffer sync flush"); + bytes.reset(); + try (GZIPOutputStream out = new GZIPOutputStream(bytes, size, true)) { + out.write(data); + out.flush(); + out.flush(); + } + require(Arrays.equals(data, readAll(new GZIPInputStream( + new ByteArrayInputStream(bytes.toByteArray())))), "tiny gzip sync flush"); + } + ByteArrayOutputStream member = new ByteArrayOutputStream(); + try (GZIPOutputStream out = new GZIPOutputStream(member)) { out.write(data); } + for (byte[] tail : new byte[][] { {42}, {31}, {31, -117}, {31, -117, 0, 0, 0, 0, 0, 0, 0, 0} }) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(member.toByteArray()); + bytes.write(tail); + require(Arrays.equals(data, readAll(new GZIPInputStream( + new ByteArrayInputStream(bytes.toByteArray())))), "gzip trailing bytes"); + } + boolean badHeader = false; + try { new GZIPInputStream(new ByteArrayInputStream(new byte[] {42})); } + catch (IOException expected) { badHeader = true; } + require(badHeader, "first gzip header remains strict"); + + for (String name : new String[] {"\u00e9", "\ud800", "\udc00"}) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(bytes, + java.nio.charset.Charset.forName("US-ASCII"))) { + boolean rejected = false; + try { out.putNextEntry(entry(name)); } + catch (IOException expected) { rejected = true; } + require(rejected, "lossy ZIP encoding rejected"); + out.putNextEntry(entry("?")); + out.closeEntry(); + } + } + try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(new ByteArrayOutputStream())) { + boolean rejected = false; + try { out.putNextEntry(entry("\ud800")); } + catch (IOException expected) { rejected = true; } + require(rejected, "malformed UTF-8 surrogate rejected"); + out.putNextEntry(entry("unicode-\ud83d\ude00")); + out.closeEntry(); + } + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ZipEntry future = entry("future"); + future.setExtra(new byte[] {0x55, 0x54, 5, 0, 1, 0, 0, 0, 0, 0x34, 0x12, 1, 0, 42}); + future.setTime(2208988800000L); // 2040-01-01, outside signed 32-bit UT time. + try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(bytes)) { + out.putNextEntry(future); + out.closeEntry(); + } + try (ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(bytes.toByteArray()))) { + ZipEntry restored = in.getNextEntry(); + require(restored.getTime() == future.getTime(), "stale UT timestamp removed"); + require(Arrays.equals(restored.getExtra(), new byte[] {0x34, 0x12, 1, 0, 42}), + "unrelated extra preserved"); + } + } public static void main(String[] args) throws Exception { byte[] data = payload(); checksums(); + compatibilityRegressions(args.length > 1); compression(data); gzip(data, args[0]); archive(data, args[0]); diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/zipserver/com/demo/ZipTestServer.java b/vm/tests/src/test/resources/com/codename1/tools/translator/zipserver/com/demo/ZipTestServer.java new file mode 100644 index 00000000000..0d0a01e1933 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/zipserver/com/demo/ZipTestServer.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.demo; + +import com.codename1.backend.Backend; +import com.codename1.backend.HttpServer; +import com.codename1.io.zip.ZipArchiveOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.zip.ZipEntry; + +/** Socket-test fixture: lazy sources expose cleanup without creating temporary files. */ +public final class ZipTestServer { + private static final AtomicInteger opened = new AtomicInteger(); + private static final AtomicInteger closed = new AtomicInteger(); + private static final AtomicInteger readBlocks = new AtomicInteger(); + + private static final class Source extends InputStream { + private final Random random = new Random(5998); + private long remaining; + Source(long length) { remaining = length; opened.incrementAndGet(); } + public int read() throws IOException { throw new IOException("Use block reads"); } + public int read(byte[] bytes, int offset, int length) { + if (remaining == 0) return -1; + int n = (int) Math.min(remaining, length); + for (int i = 0; i < n; i++) bytes[offset + i] = (byte) random.nextInt(); + remaining -= n; + readBlocks.incrementAndGet(); + return n; + } + public void close() { closed.incrementAndGet(); } + } + + private static void archive(OutputStream destination, long length) throws IOException { + try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(destination)) { + for (String name : new String[] {"project/", "project/run.sh", "project/empty", "project/caf\u00e9.bin"}) { + ZipEntry entry = new ZipEntry(name); + entry.setTime(0); + zip.putNextEntry(entry, name.endsWith("/") || name.endsWith(".sh") ? 0755 : 0644); + if (name.endsWith(".sh")) zip.write("#!/bin/sh\necho ZIP_STREAM_OK\n".getBytes("UTF-8")); + if (name.endsWith(".bin")) { + try (InputStream source = new Source(length)) { + byte[] buffer = new byte[8192]; + int n; + while ((n = source.read(buffer)) != -1) zip.write(buffer, 0, n); + } + } + zip.closeEntry(); + } + } + } + + public static void main(String[] args) throws Exception { + Backend.Builder builder = Backend.builder().host(System.getenv("ZIP_TEST_HOST") == null + ? "127.0.0.1" : System.getenv("ZIP_TEST_HOST")).port(Integer.parseInt(args[0])) + .handler(new HttpServer.Handler() { + public HttpServer.Response handle(HttpServer.Request request) { + String path = request.getTarget(); + if ("/metrics".equals(path)) return HttpServer.Response.text(200, + opened.get() + " " + closed.get() + " " + readBlocks.get()); + final long length = "/cancel.zip".equals(path) ? 1024L * 1024 * 1024 + : "/large.zip".equals(path) ? 64L * 1024 * 1024 : 1024; + final boolean fail = "/fail".equals(path); + return HttpServer.Response.stream("/no-body".equals(path) ? 204 : 200, + "application/zip", new HttpServer.BodyWriter() { + public void writeTo(OutputStream destination) throws IOException { + if (fail) { + destination.write(new byte[32768]); + throw new IOException("deliberate producer failure"); + } + archive(destination, length); + } + }).header("Content-Disposition", "attachment; filename=project.zip"); + } + }); + if (args.length == 3) builder.tls(args[1], args[2]); + Backend server = builder.start(); + try { server.awaitTermination(); } finally { server.stop(); } + } +} From c933bcb1de0d73b0c63b3d9bfd663f056c4eb0e7 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:34:46 +0300 Subject: [PATCH 03/16] Remove trailing whitespace from ZIP socket harness --- vm/backend/tests/zip-streaming.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vm/backend/tests/zip-streaming.py b/vm/backend/tests/zip-streaming.py index 485c6d85fe5..5db140e0f02 100644 --- a/vm/backend/tests/zip-streaming.py +++ b/vm/backend/tests/zip-streaming.py @@ -181,7 +181,7 @@ def h2start(path, window=65535, method=b"\x82"): if stream == 1 and flags & 1: break assert metrics() == before - path = args.artifacts / 'http2.zip' + path = args.artifacts / 'http2.zip' with h2start('/large.zip', 1024) as sock, path.open('wb') as output: total = 0 while True: From bafc1e8590a243ff942123567a4539870bfa9859 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:04:02 +0300 Subject: [PATCH 04/16] Align ZIP guide text with API and prose checks --- .../Backend-Operations.asciidoc | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/developer-guide/Backend-Operations.asciidoc b/docs/developer-guide/Backend-Operations.asciidoc index e1c03d39cdf..7be890aa3b6 100644 --- a/docs/developer-guide/Backend-Operations.asciidoc +++ b/docs/developer-guide/Backend-Operations.asciidoc @@ -201,11 +201,11 @@ Compression uses the existing pure-Java JZlib engine. JVM and Android targets use their platform implementation of the standard classes. This is a streaming API, not the entire Java SE ZIP package. Random-access -`ZipFile`, `DeflaterInputStream`, `InflaterOutputStream`, and the `FileTime` -methods on `ZipEntry` are not provided. Reading a ZIP entry verifies its CRC +`ZipFile`, reverse compression filters, and the `FileTime` methods on +`ZipEntry` aren't provided. Reading a ZIP entry verifies its CRC and sizes when its end is reached; call `closeEntry()` to consume and verify -an entry you have only partly read. Closing the input stream does not drain -or verify unread entries. ZIP input does not extract files or interpret Unix +an entry you have only partly read. Closing the input stream doesn't drain +or verify unread entries. ZIP input doesn't extract files or interpret Unix permissions. ==== Executable files in a project download @@ -223,9 +223,9 @@ include::../demos/backend/src/main/java/com/codenameone/developerguide/backend/Z ---- The permission argument accepts `0000` through `0777`. The writer infers a -regular file or directory from the trailing slash; it does not create symlinks +regular file or directory from the trailing slash; it doesn't create symlinks or special files. Use `0644` for ordinary files. Names are archive paths with -`/` separators. The writer does not sanitize names for extraction, so choose +`/` separators. The writer doesn't sanitize names for extraction, so choose relative names without `..` components. The runnable example is `vm/backend/demo/zipdownload/com/demo/ZipDownload.java`. @@ -243,7 +243,7 @@ The same binary can generate a file without starting HTTP: `HttpServer.Response.stream` invokes the writer when it sends the response. Open sources inside this callback and close them with try-with-resources. HEAD and -statuses without a body do not invoke it, so they do not open sources. Closing +statuses without a body don't invoke it, so they don't open sources. Closing the supplied output flushes it without closing the connection. The server ends the response only after the callback returns successfully. @@ -265,8 +265,8 @@ The local JVM transport supports HTTP/1; test HTTP/2 with the packaged backend. ==== Format limits and ownership * The default name/comment encoding is UTF-8, with the ZIP UTF-8 flag set. - Charset constructors also accept encodings available in the runtime. Names and - comments that cannot be encoded without replacement are rejected. + `Charset` constructors also accept encodings available in the runtime. Names and + comments that can't be encoded without replacement are rejected. * Files may be empty. Directories are explicit entries ending in `/`. Duplicate entry names and empty entry names are rejected. * `DEFLATED` entries stream with data descriptors. `STORED` entries require a @@ -276,7 +276,7 @@ The local JVM transport supports HTTP/1; test HTTP/2 with the packaged backend. bytes. There may be at most 65,534 entries. Caller-supplied ZIP64 extra fields are rejected. A limit discovered while streaming raises `ZipException` and leaves an incomplete archive; discard that output. -* Payload memory does not grow with file size. Compression uses a fixed window +* Payload memory doesn't grow with file size. Compression uses a fixed window and output buffer. Central-directory records grow with entry count and encoded names, extras, and comments; the writer caps their serialized total at 16 MiB. Java objects and strings add overhead beyond that serialized limit. From 291427fe256c2e1b6a44e32de1a36fdb381e11a7 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:59:02 +0300 Subject: [PATCH 05/16] Include ZIP filter stream bases in the mobile API --- .../CLDC11/src/java/io/FilterInputStream.java | 37 +++++++++++++++++++ .../src/java/io/FilterOutputStream.java | 33 +++++++++++++++++ scripts/generate-zip-api-stubs.py | 19 +++++++--- 3 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 Ports/CLDC11/src/java/io/FilterInputStream.java create mode 100644 Ports/CLDC11/src/java/io/FilterOutputStream.java diff --git a/Ports/CLDC11/src/java/io/FilterInputStream.java b/Ports/CLDC11/src/java/io/FilterInputStream.java new file mode 100644 index 00000000000..dc0b52f0b45 --- /dev/null +++ b/Ports/CLDC11/src/java/io/FilterInputStream.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.io; + +public class FilterInputStream extends java.io.InputStream { + protected volatile java.io.InputStream in; + protected FilterInputStream(java.io.InputStream arg0) { throw new RuntimeException("Compile-time stub"); } + public int available() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void close() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public synchronized void mark(int arg0) { throw new RuntimeException("Compile-time stub"); } + public boolean markSupported() { throw new RuntimeException("Compile-time stub"); } + public int read() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public int read(byte[] arg0, int arg1, int arg2) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public synchronized void reset() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public long skip(long arg0) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } +} diff --git a/Ports/CLDC11/src/java/io/FilterOutputStream.java b/Ports/CLDC11/src/java/io/FilterOutputStream.java new file mode 100644 index 00000000000..3b5709998a5 --- /dev/null +++ b/Ports/CLDC11/src/java/io/FilterOutputStream.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only. +package java.io; + +public class FilterOutputStream extends java.io.OutputStream { + protected java.io.OutputStream out; + public FilterOutputStream(java.io.OutputStream arg0) { throw new RuntimeException("Compile-time stub"); } + public void close() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void flush() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void write(byte[] arg0, int arg1, int arg2) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void write(int arg0) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } +} diff --git a/scripts/generate-zip-api-stubs.py b/scripts/generate-zip-api-stubs.py index f3f97099021..bf7a0530118 100644 --- a/scripts/generate-zip-api-stubs.py +++ b/scripts/generate-zip-api-stubs.py @@ -13,19 +13,26 @@ parser.add_argument('--check', action='store_true') args = parser.parse_args() java_home = Path(os.environ['JAVA_HOME']) -for source in sorted((ROOT / 'vm/JavaAPI/src/java/util/zip').glob('*.java')): - compiled = ROOT / 'vm/JavaAPI/target/classes/java/util/zip' / (source.stem + '.class') +sources = sorted((ROOT / 'vm/JavaAPI/src/java/util/zip').glob('*.java')) +# ZIP streams inherit protected fields and methods from these JDK classes. +# Include them in the mobile boot class path, which does not use the host JDK. +sources += [ROOT / 'vm/JavaAPI/src/java/io' / name + for name in ('FilterInputStream.java', 'FilterOutputStream.java')] +for source in sources: + relative = source.relative_to(ROOT / 'vm/JavaAPI/src') + package = '.'.join(relative.parent.parts) + compiled = (ROOT / 'vm/JavaAPI/target/classes' / relative).with_suffix('.class') api = subprocess.check_output([str(java_home / 'bin/javap'), '-protected', '-constants', str(compiled)], text=True) - lines = ['// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only.', 'package java.util.zip;', ''] + lines = ['// Generated by scripts/generate-zip-api-stubs.py. Compile-time API only.', 'package ' + package + ';', ''] is_interface = False for line in api.splitlines(): if line.startswith('Compiled from '): continue if line.startswith('public '): is_interface = ' interface ' in line - line = line.replace('java.util.zip.' + source.stem, source.stem, 1) + line = line.replace(package + '.' + source.stem, source.stem, 1) elif '(' in line: - line = line.replace('java.util.zip.' + source.stem + '(', source.stem + '(') + line = line.replace(package + '.' + source.stem + '(', source.stem + '(') if not is_interface: body = 'throw new RuntimeException("Compile-time stub");' # The JDK filter base classes require an explicit constructor call. @@ -46,7 +53,7 @@ lines.append(line) header = (ROOT / 'CodenameOne/src/com/codename1/impl/SharedWithBackend.java').read_text().split('*/', 1)[0] + '*/\n' text = header + '\n'.join(lines) + '\n' - target = ROOT / 'Ports/CLDC11/src/java/util/zip' / source.name + target = ROOT / 'Ports/CLDC11/src' / relative if args.check: if not target.exists() or target.read_text() != text: raise SystemExit('Stale ZIP API stub: '+str(target)) From 14460aeb2e33909ab24e4133a753a41911014883 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 12 Oct 2026 04:53:46 +0300 Subject: [PATCH 06/16] Calibrate self-translation timing for the expanded ZIP API corpus --- vm/selfhost/perf-baseline/pr/6006.json | 45 ++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 vm/selfhost/perf-baseline/pr/6006.json diff --git a/vm/selfhost/perf-baseline/pr/6006.json b/vm/selfhost/perf-baseline/pr/6006.json new file mode 100644 index 00000000000..74bed6b1584 --- /dev/null +++ b/vm/selfhost/perf-baseline/pr/6006.json @@ -0,0 +1,45 @@ +{ + "pr": 6006, + "rebaseline": { + "linux-x64@amd-epyc-7763-64-core-processor": { + "translator": { + "all": { + "from": { + "memory": 0.519, + "time": 0.515, + "tolerance": { + "time": 0.2 + } + }, + "memory": 0.519, + "reason": "Recalibrate self-translation timing for the ZIP-expanded JavaAPI corpus (471 to 512 compiled classes), from five paired rounds of commit 291427fe25 in run 38170288209 attempt 1 (EPYC 9V74, median 0.628) and attempt 2 (EPYC 7763, median 0.626). Control: pre-PR master run 38159828789 on EPYC 7763 measured 0.596; run 38112530874 on EPYC 9V74 measured 0.586. Local before/after native binaries generated from the same translator sources differ only in a JZlib version constant and constant-pool indexes; median native time on the current corpus was 979 ms without ZIP and 986 ms with ZIP. Paired input-only comparisons also found no repeatable slowdown. These controls do not attribute the full shift from the older 0.515/0.516 calibration to ZIP execution. Preserve RAM measurements, the 20 percent time tolerance, and every other benchmark.", + "runs": 1, + "time": 0.626, + "tolerance": { + "time": 0.2 + } + } + } + }, + "linux-x64@amd-epyc-9v74-80-core-processor": { + "translator": { + "all": { + "from": { + "memory": 0.43, + "time": 0.516, + "tolerance": { + "time": 0.2 + } + }, + "memory": 0.43, + "reason": "Recalibrate self-translation timing for the ZIP-expanded JavaAPI corpus (471 to 512 compiled classes), from five paired rounds of commit 291427fe25 in run 38170288209 attempt 1 (EPYC 9V74, median 0.628) and attempt 2 (EPYC 7763, median 0.626). Control: pre-PR master run 38159828789 on EPYC 7763 measured 0.596; run 38112530874 on EPYC 9V74 measured 0.586. Local before/after native binaries generated from the same translator sources differ only in a JZlib version constant and constant-pool indexes; median native time on the current corpus was 979 ms without ZIP and 986 ms with ZIP. Paired input-only comparisons also found no repeatable slowdown. These controls do not attribute the full shift from the older 0.515/0.516 calibration to ZIP execution. Preserve RAM measurements, the 20 percent time tolerance, and every other benchmark.", + "runs": 1, + "time": 0.628, + "tolerance": { + "time": 0.2 + } + } + } + } + } +} From 7f3d9d202bc4019738c772b9782c54f524acee75 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 12 Oct 2026 05:58:43 +0300 Subject: [PATCH 07/16] Fix ZIP decoding, close errors and parameter-change flushes --- .../java/util/zip/CheckedOutputStream.java | 1 + .../java/util/zip/CheckedOutputStream.java | 9 ++ vm/JavaAPI/src/java/util/zip/Deflater.java | 7 +- .../src/java/util/zip/ZipInputStream.java | 6 +- .../translator/ZipApiIntegrationTest.java | 5 +- .../codename1/tools/translator/ZipApiApp.java | 120 ++++++++++++++++++ 6 files changed, 144 insertions(+), 4 deletions(-) diff --git a/Ports/CLDC11/src/java/util/zip/CheckedOutputStream.java b/Ports/CLDC11/src/java/util/zip/CheckedOutputStream.java index d8066d398da..7ecf4c27c8b 100644 --- a/Ports/CLDC11/src/java/util/zip/CheckedOutputStream.java +++ b/Ports/CLDC11/src/java/util/zip/CheckedOutputStream.java @@ -28,4 +28,5 @@ public class CheckedOutputStream extends java.io.FilterOutputStream { public java.util.zip.Checksum getChecksum() { throw new RuntimeException("Compile-time stub"); } public void write(int arg0) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } public void write(byte[] arg0, int arg1, int arg2) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } + public void close() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } } diff --git a/vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java b/vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java index 5dc39b3b6d2..db9e4d9438b 100644 --- a/vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java +++ b/vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java @@ -43,4 +43,13 @@ public void write(byte[] b, int off, int len) throws IOException { out.write(b, off, len); checksum.update(b, off, len); } + public void close() throws IOException { + // Always close the destination, even when flushing fails. The runtime's + // FilterOutputStream.close() currently swallows both kinds of failure. + try { + flush(); + } finally { + out.close(); + } + } } diff --git a/vm/JavaAPI/src/java/util/zip/Deflater.java b/vm/JavaAPI/src/java/util/zip/Deflater.java index 50bd9ef1e94..05edfe6a865 100644 --- a/vm/JavaAPI/src/java/util/zip/Deflater.java +++ b/vm/JavaAPI/src/java/util/zip/Deflater.java @@ -127,8 +127,11 @@ public int deflate(byte[] b, int off, int len, int flush) { paramsChanged = false; else if (rc != -5) throw new IllegalStateException(engine.msg); - // deflateParams may itself produce output, including a partial flush. - return len - engine.availOut; + // Parameter changes may emit a partial flush, but this does not + // satisfy a requested sync/full flush or finish. Continue using + // the remaining output space, retaining any bytes already emitted. + if (engine.availOut == 0) + return len; } int rc = engine.deflate(finishing ? 4 : flush); if (rc == 1) diff --git a/vm/JavaAPI/src/java/util/zip/ZipInputStream.java b/vm/JavaAPI/src/java/util/zip/ZipInputStream.java index d4e3f64aa2c..9890807bf69 100644 --- a/vm/JavaAPI/src/java/util/zip/ZipInputStream.java +++ b/vm/JavaAPI/src/java/util/zip/ZipInputStream.java @@ -87,7 +87,11 @@ public ZipEntry getNextEntry() throws IOException { throw new ZipException("Malformed ZIP extra"); at += 4 + n; } - entry = createZipEntry(new String(name, (flags & 0x800) != 0 ? "UTF-8" : encoding)); + String nameEncoding = (flags & 0x800) != 0 ? "UTF-8" : encoding; + String decodedName = new String(name, nameEncoding); + if (!java.util.Arrays.equals(name, decodedName.getBytes(nameEncoding))) + throw new ZipException("Invalid ZIP entry name encoding: " + nameEncoding); + entry = createZipEntry(decodedName); entry.setMethod(method); Calendar c = Calendar.getInstance(); c.set(Calendar.YEAR, 1980 + (int) (time >>> 25)); diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java index 459bf036417..e28e7f178ff 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java @@ -56,9 +56,12 @@ void streamingArchivesInteroperateOnJvmAndTranslatedRuntime() throws Exception { String java = config.jdkHome.resolve("bin/java").toString(); Path jdk = work.resolve("jdk"), patched = work.resolve("patched"), nativePrefix = work.resolve("native"); assertTrue(run(work, java, "-cp", app.toString(), "ZipApiApp", jdk.toString()).contains("ZIP_API_OK")); - // Patch only ZIP and its engine. Replacing Object/String on HotSpot is invalid. + // Patch ZIP, its engine and its output wrapper base so close behavior is + // tested against JavaAPI. Replacing Object/String on HotSpot is invalid. CompilerHelper.copyDirectory(api.resolve("java/util/zip"), patch.resolve("java/util/zip")); CompilerHelper.copyDirectory(api.resolve("com/codename1/impl/zip"), patch.resolve("com/codename1/impl/zip")); + Files.createDirectories(patch.resolve("java/io")); + Files.copy(api.resolve("java/io/FilterOutputStream.class"), patch.resolve("java/io/FilterOutputStream.class")); assertTrue(run(work, java, "-Xbootclasspath/p:" + patch, "-cp", app.toString(), "ZipApiApp", patched.toString(), jdk + "-standard.zip") .contains("ZIP_API_OK")); diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java index c1a30528625..81b8837d0dd 100644 --- a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java @@ -373,6 +373,121 @@ static void badZip() throws Exception { } require(truncated, "truncated ZIP header"); } + static void checkedCloseFailures() throws Exception { + for (int mode = 1; mode <= 3; mode++) { + final int failures = mode; + final boolean[] closed = {false}; + OutputStream sink = new OutputStream() { + public void write(int value) { } + public void flush() throws IOException { + if ((failures & 1) != 0) throw new IOException("flush failed"); + } + public void close() throws IOException { + closed[0] = true; + if ((failures & 2) != 0) throw new IOException("close failed"); + } + }; + boolean rejected = false; + try { new CheckedOutputStream(sink, new CRC32()).close(); } + catch (IOException expected) { rejected = true; } + require(rejected && closed[0], "checked stream reports errors and closes destination"); + } + } + static byte[] zipNameHeader(byte[] name, boolean utf8) { + byte[] header = new byte[30 + name.length]; + header[0] = 0x50; header[1] = 0x4b; header[2] = 3; header[3] = 4; + header[4] = 20; + header[7] = (byte) (utf8 ? 8 : 0); + header[12] = 0x21; // 1980-01-01 DOS date + header[26] = (byte) name.length; + header[27] = (byte) (name.length >>> 8); + System.arraycopy(name, 0, header, 30, name.length); + return header; + } + static void zipNameDecoding(boolean customRuntime) throws Exception { + for (byte[] name : new byte[][] {{-1}, {-64, -81}, {-30, -126}, {-19, -96, -128}}) { + // The UTF-8 flag overrides the caller's otherwise valid Latin-1 charset. + boolean rejected = false; + try (ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(zipNameHeader(name, true)), + java.nio.charset.Charset.forName("ISO-8859-1"))) { + in.getNextEntry(); + } catch (ZipException expected) { rejected = true; } + catch (IllegalArgumentException expected) { require(!customRuntime, "runtime uses ZipException"); rejected = true; } + require(rejected, "malformed UTF-8 ZIP name rejected: " + Arrays.toString(name)); + } + boolean rejected = false; + try (ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(zipNameHeader(new byte[] {-1}, false)), + java.nio.charset.Charset.forName("US-ASCII"))) { + in.getNextEntry(); + } catch (ZipException expected) { rejected = true; } + catch (IllegalArgumentException expected) { require(!customRuntime, "runtime uses ZipException"); rejected = true; } + require(rejected, "malformed caller charset ZIP name rejected"); + for (String name : new String[] {"replacement-\ufffd", "unicode-\ud83d\ude00"}) { + try (ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(zipNameHeader(name.getBytes("UTF-8"), true)))) { + require(name.equals(in.getNextEntry().getName()), "valid UTF-8 name preserved"); + } + } + try (ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(zipNameHeader(new byte[] {-1}, false)), + java.nio.charset.Charset.forName("ISO-8859-1"))) { + require("\u00ff".equals(in.getNextEntry().getName()), "valid caller charset name preserved"); + } + } + static void changeParameters(Deflater def, int change) { + if (change == 0) def.setLevel(9); // Same compression algorithm as the default level. + else if (change == 1) def.setLevel(1); // Changing algorithm may itself emit bytes. + else def.setStrategy(Deflater.HUFFMAN_ONLY); + } + static void requireFlushed(byte[] compressed, byte[] expected) throws Exception { + Inflater inf = new Inflater(); + try { + inf.setInput(compressed); + byte[] actual = new byte[expected.length + 1]; + int count = 0, n; + while ((n = inf.inflate(actual, count, actual.length - count)) > 0) count += n; + require(count == expected.length && Arrays.equals(expected, Arrays.copyOf(actual, count)), + "parameter change flush exposes entire payload before finish"); + } finally { inf.end(); } + } + static void parameterFlushes() throws Exception { + byte[] data = "buffered payload before parameter change".getBytes("UTF-8"); + for (int change = 0; change < 3; change++) { + for (int size : new int[] {1, 8, 512}) { + Deflater def = new Deflater(); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (DeflaterOutputStream out = new DeflaterOutputStream(bytes, def, size, true)) { + out.write(data); + changeParameters(def, change); + out.flush(); + requireFlushed(bytes.toByteArray(), data); + } finally { def.end(); } + } + for (int flush : new int[] {Deflater.SYNC_FLUSH, Deflater.FULL_FLUSH}) { + for (int size : new int[] {1, 8, 512}) { + Deflater def = new Deflater(); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try { + byte[] output = new byte[512]; + def.setInput(data); + while (!def.needsInput()) { + int n = def.deflate(output); + bytes.write(output, 0, n); + } + changeParameters(def, change); + int n = def.deflate(output, 0, size, flush); + bytes.write(output, 0, n); + if (n == size) { + // Drain tiny initial buffers with enough room to finish the flush marker. + do { + n = def.deflate(output, 0, output.length, flush); + bytes.write(output, 0, n); + } while (n == output.length); + } + requireFlushed(bytes.toByteArray(), data); + } finally { def.end(); } + } + } + } + } static void compatibilityRegressions(boolean customRuntime) throws Exception { byte[] data = "flushed payload".getBytes("UTF-8"); // JDK 8 itself loops indefinitely with buffers smaller than the flush marker. @@ -455,6 +570,11 @@ static void compatibilityRegressions(boolean customRuntime) throws Exception { public static void main(String[] args) throws Exception { byte[] data = payload(); checksums(); + checkedCloseFailures(); + zipNameDecoding(args.length > 1); + // JDK 8 also returns early after parameter changes; enforce the stronger + // flush guarantee on the patched JavaAPI and translated runtime. + if (args.length > 1) parameterFlushes(); compatibilityRegressions(args.length > 1); compression(data); gzip(data, args[0]); From df77e12f41474a6ccc67169b6e2ffb762167c18f Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:24:20 +0300 Subject: [PATCH 08/16] Isolate HTTP/2 producer failures and harden ZIP stream lifecycle --- .../io/zip/ZipArchiveOutputStream.java | 8 +-- Ports/CLDC11/src/java/lang/String.java | 4 +- .../CLDC11/src/java/nio/charset/Charset.java | 4 ++ vm/JavaAPI/src/java/lang/String.java | 4 +- vm/JavaAPI/src/java/nio/charset/Charset.java | 4 ++ .../java/util/zip/CheckedOutputStream.java | 4 ++ .../src/java/util/zip/ZipInputStream.java | 16 +++-- .../src/java/util/zip/ZipOutputStream.java | 8 +-- .../javase/com/codename1/backend/Http2.java | 5 ++ .../parparvm/com/codename1/backend/Http2.java | 8 +++ vm/backend/native/cn1_backend_http2.c | 8 +++ .../src/com/codename1/backend/HttpServer.java | 6 ++ vm/backend/tests/zip-streaming.py | 32 ++++++++- .../translator/ZipApiIntegrationTest.java | 15 +++++ .../codename1/tools/translator/ZipApiApp.java | 39 +++++++++-- .../tools/translator/ZipCharsetApp.java | 66 +++++++++++++++++++ .../zipserver/com/demo/ZipTestServer.java | 12 +++- 17 files changed, 214 insertions(+), 29 deletions(-) create mode 100644 vm/tests/src/test/resources/com/codename1/tools/translator/ZipCharsetApp.java diff --git a/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java b/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java index ac855536715..e31491741c7 100644 --- a/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java +++ b/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java @@ -49,7 +49,7 @@ public class ZipArchiveOutputStream extends DeflaterOutputStream { private final ArrayList entries = new ArrayList(); private final HashSet names = new HashSet(); private final CRC32 crc = new CRC32(); - private final String encoding; + private final Charset encoding; private final int encodingFlag; private Record current; private ZipEntry original; @@ -83,8 +83,8 @@ public ZipArchiveOutputStream(OutputStream out, Charset charset) { if (charset == null) { throw new NullPointerException(); } - encoding = charset.displayName(); - encodingFlag = "UTF-8".equalsIgnoreCase(encoding) ? 0x800 : 0; + encoding = charset; + encodingFlag = "UTF-8".equalsIgnoreCase(encoding.name()) ? 0x800 : 0; } public void setMethod(int method) { @@ -106,7 +106,7 @@ public void setComment(String comment) { private byte[] encode(String value) throws IOException { byte[] bytes = value.getBytes(encoding); if (!value.equals(new String(bytes, encoding))) { - throw new ZipException("ZIP field cannot be represented in " + encoding); + throw new ZipException("ZIP field cannot be represented in " + encoding.name()); } if (bytes.length > 65535) { throw new ZipException("Encoded ZIP field exceeds 65535 bytes"); diff --git a/Ports/CLDC11/src/java/lang/String.java b/Ports/CLDC11/src/java/lang/String.java index a99db9a4792..853d60a73fc 100644 --- a/Ports/CLDC11/src/java/lang/String.java +++ b/Ports/CLDC11/src/java/lang/String.java @@ -75,7 +75,7 @@ public String(byte[] bytes, int off, int len, java.lang.String enc) throws java. /// /// 8.0 public String(byte[] bytes, java.nio.charset.Charset charset) throws java.io.UnsupportedEncodingException { - this(bytes, 0, bytes.length, charset.displayName()); + this(bytes, 0, bytes.length, charset.name()); } /// Construct a new String by converting the specified array of bytes using the specified character encoding. The length of the new String is a function of the encoding, and hence may not be equal to the length of the byte array. @@ -187,7 +187,7 @@ public byte[] getBytes(java.lang.String enc) throws java.io.UnsupportedEncodingE /// Convert this String into bytes according to the specified character encoding, storing the result into a new byte array. public byte[] getBytes(Charset charset) throws java.io.UnsupportedEncodingException { - return getBytes(charset.displayName()); + return getBytes(charset.name()); } /// Copies characters from this string into the destination character array. diff --git a/Ports/CLDC11/src/java/nio/charset/Charset.java b/Ports/CLDC11/src/java/nio/charset/Charset.java index e4b94adb679..c851a1c5eff 100644 --- a/Ports/CLDC11/src/java/nio/charset/Charset.java +++ b/Ports/CLDC11/src/java/nio/charset/Charset.java @@ -34,6 +34,10 @@ public int compareTo(Charset another) { return name.compareTo(another.name); } + public final String name() { + return name; + } + public String displayName() { return name; } diff --git a/vm/JavaAPI/src/java/lang/String.java b/vm/JavaAPI/src/java/lang/String.java index 6cb756495fa..4a2d407ccb0 100644 --- a/vm/JavaAPI/src/java/lang/String.java +++ b/vm/JavaAPI/src/java/lang/String.java @@ -250,7 +250,7 @@ public String(byte[] bytes, int off, int len, java.lang.String enc) throws java. } public String(byte[] bytes, java.nio.charset.Charset charset) throws java.io.UnsupportedEncodingException { - this(bytes, 0, bytes.length, charset.displayName()); + this(bytes, 0, bytes.length, charset.name()); } /** @@ -684,7 +684,7 @@ public byte[] getBytes(java.lang.String enc) throws java.io.UnsupportedEncodingE } public byte[] getBytes(Charset charset) throws java.io.UnsupportedEncodingException { - return getBytes(charset.displayName()); + return getBytes(charset.name()); } /** diff --git a/vm/JavaAPI/src/java/nio/charset/Charset.java b/vm/JavaAPI/src/java/nio/charset/Charset.java index eeedca775ec..a45d0e24746 100644 --- a/vm/JavaAPI/src/java/nio/charset/Charset.java +++ b/vm/JavaAPI/src/java/nio/charset/Charset.java @@ -39,6 +39,10 @@ public int compareTo(Charset another) { return name.compareTo(another.name); } + public final String name() { + return name; + } + public String displayName() { return name; } diff --git a/vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java b/vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java index db9e4d9438b..97e8a18274d 100644 --- a/vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java +++ b/vm/JavaAPI/src/java/util/zip/CheckedOutputStream.java @@ -26,6 +26,7 @@ /** A stream which updates the supplied checksum. */ public class CheckedOutputStream extends FilterOutputStream { private final Checksum checksum; + private boolean closed; public CheckedOutputStream(OutputStream stream, Checksum checksum) { super(stream); if (checksum == null) @@ -44,6 +45,9 @@ public void write(byte[] b, int off, int len) throws IOException { checksum.update(b, off, len); } public void close() throws IOException { + if (closed) + return; + closed = true; // Always close the destination, even when flushing fails. The runtime's // FilterOutputStream.close() currently swallows both kinds of failure. try { diff --git a/vm/JavaAPI/src/java/util/zip/ZipInputStream.java b/vm/JavaAPI/src/java/util/zip/ZipInputStream.java index 9890807bf69..266585ae30f 100644 --- a/vm/JavaAPI/src/java/util/zip/ZipInputStream.java +++ b/vm/JavaAPI/src/java/util/zip/ZipInputStream.java @@ -29,7 +29,8 @@ /** Sequential ZIP reader for STORED and DEFLATED entries. ZIP64 and encryption are refused. */ public class ZipInputStream extends InflaterInputStream { private final CRC32 crc = new CRC32(); - private final String encoding; + private final Charset encoding; + private byte[] drainBuffer; private ZipEntry entry; private long remaining, count; private int flags; @@ -41,7 +42,7 @@ public ZipInputStream(InputStream in, Charset charset) { super(new PushbackInputStream(in, 8192), new Inflater(true), 8192); if (charset == null) throw new NullPointerException(); - encoding = charset.displayName(); + encoding = charset; } public ZipEntry getNextEntry() throws IOException { ensureOpen(); @@ -87,10 +88,10 @@ public ZipEntry getNextEntry() throws IOException { throw new ZipException("Malformed ZIP extra"); at += 4 + n; } - String nameEncoding = (flags & 0x800) != 0 ? "UTF-8" : encoding; + Charset nameEncoding = (flags & 0x800) != 0 ? Charset.forName("UTF-8") : encoding; String decodedName = new String(name, nameEncoding); if (!java.util.Arrays.equals(name, decodedName.getBytes(nameEncoding))) - throw new ZipException("Invalid ZIP entry name encoding: " + nameEncoding); + throw new ZipException("Invalid ZIP entry name encoding: " + nameEncoding.name()); entry = createZipEntry(decodedName); entry.setMethod(method); Calendar c = Calendar.getInstance(); @@ -168,8 +169,11 @@ public int read(byte[] b, int off, int length) throws IOException { } public void closeEntry() throws IOException { ensureOpen(); - byte[] b = new byte[8192]; - while (entry != null && read(b, 0, b.length) >= 0) { + if (entry == null) + return; + if (drainBuffer == null) + drainBuffer = new byte[8192]; + while (entry != null && read(drainBuffer, 0, drainBuffer.length) >= 0) { } } public int available() throws IOException { diff --git a/vm/JavaAPI/src/java/util/zip/ZipOutputStream.java b/vm/JavaAPI/src/java/util/zip/ZipOutputStream.java index 4b2775fb17b..5ac528367f4 100644 --- a/vm/JavaAPI/src/java/util/zip/ZipOutputStream.java +++ b/vm/JavaAPI/src/java/util/zip/ZipOutputStream.java @@ -48,7 +48,7 @@ public class ZipOutputStream extends DeflaterOutputStream { private final ArrayList entries = new ArrayList(); private final HashSet names = new HashSet(); private final CRC32 crc = new CRC32(); - private final String encoding; + private final Charset encoding; private final int encodingFlag; private Record current; private ZipEntry original; @@ -82,8 +82,8 @@ public ZipOutputStream(OutputStream out, Charset charset) { if (charset == null) { throw new NullPointerException(); } - encoding = charset.displayName(); - encodingFlag = "UTF-8".equalsIgnoreCase(encoding) ? 0x800 : 0; + encoding = charset; + encodingFlag = "UTF-8".equalsIgnoreCase(encoding.name()) ? 0x800 : 0; } public void setMethod(int method) { @@ -105,7 +105,7 @@ public void setComment(String comment) { private byte[] encode(String value) throws IOException { byte[] bytes = value.getBytes(encoding); if (!value.equals(new String(bytes, encoding))) { - throw new ZipException("ZIP field cannot be represented in " + encoding); + throw new ZipException("ZIP field cannot be represented in " + encoding.name()); } if (bytes.length > 65535) { throw new ZipException("Encoded ZIP field exceeds 65535 bytes"); diff --git a/vm/backend/impl/javase/com/codename1/backend/Http2.java b/vm/backend/impl/javase/com/codename1/backend/Http2.java index f4cc5c01946..062df877809 100644 --- a/vm/backend/impl/javase/com/codename1/backend/Http2.java +++ b/vm/backend/impl/javase/com/codename1/backend/Http2.java @@ -141,6 +141,11 @@ public void streamChunk(int streamId, byte[] bytes, int length, boolean end) thr throw new IOException(UNSUPPORTED); } + /// Aborts a failed response without terminating other streams in the session. + public void resetStream(int streamId) throws IOException { + throw new IOException(UNSUPPORTED); + } + /// Unsent bytes, or -1 if the stream has closed or been reset. public int streamPending(int streamId) { return -1; diff --git a/vm/backend/impl/parparvm/com/codename1/backend/Http2.java b/vm/backend/impl/parparvm/com/codename1/backend/Http2.java index c1e6e0a7ba7..e4a544eea5c 100644 --- a/vm/backend/impl/parparvm/com/codename1/backend/Http2.java +++ b/vm/backend/impl/parparvm/com/codename1/backend/Http2.java @@ -249,6 +249,13 @@ public void streamChunk(int streamId, byte[] bytes, int length, boolean end) thr } } + /// Aborts a failed response without terminating other streams in the session. + public void resetStream(int streamId) throws IOException { + if (resetStreamImpl(session, streamId) != 0) { + throw new IOException("Could not reset HTTP/2 stream " + streamId); + } + } + /// Unsent bytes, or -1 if the stream has closed or been reset. public int streamPending(int streamId) { return streamPendingImpl(session, streamId); @@ -382,6 +389,7 @@ private static native int respondFileImpl(long session, int streamId, String sta private static native int respondStreamImpl(long session, int streamId, String status, byte[] headers); private static native int streamChunkImpl(long session, int streamId, byte[] bytes, int length, boolean end); + private static native int resetStreamImpl(long session, int streamId); private static native int streamPendingImpl(long session, int streamId); private static native int respondImpl(long session, int streamId, String status, diff --git a/vm/backend/native/cn1_backend_http2.c b/vm/backend/native/cn1_backend_http2.c index 3ea4431784e..ac4cb3ab707 100644 --- a/vm/backend/native/cn1_backend_http2.c +++ b/vm/backend/native/cn1_backend_http2.c @@ -1650,6 +1650,14 @@ JAVA_INT com_codename1_backend_Http2_streamChunkImpl___long_int_byte_1ARRAY_int_ return 0; } +JAVA_INT com_codename1_backend_Http2_resetStreamImpl___long_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle, JAVA_INT streamId) { + CN1H2Session* s = (CN1H2Session*)(intptr_t)handle; + if(s == NULL) return -1; + /* nghttp2 closes the stream and releases its provider through the usual + on_stream_close callback when the reset is sent. */ + return nghttp2_submit_rst_stream(s->session, NGHTTP2_FLAG_NONE, streamId, NGHTTP2_INTERNAL_ERROR); +} + JAVA_INT com_codename1_backend_Http2_streamPendingImpl___long_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle, JAVA_INT streamId) { CN1H2Body* body = cn1H2StreamingBody((CN1H2Session*)(intptr_t)handle, streamId); return body == NULL ? -1 : (JAVA_INT)(body->length - body->offset); diff --git a/vm/backend/src/com/codename1/backend/HttpServer.java b/vm/backend/src/com/codename1/backend/HttpServer.java index 4f50a355180..592bb857d37 100644 --- a/vm/backend/src/com/codename1/backend/HttpServer.java +++ b/vm/backend/src/com/codename1/backend/HttpServer.java @@ -6560,6 +6560,12 @@ private void serveHttp2(int fd, long session, byte[] pending, int pendingLength) } catch (StreamCancelledException cancelled) { // RST_STREAM cancels this producer, not other streams on the connection. handlerError = cancelled; + } catch (IOException error) { + handlerError = error; + h2.resetStream(stream.getId()); + } catch (RuntimeException error) { + handlerError = error; + h2.resetStream(stream.getId()); } } } else if (response.fileFd >= 0 && !noBody diff --git a/vm/backend/tests/zip-streaming.py b/vm/backend/tests/zip-streaming.py index 5db140e0f02..8217d4a7f5d 100644 --- a/vm/backend/tests/zip-streaming.py +++ b/vm/backend/tests/zip-streaming.py @@ -218,7 +218,37 @@ def h2start(path, window=65535, method=b"\x82"): if stream == 3 and flags & 1: break break - print('HTTP/2 flow control and RST_STREAM cleanup passed') + for route in ('/fail', '/fail-runtime'): + before = metrics() + with h2start(route) as sock: + # Another request is already on this connection when the producer fails. + frame(sock, 1, 5, 3, b'\x82\x86\x04\x08/metrics\x01\x09localhost') + reset = False + other_finished = False + while not (reset and other_finished): + kind, flags, stream, data = receive(sock) + if kind == 4 and not flags & 1: + frame(sock, 4, 1, 0) + assert kind != 7, 'Producer failure closed the connection' + if stream == 1: + if kind == 3: + assert int.from_bytes(data, 'big') == 2, 'Expected INTERNAL_ERROR reset' + reset = True + elif kind in (0, 1): + assert not flags & 1, 'Failed producer ended its stream successfully' + if stream == 3: + assert kind != 3, 'Unrelated stream was reset' + if kind in (0, 1) and flags & 1: + other_finished = True + # A subsequent request must also succeed on the very same socket. + frame(sock, 1, 5, 5, b'\x82\x86\x04\x08/metrics\x01\x09localhost') + while True: + kind, flags, stream, data = receive(sock) + assert kind not in (3, 7), 'Session unusable after producer failure' + if stream == 5 and kind in (0, 1) and flags & 1: + break + cleaned(before) + print('HTTP/2 flow control, producer failure isolation and RST_STREAM cleanup passed') assert metrics()[0] == metrics()[1] print('ZIP_STREAMING_OK', args.artifacts) diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java index e28e7f178ff..34d7f9fdd52 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/ZipApiIntegrationTest.java @@ -65,6 +65,21 @@ void streamingArchivesInteroperateOnJvmAndTranslatedRuntime() throws Exception { assertTrue(run(work, java, "-Xbootclasspath/p:" + patch, "-cp", app.toString(), "ZipApiApp", patched.toString(), jdk + "-standard.zip") .contains("ZIP_API_OK")); + // Compile this fixture against the JDK charset-provider API. The native + // runtime only exposes built-in charsets, while the shared writer also + // serves JVM ports where custom providers are supported. + Path charsetSource = work.resolve("ZipCharsetApp.java"); + try (InputStream in = getClass().getResourceAsStream("/com/codename1/tools/translator/ZipCharsetApp.java")) { + assertNotNull(in); + Files.copy(in, charsetSource); + } + Path charsetApp = Files.createDirectories(work.resolve("charset-app")); + assertEquals(0, CompilerHelper.compile(config.jdkHome, Arrays.asList("-cp", app.toString(), "-d", + charsetApp.toString(), charsetSource.toString())), CompilerHelper.getLastErrorLog()); + String charsetClasspath = charsetApp + File.pathSeparator + app; + assertTrue(run(work, java, "-cp", charsetClasspath, "ZipCharsetApp").contains("ZIP_CHARSET_OK")); + assertTrue(run(work, java, "-Xbootclasspath/p:" + patch, "-cp", charsetClasspath, "ZipCharsetApp") + .contains("ZIP_CHARSET_OK")); verifyArchive(patched, jdk); CompilerHelper.copyDirectory(api, app); Path output = work.resolve("translated"); diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java index 81b8837d0dd..69b78fc062c 100644 --- a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java @@ -373,24 +373,29 @@ static void badZip() throws Exception { } require(truncated, "truncated ZIP header"); } - static void checkedCloseFailures() throws Exception { - for (int mode = 1; mode <= 3; mode++) { + static void checkedCloseFailures(boolean customRuntime) throws Exception { + for (int mode = 0; mode <= 3; mode++) { final int failures = mode; - final boolean[] closed = {false}; + final int[] calls = {0, 0}; OutputStream sink = new OutputStream() { public void write(int value) { } public void flush() throws IOException { + calls[0]++; if ((failures & 1) != 0) throw new IOException("flush failed"); } public void close() throws IOException { - closed[0] = true; + calls[1]++; if ((failures & 2) != 0) throw new IOException("close failed"); } }; boolean rejected = false; - try { new CheckedOutputStream(sink, new CRC32()).close(); } + CheckedOutputStream checked = new CheckedOutputStream(sink, new CRC32()); + try { checked.close(); } catch (IOException expected) { rejected = true; } - require(rejected && closed[0], "checked stream reports errors and closes destination"); + require(rejected == (mode != 0), "checked stream reports destination errors"); + // JDK 8 predates the idempotent FilterOutputStream close guard. + if (customRuntime) checked.close(); + require(calls[0] == 1 && calls[1] == 1, "checked close attempts flush/close exactly once"); } } static byte[] zipNameHeader(byte[] name, boolean utf8) { @@ -432,6 +437,25 @@ static void zipNameDecoding(boolean customRuntime) throws Exception { require("\u00ff".equals(in.getNextEntry().getName()), "valid caller charset name preserved"); } } + static void skipZipEntries() throws Exception { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (ZipOutputStream out = new ZipOutputStream(bytes)) { + for (int i = 0; i < 100; i++) { + out.putNextEntry(entry("entry-" + i)); + if ((i & 1) == 0) out.write(new byte[9000]); + out.closeEntry(); + } + } + try (ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(bytes.toByteArray()))) { + for (int i = 0; i < 100; i++) { + require(("entry-" + i).equals(in.getNextEntry().getName()), "drain advances to next entry"); + if (i % 3 == 0) readAll(in); + else if (i % 3 == 1) in.closeEntry(); + } + require(in.getNextEntry() == null, "drained all entries"); + in.closeEntry(); + } + } static void changeParameters(Deflater def, int change) { if (change == 0) def.setLevel(9); // Same compression algorithm as the default level. else if (change == 1) def.setLevel(1); // Changing algorithm may itself emit bytes. @@ -570,7 +594,8 @@ static void compatibilityRegressions(boolean customRuntime) throws Exception { public static void main(String[] args) throws Exception { byte[] data = payload(); checksums(); - checkedCloseFailures(); + checkedCloseFailures(args.length > 1); + skipZipEntries(); zipNameDecoding(args.length > 1); // JDK 8 also returns early after parameter changes; enforce the stronger // flush guarantee on the patched JavaAPI and translated runtime. diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipCharsetApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipCharsetApp.java new file mode 100644 index 00000000000..3b653de4ae1 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipCharsetApp.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +import com.codename1.io.zip.ZipArchiveOutputStream; +import java.io.*; +import java.nio.charset.*; +import java.util.zip.*; + +/** Tests provider-backed charset objects that cannot be looked up by name. */ +public class ZipCharsetApp { + public static void main(String[] args) throws Exception { + for (final Charset delegate : new Charset[] {StandardCharsets.ISO_8859_1, StandardCharsets.UTF_8}) { + Charset charset = new Charset(delegate == StandardCharsets.UTF_8 ? "UTF-8" : "X-CN1-ZIP-Test", null) { + public String displayName() { return "Human readable ZIP charset"; } + public boolean contains(Charset other) { return delegate.contains(other); } + public CharsetEncoder newEncoder() { return delegate.newEncoder(); } + public CharsetDecoder newDecoder() { return delegate.newDecoder(); } + }; + for (int writer = 0; writer < 2; writer++) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ZipEntry entry = new ZipEntry("caf\u00e9"); + entry.setComment("\u00e9t\u00e9"); + if (writer == 0) { + try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(bytes, charset)) { + out.setComment("\u00e9t\u00e9"); + out.putNextEntry(entry); + out.write(42); + } + } else { + try (ZipOutputStream out = new ZipOutputStream(bytes, charset)) { + out.setComment("\u00e9t\u00e9"); + out.putNextEntry(entry); + out.write(42); + } + } + byte[] archive = bytes.toByteArray(); + if (((archive[7] & 8) != 0) != (delegate == StandardCharsets.UTF_8)) + throw new AssertionError("UTF-8 flag must use canonical charset name"); + try (ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(archive), charset)) { + if (!"caf\u00e9".equals(in.getNextEntry().getName()) || in.read() != 42 || in.read() != -1) + throw new AssertionError("Custom charset did not round trip"); + } + } + } + System.out.println("ZIP_CHARSET_OK"); + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/zipserver/com/demo/ZipTestServer.java b/vm/tests/src/test/resources/com/codename1/tools/translator/zipserver/com/demo/ZipTestServer.java index 0d0a01e1933..78ff9fc9c99 100644 --- a/vm/tests/src/test/resources/com/codename1/tools/translator/zipserver/com/demo/ZipTestServer.java +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/zipserver/com/demo/ZipTestServer.java @@ -83,13 +83,19 @@ public HttpServer.Response handle(HttpServer.Request request) { opened.get() + " " + closed.get() + " " + readBlocks.get()); final long length = "/cancel.zip".equals(path) ? 1024L * 1024 * 1024 : "/large.zip".equals(path) ? 64L * 1024 * 1024 : 1024; - final boolean fail = "/fail".equals(path); + final boolean fail = "/fail".equals(path) || "/fail-runtime".equals(path); + final boolean runtimeFailure = "/fail-runtime".equals(path); return HttpServer.Response.stream("/no-body".equals(path) ? 204 : 200, "application/zip", new HttpServer.BodyWriter() { public void writeTo(OutputStream destination) throws IOException { if (fail) { - destination.write(new byte[32768]); - throw new IOException("deliberate producer failure"); + try (InputStream source = new Source(32768)) { + byte[] bytes = new byte[8192]; + int n; + while ((n = source.read(bytes)) != -1) destination.write(bytes, 0, n); + if (runtimeFailure) throw new IllegalStateException("deliberate producer failure"); + throw new IOException("deliberate producer failure"); + } } archive(destination, length); } From 97a2baea26cabacecd0fdcf5627e73ed866a00b2 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:24:48 +0300 Subject: [PATCH 09/16] Avoid rebuilding the Maven plugin reactor during workspace setup --- scripts/setup-workspace.sh | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/scripts/setup-workspace.sh b/scripts/setup-workspace.sh index 158d38270ca..c40475aa929 100755 --- a/scripts/setup-workspace.sh +++ b/scripts/setup-workspace.sh @@ -273,7 +273,7 @@ if [ ! -d "$CN1_BINARIES/.git" ]; then clone_cn1_binaries fi -# Both builds below run with -T 1C, so several modules install into the local +# The reactor below runs with -T 1C, so several modules install into the local # repository at once and contend for its lock. Maven's default wait is 30 seconds, # and on a cold cache under a loaded runner that is not always enough -- the build # then dies with "Could not acquire lock(s)" having compiled nothing wrong. Waiting @@ -320,17 +320,12 @@ mvn_retry() { done } -log "Building Codename One core modules" +log "Building Codename One core modules and Maven plugin" mvn_retry -f maven/pom.xml $MVN_LOCK_ARGS -T 1C -Dmaven.javadoc.skip=true -Dmaven.source.skip=true -DskipTests -Djava.awt.headless=true -Dcn1.binaries="$CN1_BINARIES" -Dcodename1.platform=javase -P local-dev-javase,compile-android,!download-cn1-binaries install "$@" -log "Building Codename One Maven plugin" -mvn_retry -f maven/pom.xml \ - -pl codenameone-maven-plugin -am \ - $MVN_LOCK_ARGS -T 1C -Dmaven.javadoc.skip=true -Dmaven.source.skip=true \ - -DskipTests -Djava.awt.headless=true \ - -Dcn1.binaries="$CN1_BINARIES" \ - -P !download-cn1-binaries \ - install "$@" +# codenameone-maven-plugin is already an unconditional module of this reactor. +# A second -pl codenameone-maven-plugin -am install rebuilt and re-analysed its +# dependencies, exhausting the macOS setup deadline after the first build passed. BUILD_CLIENT="$HOME/.codenameone/CodeNameOneBuildClient.jar" log "Ensuring CodeNameOneBuildClient.jar is installed" From d203b928a3681d7df1f1e3b08c55a1f92daf786a Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:24:48 +0300 Subject: [PATCH 10/16] Preserve retry trace state across EDT and network-thread races --- .../com/codename1/io/ConnectionRequest.java | 14 +- .../src/com/codename1/io/NetworkManager.java | 127 ++++++++++-------- .../codename1/telemetry/TelemetryTest.java | 6 +- 3 files changed, 83 insertions(+), 64 deletions(-) diff --git a/CodenameOne/src/com/codename1/io/ConnectionRequest.java b/CodenameOne/src/com/codename1/io/ConnectionRequest.java index fd491b4f1e1..5754340e66e 100644 --- a/CodenameOne/src/com/codename1/io/ConnectionRequest.java +++ b/CodenameOne/src/com/codename1/io/ConnectionRequest.java @@ -1544,10 +1544,16 @@ boolean performOperationComplete() throws IOException { // copies them before it calls runCurrentRequest -- so a traceparent the // app supplies as a default is seen here and kept, not replaced. try { - tracerAttempt = tracer.beforeRequest(this, - tracer == tracerParentOwner ? tracerParent : null); //NOPMD CompareObjectsWithEquals - tracerOwner = tracerAttempt == null ? null : tracer; - tracerThread = Thread.currentThread(); + Object parent; + synchronized (this) { + parent = tracer == tracerParentOwner ? tracerParent : null; //NOPMD CompareObjectsWithEquals + } + Object attempt = tracer.beforeRequest(this, parent); + synchronized (this) { + tracerAttempt = attempt; + tracerOwner = attempt == null ? null : tracer; + tracerThread = Thread.currentThread(); + } } catch (Throwable t) { Log.e(t); } diff --git a/CodenameOne/src/com/codename1/io/NetworkManager.java b/CodenameOne/src/com/codename1/io/NetworkManager.java index a33b8dbfb70..0fdd2fc76b2 100644 --- a/CodenameOne/src/com/codename1/io/NetworkManager.java +++ b/CodenameOne/src/com/codename1/io/NetworkManager.java @@ -1210,18 +1210,23 @@ public void run() { /// attempt by the time this one reaches its finally, and that attempt is /// not this thread's to end. static void endTracerAttempt(ConnectionRequest req, Throwable failure) { - Object attempt = req.tracerAttempt; - NetworkTracer owner = req.tracerOwner; - if (attempt == null || owner == null - || req.tracerThread != Thread.currentThread()) { //NOPMD CompareObjectsWithEquals - return; + Object attempt; + NetworkTracer owner; + synchronized (req) { + attempt = req.tracerAttempt; + owner = req.tracerOwner; + if (attempt == null || owner == null + || req.tracerThread != Thread.currentThread()) { //NOPMD CompareObjectsWithEquals + return; + } + // Publish the ended attempt atomically with clearing the active one. + // An EDT retry must never observe a gap between the two states. + req.tracerLastAttempt = attempt; + req.tracerLastOwner = owner; + req.tracerAttempt = null; + req.tracerOwner = null; + req.tracerThread = null; } - req.tracerAttempt = null; - req.tracerOwner = null; - req.tracerThread = null; - // Kept for a retry of a request queued with no parent: see addToQueue. - req.tracerLastAttempt = attempt; - req.tracerLastOwner = owner; try { // Only a status THIS attempt received: a reused request still holds // the last one's. @@ -1243,8 +1248,10 @@ private static void scheduleTracerClear(final ConnectionRequest req, final int r Display.getInstance().callSerially(new Runnable() { @Override public void run() { - if (req.tracerRequeues == requeues) { - clearTracerState(req); + synchronized (req) { + if (req.tracerRequeues == requeues) { + clearTracerState(req); + } } } }); @@ -1252,11 +1259,13 @@ public void run() { /// Forgets every tracer object a finished request holds. static void clearTracerState(ConnectionRequest req) { - req.tracerParent = null; - req.tracerParentOwner = null; - req.tracerParentChained = false; - req.tracerLastAttempt = null; - req.tracerLastOwner = null; + synchronized (req) { + req.tracerParent = null; + req.tracerParentOwner = null; + req.tracerParentChained = false; + req.tracerLastAttempt = null; + req.tracerLastOwner = null; + } } /// Adds the given network connection to the queue of execution @@ -1282,33 +1291,35 @@ private void addToQueue(@Async.Schedule ConnectionRequest request, boolean retry // here, on the thread that ran it, before the request is visible to // anyone else. endTracerAttempt(request, null); - request.tracerRequeues++; - // A request queued with no context -- the usual case for a generated - // client used outside Telemetry.run -- would start a NEW trace on every - // attempt: a 302 and the 200 it led to, or a failure and the retry that - // succeeded, came out as unrelated traces with separate sampling - // decisions, and the logical request could not be followed. So the - // attempt that just ended becomes the next one's parent: one trace, - // one decision, each attempt still its own span. A request that WAS - // queued inside an action keeps that action as every attempt's parent. - // A parent some OTHER tracer captured is as good as none: the tracer - // that ran this attempt will not use it (it only takes its own), and - // keeping it blocked the chain, so every retry started a new root. - // The attempt to continue from is the last one that ENDED -- or, when a - // listener retries from the EDT before the network thread has finished - // the attempt it is reacting to, that attempt, still in flight. Waiting - // for "ended" alone lost the race on a fast EDT, and the retry started - // an unrelated trace. - Object previous = request.tracerAttempt != null - ? request.tracerAttempt : request.tracerLastAttempt; - NetworkTracer previousOwner = request.tracerAttempt != null - ? request.tracerOwner : request.tracerLastOwner; - if ((request.tracerParent == null || request.tracerParentChained - || request.tracerParentOwner != previousOwner) //NOPMD CompareObjectsWithEquals - && previous != null) { - request.tracerParent = previous; - request.tracerParentOwner = previousOwner; - request.tracerParentChained = true; + synchronized (request) { + request.tracerRequeues++; + // A request queued with no context -- the usual case for a generated + // client used outside Telemetry.run -- would start a NEW trace on every + // attempt: a 302 and the 200 it led to, or a failure and the retry that + // succeeded, came out as unrelated traces with separate sampling + // decisions, and the logical request could not be followed. So the + // attempt that just ended becomes the next one's parent: one trace, + // one decision, each attempt still its own span. A request that WAS + // queued inside an action keeps that action as every attempt's parent. + // A parent some OTHER tracer captured is as good as none: the tracer + // that ran this attempt will not use it (it only takes its own), and + // keeping it blocked the chain, so every retry started a new root. + // The attempt to continue from is the last one that ENDED -- or, when a + // listener retries from the EDT before the network thread has finished + // the attempt it is reacting to, that attempt, still in flight. Waiting + // for "ended" alone lost the race on a fast EDT, and the retry started + // an unrelated trace. + Object previous = request.tracerAttempt != null + ? request.tracerAttempt : request.tracerLastAttempt; + NetworkTracer previousOwner = request.tracerAttempt != null + ? request.tracerOwner : request.tracerLastOwner; + if ((request.tracerParent == null || request.tracerParentChained + || request.tracerParentOwner != previousOwner) //NOPMD CompareObjectsWithEquals + && previous != null) { + request.tracerParent = previous; + request.tracerParentOwner = previousOwner; + request.tracerParentChained = true; + } } } // Captured HERE, on the thread that asked for the request, so the span it @@ -1368,17 +1379,19 @@ private void addToQueue(@Async.Schedule ConnectionRequest request, boolean retry return; } } - request.tracerParent = queuedParent; - request.tracerParentOwner = queuedBy; - // A fresh enqueue is a new logical request, not a retry of the last. - // It advances the generation too: a cleanup the previous run queued - // on the EDT would otherwise still match, and clear the parent this - // enqueue just captured -- a listener can reuse a finished request - // with addToQueue before that cleanup runs. - request.tracerRequeues++; - request.tracerParentChained = false; - request.tracerLastAttempt = null; - request.tracerLastOwner = null; + synchronized (request) { + request.tracerParent = queuedParent; + request.tracerParentOwner = queuedBy; + // A fresh enqueue is a new logical request, not a retry of the last. + // It advances the generation too: a cleanup the previous run queued + // on the EDT would otherwise still match, and clear the parent this + // enqueue just captured -- a listener can reuse a finished request + // with addToQueue before that cleanup runs. + request.tracerRequeues++; + request.tracerParentChained = false; + request.tracerLastAttempt = null; + request.tracerLastOwner = null; + } // And nothing of the last run's dealings with its authorizer: a request the // application queues again gets its one renewal again. A retry mark the last // run left unread goes too -- with nobody waiting on that run it was never @@ -1929,7 +1942,7 @@ private boolean runCurrentRequest(@Async.Execute ConnectionRequest req) { // call retry(), which needs the last attempt to continue its // trace. So the clear is queued on the EDT behind those // listener callbacks, and skipped if one of them retried. - scheduleTracerClear(req, req.tracerRequeues); + scheduleTracerClear(req, requeuesBefore); } NetworkGuard guard = getNetworkGuard(); if (guard != null && req.hasGuardResponse()) { diff --git a/maven/core-unittests/src/test/java/com/codename1/telemetry/TelemetryTest.java b/maven/core-unittests/src/test/java/com/codename1/telemetry/TelemetryTest.java index 12dd38c07ff..51802c41e88 100644 --- a/maven/core-unittests/src/test/java/com/codename1/telemetry/TelemetryTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/telemetry/TelemetryTest.java @@ -916,10 +916,10 @@ void anExportIsNeverRedirectedWithItsCredential() throws Exception { "the export followed a redirect, credential and all"); } - @Test + @org.junit.jupiter.api.RepeatedTest(50) void aRetryFromAResponseCodeListenerContinuesTheTrace() throws Exception { - // The listener runs LATER, on the EDT, after the network thread has ended - // the attempt; its retry must still continue the failed attempt's trace. + // The EDT listener races with the network thread ending the attempt. + // Repeat to exercise both orderings; retries must preserve the trace. TestCodenameOneImplementation impl = TestCodenameOneImplementation.getInstance(); final TestCodenameOneImplementation.TestConnection mock = impl.createConnection("http://flaky.test/api"); From 53933f164ea9c665b9170ffea1070f32240d73ea Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:33:18 +0300 Subject: [PATCH 11/16] Read retry cleanup state under the request lock --- .../src/com/codename1/io/NetworkManager.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/CodenameOne/src/com/codename1/io/NetworkManager.java b/CodenameOne/src/com/codename1/io/NetworkManager.java index 0fdd2fc76b2..fbd080a0c13 100644 --- a/CodenameOne/src/com/codename1/io/NetworkManager.java +++ b/CodenameOne/src/com/codename1/io/NetworkManager.java @@ -1242,7 +1242,11 @@ static void endTracerAttempt(ConnectionRequest req, Throwable failure) { /// queued there, unless the request was queued again in the meantime. private static void scheduleTracerClear(final ConnectionRequest req, final int requeues) { if (!Display.isInitialized()) { - clearTracerState(req); + synchronized (req) { + if (req.tracerRequeues == requeues) { + clearTracerState(req); + } + } return; } Display.getInstance().callSerially(new Runnable() { @@ -1930,9 +1934,13 @@ private boolean runCurrentRequest(@Async.Execute ConnectionRequest req) { // Any tracer field, the owners included: a request queued outside an // action holds only tracerParentOwner, and that alone pins the whole // telemetry installation. - if (req.tracerRequeues == requeuesBefore && !req.authorizationPending - && (req.tracerParent != null || req.tracerLastAttempt != null - || req.tracerParentOwner != null || req.tracerLastOwner != null)) { + boolean clearTracer; + synchronized (req) { + clearTracer = req.tracerRequeues == requeuesBefore && !req.authorizationPending + && (req.tracerParent != null || req.tracerLastAttempt != null + || req.tracerParentOwner != null || req.tracerLastOwner != null); + } + if (clearTracer) { // Nothing queued this request again YET. Its tracer state has // to go once it is done -- the parent and the last attempt are // the tracer's own objects, a span and through it the whole From 85b827f8dcdcfa6adef5cba8c0a82450bd608561 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:08:20 +0300 Subject: [PATCH 12/16] Avoid blocking GZIP member probes on persistent input streams --- .../src/java/util/zip/GZIPInputStream.java | 1 + .../src/java/util/zip/GZIPInputStream.java | 23 ++++++++++++ .../codename1/tools/translator/ZipApiApp.java | 35 +++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/Ports/CLDC11/src/java/util/zip/GZIPInputStream.java b/Ports/CLDC11/src/java/util/zip/GZIPInputStream.java index 83d8aa76168..e65a4c8703a 100644 --- a/Ports/CLDC11/src/java/util/zip/GZIPInputStream.java +++ b/Ports/CLDC11/src/java/util/zip/GZIPInputStream.java @@ -29,6 +29,7 @@ public class GZIPInputStream extends java.util.zip.InflaterInputStream { protected boolean eos; public GZIPInputStream(java.io.InputStream arg0) throws java.io.IOException { super(null); throw new RuntimeException("Compile-time stub"); } public GZIPInputStream(java.io.InputStream arg0, int arg1) throws java.io.IOException { super(null); throw new RuntimeException("Compile-time stub"); } + protected void fill() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } public int read(byte[] arg0, int arg1, int arg2) throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } public int available() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } public void close() throws java.io.IOException { throw new RuntimeException("Compile-time stub"); } diff --git a/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java b/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java index e7f09af1fb8..2b1c80f674c 100644 --- a/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java +++ b/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java @@ -81,6 +81,23 @@ private long word() throws IOException { for (int i = 0; i < 4; i++) result |= (long) byteValue() << (8 * i); return result; } + protected void fill() throws IOException { + ensureOpen(); + // A concatenated member may be wholly in the pushback buffer. Asking + // for more than its available bytes would also read the idle source. + int ready = in.available(); + len = in.read(buf, 0, ready > 0 ? Math.min(ready, buf.length) : buf.length); + if (len < 0) + throw new EOFException("Truncated compressed stream"); + if (len == 0) { + int b = in.read(); + if (b < 0) + throw new EOFException(); + buf[0] = (byte) b; + len = 1; + } + inf.setInput(buf, 0, len); + } public int read(byte[] b, int off, int length) throws IOException { ensureOpen(); Deflater.bounds(b, off, length); @@ -99,6 +116,12 @@ public int read(byte[] b, int off, int length) throws IOException { ((PushbackInputStream) in).unread(buf, len - left, left); if (word() != crc.getValue() || word() != (inf.getBytesWritten() & 0xffffffffL)) throw new ZipException("Invalid GZIP trailer"); + // PushbackInputStream counts both buffered bytes and source data. + // A completed member must not wait for a persistent source to close. + if (in.available() == 0) { + eos = true; + break; + } int first = in.read(); if (first < 0) { eos = true; diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java index 69b78fc062c..8fd27313a33 100644 --- a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java @@ -512,6 +512,40 @@ static void parameterFlushes() throws Exception { } } } + static void gzipLiveSource() throws Exception { + byte[] data = "completed member on an open source".getBytes("UTF-8"); + ByteArrayOutputStream member = new ByteArrayOutputStream(); + try (GZIPOutputStream out = new GZIPOutputStream(member)) { out.write(data); } + for (int members = 1; members <= 2; members++) { + for (int size : new int[] {1, 512}) { + ByteArrayOutputStream encoded = new ByteArrayOutputStream(); + ByteArrayOutputStream expected = new ByteArrayOutputStream(); + for (int i = 0; i < members; i++) { + encoded.write(member.toByteArray()); + expected.write(data); + } + final ByteArrayInputStream arrived = new ByteArrayInputStream(encoded.toByteArray()); + final boolean advertise = size == 1; + InputStream live = new InputStream() { + public int available() { return advertise ? arrived.available() : 0; } + public int read() { + require(arrived.available() > 0, "GZIP must not probe an idle live source"); + return arrived.read(); + } + public int read(byte[] b, int off, int len) { + require(arrived.available() > 0, "GZIP must not fill from an idle live source"); + return arrived.read(b, off, len); + } + }; + try (GZIPInputStream in = new GZIPInputStream(live, size)) { + // With size 512, concatenated members are already buffered + // even though the source advertises no additional bytes. + require(Arrays.equals(expected.toByteArray(), readAll(in)), "live GZIP members"); + require(in.read() == -1 && in.available() == 0, "completed live GZIP stays at EOF"); + } + } + } + } static void compatibilityRegressions(boolean customRuntime) throws Exception { byte[] data = "flushed payload".getBytes("UTF-8"); // JDK 8 itself loops indefinitely with buffers smaller than the flush marker. @@ -594,6 +628,7 @@ static void compatibilityRegressions(boolean customRuntime) throws Exception { public static void main(String[] args) throws Exception { byte[] data = payload(); checksums(); + gzipLiveSource(); checkedCloseFailures(args.length > 1); skipZipEntries(); zipNameDecoding(args.length > 1); From b6e5c2b95331f5bf36ee9b7c82c17b6682f69f00 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:45:29 +0300 Subject: [PATCH 13/16] Fix ZIP descriptor ambiguity and configurable GZIP read-ahead --- Ports/CLDC11/src/java/lang/System.java | 12 ++ .../Backend-Operations.asciidoc | 9 +- vm/JavaAPI/src/java/lang/System.java | 29 +++- vm/JavaAPI/src/java/util/zip/Adler32.java | 4 +- vm/JavaAPI/src/java/util/zip/CRC32.java | 4 +- .../src/java/util/zip/GZIPInputStream.java | 11 +- .../java/util/zip/InflaterInputStream.java | 4 +- .../src/java/util/zip/ZipInputStream.java | 31 ++++- .../codename1/tools/translator/ZipApiApp.java | 129 ++++++++++++++++++ 9 files changed, 219 insertions(+), 14 deletions(-) diff --git a/Ports/CLDC11/src/java/lang/System.java b/Ports/CLDC11/src/java/lang/System.java index 3c067b63b25..359682fac8c 100644 --- a/Ports/CLDC11/src/java/lang/System.java +++ b/Ports/CLDC11/src/java/lang/System.java @@ -80,6 +80,18 @@ public static java.lang.String getProperty(java.lang.String key){ return null; //TODO codavaj!! } + public static String getProperty(String key, String defaultValue) { + throw new RuntimeException("Compile-time stub"); + } + + public static String setProperty(String key, String value) { + throw new RuntimeException("Compile-time stub"); + } + + public static String clearProperty(String key) { + throw new RuntimeException("Compile-time stub"); + } + /// Returns the same hashcode for the given object as would be returned by the default method hashCode(), whether or not the given object's class overrides hashCode(). The hashcode for the null reference is zero. public static int identityHashCode(java.lang.Object x){ return 0; //TODO codavaj!! diff --git a/docs/developer-guide/Backend-Operations.asciidoc b/docs/developer-guide/Backend-Operations.asciidoc index 7be890aa3b6..d0622fe4be5 100644 --- a/docs/developer-guide/Backend-Operations.asciidoc +++ b/docs/developer-guide/Backend-Operations.asciidoc @@ -296,7 +296,14 @@ The local JVM transport supports HTTP/1; test HTTP/2 with the packaged backend. * Reading supports STORED and DEFLATED ZIP entries, with or without descriptors for DEFLATED entries. ZIP64, encryption, other compression methods, and STORED descriptors are explicitly rejected. GZIP supports concatenated members and - validates header and trailer checksums. + validates header and trailer checksums. By default it probes for another + GZIP member only when buffered or available bytes remain, avoiding a blocking + read on an idle open connection. For concatenated members on sources whose + `available()` method does not report readable bytes, call + `System.setProperty("jdk.util.gzip.tryReadAheadAfterTrailer", "true")` before + constructing the stream. This follows OpenJDK's read-ahead option: it reads + until the source ends and can block on a persistent connection. ParparVM + captures the setting when each GZIP stream is constructed. These explicit ZIP limits belong to the ParparVM implementation and the CN1 helper. The platform's own `java.util.zip.ZipOutputStream` may have broader diff --git a/vm/JavaAPI/src/java/lang/System.java b/vm/JavaAPI/src/java/lang/System.java index 0c6df307818..24067773bcd 100644 --- a/vm/JavaAPI/src/java/lang/System.java +++ b/vm/JavaAPI/src/java/lang/System.java @@ -185,8 +185,33 @@ public static void gc() { /** * Gets the system property indicated by the specified key. */ - public static java.lang.String getProperty(java.lang.String key){ - return null; + public static synchronized java.lang.String getProperty(java.lang.String key){ + checkPropertyKey(key); + return properties == null ? null : properties.get(key); + } + + private static java.util.Hashtable properties; + + private static void checkPropertyKey(String key) { + if (key == null) throw new NullPointerException(); + if (key.length() == 0) throw new IllegalArgumentException("Empty property key"); + } + + public static String getProperty(String key, String defaultValue) { + String value = getProperty(key); + return value == null ? defaultValue : value; + } + + public static synchronized String setProperty(String key, String value) { + checkPropertyKey(key); + if (value == null) throw new NullPointerException(); + if (properties == null) properties = new java.util.Hashtable(); + return properties.put(key, value); + } + + public static synchronized String clearProperty(String key) { + checkPropertyKey(key); + return properties == null ? null : properties.remove(key); } /** diff --git a/vm/JavaAPI/src/java/util/zip/Adler32.java b/vm/JavaAPI/src/java/util/zip/Adler32.java index 60b5e63ed42..e30354e4563 100644 --- a/vm/JavaAPI/src/java/util/zip/Adler32.java +++ b/vm/JavaAPI/src/java/util/zip/Adler32.java @@ -25,8 +25,10 @@ /** Standard Adler32 checksum. */ public class Adler32 implements Checksum { private final com.codename1.impl.zip.Adler32 delegate = new com.codename1.impl.zip.Adler32(); + private final byte[] singleByte = new byte[1]; public void update(int value) { - update(new byte[] {(byte) value}, 0, 1); + singleByte[0] = (byte) value; + delegate.update(singleByte, 0, 1); } public void update(byte[] data) { update(data, 0, data.length); diff --git a/vm/JavaAPI/src/java/util/zip/CRC32.java b/vm/JavaAPI/src/java/util/zip/CRC32.java index e02f9456eda..af42202a4f7 100644 --- a/vm/JavaAPI/src/java/util/zip/CRC32.java +++ b/vm/JavaAPI/src/java/util/zip/CRC32.java @@ -25,8 +25,10 @@ /** Standard CRC32 checksum. */ public class CRC32 implements Checksum { private final com.codename1.impl.zip.CRC32 delegate = new com.codename1.impl.zip.CRC32(); + private final byte[] singleByte = new byte[1]; public void update(int value) { - update(new byte[] {(byte) value}, 0, 1); + singleByte[0] = (byte) value; + delegate.update(singleByte, 0, 1); } public void update(byte[] data) { update(data, 0, data.length); diff --git a/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java b/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java index 2b1c80f674c..98e4b35cdba 100644 --- a/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java +++ b/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java @@ -23,11 +23,18 @@ package java.util.zip; import java.io.*; -/** RFC 1952 decoder, including concatenated members and header/trailer CRC checks. */ +/** + * RFC 1952 decoder, including concatenated members and header/trailer CRC checks. + * Set jdk.util.gzip.tryReadAheadAfterTrailer=true before constructing a stream + * to read subsequent members even when the source reports zero available bytes. + * This may block on a live source; the default only probes available data. + */ public class GZIPInputStream extends InflaterInputStream { public static final int GZIP_MAGIC = 0x8b1f; protected CRC32 crc = new CRC32(); protected boolean eos; + private final boolean readAhead = Boolean.parseBoolean( + System.getProperty("jdk.util.gzip.tryReadAheadAfterTrailer")); public GZIPInputStream(InputStream in) throws IOException { this(in, 512); } @@ -118,7 +125,7 @@ public int read(byte[] b, int off, int length) throws IOException { throw new ZipException("Invalid GZIP trailer"); // PushbackInputStream counts both buffered bytes and source data. // A completed member must not wait for a persistent source to close. - if (in.available() == 0) { + if (!readAhead && in.available() == 0) { eos = true; break; } diff --git a/vm/JavaAPI/src/java/util/zip/InflaterInputStream.java b/vm/JavaAPI/src/java/util/zip/InflaterInputStream.java index 4b40aac8f7f..29a493f6945 100644 --- a/vm/JavaAPI/src/java/util/zip/InflaterInputStream.java +++ b/vm/JavaAPI/src/java/util/zip/InflaterInputStream.java @@ -30,6 +30,7 @@ public class InflaterInputStream extends FilterInputStream { protected byte[] buf; protected int len; private boolean own, closed, eof; + private final byte[] singleByte = new byte[1]; public InflaterInputStream(InputStream in) { this(in, new Inflater(), 512); own = true; @@ -51,8 +52,7 @@ protected void ensureOpen() throws IOException { throw new IOException("Stream closed"); } public int read() throws IOException { - byte[] b = new byte[1]; - return read(b, 0, 1) < 0 ? -1 : b[0] & 255; + return read(singleByte, 0, 1) < 0 ? -1 : singleByte[0] & 255; } public int read(byte[] b, int off, int length) throws IOException { ensureOpen(); diff --git a/vm/JavaAPI/src/java/util/zip/ZipInputStream.java b/vm/JavaAPI/src/java/util/zip/ZipInputStream.java index 266585ae30f..eae06542cd1 100644 --- a/vm/JavaAPI/src/java/util/zip/ZipInputStream.java +++ b/vm/JavaAPI/src/java/util/zip/ZipInputStream.java @@ -155,12 +155,33 @@ public int read(byte[] b, int off, int length) throws IOException { ((PushbackInputStream) in).unread(buf, len - left, left); } if ((flags & 8) != 0) { - long value = word(4); - if (value == 0x08074b50L) - value = word(4); + long value = word(4), compressedSize = word(4), size = word(4); + if (value == 0x08074b50L) { + boolean unsigned = value == crc.getValue() && compressedSize == compressed && size == count; + if (!unsigned) { + value = compressedSize; + compressedSize = size; + size = word(4); + } else if (compressedSize == value && size == compressed) { + // All three values equal the signature: both layouts fit. + // A signed descriptor has one more size word; otherwise put + // the following header (or its partial bytes) back unchanged. + byte[] next = new byte[4]; + int ahead = 0; + long last = 0; + while (ahead < next.length) { + int nextByte = in.read(); + if (nextByte < 0) break; + next[ahead] = (byte) nextByte; + last |= (long) nextByte << (8 * ahead++); + } + if (ahead < next.length || last != count) + ((PushbackInputStream) in).unread(next, 0, ahead); + } + } entry.setCrc(value); - entry.setCompressedSize(word(4)); - entry.setSize(word(4)); + entry.setCompressedSize(compressedSize); + entry.setSize(size); } if (entry.getCrc() != crc.getValue() || entry.getSize() != count || entry.getCompressedSize() != compressed) throw new ZipException("ZIP entry CRC or size mismatch"); diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java index 8fd27313a33..6b87dcd9224 100644 --- a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java @@ -512,6 +512,128 @@ static void parameterFlushes() throws Exception { } } } + static void singleByteStreams() throws Exception { + byte[] data = new byte[32768]; + for (int i = 0; i < data.length; i++) data[i] = (byte) i; + for (Checksum sum : new Checksum[] {new CRC32(), new Adler32()}) { + sum.update(data, 0, data.length); + long expected = sum.getValue(); + sum.reset(); + CheckedOutputStream checked = new CheckedOutputStream(new ByteArrayOutputStream(), sum); + for (byte b : data) checked.write((b & 255) | 0x100); + require(sum.getValue() == expected, "single byte checksum output masks high bits"); + sum.reset(); + CheckedInputStream input = new CheckedInputStream(new ByteArrayInputStream(data), sum); + while (input.read() >= 0) { } + require(sum.getValue() == expected, "single byte checksum input"); + } + for (int format = 0; format < 3; format++) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + if (format == 0) { + try (DeflaterOutputStream out = new DeflaterOutputStream(bytes)) { out.write(data); } + } else if (format == 1) { + try (GZIPOutputStream out = new GZIPOutputStream(bytes)) { out.write(data); } + } else { + try (ZipOutputStream out = new ZipOutputStream(bytes)) { + out.putNextEntry(entry("single-byte")); + out.write(data); + } + } + InputStream source = new ByteArrayInputStream(bytes.toByteArray()); + InputStream decoded = format == 0 ? new InflaterInputStream(source) + : format == 1 ? new GZIPInputStream(source) : new ZipInputStream(source); + try { + if (format == 2) ((ZipInputStream) decoded).getNextEntry(); + for (byte b : data) require(decoded.read() == (b & 255), "single byte decompression"); + require(decoded.read() == -1, "single byte decompression EOF"); + } finally { decoded.close(); } + } + } + static void zipWord(OutputStream out, long value) throws IOException { + for (int i = 0; i < 4; i++) out.write((int) (value >>> (8 * i)) & 255); + } + static void ambiguousZipDescriptor() throws Exception { + byte[] data = {(byte) 172, 10, 122, (byte) 213}; + CRC32 crc = new CRC32(); + crc.update(data); + require(crc.getValue() == 0x08074b50L, "descriptor-signature CRC fixture"); + ByteArrayOutputStream compressed = new ByteArrayOutputStream(); + Deflater def = new Deflater(Deflater.DEFAULT_COMPRESSION, true); + try (DeflaterOutputStream out = new DeflaterOutputStream(compressed, def)) { out.write(data); } + finally { def.end(); } + for (boolean signed : new boolean[] {false, true}) { + ByteArrayOutputStream archive = new ByteArrayOutputStream(); + byte[] header = zipNameHeader("collision".getBytes("UTF-8"), true); + header[6] = 8; // Descriptor follows the compressed payload. + header[8] = 8; // DEFLATED + archive.write(header); + archive.write(compressed.toByteArray()); + if (signed) zipWord(archive, 0x08074b50L); + zipWord(archive, crc.getValue()); + zipWord(archive, compressed.size()); + zipWord(archive, data.length); + archive.write(zipNameHeader("following".getBytes("UTF-8"), true)); + try (ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(archive.toByteArray()))) { + require("collision".equals(in.getNextEntry().getName()), "descriptor collision entry"); + require(Arrays.equals(data, readAll(in)), "descriptor signature and CRC disambiguated"); + require("following".equals(in.getNextEntry().getName()), "descriptor leaves next header intact"); + require(in.read() == -1 && in.getNextEntry() == null, "following empty entry preserved"); + } + } + } + static void systemPropertyConfiguration() { + String key = "cn1.zip.test.configuration"; + String old = System.clearProperty(key); + try { + require("fallback".equals(System.getProperty(key, "fallback")), "property fallback"); + require(System.setProperty(key, "first") == null, "first property value"); + require("first".equals(System.setProperty(key, "second")), "property returns previous value"); + require("second".equals(System.getProperty(key)), "property replacement"); + require("second".equals(System.clearProperty(key)), "property removal returns previous value"); + require(System.getProperty(key) == null, "removed property absent"); + boolean rejected = false; + try { System.setProperty(key, null); } catch (NullPointerException expected) { rejected = true; } + require(rejected, "null property value rejected"); + rejected = false; + try { System.getProperty(null); } catch (NullPointerException expected) { rejected = true; } + require(rejected, "null property key rejected"); + rejected = false; + try { System.clearProperty(""); } catch (IllegalArgumentException expected) { rejected = true; } + require(rejected, "empty property key rejected"); + } finally { + if (old == null) System.clearProperty(key); + else System.setProperty(key, old); + } + } + static void gzipReadAhead() throws Exception { + String key = "jdk.util.gzip.tryReadAheadAfterTrailer"; + String old = System.setProperty(key, "true"); + try { + require("true".equals(System.getProperty(key, "false")), "read-ahead property stored"); + byte[] data = "member on a source with default available()".getBytes("UTF-8"); + ByteArrayOutputStream member = new ByteArrayOutputStream(); + try (GZIPOutputStream out = new GZIPOutputStream(member)) { out.write(data); } + ByteArrayOutputStream encoded = new ByteArrayOutputStream(); + ByteArrayOutputStream expected = new ByteArrayOutputStream(); + for (int i = 0; i < 3; i++) { encoded.write(member.toByteArray()); expected.write(data); } + for (final int chunk : new int[] {1, 7, member.size(), 512}) { + final ByteArrayInputStream source = new ByteArrayInputStream(encoded.toByteArray()); + InputStream zeroAvailable = new InputStream() { + public int read() { return source.read(); } + public int read(byte[] b, int off, int len) { return source.read(b, off, Math.min(chunk, len)); } + }; + require(zeroAvailable.available() == 0, "fixture inherits zero availability"); + try (GZIPInputStream in = new GZIPInputStream(zeroAvailable)) { + require(Arrays.equals(expected.toByteArray(), readAll(in)), "read-ahead preserves all GZIP members"); + } + } + } finally { + if (old == null) System.clearProperty(key); + else System.setProperty(key, old); + } + require(old == null ? System.getProperty(key) == null : old.equals(System.getProperty(key)), + "read-ahead property restored"); + } static void gzipLiveSource() throws Exception { byte[] data = "completed member on an open source".getBytes("UTF-8"); ByteArrayOutputStream member = new ByteArrayOutputStream(); @@ -628,7 +750,14 @@ static void compatibilityRegressions(boolean customRuntime) throws Exception { public static void main(String[] args) throws Exception { byte[] data = payload(); checksums(); + systemPropertyConfiguration(); + singleByteStreams(); gzipLiveSource(); + if (args.length > 1) { + // JDK 8 lacks the read-ahead option and has the descriptor CRC ambiguity. + gzipReadAhead(); + ambiguousZipDescriptor(); + } checkedCloseFailures(args.length > 1); skipZipEntries(); zipNameDecoding(args.length > 1); From 4fbe703e9789369b6f5f2494a17bc54aadb7b899 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 12 Oct 2026 10:12:21 +0300 Subject: [PATCH 14/16] Avoid blocking while probing partial GZIP headers --- .../Backend-Operations.asciidoc | 7 +-- .../src/java/util/zip/GZIPInputStream.java | 47 ++++++++-------- .../codename1/tools/translator/ZipApiApp.java | 54 +++++++++++++++++++ 3 files changed, 80 insertions(+), 28 deletions(-) diff --git a/docs/developer-guide/Backend-Operations.asciidoc b/docs/developer-guide/Backend-Operations.asciidoc index d0622fe4be5..2e5b4ec6cfd 100644 --- a/docs/developer-guide/Backend-Operations.asciidoc +++ b/docs/developer-guide/Backend-Operations.asciidoc @@ -297,9 +297,10 @@ The local JVM transport supports HTTP/1; test HTTP/2 with the packaged backend. for DEFLATED entries. ZIP64, encryption, other compression methods, and STORED descriptors are explicitly rejected. GZIP supports concatenated members and validates header and trailer checksums. By default it probes for another - GZIP member only when buffered or available bytes remain, avoiding a blocking - read on an idle open connection. For concatenated members on sources whose - `available()` method does not report readable bytes, call + GZIP member only when its entire header can be read from buffered or available + bytes, avoiding a blocking header read on an idle open connection. For + concatenated members on sources whose + `available()` method doesn't report readable bytes, call `System.setProperty("jdk.util.gzip.tryReadAheadAfterTrailer", "true")` before constructing the stream. This follows OpenJDK's read-ahead option: it reads until the source ends and can block on a persistent connection. ParparVM diff --git a/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java b/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java index 98e4b35cdba..d87a327159d 100644 --- a/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java +++ b/vm/JavaAPI/src/java/util/zip/GZIPInputStream.java @@ -41,7 +41,7 @@ public GZIPInputStream(InputStream in) throws IOException { public GZIPInputStream(InputStream in, int size) throws IOException { super(new PushbackInputStream(in, Math.max(size, 2)), new Inflater(true), size); try { - header(); + header(false); } catch (IOException ex) { inf.end(); throw ex; @@ -53,32 +53,39 @@ private int byteValue() throws IOException { throw new EOFException("Truncated GZIP"); return b; } - private int headerByte() throws IOException { - int b = byteValue(); + private int headerValue(boolean availableOnly) throws IOException { + // Probe a subsequent header only while bytes can be read without + // blocking. This includes optional fields and their CRC bytes. + if (availableOnly && in.available() == 0) + throw new EOFException("Incomplete available GZIP header"); + return byteValue(); + } + private int headerByte(boolean availableOnly) throws IOException { + int b = headerValue(availableOnly); crc.update(b); return b; } - private void header() throws IOException { + private void header(boolean availableOnly) throws IOException { crc.reset(); - if (headerByte() != 31 || headerByte() != 139 || headerByte() != 8) + if (headerByte(availableOnly) != 31 || headerByte(availableOnly) != 139 || headerByte(availableOnly) != 8) throw new ZipException("Invalid GZIP header"); - int flags = headerByte(); + int flags = headerByte(availableOnly); if ((flags & 224) != 0) throw new ZipException("Reserved GZIP flags"); - for (int i = 0; i < 6; i++) headerByte(); + for (int i = 0; i < 6; i++) headerByte(availableOnly); if ((flags & 4) != 0) { - int n = headerByte() | (headerByte() << 8); - while (n-- > 0) headerByte(); + int n = headerByte(availableOnly) | (headerByte(availableOnly) << 8); + while (n-- > 0) headerByte(availableOnly); } if ((flags & 8) != 0) - while (headerByte() != 0) { + while (headerByte(availableOnly) != 0) { } if ((flags & 16) != 0) - while (headerByte() != 0) { + while (headerByte(availableOnly) != 0) { } if ((flags & 2) != 0) { int expected = (int) crc.getValue() & 65535; - if ((byteValue() | (byteValue() << 8)) != expected) + if ((headerValue(availableOnly) | (headerValue(availableOnly) << 8)) != expected) throw new ZipException("Invalid GZIP header CRC"); } crc.reset(); @@ -123,20 +130,10 @@ public int read(byte[] b, int off, int length) throws IOException { ((PushbackInputStream) in).unread(buf, len - left, left); if (word() != crc.getValue() || word() != (inf.getBytesWritten() & 0xffffffffL)) throw new ZipException("Invalid GZIP trailer"); - // PushbackInputStream counts both buffered bytes and source data. - // A completed member must not wait for a persistent source to close. - if (!readAhead && in.available() == 0) { - eos = true; - break; - } - int first = in.read(); - if (first < 0) { - eos = true; - break; - } - ((PushbackInputStream) in).unread(first); try { - header(); + // Validate the entire candidate header before accepting it. + // PushbackInputStream includes buffered bytes in available(). + header(!readAhead); } catch (IOException trailingData) { // Like the JDK, ignore bytes that are not another complete header. // Payload and trailer errors after a valid header remain errors. diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java index 6b87dcd9224..8864a3872dc 100644 --- a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java @@ -668,6 +668,59 @@ public int read(byte[] b, int off, int len) { } } } + static void gzipPartialLiveHeaders() throws Exception { + byte[] data = "complete member before a partial header".getBytes("UTF-8"); + ByteArrayOutputStream member = new ByteArrayOutputStream(); + try (GZIPOutputStream out = new GZIPOutputStream(member)) { out.write(data); } + byte[] plainMember = member.toByteArray(); + // Exercise every optional field, including both extra-length bytes, + // zero-terminated name/comment fields, and both header checksum bytes. + for (int flags : new int[] {0, 2, 4, 8, 16, 30}) { + ByteArrayOutputStream header = new ByteArrayOutputStream(); + header.write(plainMember, 0, 3); + header.write(flags); + header.write(plainMember, 4, 6); + if ((flags & 4) != 0) header.write(new byte[] {3, 0, 31, 0, 42}); + if ((flags & 8) != 0) header.write(new byte[] {'n', 'a', 'm', 'e', 0}); + if ((flags & 16) != 0) header.write(new byte[] {'n', 'o', 't', 'e', 0}); + if ((flags & 2) != 0) { + CRC32 crc = new CRC32(); + crc.update(header.toByteArray()); + header.write((int) crc.getValue() & 255); + header.write((int) (crc.getValue() >>> 8) & 255); + } + byte[] candidate = header.toByteArray(); + for (int prefix = 0; prefix <= candidate.length; prefix++) { + boolean complete = prefix == candidate.length; + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(plainMember); + bytes.write(candidate, 0, prefix); + // A complete header must still accept and decode the next member. + if (complete) bytes.write(plainMember, 10, plainMember.length - 10); + ByteArrayOutputStream expected = new ByteArrayOutputStream(); + expected.write(data); + if (complete) expected.write(data); + for (int size : new int[] {1, 512}) { + final ByteArrayInputStream arrived = new ByteArrayInputStream(bytes.toByteArray()); + InputStream live = new InputStream() { + public int available() { return arrived.available(); } + public int read() { + require(arrived.available() > 0, "partial GZIP header must not read idle source"); + return arrived.read(); + } + public int read(byte[] b, int off, int len) { + require(arrived.available() > 0, "partial GZIP header must not fill idle source"); + return arrived.read(b, off, len); + } + }; + try (GZIPInputStream in = new GZIPInputStream(live, size)) { + require(Arrays.equals(expected.toByteArray(), readAll(in)), "partial or complete live header"); + require(in.read() == -1 && in.available() == 0, "partial header stays at EOF"); + } + } + } + } + } static void compatibilityRegressions(boolean customRuntime) throws Exception { byte[] data = "flushed payload".getBytes("UTF-8"); // JDK 8 itself loops indefinitely with buffers smaller than the flush marker. @@ -756,6 +809,7 @@ public static void main(String[] args) throws Exception { if (args.length > 1) { // JDK 8 lacks the read-ahead option and has the descriptor CRC ambiguity. gzipReadAhead(); + gzipPartialLiveHeaders(); ambiguousZipDescriptor(); } checkedCloseFailures(args.length > 1); From acea8137e3b43aae19628072863b6ce70f329abe Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 12 Oct 2026 10:46:09 +0300 Subject: [PATCH 15/16] Reuse single-byte buffers for compressed output streams --- .../io/zip/ZipArchiveOutputStream.java | 4 ++- .../java/util/zip/DeflaterOutputStream.java | 4 ++- .../src/java/util/zip/ZipOutputStream.java | 4 ++- .../codename1/tools/translator/ZipApiApp.java | 33 ++++++++++++++----- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java b/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java index e31491741c7..0a6d31e5b6f 100644 --- a/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java +++ b/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java @@ -62,6 +62,7 @@ public class ZipArchiveOutputStream extends DeflaterOutputStream { private boolean closed; private IOException failure; private byte[] comment = new byte[0]; + private final byte[] singleByte = new byte[1]; private static final class Record { ZipEntry entry; @@ -302,7 +303,8 @@ private static long dosTime(long millis) { } @Override public void write(int value) throws IOException { - write(new byte[] {(byte) value}, 0, 1); + singleByte[0] = (byte) value; + write(singleByte, 0, 1); } @Override public void write(byte[] data, int off, int len) throws IOException { diff --git a/vm/JavaAPI/src/java/util/zip/DeflaterOutputStream.java b/vm/JavaAPI/src/java/util/zip/DeflaterOutputStream.java index 8201f6f021f..ca8bf907388 100644 --- a/vm/JavaAPI/src/java/util/zip/DeflaterOutputStream.java +++ b/vm/JavaAPI/src/java/util/zip/DeflaterOutputStream.java @@ -29,6 +29,7 @@ public class DeflaterOutputStream extends FilterOutputStream { protected Deflater def; protected byte[] buf; private boolean own, closed, syncFlush; + private final byte[] singleByte = new byte[1]; public DeflaterOutputStream(OutputStream out) { this(out, new Deflater(), 512, false); own = true; @@ -57,7 +58,8 @@ public DeflaterOutputStream(OutputStream out, Deflater def, int size, boolean sy this.syncFlush = syncFlush; } public void write(int b) throws IOException { - write(new byte[] {(byte) b}, 0, 1); + singleByte[0] = (byte) b; + write(singleByte, 0, 1); } public void write(byte[] b, int off, int len) throws IOException { if (closed) diff --git a/vm/JavaAPI/src/java/util/zip/ZipOutputStream.java b/vm/JavaAPI/src/java/util/zip/ZipOutputStream.java index 5ac528367f4..f5f25666b86 100644 --- a/vm/JavaAPI/src/java/util/zip/ZipOutputStream.java +++ b/vm/JavaAPI/src/java/util/zip/ZipOutputStream.java @@ -61,6 +61,7 @@ public class ZipOutputStream extends DeflaterOutputStream { private boolean closed; private IOException failure; private byte[] comment = new byte[0]; + private final byte[] singleByte = new byte[1]; private static final class Record { ZipEntry entry; @@ -291,7 +292,8 @@ private static long dosTime(long millis) { } @Override public void write(int value) throws IOException { - write(new byte[] {(byte) value}, 0, 1); + singleByte[0] = (byte) value; + write(singleByte, 0, 1); } @Override public void write(byte[] data, int off, int len) throws IOException { diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java index 8864a3872dc..bba473bdfb8 100644 --- a/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/ZipApiApp.java @@ -527,28 +527,45 @@ static void singleByteStreams() throws Exception { while (input.read() >= 0) { } require(sum.getValue() == expected, "single byte checksum input"); } - for (int format = 0; format < 3; format++) { + for (int format = 0; format < 6; format++) { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ZipEntry e = entry("single-byte"); + if (format >= 4) { + CRC32 crc = new CRC32(); + crc.update(data); + e.setMethod(ZipEntry.STORED); + e.setSize(data.length); + e.setCrc(crc.getValue()); + } if (format == 0) { - try (DeflaterOutputStream out = new DeflaterOutputStream(bytes)) { out.write(data); } + try (DeflaterOutputStream out = new DeflaterOutputStream(bytes)) { writeSingleBytes(out, data); } } else if (format == 1) { - try (GZIPOutputStream out = new GZIPOutputStream(bytes)) { out.write(data); } - } else { + try (GZIPOutputStream out = new GZIPOutputStream(bytes)) { writeSingleBytes(out, data); } + } else if (format == 2 || format == 4) { try (ZipOutputStream out = new ZipOutputStream(bytes)) { - out.putNextEntry(entry("single-byte")); - out.write(data); + out.putNextEntry(e); + writeSingleBytes(out, data); + } + } else { + try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(bytes)) { + out.putNextEntry(e); + writeSingleBytes(out, data); } } InputStream source = new ByteArrayInputStream(bytes.toByteArray()); InputStream decoded = format == 0 ? new InflaterInputStream(source) : format == 1 ? new GZIPInputStream(source) : new ZipInputStream(source); try { - if (format == 2) ((ZipInputStream) decoded).getNextEntry(); - for (byte b : data) require(decoded.read() == (b & 255), "single byte decompression"); + if (format >= 2) ((ZipInputStream) decoded).getNextEntry(); + for (byte b : data) require(decoded.read() == (b & 255), "single byte compression roundtrip"); require(decoded.read() == -1, "single byte decompression EOF"); } finally { decoded.close(); } } } + static void writeSingleBytes(OutputStream out, byte[] data) throws IOException { + // Values outside 0..255 must still write only the low eight bits. + for (byte b : data) out.write((b & 255) | 0x100); + } static void zipWord(OutputStream out, long value) throws IOException { for (int i = 0; i < 4; i++) out.write((int) (value >>> (8 * i)) & 255); } From 471e51c3a350520099338e337a1378953eae0aba Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:56:36 +0300 Subject: [PATCH 16/16] Compose preset dictionaries and reuse ZIP header buffers --- .../src/com/codename1/io/gzip/Deflate.java | 28 +++++--- .../io/zip/ZipArchiveOutputStream.java | 11 ++-- .../test-javascript-composited-rendering.mjs | 8 +++ .../src/com/codename1/impl/zip/Deflate.java | 28 +++++--- .../src/java/util/zip/ZipOutputStream.java | 11 ++-- .../codename1/tools/translator/ZipApiApp.java | 66 +++++++++++++++++++ 6 files changed, 120 insertions(+), 32 deletions(-) diff --git a/CodenameOne/src/com/codename1/io/gzip/Deflate.java b/CodenameOne/src/com/codename1/io/gzip/Deflate.java index fd418ed7647..7629d245c8a 100644 --- a/CodenameOne/src/com/codename1/io/gzip/Deflate.java +++ b/CodenameOne/src/com/codename1/io/gzip/Deflate.java @@ -1490,9 +1490,6 @@ int deflateParams(int levelParam, int strategyParam) { } int deflateSetDictionary(byte[] dictionary, int dictLength) { - int length = dictLength; - int index = 0; - // Raw DEFLATE begins in BUSY_STATE because it has no zlib header. // A preset dictionary is still valid before any input is compressed. if (dictionary == null || (status != INIT_STATE && @@ -1504,16 +1501,27 @@ int deflateSetDictionary(byte[] dictionary, int dictLength) { strm.adler.update(dictionary, 0, dictLength); } + // Repeated calls append to the history, while the Adler checksum + // above covers every supplied byte. Keep only the usable window tail. + int capacity = wSize - MIN_LOOKAHEAD; + int copied = Math.min(dictLength, capacity); + int retained = Math.min(strStart, capacity - copied); + System.arraycopy(window, strStart - retained, window, 0, retained); + System.arraycopy(dictionary, dictLength - copied, window, retained, copied); + int length = retained + copied; + strStart = length; + blockStart = length; + // The window has moved, so no hash chain may retain its old offsets. + for (int n = 0; n < head.length; n++) { + head[n] = 0; + } + for (int n = 0; n < prev.length; n++) { + prev[n] = 0; + } if (length < MIN_MATCH) { + insH = 0; return Z_OK; } - if (length > wSize - MIN_LOOKAHEAD) { - length = wSize - MIN_LOOKAHEAD; - index = dictLength - length; // use the tail of the dictionary - } - System.arraycopy(dictionary, index, window, 0, length); - strStart = length; - blockStart = length; // Insert all strings in the hash table (except for the last two bytes). // s->lookahead stays null, so s->ins_h will be recomputed at the next diff --git a/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java b/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java index 0a6d31e5b6f..7fc6a62bb4f 100644 --- a/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java +++ b/CodenameOne/src/com/codename1/io/zip/ZipArchiveOutputStream.java @@ -62,7 +62,7 @@ public class ZipArchiveOutputStream extends DeflaterOutputStream { private boolean closed; private IOException failure; private byte[] comment = new byte[0]; - private final byte[] singleByte = new byte[1]; + private final byte[] writeBuffer = new byte[4]; private static final class Record { ZipEntry entry; @@ -303,8 +303,8 @@ private static long dosTime(long millis) { } @Override public void write(int value) throws IOException { - singleByte[0] = (byte) value; - write(singleByte, 0, 1); + writeBuffer[0] = (byte) value; + write(writeBuffer, 0, 1); } @Override public void write(byte[] data, int off, int len) throws IOException { @@ -488,12 +488,11 @@ public void close() throws IOException { } } private void word(long value, int bytes) throws IOException { - byte[] b = new byte[bytes]; for (int i = 0; i < bytes; i++) { - b[i] = (byte) value; + writeBuffer[i] = (byte) value; value >>>= 8; } - raw(b); + raw(writeBuffer, 0, bytes); } private void raw(byte[] data) throws IOException { raw(data, 0, data.length); diff --git a/scripts/test-javascript-composited-rendering.mjs b/scripts/test-javascript-composited-rendering.mjs index 955e0b458c4..3a0243e076e 100644 --- a/scripts/test-javascript-composited-rendering.mjs +++ b/scripts/test-javascript-composited-rendering.mjs @@ -225,6 +225,14 @@ async function picker(name, page, press) { check(await select.inputValue() === '0', `${name}: the