diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 7fd15974b..83e109aea 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "ff4d4642a"; + public static final String gitCommitId = "92020251e"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/CompressZlib.java b/src/main/java/org/perlonjava/runtime/perlmodule/CompressZlib.java index f0d9993d7..27ccf688e 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/CompressZlib.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/CompressZlib.java @@ -5,11 +5,7 @@ import java.io.*; import java.nio.charset.StandardCharsets; -import java.util.zip.DataFormatException; -import java.util.zip.Deflater; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; -import java.util.zip.Inflater; +import java.util.zip.*; import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarUndef; @@ -27,11 +23,30 @@ public static void initialize() { try { cz.registerMethod("inflateInit", null); cz.registerMethod("deflateInit", null); + cz.registerMethod("compress", null); + cz.registerMethod("uncompress", null); + cz.registerMethod("memGzip", null); + cz.registerMethod("memGunzip", null); + cz.registerMethod("crc32", null); + cz.registerMethod("adler32", null); cz.registerMethod("Z_OK", null); cz.registerMethod("Z_STREAM_END", null); cz.registerMethod("Z_STREAM_ERROR", null); cz.registerMethod("Z_DATA_ERROR", null); cz.registerMethod("Z_BUF_ERROR", null); + cz.registerMethod("Z_NO_FLUSH", null); + cz.registerMethod("Z_SYNC_FLUSH", null); + cz.registerMethod("Z_FULL_FLUSH", null); + cz.registerMethod("Z_FINISH", null); + cz.registerMethod("Z_DEFAULT_COMPRESSION", null); + cz.registerMethod("Z_BEST_SPEED", null); + cz.registerMethod("Z_BEST_COMPRESSION", null); + cz.registerMethod("Z_FILTERED", null); + cz.registerMethod("Z_HUFFMAN_ONLY", null); + cz.registerMethod("Z_DEFAULT_STRATEGY", null); + cz.registerMethod("Z_DEFLATED", null); + cz.registerMethod("WANT_GZIP", null); + cz.registerMethod("WANT_GZIP_OR_ZLIB", null); cz.registerMethod("MAX_WBITS", null); cz.registerMethod("inflate", "inflateMethod", null); cz.registerMethod("deflate", "deflateMethod", null); @@ -65,10 +80,311 @@ public static RuntimeList Z_BUF_ERROR(RuntimeArray args, int ctx) { return new RuntimeScalar(-5).getList(); } + public static RuntimeList Z_NO_FLUSH(RuntimeArray args, int ctx) { + return new RuntimeScalar(0).getList(); + } + + public static RuntimeList Z_SYNC_FLUSH(RuntimeArray args, int ctx) { + return new RuntimeScalar(2).getList(); + } + + public static RuntimeList Z_FULL_FLUSH(RuntimeArray args, int ctx) { + return new RuntimeScalar(3).getList(); + } + + public static RuntimeList Z_FINISH(RuntimeArray args, int ctx) { + return new RuntimeScalar(4).getList(); + } + + public static RuntimeList Z_DEFAULT_COMPRESSION(RuntimeArray args, int ctx) { + return new RuntimeScalar(-1).getList(); + } + + public static RuntimeList Z_BEST_SPEED(RuntimeArray args, int ctx) { + return new RuntimeScalar(1).getList(); + } + + public static RuntimeList Z_BEST_COMPRESSION(RuntimeArray args, int ctx) { + return new RuntimeScalar(9).getList(); + } + + public static RuntimeList Z_FILTERED(RuntimeArray args, int ctx) { + return new RuntimeScalar(1).getList(); + } + + public static RuntimeList Z_HUFFMAN_ONLY(RuntimeArray args, int ctx) { + return new RuntimeScalar(2).getList(); + } + + public static RuntimeList Z_DEFAULT_STRATEGY(RuntimeArray args, int ctx) { + return new RuntimeScalar(0).getList(); + } + + public static RuntimeList Z_DEFLATED(RuntimeArray args, int ctx) { + return new RuntimeScalar(8).getList(); + } + + public static RuntimeList WANT_GZIP(RuntimeArray args, int ctx) { + return new RuntimeScalar(16).getList(); + } + + public static RuntimeList WANT_GZIP_OR_ZLIB(RuntimeArray args, int ctx) { + return new RuntimeScalar(32).getList(); + } + public static RuntimeList MAX_WBITS(RuntimeArray args, int ctx) { return new RuntimeScalar(15).getList(); } + /** + * Helper to extract byte data from a scalar or scalar reference. + */ + private static byte[] getInputBytes(RuntimeScalar dataScalar) { + RuntimeScalar actual = dataScalar; + if (dataScalar.type == RuntimeScalarType.REFERENCE) { + actual = dataScalar.scalarDeref(); + } + return actual.toString().getBytes(StandardCharsets.ISO_8859_1); + } + + /** + * compress($data [, $level]) + * One-shot zlib compression (RFC 1950 format). + * Returns compressed data or undef on error. + */ + public static RuntimeList compress(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarUndef.getList(); + } + + int level = Deflater.DEFAULT_COMPRESSION; + byte[] input = getInputBytes(args.get(0)); + + if (args.size() > 1) { + level = args.get(1).getInt(); + } + + try { + Deflater deflater = new Deflater(level, false); // nowrap=false for zlib format + deflater.setInput(input); + deflater.finish(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(input.length + 64); + byte[] buf = new byte[Math.max(input.length + 64, 1024)]; + + while (!deflater.finished()) { + int count = deflater.deflate(buf); + if (count > 0) { + baos.write(buf, 0, count); + } else { + break; + } + } + deflater.end(); + + RuntimeScalar result = new RuntimeScalar(baos.toString(StandardCharsets.ISO_8859_1)); + result.type = RuntimeScalarType.BYTE_STRING; + return result.getList(); + } catch (Exception e) { + return scalarUndef.getList(); + } + } + + /** + * uncompress($data) + * One-shot zlib decompression (RFC 1950 format). + * Returns decompressed data or undef on error. + */ + public static RuntimeList uncompress(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarUndef.getList(); + } + + byte[] input = getInputBytes(args.get(0)); + + try { + Inflater inflater = new Inflater(false); // nowrap=false for zlib format + inflater.setInput(input); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(input.length * 4); + byte[] buf = new byte[Math.max(input.length * 4, 1024)]; + + while (!inflater.finished()) { + int count = inflater.inflate(buf); + if (count > 0) { + baos.write(buf, 0, count); + } else if (inflater.needsInput()) { + break; + } + } + + if (!inflater.finished()) { + inflater.end(); + return scalarUndef.getList(); + } + + inflater.end(); + + RuntimeScalar result = new RuntimeScalar(baos.toString(StandardCharsets.ISO_8859_1)); + result.type = RuntimeScalarType.BYTE_STRING; + return result.getList(); + } catch (DataFormatException | IllegalArgumentException e) { + return scalarUndef.getList(); + } + } + + /** + * memGzip($data) + * Compress data in gzip format (RFC 1952). + * Returns gzipped data or undef on error. + */ + public static RuntimeList memGzip(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarUndef.getList(); + } + + byte[] input = getInputBytes(args.get(0)); + + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(input.length + 64); + try (GZIPOutputStream gos = new GZIPOutputStream(baos)) { + gos.write(input); + } + + RuntimeScalar result = new RuntimeScalar(baos.toString(StandardCharsets.ISO_8859_1)); + result.type = RuntimeScalarType.BYTE_STRING; + return result.getList(); + } catch (IOException e) { + return scalarUndef.getList(); + } + } + + /** + * memGunzip($data) + * Decompress gzip format data (RFC 1952). + * Returns decompressed data or undef on error. + */ + public static RuntimeList memGunzip(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarUndef.getList(); + } + + byte[] input = getInputBytes(args.get(0)); + + try { + ByteArrayInputStream bais = new ByteArrayInputStream(input); + GZIPInputStream gis = new GZIPInputStream(bais); + ByteArrayOutputStream baos = new ByteArrayOutputStream(input.length * 4); + + byte[] buf = new byte[4096]; + int count; + while ((count = gis.read(buf)) != -1) { + baos.write(buf, 0, count); + } + gis.close(); + + RuntimeScalar result = new RuntimeScalar(baos.toString(StandardCharsets.ISO_8859_1)); + result.type = RuntimeScalarType.BYTE_STRING; + return result.getList(); + } catch (IOException e) { + return scalarUndef.getList(); + } + } + + /** + * crc32($data [, $crc]) + * Calculate CRC-32 checksum. If $crc is provided, continue from that value. + * Returns unsigned 32-bit integer. + */ + public static RuntimeList crc32(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return new RuntimeScalar(0).getList(); + } + + byte[] input = getInputBytes(args.get(0)); + CRC32 crc = new CRC32(); + + if (args.size() > 1 && args.get(1).getDefinedBoolean()) { + // Continue from previous CRC value — CRC32 doesn't support direct seeding, + // but Perl's crc32() with a second arg does a running checksum. + // Java's CRC32 doesn't allow seeding, so we use Adler32-style workaround: + // For compatibility, we use the combine approach if needed. + // Actually, Perl's Compress::Zlib::crc32 with initial value works by + // calling zlib's crc32() C function which supports seeding. + // Java's CRC32 doesn't support this directly, but we can use the + // crc32_combine concept. For simplicity, if the initial value is 0, + // just compute normally. For non-zero seed, we need a different approach. + long seed = args.get(1).getLong() & 0xFFFFFFFFL; + if (seed != 0) { + // Use reflection or a manual CRC32 table for seeding. + // Simpler: compute via direct table calculation. + long crcVal = crc32WithSeed(input, seed); + return new RuntimeScalar(crcVal).getList(); + } + } + + crc.update(input); + return new RuntimeScalar(crc.getValue()).getList(); + } + + /** + * CRC-32 computation with an initial seed value. + * Implements the CRC-32 algorithm directly with a lookup table. + */ + private static final long[] CRC32_TABLE = new long[256]; + static { + for (int i = 0; i < 256; i++) { + long c = i; + for (int j = 0; j < 8; j++) { + if ((c & 1) != 0) { + c = 0xEDB88320L ^ (c >>> 1); + } else { + c >>>= 1; + } + } + CRC32_TABLE[i] = c; + } + } + + private static long crc32WithSeed(byte[] data, long seed) { + long crc = seed ^ 0xFFFFFFFFL; + for (byte b : data) { + crc = CRC32_TABLE[(int) ((crc ^ b) & 0xFF)] ^ (crc >>> 8); + } + return (crc ^ 0xFFFFFFFFL) & 0xFFFFFFFFL; + } + + /** + * adler32($data [, $adler]) + * Calculate Adler-32 checksum. If $adler is provided, continue from that value. + * Returns unsigned 32-bit integer. + */ + public static RuntimeList adler32(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return new RuntimeScalar(1).getList(); // Adler-32 of empty data is 1 + } + + byte[] input = getInputBytes(args.get(0)); + Adler32 adler = new Adler32(); + + if (args.size() > 1 && args.get(1).getDefinedBoolean()) { + long seed = args.get(1).getLong() & 0xFFFFFFFFL; + if (seed != 1) { + // Adler32 with seed: compute manually + long s1 = seed & 0xFFFF; + long s2 = (seed >>> 16) & 0xFFFF; + for (byte b : input) { + s1 = (s1 + (b & 0xFF)) % 65521; + s2 = (s2 + s1) % 65521; + } + return new RuntimeScalar((s2 << 16) | s1).getList(); + } + } + + adler.update(input); + return new RuntimeScalar(adler.getValue()).getList(); + } + public static RuntimeList inflateInit(RuntimeArray args, int ctx) { boolean nowrap = false; @@ -97,17 +413,26 @@ public static RuntimeList inflateInit(RuntimeArray args, int ctx) { public static RuntimeList deflateInit(RuntimeArray args, int ctx) { int level = Deflater.DEFAULT_COMPRESSION; + boolean nowrap = false; for (int i = 0; i < args.size() - 1; i++) { String key = args.get(i).toString(); if (key.equals("-Level") || key.equals("Level")) { level = args.get(i + 1).getInt(); - break; + i++; + } else if (key.equals("-WindowBits") || key.equals("WindowBits")) { + int wbits = args.get(i + 1).getInt(); + if (wbits < 0) { + nowrap = true; // raw deflate + } + // wbits >= 16 would mean gzip format, but Java's Deflater + // doesn't support that directly — use memGzip instead + i++; } } try { - Deflater deflater = new Deflater(level); + Deflater deflater = new Deflater(level, nowrap); RuntimeHash self = new RuntimeHash(); self.put(DEFLATER_KEY, new RuntimeScalar(deflater)); RuntimeScalar ref = self.createReference(); diff --git a/src/main/perl/lib/Compress/Zlib.pm b/src/main/perl/lib/Compress/Zlib.pm index 62ca3381e..9e4736daf 100644 --- a/src/main/perl/lib/Compress/Zlib.pm +++ b/src/main/perl/lib/Compress/Zlib.pm @@ -14,7 +14,7 @@ package Compress::Zlib; use strict; use warnings; -our $VERSION = '2.106'; +our $VERSION = '2.219'; use Exporter; our @ISA = qw(Exporter); @@ -22,6 +22,12 @@ our @ISA = qw(Exporter); XSLoader::load('Compress::Zlib'); our @EXPORT = qw( + compress + uncompress + memGzip + memGunzip + crc32 + adler32 inflateInit deflateInit gzopen @@ -30,6 +36,19 @@ our @EXPORT = qw( Z_STREAM_ERROR Z_DATA_ERROR Z_BUF_ERROR + Z_NO_FLUSH + Z_SYNC_FLUSH + Z_FULL_FLUSH + Z_FINISH + Z_DEFAULT_COMPRESSION + Z_BEST_SPEED + Z_BEST_COMPRESSION + Z_FILTERED + Z_HUFFMAN_ONLY + Z_DEFAULT_STRATEGY + Z_DEFLATED + WANT_GZIP + WANT_GZIP_OR_ZLIB MAX_WBITS );