Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ dependencies {
implementation libs.snakeyaml.engine // YAML processing
implementation libs.tomlj // TOML processing
implementation libs.commons.csv // CSV processing
implementation libs.commons.compress // BZip2/tar/etc. compressors
implementation libs.sqlite.jdbc // SQLite JDBC driver
implementation libs.bcprov // Bouncy Castle crypto (SHA-3, Keccak, etc.)
implementation libs.bcpkix // Bouncy Castle PEM/PKCS parsing
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
asm = "9.9.1"
bouncycastle = "1.78.1"
commons-csv = "1.14.1"
commons-compress = "1.27.1"
icu4j = "78.3"
junit-jupiter = "6.1.0-M1"
snakeyaml-engine = "3.0.1"
Expand All @@ -14,6 +15,7 @@ asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asm" }
bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" }
bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bouncycastle" }
commons-csv = { module = "org.apache.commons:commons-csv", version.ref = "commons-csv" }
commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "commons-compress" }
icu4j = { module = "com.ibm.icu:icu4j", version.ref = "icu4j" }
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit-jupiter" }
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter" }
Expand Down
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
<artifactId>commons-csv</artifactId>
<version>1.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.27.1</version>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
Expand Down
14 changes: 13 additions & 1 deletion src/main/java/org/perlonjava/app/cli/ArgumentParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,19 @@ private static void processShebangLine(String[] args, CompilerOptions parsedArgs
int perlIndex = shebangLine.indexOf("perl");
if (perlIndex != -1) {
String relevantPart = shebangLine.substring(perlIndex + 4).trim();
String[] shebangArgs = relevantPart.split("\\s+");
// Strip emacs mode line marker (e.g. "-*- mode: cperl -*-") which real
// perl tolerates in #! lines but not on the command line.
int emacsStart = relevantPart.indexOf("-*-");
if (emacsStart != -1) {
int emacsEnd = relevantPart.indexOf("-*-", emacsStart + 3);
if (emacsEnd != -1) {
relevantPart = relevantPart.substring(0, emacsStart)
+ relevantPart.substring(emacsEnd + 3);
} else {
relevantPart = relevantPart.substring(0, emacsStart);
}
}
String[] shebangArgs = relevantPart.trim().split("\\s+");
// Filter out empty args from shebang processing
String[] nonEmptyArgs = Arrays.stream(shebangArgs)
.filter(arg -> !arg.isEmpty())
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "5f8a9d5e5";
public static final String gitCommitId = "ccf4d54b3";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
Expand All @@ -48,7 +48,7 @@ public final class Configuration {
* Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at"
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String buildTimestamp = "Apr 29 2026 19:52:04";
public static final String buildTimestamp = "Apr 29 2026 19:59:08";

// Prevent instantiation
private Configuration() {
Expand Down
202 changes: 202 additions & 0 deletions src/main/java/org/perlonjava/runtime/perlmodule/CompressBzip2.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package org.perlonjava.runtime.perlmodule;

import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
import org.perlonjava.runtime.operators.ReferenceOperators;
import org.perlonjava.runtime.runtimetypes.*;

import java.io.*;
import java.nio.charset.StandardCharsets;

import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarUndef;

/**
* Implements the Compress::Bzip2 module on top of Apache Commons Compress.
*
* <p>The upstream CPAN module (Rob Janes) is XS-only. This Java backend
* provides the API surface that pure-Perl callers use:
* <ul>
* <li>One-shot helpers: memBzip, memBunzip, bzip2, bzunzip</li>
* <li>File handle API: bzopen returning a Compress::Bzip2::bzFile object</li>
* <li>The error-code constants exported by :constants</li>
* </ul>
* Companion class {@link CompressBzip2BzFile} provides the bzread / bzwrite /
* bzclose / bzreadline / bzeof methods on the returned handle objects.
*/
public class CompressBzip2 extends PerlModuleBase {

public CompressBzip2() {
super("Compress::Bzip2", false);
}

public static void initialize() {
CompressBzip2 cb = new CompressBzip2();
try {
cb.registerMethod("memBzip", null);
cb.registerMethod("memBunzip", null);
cb.registerMethod("bzip2", null);
cb.registerMethod("bzunzip", null);
cb.registerMethod("bzopen", null);

// libbzip2 status / mode constants. The numeric values mirror the
// upstream Compress::Bzip2 / libbzip2 headers so callers comparing
// against e.g. BZ_STREAM_END keep working.
cb.registerMethod("BZ_OK", null);
cb.registerMethod("BZ_RUN_OK", null);
cb.registerMethod("BZ_FLUSH_OK", null);
cb.registerMethod("BZ_FINISH_OK", null);
cb.registerMethod("BZ_STREAM_END", null);
cb.registerMethod("BZ_SEQUENCE_ERROR", null);
cb.registerMethod("BZ_PARAM_ERROR", null);
cb.registerMethod("BZ_MEM_ERROR", null);
cb.registerMethod("BZ_DATA_ERROR", null);
cb.registerMethod("BZ_DATA_ERROR_MAGIC", null);
cb.registerMethod("BZ_IO_ERROR", null);
cb.registerMethod("BZ_UNEXPECTED_EOF", null);
cb.registerMethod("BZ_OUTBUFF_FULL", null);
cb.registerMethod("BZ_CONFIG_ERROR", null);
cb.registerMethod("BZ_RUN", null);
cb.registerMethod("BZ_FLUSH", null);
cb.registerMethod("BZ_FINISH", null);
cb.registerMethod("BZ_MAX_UNUSED", null);
} catch (NoSuchMethodException e) {
System.err.println("Warning: Missing Compress::Bzip2 method: " + e.getMessage());
}

CompressBzip2BzFile.initialize();
}

public static RuntimeList BZ_OK(RuntimeArray args, int ctx) { return new RuntimeScalar(0).getList(); }
public static RuntimeList BZ_RUN_OK(RuntimeArray args, int ctx) { return new RuntimeScalar(1).getList(); }
public static RuntimeList BZ_FLUSH_OK(RuntimeArray args, int ctx) { return new RuntimeScalar(2).getList(); }
public static RuntimeList BZ_FINISH_OK(RuntimeArray args, int ctx) { return new RuntimeScalar(3).getList(); }
public static RuntimeList BZ_STREAM_END(RuntimeArray args, int ctx) { return new RuntimeScalar(4).getList(); }
public static RuntimeList BZ_SEQUENCE_ERROR(RuntimeArray args, int ctx) { return new RuntimeScalar(-1).getList(); }
public static RuntimeList BZ_PARAM_ERROR(RuntimeArray args, int ctx) { return new RuntimeScalar(-2).getList(); }
public static RuntimeList BZ_MEM_ERROR(RuntimeArray args, int ctx) { return new RuntimeScalar(-3).getList(); }
public static RuntimeList BZ_DATA_ERROR(RuntimeArray args, int ctx) { return new RuntimeScalar(-4).getList(); }
public static RuntimeList BZ_DATA_ERROR_MAGIC(RuntimeArray args, int ctx) { return new RuntimeScalar(-5).getList(); }
public static RuntimeList BZ_IO_ERROR(RuntimeArray args, int ctx) { return new RuntimeScalar(-6).getList(); }
public static RuntimeList BZ_UNEXPECTED_EOF(RuntimeArray args, int ctx) { return new RuntimeScalar(-7).getList(); }
public static RuntimeList BZ_OUTBUFF_FULL(RuntimeArray args, int ctx) { return new RuntimeScalar(-8).getList(); }
public static RuntimeList BZ_CONFIG_ERROR(RuntimeArray args, int ctx) { return new RuntimeScalar(-9).getList(); }
public static RuntimeList BZ_RUN(RuntimeArray args, int ctx) { return new RuntimeScalar(0).getList(); }
public static RuntimeList BZ_FLUSH(RuntimeArray args, int ctx) { return new RuntimeScalar(1).getList(); }
public static RuntimeList BZ_FINISH(RuntimeArray args, int ctx) { return new RuntimeScalar(2).getList(); }
public static RuntimeList BZ_MAX_UNUSED(RuntimeArray args, int ctx) { return new RuntimeScalar(5000).getList(); }

/**
* Treat a scalar (or scalar reference) as a byte string. Mirrors the
* helper in {@link CompressZlib}.
*/
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);
}

private static RuntimeScalar bytesToScalar(byte[] bytes, int len) {
RuntimeScalar s = new RuntimeScalar(new String(bytes, 0, len, StandardCharsets.ISO_8859_1));
s.type = RuntimeScalarType.BYTE_STRING;
return s;
}

/**
* memBzip($data) — one-shot compression. Returns the bzip2 stream or
* undef on error (matching the upstream module).
*/
public static RuntimeList memBzip(RuntimeArray args, int ctx) {
if (args.isEmpty()) return scalarUndef.getList();
byte[] input = getInputBytes(args.get(0));
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(Math.max(64, input.length / 2));
try (BZip2CompressorOutputStream out = new BZip2CompressorOutputStream(baos)) {
out.write(input);
}
byte[] result = baos.toByteArray();
return bytesToScalar(result, result.length).getList();
} catch (IOException e) {
return scalarUndef.getList();
}
}

/**
* memBunzip($data) — one-shot decompression. Returns the inflated bytes
* or undef on error.
*/
public static RuntimeList memBunzip(RuntimeArray args, int ctx) {
if (args.isEmpty()) return scalarUndef.getList();
byte[] input = getInputBytes(args.get(0));
try (BZip2CompressorInputStream in = new BZip2CompressorInputStream(
new ByteArrayInputStream(input), true)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(input.length * 4);
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) != -1) {
baos.write(buf, 0, n);
}
byte[] result = baos.toByteArray();
return bytesToScalar(result, result.length).getList();
} catch (IOException e) {
return scalarUndef.getList();
}
}

/** bzip2($data) — alias of memBzip in the upstream module. */
public static RuntimeList bzip2(RuntimeArray args, int ctx) {
return memBzip(args, ctx);
}

/** bzunzip($data) — alias of memBunzip. */
public static RuntimeList bzunzip(RuntimeArray args, int ctx) {
return memBunzip(args, ctx);
}

/**
* bzopen($filename, $mode) — opens a bzip2 file for reading ('rb') or
* writing ('wb'). Returns a blessed Compress::Bzip2::bzFile handle, or
* undef on error. Method-call syntax (Compress::Bzip2-&gt;bzopen) is
* also accepted.
*/
public static RuntimeList bzopen(RuntimeArray args, int ctx) {
int argOffset = 0;
if (!args.isEmpty()) {
String first = args.get(0).toString();
if (first.equals("Compress::Bzip2") || first.contains("::")) {
argOffset = 1;
}
}
if (args.size() < argOffset + 2) return scalarUndef.getList();

String filename = args.get(argOffset).toString();
String mode = args.get(argOffset + 1).toString();

try {
RuntimeHash self = new RuntimeHash();
self.put("_mode", new RuntimeScalar(mode));
self.put("_eof", new RuntimeScalar(0));

if (mode.startsWith("r")) {
// Concatenated streams (true) so multi-block .bz2 archives
// (typical for tarballs) are read end-to-end.
InputStream fis = new FileInputStream(filename);
BZip2CompressorInputStream in = new BZip2CompressorInputStream(fis, true);
self.put("_stream", new RuntimeScalar(in));
} else if (mode.startsWith("w")) {
OutputStream fos = new FileOutputStream(filename);
BZip2CompressorOutputStream out = new BZip2CompressorOutputStream(fos);
self.put("_stream", new RuntimeScalar(out));
} else {
return scalarUndef.getList();
}

RuntimeScalar ref = self.createReference();
ReferenceOperators.bless(ref, new RuntimeScalar("Compress::Bzip2::bzFile"));
return ref.getList();
} catch (IOException e) {
return scalarUndef.getList();
}
}
}
Loading
Loading