From 16ccbdbdf14f67876680af3a5aa08d4505078bf8 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 28 Apr 2026 17:54:36 +0200 Subject: [PATCH] fix(Archive::Zip): add extractToFileNamed and honor Perl chdir Two issues prevented `jcpan -t MP3::Tag` (and any zip-distributed CPAN module) from being unpacked by CPAN::Tarzip::unzip: 1. Archive::Zip::Member::extractToFileNamed was not implemented, so CPAN::Tarzip::unzip died with "Can't locate object method extractToFileNamed". CPAN reported "Had problems unarchiving." 2. The extraction methods used Paths.get(name) directly, which resolves relative paths against the JVM's working directory. PerlOnJava's chdir only updates System "user.dir", not the JVM cwd, so files were being written to the launch directory instead of the build tmp-$$ directory CPAN had chdir'd into. The build dir ended up empty and CPAN proceeded as if the package had no Makefile.PL. Add a resolvePath helper that resolves relative paths against "user.dir" and use it in extractMember, extractMemberWithoutPaths, extractTree, extractToFileNamed, addFile, read, and writeToFileNamed. After this fix `jcpan -t MP3::Tag` actually configures, builds, and runs the test suite. Some tests still fail due to unrelated regex lookbehind limits and a separate NPE in string handling, but the distribution is no longer dead-on-arrival. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/core/Configuration.java | 4 +- .../runtime/perlmodule/ArchiveZip.java | 73 ++++++++++++++++--- 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 82953a4d0..1484eb6f8 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 = "46b0ccb80"; + public static final String gitCommitId = "25b6fa935"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -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 28 2026 18:01:32"; + public static final String buildTimestamp = "Apr 28 2026 18:03:29"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/ArchiveZip.java b/src/main/java/org/perlonjava/runtime/perlmodule/ArchiveZip.java index cafc3edc9..24af74258 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/ArchiveZip.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/ArchiveZip.java @@ -34,6 +34,23 @@ public class ArchiveZip extends PerlModuleBase { private static final String FILENAME_KEY = "_filename"; private static final String COMMENT_KEY = "_zipfileComment"; + /** + * Resolve a path string against Perl's notion of the current working + * directory (System "user.dir"), since Java's Paths.get does not honor + * Perl chdir() updates to user.dir. + */ + private static Path resolvePath(String name) { + Path p = Paths.get(name); + if (p.isAbsolute()) return p; + return Paths.get(System.getProperty("user.dir")).resolve(p); + } + + private static Path resolvePath(String first, String... more) { + Path p = Paths.get(first, more); + if (p.isAbsolute()) return p; + return Paths.get(System.getProperty("user.dir")).resolve(p); + } + // Constants (matching Archive::Zip) public static final int AZ_OK = 0; public static final int AZ_STREAM_END = 1; @@ -90,6 +107,7 @@ public static void initialize() { az.registerMethod("versionNeededToExtract", null); az.registerMethod("bitFlag", null); az.registerMethod("fileComment", null); + az.registerMethod("extractToFileNamed", null); // Constants az.registerMethod("AZ_OK", null); @@ -203,16 +221,17 @@ public static RuntimeList read(RuntimeArray args, int ctx) { RuntimeArray members = getMembers(self); members.undefine(); // Clear existing members - Path path = Paths.get(filename); + Path path = resolvePath(filename); if (!Files.exists(path)) { return new RuntimeScalar(AZ_IO_ERROR).getList(); } + String resolvedName = path.toString(); // Extract raw DOS timestamps from central directory // (Java's ZipEntry uses extended timestamps when available) - java.util.Map rawDosTimestamps = extractRawDosTimestamps(filename); + java.util.Map rawDosTimestamps = extractRawDosTimestamps(resolvedName); - try (ZipFile zipFile = new ZipFile(filename)) { + try (ZipFile zipFile = new ZipFile(resolvedName)) { // Store the zipfile comment String comment = zipFile.getComment(); if (comment != null) { @@ -399,7 +418,7 @@ public static RuntimeList writeToFileNamed(RuntimeArray args, int ctx) { try { RuntimeArray members = getMembers(self); - try (FileOutputStream fos = new FileOutputStream(filename); + try (FileOutputStream fos = new FileOutputStream(resolvePath(filename).toFile()); ZipOutputStream zos = new ZipOutputStream(fos)) { for (int i = 0; i < members.size(); i++) { @@ -577,7 +596,7 @@ public static RuntimeList addFile(RuntimeArray args, int ctx) { String memberName = args.size() > 2 ? args.get(2).toString() : filename; try { - Path path = Paths.get(filename); + Path path = resolvePath(filename); if (!Files.exists(path)) { return scalarUndef.getList(); } @@ -704,13 +723,13 @@ public static RuntimeList extractMember(RuntimeArray args, int ctx) { RuntimeScalar isDir = member.get("_isDirectory"); if (isDir != null && isDir.getBoolean()) { // Create directory - Path path = Paths.get(destName); + Path path = resolvePath(destName); Files.createDirectories(path); } else { // Extract file RuntimeScalar contents = member.get("_contents"); if (contents != null) { - Path path = Paths.get(destName); + Path path = resolvePath(destName); // Create parent directories if needed Path parent = path.getParent(); if (parent != null) { @@ -776,7 +795,7 @@ public static RuntimeList extractMemberWithoutPaths(RuntimeArray args, int ctx) RuntimeScalar contents = member.get("_contents"); if (contents != null) { - Path destPath = Paths.get(destDir, baseName); + Path destPath = resolvePath(destDir, baseName); Path parent = destPath.getParent(); if (parent != null) { Files.createDirectories(parent); @@ -792,6 +811,42 @@ public static RuntimeList extractMemberWithoutPaths(RuntimeArray args, int ctx) } } + /** + * Member method: extract this member to a specified file name. + * Usage: $status = $member->extractToFileNamed($filename); + */ + public static RuntimeList extractToFileNamed(RuntimeArray args, int ctx) { + if (args.size() < 2) { + return new RuntimeScalar(AZ_ERROR).getList(); + } + + RuntimeHash member = args.get(0).hashDeref(); + String destName = args.get(1).toString(); + + try { + RuntimeScalar isDir = member.get("_isDirectory"); + if (isDir != null && isDir.getBoolean()) { + Path path = resolvePath(destName); + Files.createDirectories(path); + return new RuntimeScalar(AZ_OK).getList(); + } + + RuntimeScalar contents = member.get("_contents"); + Path path = resolvePath(destName); + Path parent = path.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + byte[] data = contents != null + ? contents.toString().getBytes(StandardCharsets.ISO_8859_1) + : new byte[0]; + Files.write(path, data); + return new RuntimeScalar(AZ_OK).getList(); + } catch (IOException e) { + return new RuntimeScalar(AZ_IO_ERROR).getList(); + } + } + /** * Extract all members to a directory. * Usage: $status = $zip->extractTree('', 'dest/'); @@ -826,7 +881,7 @@ public static RuntimeList extractTree(RuntimeArray args, int ctx) { destName = memberName.substring(root.length()); } - Path destPath = Paths.get(dest, destName); + Path destPath = resolvePath(dest, destName); RuntimeScalar isDir = member.get("_isDirectory"); if (isDir != null && isDir.getBoolean()) {