From aa6522d5857761d3aad7c876aa5633f87a47f82b Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 14 Mar 2026 07:32:59 +0100 Subject: [PATCH] Implement Archive::Zip and complete CPAN Phase 4 - Add Archive::Zip Java implementation using java.util.zip - Read/write zip files - Add files, strings, and directories - Extract individual members or entire archive - Full member accessors (fileName, contents, size, etc.) - Create user documentation for CPAN modules (docs/guides/using-cpan-modules.md) - Update cpan_client.md design doc with Phase 4 completion - Document cpanm analysis: requires ExtUtils::MakeMaker which is infeasible Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- dev/design/cpan_client.md | 34 +- .../slides-part3-integration.md | 18 + docs/about/changelog.md | 2 +- docs/guides/using-cpan-modules.md | 273 +++++ docs/reference/feature-matrix.md | 1 + .../org/perlonjava/core/Configuration.java | 2 +- .../runtime/perlmodule/ArchiveZip.java | 932 ++++++++++++++++++ src/main/perl/lib/Archive/Zip.pm | 254 +++++ 8 files changed, 1507 insertions(+), 9 deletions(-) create mode 100644 docs/guides/using-cpan-modules.md create mode 100644 src/main/java/org/perlonjava/runtime/perlmodule/ArchiveZip.java create mode 100644 src/main/perl/lib/Archive/Zip.pm diff --git a/dev/design/cpan_client.md b/dev/design/cpan_client.md index 49e9b3f5d..4ce3a0a11 100644 --- a/dev/design/cpan_client.md +++ b/dev/design/cpan_client.md @@ -35,7 +35,7 @@ CPAN.pm has deep dependencies that make it challenging to port. The main blocker | **ExtUtils::MakeMaker** | ❌ Missing | Very High | Build system - huge module with many dependencies | | **LWP::UserAgent** | ❌ Missing | Medium | Web client (HTTP::Tiny exists as alternative) | | **Archive::Tar** | ✅ Done | Medium | Imported via sync.pl | -| **Archive::Zip** | ❌ Missing | Medium | Zip handling - Java has built-in support | +| **Archive::Zip** | ✅ Done | Medium | Java implementation using java.util.zip | | **Net::FTP** | ✅ Done | Medium | Imported via sync.pl | | **IPC::Open3** | ✅ Done | Medium | Custom implementation using Java ProcessBuilder | | **IO::Socket** | ✅ Done | Medium | Imported via sync.pl | @@ -224,7 +224,7 @@ This is already working for many modules (Pod::*, Test::*, Getopt::Long, etc.) ## Progress Tracking -### Current Status: Phase 3 complete +### Current Status: Phase 4 complete ### Completed - [x] Analyze CPAN.pm dependencies (2024-03-13) @@ -258,6 +258,20 @@ This is already working for many modules (Pod::*, Test::*, Getopt::Long, etc.) - ioctl() - implemented with jnr-posix native support and fallback stub - Prototype parsing fix - typeglob arguments now use =~ precedence level - Reference comparison fix - `\$x == \undef` no longer crashes (NPE in getDoubleRef) +- [x] **Phase 4: CPAN Client Evaluation & Archive::Zip** (2024-03-13) + - **cpanm analysis complete**: cpanm bundles (fatpacks) its dependencies but requires ExtUtils::MakeMaker + - ExtUtils::MakeMaker is the critical blocker - it's the CPAN build system that runs `make` + - Since PerlOnJava doesn't use native compilation, a traditional CPAN client isn't feasible + - **Archive::Zip implemented**: Full Java implementation using java.util.zip + - Read/write zip files + - Add files, strings, and directories + - Extract individual members or entire archive + - Works with system `unzip` command for verification + - Created user documentation: `docs/guides/using-cpan-modules.md` + - How to check module availability + - List of included modules + - How to add pure Perl modules + - Example scripts for downloading CPAN modules ### Files Changed (Phase 2) - `dev/import-perl5/config.yaml` - Added IO::Socket, IO::Zlib, Archive::Tar, Net::*, Tie::StdHandle, File::Spec imports @@ -278,15 +292,21 @@ This is already working for many modules (Pod::*, Test::*, Getopt::Long, etc.) - `src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java` - Fixed getIntRef()/getDoubleRef() NPE - `dev/import-perl5/config.yaml` - Removed IPC::Open2/Open3 imports (custom implementation) +### Files Changed (Phase 4) +- `src/main/java/org/perlonjava/runtime/perlmodule/ArchiveZip.java` - New Java implementation +- `src/main/perl/lib/Archive/Zip.pm` - Perl wrapper with XSLoader +- `docs/guides/using-cpan-modules.md` - User documentation for adding CPAN modules + ### Next Steps -1. Phase 4: Evaluate cpanm as alternative to CPAN.pm -2. Consider Archive::Zip implementation using java.util.zip -3. Document "how to add a CPAN module" for users +1. Consider a minimal CPAN download helper (pure Perl, no build step) +2. Expand user documentation with more examples +3. Add more commonly-needed pure Perl modules ### Open Questions -- Is cpanm lighter on dependencies than CPAN.pm? -- Should we create a PerlOnJava-specific minimal CPAN client? +- Should we create a PerlOnJava-specific minimal CPAN download tool? - How important is Safe compartmentalization for users? ### Resolved Questions - ✅ fork() alternative: IPC::Open2/Open3 now use Java ProcessBuilder +- ✅ cpanm feasibility: cpanm requires ExtUtils::MakeMaker which needs `make` - not suitable for PerlOnJava +- ✅ Archive::Zip: Implemented using java.util.zip diff --git a/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part3-integration.md b/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part3-integration.md index fbfcbd215..c84e14d1f 100644 --- a/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part3-integration.md +++ b/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part3-integration.md @@ -19,6 +19,24 @@ Java equivalents are easier to write and maintain than C/XS. The same API surfac --- +## CPAN Installation Challenges + +**ExtUtils::MakeMaker assumes a C toolchain.** + +What breaks without C: +- `Makefile.PL` → generates C build rules +- XS modules → need compiler + linker +- Shared libraries → not produced + +**What works today:** +- 300+ modules bundled in JAR +- Pure-Perl CPAN modules run unmodified +- Hand-written Java replacements for XS + +**Open question:** cpanm integration, automated XS→Java + +--- + ## Module Loading `require` converts `Module::Name` → `Module/Name.pm`, searches `@INC`, caches in `%INC`. diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 2c31f41e9..309dbdd44 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -9,7 +9,7 @@ Release history of PerlOnJava. See [Roadmap](roadmap.md) for future plans. - Add `defer` feature - Non-local control flow: `last`/`next`/`redo`/`goto LABEL` - Tail call with trampoline for `goto &NAME` and `goto __SUB__` -- Add modules: `Time::Piece`, `TOML`, `DirHandle`, `Dumpvalue`, `Sys::Hostname`, `IO::Socket`, `IO::Socket::INET`, `IO::Socket::UNIX`, `IO::Zlib`, `Archive::Tar`, `Net::FTP`, `Net::Cmd`, `IPC::Open2`, `IPC::Open3`. +- Add modules: `Time::Piece`, `TOML`, `DirHandle`, `Dumpvalue`, `Sys::Hostname`, `IO::Socket`, `IO::Socket::INET`, `IO::Socket::UNIX`, `IO::Zlib`, `Archive::Tar`, `Archive::Zip`, `Net::FTP`, `Net::Cmd`, `IPC::Open2`, `IPC::Open3`. - Add operators: `flock`, `syscall`, `fcntl`, `ioctl`. - Bugfix: parser now handles `@{${...}}` nested dereference in push/unshift. - Bugfix: regex octal escapes `\10`-`\377` now work correctly. diff --git a/docs/guides/using-cpan-modules.md b/docs/guides/using-cpan-modules.md new file mode 100644 index 000000000..9101f13d1 --- /dev/null +++ b/docs/guides/using-cpan-modules.md @@ -0,0 +1,273 @@ +# Using CPAN Modules with PerlOnJava + +## Overview + +PerlOnJava includes many common CPAN modules and supports adding additional pure Perl modules to your projects. + +## Checking Module Availability + +To check if a module is available in PerlOnJava: + +```bash +./jperl -e 'use Module::Name; print "Available\n"' +``` + +## Available Modules + +PerlOnJava includes: + +### Core Modules +- `strict`, `warnings`, `utf8`, `feature` +- `Carp`, `Config`, `Cwd`, `Exporter` +- `File::Spec`, `File::Basename`, `File::Copy`, `File::Find`, `File::Path`, `File::Temp` +- `IO::File`, `IO::Handle`, `FileHandle` +- `Getopt::Long`, `Getopt::Std` + +### Data Processing +- `JSON` - JSON encoding/decoding +- `YAML` - YAML parsing +- `TOML` - TOML parsing +- `Text::CSV` - CSV parsing +- `Data::Dumper` - Data structure dumping +- `Storable` - Data serialization + +### Cryptography & Encoding +- `Digest::MD5`, `Digest::SHA` - Hash algorithms +- `MIME::Base64`, `MIME::QuotedPrint` - Encoding +- `Encode` - Character encoding + +### Network & Web +- `HTTP::Tiny` - HTTP client +- `Socket` - Low-level socket support +- `IO::Socket::INET` - TCP/IP sockets +- `Net::FTP` - FTP client + +### Archives +- `Archive::Tar` - Tar file handling +- `Archive::Zip` - Zip file handling +- `Compress::Zlib` - Compression +- `IO::Zlib` - Compressed I/O + +### Database +- `DBI` - Database interface (with JDBC backend) + +### Testing +- `Test::More`, `Test::Simple`, `Test::Builder` + +### Time +- `Time::HiRes` - High-resolution time +- `Time::Piece` - Time manipulation +- `POSIX` - POSIX functions including strftime + +## Adding Pure Perl Modules + +If you need a CPAN module that's not included, you can often add pure Perl modules directly. + +### Method 1: Local lib Directory + +Create a `lib` directory in your project and add modules there: + +```bash +mkdir -p myproject/lib +cp /path/to/Some/Module.pm myproject/lib/Some/Module.pm +``` + +Run with: + +```bash +./jperl -Imyproject/lib myscript.pl +``` + +### Method 2: PERL5LIB Environment Variable + +```bash +export PERL5LIB=/path/to/your/modules +./jperl myscript.pl +``` + +### Finding Pure Perl Modules + +To check if a CPAN module is pure Perl: + +1. Visit https://metacpan.org/pod/Module::Name +2. Look at the source files +3. If there's only `.pm` files (no `.xs` or `.c` files), it's pure Perl + +### Example: Adding a Simple Module + +```bash +# Download a module from CPAN +curl -O https://cpan.metacpan.org/authors/id/X/XX/XXXX/Module-Name-1.00.tar.gz + +# Extract +tar xzf Module-Name-1.00.tar.gz + +# Copy the lib directory +cp -r Module-Name-1.00/lib/* myproject/lib/ +``` + +## Modules with XS Components + +Some CPAN modules have XS (C/C++) components that won't work directly. For these modules: + +1. **Check if PerlOnJava has a Java port** - Many common XS modules have Java implementations +2. **Look for pure Perl alternatives** - e.g., use `JSON` instead of `JSON::XS` +3. **Request a port** - Open an issue at the PerlOnJava repository + +### Common XS Modules with Java Alternatives + +| XS Module | Java Alternative in PerlOnJava | +|-----------|-------------------------------| +| JSON::XS | JSON (built-in) | +| Compress::Raw::Zlib | Compress::Zlib (built-in) | +| Digest::MD5 (XS part) | Digest::MD5 (Java implementation) | +| DBI (XS part) | DBI (JDBC backend) | +| Time::HiRes (XS part) | Time::HiRes (Java implementation) | + +## Working with Archive Files + +### Reading a Zip File + +```perl +use Archive::Zip qw(:ERROR_CODES); + +my $zip = Archive::Zip->new(); +my $status = $zip->read('archive.zip'); +die "Read failed" unless $status == AZ_OK; + +for my $member ($zip->members()) { + print $member->fileName(), "\n"; +} +``` + +### Creating a Zip File + +```perl +use Archive::Zip qw(:ERROR_CODES); + +my $zip = Archive::Zip->new(); +$zip->addFile('document.txt'); +$zip->addString("Hello!", 'hello.txt'); + +my $status = $zip->writeToFileNamed('output.zip'); +``` + +### Working with Tar Files + +```perl +use Archive::Tar; + +# Read +my $tar = Archive::Tar->new('archive.tar.gz'); +my @files = $tar->list_files(); +$tar->extract(); + +# Create +my $tar = Archive::Tar->new(); +$tar->add_files('file1.txt', 'file2.txt'); +$tar->write('output.tar.gz', COMPRESS_GZIP); +``` + +## HTTP Requests + +```perl +use HTTP::Tiny; + +my $http = HTTP::Tiny->new(); +my $response = $http->get('https://api.github.com/repos/perl/perl5'); + +if ($response->{success}) { + print $response->{content}; +} +``` + +## Downloading and Extracting CPAN Modules + +Here's a helper script to download and extract a CPAN module: + +```perl +#!/usr/bin/env jperl +use strict; +use warnings; +use HTTP::Tiny; +use Archive::Tar; +use File::Temp qw(tempfile); +use File::Path qw(make_path); + +my $module = shift or die "Usage: $0 Module::Name\n"; +my $dest = shift || 'lib'; + +# Convert module name to path +(my $path = $module) =~ s/::/-/g; + +# Query MetaCPAN for download URL +my $http = HTTP::Tiny->new(); +my $resp = $http->get("https://fastapi.metacpan.org/v1/download_url/$module"); + +if (!$resp->{success}) { + die "Could not find $module on CPAN\n"; +} + +# Parse JSON response +use JSON; +my $data = decode_json($resp->{content}); +my $url = $data->{download_url}; + +print "Downloading $url\n"; +my $tarball = $http->get($url); + +if (!$tarball->{success}) { + die "Download failed\n"; +} + +# Save to temp file +my ($fh, $filename) = tempfile(SUFFIX => '.tar.gz'); +print $fh $tarball->{content}; +close $fh; + +# Extract +my $tar = Archive::Tar->new($filename); +my @files = $tar->list_files(); + +make_path($dest); +for my $file (@files) { + next unless $file =~ m{/lib/(.+\.pm)$}; + my $target = "$dest/$1"; + my $dir = $target; + $dir =~ s{/[^/]+$}{}; + make_path($dir); + + my $content = $tar->get_content($file); + open my $out, '>', $target or die "Cannot write $target: $!"; + print $out $content; + close $out; + print "Installed $target\n"; +} + +unlink $filename; +print "Done!\n"; +``` + +## Troubleshooting + +### "Can't locate Module.pm in @INC" + +The module is not installed. Check: +1. Is it a pure Perl module? XS modules won't work directly. +2. Is the module in your lib path? Use `-I/path/to/lib` + +### "Can't load Java XS module" + +This means the module requires an XS implementation that hasn't been ported to Java. Check if there's a pure Perl alternative. + +### Module loads but functions don't work + +Some modules may load but have unsupported features: +- Check if the module uses XS functions internally +- Some Perl built-in functions may not be fully implemented + +## Getting Help + +- **PerlOnJava Repository**: https://github.com/fglock/PerlOnJava +- **Issues**: Report missing modules or compatibility problems +- **Feature Matrix**: See `docs/reference/feature-matrix.md` for supported features diff --git a/docs/reference/feature-matrix.md b/docs/reference/feature-matrix.md index 42f8a0feb..c9ff87ace 100644 --- a/docs/reference/feature-matrix.md +++ b/docs/reference/feature-matrix.md @@ -727,6 +727,7 @@ The `:encoding()` layer supports all encodings provided by Java's `Charset.forNa - 🚧 **POSIX** module. - 🚧 **Unicode::Normalize** `normalize`, `NFC`, `NFD`, `NFKC`, `NFKD`. - ✅ **Archive::Tar** module. +- ✅ **Archive::Zip** module. - ✅ **IPC::Open2** module. - ✅ **IPC::Open3** module. - ✅ **Net::FTP** module. diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 500181f38..bde4beafa 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 = "d0091f5b5"; + public static final String gitCommitId = "ccaba0dc3"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/ArchiveZip.java b/src/main/java/org/perlonjava/runtime/perlmodule/ArchiveZip.java new file mode 100644 index 000000000..e35bcfff1 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/perlmodule/ArchiveZip.java @@ -0,0 +1,932 @@ +package org.perlonjava.runtime.perlmodule; + +import org.perlonjava.runtime.operators.ReferenceOperators; +import org.perlonjava.runtime.runtimetypes.*; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.*; + +/** + * Archive::Zip module implementation for PerlOnJava. + * This class provides zip file handling using Java's java.util.zip package. + * + * Implements core Archive::Zip functionality: + * - Reading zip files + * - Listing members + * - Extracting members + * - Adding new members (files/strings) + * - Writing zip files + */ +public class ArchiveZip extends PerlModuleBase { + + // Keys for internal hash storage + private static final String MEMBERS_KEY = "_members"; + private static final String FILENAME_KEY = "_filename"; + + // Constants (matching Archive::Zip) + public static final int AZ_OK = 0; + public static final int AZ_STREAM_END = 1; + public static final int AZ_ERROR = 2; + public static final int AZ_FORMAT_ERROR = 3; + public static final int AZ_IO_ERROR = 4; + + public static final int COMPRESSION_STORED = 0; + public static final int COMPRESSION_DEFLATED = 8; + public static final int COMPRESSION_LEVEL_NONE = 0; + public static final int COMPRESSION_LEVEL_DEFAULT = -1; + public static final int COMPRESSION_LEVEL_FASTEST = 1; + public static final int COMPRESSION_LEVEL_BEST_COMPRESSION = 9; + + public ArchiveZip() { + super("Archive::Zip", false); + } + + public static void initialize() { + ArchiveZip az = new ArchiveZip(); + try { + // Archive methods + az.registerMethod("new", "newArchive", null); + az.registerMethod("read", null); + az.registerMethod("writeToFileNamed", null); + az.registerMethod("writeToFileHandle", null); + az.registerMethod("members", null); + az.registerMethod("memberNames", null); + az.registerMethod("numberOfMembers", null); + az.registerMethod("memberNamed", null); + az.registerMethod("membersMatching", null); + az.registerMethod("addFile", null); + az.registerMethod("addString", null); + az.registerMethod("addDirectory", null); + az.registerMethod("extractMember", null); + az.registerMethod("extractMemberWithoutPaths", null); + az.registerMethod("extractTree", null); + az.registerMethod("removeMember", null); + + // Member methods (called on member objects) + az.registerMethod("fileName", null); + az.registerMethod("contents", null); + az.registerMethod("isDirectory", null); + az.registerMethod("uncompressedSize", null); + az.registerMethod("compressedSize", null); + az.registerMethod("compressionMethod", null); + az.registerMethod("lastModTime", null); + az.registerMethod("crc32", null); + az.registerMethod("externalFileName", null); + + // Constants + az.registerMethod("AZ_OK", null); + az.registerMethod("AZ_STREAM_END", null); + az.registerMethod("AZ_ERROR", null); + az.registerMethod("AZ_FORMAT_ERROR", null); + az.registerMethod("AZ_IO_ERROR", null); + az.registerMethod("COMPRESSION_STORED", null); + az.registerMethod("COMPRESSION_DEFLATED", null); + az.registerMethod("COMPRESSION_LEVEL_NONE", null); + az.registerMethod("COMPRESSION_LEVEL_DEFAULT", null); + az.registerMethod("COMPRESSION_LEVEL_FASTEST", null); + az.registerMethod("COMPRESSION_LEVEL_BEST_COMPRESSION", null); + + } catch (NoSuchMethodException e) { + System.err.println("Warning: Missing Archive::Zip method: " + e.getMessage()); + } + } + + // Constants + public static RuntimeList AZ_OK(RuntimeArray args, int ctx) { + return new RuntimeScalar(AZ_OK).getList(); + } + + public static RuntimeList AZ_STREAM_END(RuntimeArray args, int ctx) { + return new RuntimeScalar(AZ_STREAM_END).getList(); + } + + public static RuntimeList AZ_ERROR(RuntimeArray args, int ctx) { + return new RuntimeScalar(AZ_ERROR).getList(); + } + + public static RuntimeList AZ_FORMAT_ERROR(RuntimeArray args, int ctx) { + return new RuntimeScalar(AZ_FORMAT_ERROR).getList(); + } + + public static RuntimeList AZ_IO_ERROR(RuntimeArray args, int ctx) { + return new RuntimeScalar(AZ_IO_ERROR).getList(); + } + + public static RuntimeList COMPRESSION_STORED(RuntimeArray args, int ctx) { + return new RuntimeScalar(COMPRESSION_STORED).getList(); + } + + public static RuntimeList COMPRESSION_DEFLATED(RuntimeArray args, int ctx) { + return new RuntimeScalar(COMPRESSION_DEFLATED).getList(); + } + + public static RuntimeList COMPRESSION_LEVEL_NONE(RuntimeArray args, int ctx) { + return new RuntimeScalar(COMPRESSION_LEVEL_NONE).getList(); + } + + public static RuntimeList COMPRESSION_LEVEL_DEFAULT(RuntimeArray args, int ctx) { + return new RuntimeScalar(COMPRESSION_LEVEL_DEFAULT).getList(); + } + + public static RuntimeList COMPRESSION_LEVEL_FASTEST(RuntimeArray args, int ctx) { + return new RuntimeScalar(COMPRESSION_LEVEL_FASTEST).getList(); + } + + public static RuntimeList COMPRESSION_LEVEL_BEST_COMPRESSION(RuntimeArray args, int ctx) { + return new RuntimeScalar(COMPRESSION_LEVEL_BEST_COMPRESSION).getList(); + } + + /** + * Create a new Archive::Zip object. + * Usage: my $zip = Archive::Zip->new(); + * my $zip = Archive::Zip->new('file.zip'); + */ + public static RuntimeList newArchive(RuntimeArray args, int ctx) { + RuntimeHash self = new RuntimeHash(); + RuntimeArray membersArray = new RuntimeArray(); + self.put(MEMBERS_KEY, membersArray.createReference()); + + RuntimeScalar ref = self.createReference(); + ReferenceOperators.bless(ref, new RuntimeScalar("Archive::Zip")); + + // If a filename is provided, read it + if (args.size() > 1) { + RuntimeScalar filename = args.get(1); + if (filename.type != RuntimeScalarType.UNDEF) { + self.put(FILENAME_KEY, filename); + RuntimeArray readArgs = new RuntimeArray(); + RuntimeArray.push(readArgs, ref); + RuntimeArray.push(readArgs, filename); + RuntimeList result = read(readArgs, RuntimeContextType.SCALAR); + int status = result.scalar().getInt(); + if (status != AZ_OK) { + return scalarUndef.getList(); + } + } + } + + return ref.getList(); + } + + /** + * Read a zip file. + * Usage: $status = $zip->read('file.zip'); + * Returns: AZ_OK on success, error code on failure + */ + public static RuntimeList read(RuntimeArray args, int ctx) { + if (args.size() < 2) { + return new RuntimeScalar(AZ_ERROR).getList(); + } + + RuntimeHash self = args.get(0).hashDeref(); + String filename = args.get(1).toString(); + + try { + RuntimeArray members = getMembers(self); + members.undefine(); // Clear existing members + + Path path = Paths.get(filename); + if (!Files.exists(path)) { + return new RuntimeScalar(AZ_IO_ERROR).getList(); + } + + try (ZipFile zipFile = new ZipFile(filename)) { + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + + // Create member object + RuntimeHash member = createMemberFromEntry(zipFile, entry); + RuntimeScalar memberRef = member.createReference(); + ReferenceOperators.bless(memberRef, new RuntimeScalar("Archive::Zip::Member")); + + RuntimeArray.push(members, memberRef); + } + } + + self.put(FILENAME_KEY, new RuntimeScalar(filename)); + return new RuntimeScalar(AZ_OK).getList(); + + } catch (IOException e) { + return new RuntimeScalar(AZ_IO_ERROR).getList(); + } catch (Exception e) { + return new RuntimeScalar(AZ_FORMAT_ERROR).getList(); + } + } + + /** + * Write zip to a file. + * Usage: $status = $zip->writeToFileNamed('output.zip'); + */ + public static RuntimeList writeToFileNamed(RuntimeArray args, int ctx) { + if (args.size() < 2) { + return new RuntimeScalar(AZ_ERROR).getList(); + } + + RuntimeHash self = args.get(0).hashDeref(); + String filename = args.get(1).toString(); + + try { + RuntimeArray members = getMembers(self); + + try (FileOutputStream fos = new FileOutputStream(filename); + ZipOutputStream zos = new ZipOutputStream(fos)) { + + for (int i = 0; i < members.size(); i++) { + RuntimeHash member = members.get(i).hashDeref(); + writeMemberToZip(zos, member); + } + } + + return new RuntimeScalar(AZ_OK).getList(); + + } catch (IOException e) { + return new RuntimeScalar(AZ_IO_ERROR).getList(); + } catch (Exception e) { + return new RuntimeScalar(AZ_ERROR).getList(); + } + } + + /** + * Write zip to a filehandle. + * Usage: $status = $zip->writeToFileHandle($fh); + */ + public static RuntimeList writeToFileHandle(RuntimeArray args, int ctx) { + if (args.size() < 2) { + return new RuntimeScalar(AZ_ERROR).getList(); + } + + RuntimeHash self = args.get(0).hashDeref(); + RuntimeScalar fhRef = args.get(1); + + try { + RuntimeIO fh = RuntimeIO.getRuntimeIO(fhRef); + if (fh == null) { + return new RuntimeScalar(AZ_ERROR).getList(); + } + + // Create a ByteArrayOutputStream to collect the zip data + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(baos)) { + RuntimeArray members = getMembers(self); + for (int i = 0; i < members.size(); i++) { + RuntimeHash member = members.get(i).hashDeref(); + writeMemberToZip(zos, member); + } + } + + // Write to filehandle + byte[] data = baos.toByteArray(); + String dataStr = new String(data, StandardCharsets.ISO_8859_1); + fh.ioHandle.write(dataStr); + + return new RuntimeScalar(AZ_OK).getList(); + + } catch (Exception e) { + return new RuntimeScalar(AZ_IO_ERROR).getList(); + } + } + + /** + * Get list of all members. + * Usage: @members = $zip->members(); + */ + public static RuntimeList members(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return new RuntimeList(); + } + + RuntimeHash self = args.get(0).hashDeref(); + RuntimeArray members = getMembers(self); + + RuntimeList result = new RuntimeList(); + for (int i = 0; i < members.size(); i++) { + result.add(members.get(i)); + } + return result; + } + + /** + * Get list of all member names. + * Usage: @names = $zip->memberNames(); + */ + public static RuntimeList memberNames(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return new RuntimeList(); + } + + RuntimeHash self = args.get(0).hashDeref(); + RuntimeArray members = getMembers(self); + + RuntimeList result = new RuntimeList(); + for (int i = 0; i < members.size(); i++) { + RuntimeHash member = members.get(i).hashDeref(); + result.add(member.get("_name")); + } + return result; + } + + /** + * Get number of members. + * Usage: $count = $zip->numberOfMembers(); + */ + public static RuntimeList numberOfMembers(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarZero.getList(); + } + + RuntimeHash self = args.get(0).hashDeref(); + RuntimeArray members = getMembers(self); + return new RuntimeScalar(members.size()).getList(); + } + + /** + * Get a member by name. + * Usage: $member = $zip->memberNamed('path/to/file.txt'); + */ + public static RuntimeList memberNamed(RuntimeArray args, int ctx) { + if (args.size() < 2) { + return scalarUndef.getList(); + } + + RuntimeHash self = args.get(0).hashDeref(); + String name = args.get(1).toString(); + RuntimeArray members = getMembers(self); + + for (int i = 0; i < members.size(); i++) { + RuntimeHash member = members.get(i).hashDeref(); + RuntimeScalar memberName = member.get("_name"); + if (memberName != null && memberName.toString().equals(name)) { + return members.get(i).getList(); + } + } + return scalarUndef.getList(); + } + + /** + * Get members matching a regex. + * Usage: @members = $zip->membersMatching('\.txt$'); + */ + public static RuntimeList membersMatching(RuntimeArray args, int ctx) { + if (args.size() < 2) { + return new RuntimeList(); + } + + RuntimeHash self = args.get(0).hashDeref(); + String regex = args.get(1).toString(); + RuntimeArray members = getMembers(self); + + RuntimeList result = new RuntimeList(); + try { + java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(regex); + for (int i = 0; i < members.size(); i++) { + RuntimeHash member = members.get(i).hashDeref(); + RuntimeScalar memberName = member.get("_name"); + if (memberName != null && pattern.matcher(memberName.toString()).find()) { + result.add(members.get(i)); + } + } + } catch (Exception e) { + // Invalid regex, return empty list + } + return result; + } + + /** + * Add a file to the archive. + * Usage: $member = $zip->addFile('file.txt'); + * $member = $zip->addFile('file.txt', 'newname.txt'); + */ + public static RuntimeList addFile(RuntimeArray args, int ctx) { + if (args.size() < 2) { + return scalarUndef.getList(); + } + + RuntimeHash self = args.get(0).hashDeref(); + String filename = args.get(1).toString(); + String memberName = args.size() > 2 ? args.get(2).toString() : filename; + + try { + Path path = Paths.get(filename); + if (!Files.exists(path)) { + return scalarUndef.getList(); + } + + byte[] content = Files.readAllBytes(path); + long lastModified = Files.getLastModifiedTime(path).toMillis(); + + RuntimeHash member = new RuntimeHash(); + member.put("_name", new RuntimeScalar(memberName)); + member.put("_externalFileName", new RuntimeScalar(filename)); + member.put("_contents", new RuntimeScalar(new String(content, StandardCharsets.ISO_8859_1))); + member.put("_isDirectory", scalarFalse); + member.put("_uncompressedSize", new RuntimeScalar(content.length)); + member.put("_compressedSize", new RuntimeScalar(content.length)); + member.put("_compressionMethod", new RuntimeScalar(COMPRESSION_DEFLATED)); + member.put("_lastModTime", new RuntimeScalar(lastModified / 1000)); + member.put("_crc32", new RuntimeScalar(computeCRC32(content))); + + RuntimeScalar memberRef = member.createReference(); + ReferenceOperators.bless(memberRef, new RuntimeScalar("Archive::Zip::Member")); + + RuntimeArray members = getMembers(self); + RuntimeArray.push(members, memberRef); + + return memberRef.getList(); + + } catch (IOException e) { + return scalarUndef.getList(); + } + } + + /** + * Add a string as a member. + * Usage: $member = $zip->addString('content', 'name.txt'); + */ + public static RuntimeList addString(RuntimeArray args, int ctx) { + if (args.size() < 3) { + return scalarUndef.getList(); + } + + RuntimeHash self = args.get(0).hashDeref(); + String content = args.get(1).toString(); + String memberName = args.get(2).toString(); + + byte[] contentBytes = content.getBytes(StandardCharsets.ISO_8859_1); + + RuntimeHash member = new RuntimeHash(); + member.put("_name", new RuntimeScalar(memberName)); + member.put("_externalFileName", new RuntimeScalar("")); + member.put("_contents", new RuntimeScalar(content)); + member.put("_isDirectory", scalarFalse); + member.put("_uncompressedSize", new RuntimeScalar(contentBytes.length)); + member.put("_compressedSize", new RuntimeScalar(contentBytes.length)); + member.put("_compressionMethod", new RuntimeScalar(COMPRESSION_DEFLATED)); + member.put("_lastModTime", new RuntimeScalar(System.currentTimeMillis() / 1000)); + member.put("_crc32", new RuntimeScalar(computeCRC32(contentBytes))); + + RuntimeScalar memberRef = member.createReference(); + ReferenceOperators.bless(memberRef, new RuntimeScalar("Archive::Zip::Member")); + + RuntimeArray members = getMembers(self); + RuntimeArray.push(members, memberRef); + + return memberRef.getList(); + } + + /** + * Add a directory entry. + * Usage: $member = $zip->addDirectory('dirname/'); + */ + public static RuntimeList addDirectory(RuntimeArray args, int ctx) { + if (args.size() < 2) { + return scalarUndef.getList(); + } + + RuntimeHash self = args.get(0).hashDeref(); + String dirName = args.get(1).toString(); + + // Ensure directory name ends with / + if (!dirName.endsWith("/")) { + dirName = dirName + "/"; + } + + RuntimeHash member = new RuntimeHash(); + member.put("_name", new RuntimeScalar(dirName)); + member.put("_externalFileName", new RuntimeScalar("")); + member.put("_contents", new RuntimeScalar("")); + member.put("_isDirectory", scalarTrue); + member.put("_uncompressedSize", scalarZero); + member.put("_compressedSize", scalarZero); + member.put("_compressionMethod", new RuntimeScalar(COMPRESSION_STORED)); + member.put("_lastModTime", new RuntimeScalar(System.currentTimeMillis() / 1000)); + member.put("_crc32", scalarZero); + + RuntimeScalar memberRef = member.createReference(); + ReferenceOperators.bless(memberRef, new RuntimeScalar("Archive::Zip::Member")); + + RuntimeArray members = getMembers(self); + RuntimeArray.push(members, memberRef); + + return memberRef.getList(); + } + + /** + * Extract a member to a file. + * Usage: $status = $zip->extractMember('name.txt', 'output.txt'); + */ + public static RuntimeList extractMember(RuntimeArray args, int ctx) { + if (args.size() < 2) { + return new RuntimeScalar(AZ_ERROR).getList(); + } + + RuntimeHash self = args.get(0).hashDeref(); + String memberName = args.get(1).toString(); + String destName = args.size() > 2 ? args.get(2).toString() : memberName; + + try { + RuntimeArray members = getMembers(self); + + for (int i = 0; i < members.size(); i++) { + RuntimeHash member = members.get(i).hashDeref(); + RuntimeScalar name = member.get("_name"); + if (name != null && name.toString().equals(memberName)) { + RuntimeScalar isDir = member.get("_isDirectory"); + if (isDir != null && isDir.getBoolean()) { + // Create directory + Path path = Paths.get(destName); + Files.createDirectories(path); + } else { + // Extract file + RuntimeScalar contents = member.get("_contents"); + if (contents != null) { + Path path = Paths.get(destName); + // Create parent directories if needed + Path parent = path.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + byte[] data = contents.toString().getBytes(StandardCharsets.ISO_8859_1); + Files.write(path, data); + } + } + return new RuntimeScalar(AZ_OK).getList(); + } + } + return new RuntimeScalar(AZ_ERROR).getList(); + + } catch (IOException e) { + return new RuntimeScalar(AZ_IO_ERROR).getList(); + } + } + + /** + * Extract a member without paths (just filename). + * Usage: $status = $zip->extractMemberWithoutPaths($member, 'dest/'); + */ + public static RuntimeList extractMemberWithoutPaths(RuntimeArray args, int ctx) { + if (args.size() < 2) { + return new RuntimeScalar(AZ_ERROR).getList(); + } + + RuntimeHash self = args.get(0).hashDeref(); + RuntimeScalar memberArg = args.get(1); + String destDir = args.size() > 2 ? args.get(2).toString() : "."; + + try { + RuntimeHash member; + if (RuntimeScalarType.isReference(memberArg)) { + member = memberArg.hashDeref(); + } else { + // It's a member name + RuntimeArray findArgs = new RuntimeArray(); + RuntimeArray.push(findArgs, args.get(0)); + RuntimeArray.push(findArgs, memberArg); + RuntimeList found = memberNamed(findArgs, ctx); + if (found.isEmpty() || found.scalar().type == RuntimeScalarType.UNDEF) { + return new RuntimeScalar(AZ_ERROR).getList(); + } + member = found.scalar().hashDeref(); + } + + RuntimeScalar name = member.get("_name"); + if (name == null) { + return new RuntimeScalar(AZ_ERROR).getList(); + } + + // Get just the filename without path + String fullName = name.toString(); + String baseName = Paths.get(fullName).getFileName().toString(); + + RuntimeScalar isDir = member.get("_isDirectory"); + if (isDir != null && isDir.getBoolean()) { + // Skip directory entries + return new RuntimeScalar(AZ_OK).getList(); + } + + RuntimeScalar contents = member.get("_contents"); + if (contents != null) { + Path destPath = Paths.get(destDir, baseName); + Path parent = destPath.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + byte[] data = contents.toString().getBytes(StandardCharsets.ISO_8859_1); + Files.write(destPath, 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/'); + */ + public static RuntimeList extractTree(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return new RuntimeScalar(AZ_ERROR).getList(); + } + + RuntimeHash self = args.get(0).hashDeref(); + String root = args.size() > 1 ? args.get(1).toString() : ""; + String dest = args.size() > 2 ? args.get(2).toString() : "."; + + try { + RuntimeArray members = getMembers(self); + + for (int i = 0; i < members.size(); i++) { + RuntimeHash member = members.get(i).hashDeref(); + RuntimeScalar name = member.get("_name"); + if (name == null) continue; + + String memberName = name.toString(); + + // Filter by root prefix + if (!root.isEmpty() && !memberName.startsWith(root)) { + continue; + } + + // Remove root prefix for destination + String destName = memberName; + if (!root.isEmpty() && memberName.startsWith(root)) { + destName = memberName.substring(root.length()); + } + + Path destPath = Paths.get(dest, destName); + + RuntimeScalar isDir = member.get("_isDirectory"); + if (isDir != null && isDir.getBoolean()) { + Files.createDirectories(destPath); + } else { + Path parent = destPath.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + RuntimeScalar contents = member.get("_contents"); + if (contents != null) { + byte[] data = contents.toString().getBytes(StandardCharsets.ISO_8859_1); + Files.write(destPath, data); + } + } + } + + return new RuntimeScalar(AZ_OK).getList(); + + } catch (IOException e) { + return new RuntimeScalar(AZ_IO_ERROR).getList(); + } + } + + /** + * Remove a member from the archive. + * Usage: $removed = $zip->removeMember($member); + */ + public static RuntimeList removeMember(RuntimeArray args, int ctx) { + if (args.size() < 2) { + return scalarUndef.getList(); + } + + RuntimeHash self = args.get(0).hashDeref(); + RuntimeScalar memberArg = args.get(1); + RuntimeArray members = getMembers(self); + + String targetName; + if (RuntimeScalarType.isReference(memberArg)) { + RuntimeHash member = memberArg.hashDeref(); + RuntimeScalar name = member.get("_name"); + targetName = name != null ? name.toString() : ""; + } else { + targetName = memberArg.toString(); + } + + for (int i = 0; i < members.size(); i++) { + RuntimeHash member = members.get(i).hashDeref(); + RuntimeScalar name = member.get("_name"); + if (name != null && name.toString().equals(targetName)) { + RuntimeScalar removed = members.get(i); + // Remove from array + RuntimeArray newMembers = new RuntimeArray(); + for (int j = 0; j < members.size(); j++) { + if (j != i) { + RuntimeArray.push(newMembers, members.get(j)); + } + } + self.put(MEMBERS_KEY, newMembers.createReference()); + return removed.getList(); + } + } + + return scalarUndef.getList(); + } + + // Member accessor methods + + /** + * Get member filename. + */ + public static RuntimeList fileName(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarUndef.getList(); + } + RuntimeHash member = args.get(0).hashDeref(); + RuntimeScalar name = member.get("_name"); + return name != null ? name.getList() : scalarUndef.getList(); + } + + /** + * Get member contents. + */ + public static RuntimeList contents(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarUndef.getList(); + } + RuntimeHash member = args.get(0).hashDeref(); + RuntimeScalar contents = member.get("_contents"); + return contents != null ? contents.getList() : scalarUndef.getList(); + } + + /** + * Check if member is a directory. + */ + public static RuntimeList isDirectory(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarFalse.getList(); + } + RuntimeHash member = args.get(0).hashDeref(); + RuntimeScalar isDir = member.get("_isDirectory"); + return isDir != null ? isDir.getList() : scalarFalse.getList(); + } + + /** + * Get uncompressed size. + */ + public static RuntimeList uncompressedSize(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarZero.getList(); + } + RuntimeHash member = args.get(0).hashDeref(); + RuntimeScalar size = member.get("_uncompressedSize"); + return size != null ? size.getList() : scalarZero.getList(); + } + + /** + * Get compressed size. + */ + public static RuntimeList compressedSize(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarZero.getList(); + } + RuntimeHash member = args.get(0).hashDeref(); + RuntimeScalar size = member.get("_compressedSize"); + return size != null ? size.getList() : scalarZero.getList(); + } + + /** + * Get compression method. + */ + public static RuntimeList compressionMethod(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarZero.getList(); + } + RuntimeHash member = args.get(0).hashDeref(); + RuntimeScalar method = member.get("_compressionMethod"); + return method != null ? method.getList() : scalarZero.getList(); + } + + /** + * Get last modification time. + */ + public static RuntimeList lastModTime(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarZero.getList(); + } + RuntimeHash member = args.get(0).hashDeref(); + RuntimeScalar time = member.get("_lastModTime"); + return time != null ? time.getList() : scalarZero.getList(); + } + + /** + * Get CRC32. + */ + public static RuntimeList crc32(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarZero.getList(); + } + RuntimeHash member = args.get(0).hashDeref(); + RuntimeScalar crc = member.get("_crc32"); + return crc != null ? crc.getList() : scalarZero.getList(); + } + + /** + * Get external filename. + */ + public static RuntimeList externalFileName(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + return scalarUndef.getList(); + } + RuntimeHash member = args.get(0).hashDeref(); + RuntimeScalar name = member.get("_externalFileName"); + return name != null ? name.getList() : scalarUndef.getList(); + } + + // Helper methods + + private static RuntimeArray getMembers(RuntimeHash self) { + RuntimeScalar membersRef = self.get(MEMBERS_KEY); + if (membersRef == null || membersRef.type == RuntimeScalarType.UNDEF) { + RuntimeArray members = new RuntimeArray(); + self.put(MEMBERS_KEY, members.createReference()); + return members; + } + return membersRef.arrayDeref(); + } + + private static RuntimeHash createMemberFromEntry(ZipFile zipFile, ZipEntry entry) throws IOException { + RuntimeHash member = new RuntimeHash(); + member.put("_name", new RuntimeScalar(entry.getName())); + member.put("_externalFileName", new RuntimeScalar("")); + member.put("_isDirectory", entry.isDirectory() ? scalarTrue : scalarFalse); + member.put("_uncompressedSize", new RuntimeScalar(entry.getSize())); + member.put("_compressedSize", new RuntimeScalar(entry.getCompressedSize())); + member.put("_compressionMethod", new RuntimeScalar(entry.getMethod())); + member.put("_lastModTime", new RuntimeScalar(entry.getTime() / 1000)); + member.put("_crc32", new RuntimeScalar(entry.getCrc())); + + // Read contents if not a directory + if (!entry.isDirectory()) { + try (InputStream is = zipFile.getInputStream(entry)) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + String contents = new String(baos.toByteArray(), StandardCharsets.ISO_8859_1); + member.put("_contents", new RuntimeScalar(contents)); + } + } else { + member.put("_contents", new RuntimeScalar("")); + } + + return member; + } + + private static void writeMemberToZip(ZipOutputStream zos, RuntimeHash member) throws IOException { + RuntimeScalar name = member.get("_name"); + if (name == null) return; + + ZipEntry entry = new ZipEntry(name.toString()); + + RuntimeScalar lastModTime = member.get("_lastModTime"); + if (lastModTime != null) { + entry.setTime(lastModTime.getLong() * 1000); + } + + RuntimeScalar isDir = member.get("_isDirectory"); + if (isDir != null && isDir.getBoolean()) { + entry.setMethod(ZipEntry.STORED); + entry.setSize(0); + entry.setCrc(0); + zos.putNextEntry(entry); + } else { + RuntimeScalar contents = member.get("_contents"); + byte[] data = contents != null + ? contents.toString().getBytes(StandardCharsets.ISO_8859_1) + : new byte[0]; + + RuntimeScalar method = member.get("_compressionMethod"); + if (method != null && method.getInt() == COMPRESSION_STORED) { + entry.setMethod(ZipEntry.STORED); + entry.setSize(data.length); + entry.setCrc(computeCRC32(data)); + } else { + entry.setMethod(ZipEntry.DEFLATED); + } + + zos.putNextEntry(entry); + zos.write(data); + } + + zos.closeEntry(); + } + + private static long computeCRC32(byte[] data) { + java.util.zip.CRC32 crc = new java.util.zip.CRC32(); + crc.update(data); + return crc.getValue(); + } +} diff --git a/src/main/perl/lib/Archive/Zip.pm b/src/main/perl/lib/Archive/Zip.pm new file mode 100644 index 000000000..b47474810 --- /dev/null +++ b/src/main/perl/lib/Archive/Zip.pm @@ -0,0 +1,254 @@ +package Archive::Zip; + +use strict; +use warnings; + +our $VERSION = '1.68'; + +# Load Java implementation +use XSLoader; +XSLoader::load('Archive::Zip', $VERSION); + +# Export constants and functions +use Exporter 'import'; +our @EXPORT_OK = qw( + AZ_OK AZ_STREAM_END AZ_ERROR AZ_FORMAT_ERROR AZ_IO_ERROR + COMPRESSION_STORED COMPRESSION_DEFLATED + COMPRESSION_LEVEL_NONE COMPRESSION_LEVEL_DEFAULT + COMPRESSION_LEVEL_FASTEST COMPRESSION_LEVEL_BEST_COMPRESSION +); +our %EXPORT_TAGS = ( + ERROR_CODES => [qw(AZ_OK AZ_STREAM_END AZ_ERROR AZ_FORMAT_ERROR AZ_IO_ERROR)], + CONSTANTS => [qw( + COMPRESSION_STORED COMPRESSION_DEFLATED + COMPRESSION_LEVEL_NONE COMPRESSION_LEVEL_DEFAULT + COMPRESSION_LEVEL_FASTEST COMPRESSION_LEVEL_BEST_COMPRESSION + )], +); + +# For Archive::Zip::Member methods - inherit from Archive::Zip +# This allows member objects to use the same Java methods +package Archive::Zip::Member; +our @ISA = ('Archive::Zip'); + +1; + +__END__ + +=head1 NAME + +Archive::Zip - Provide an interface to ZIP archive files + +=head1 SYNOPSIS + + use Archive::Zip qw( :ERROR_CODES :CONSTANTS ); + + # Read a zip file + my $zip = Archive::Zip->new(); + my $status = $zip->read('archive.zip'); + die 'read error' unless $status == AZ_OK; + + # List members + my @members = $zip->memberNames(); + + # Extract a file + $status = $zip->extractMember('file.txt', 'output.txt'); + + # Create a new zip + my $zip = Archive::Zip->new(); + $zip->addFile('file.txt'); + $zip->addString('hello', 'hello.txt'); + $zip->writeToFileNamed('output.zip'); + +=head1 DESCRIPTION + +This is a port of the CPAN Archive::Zip module for PerlOnJava. + +It provides an interface to ZIP archive files using Java's built-in +java.util.zip package. + +=head1 METHODS + +=head2 Archive Methods + +=over 4 + +=item new( [$filename] ) + +Create a new Archive::Zip object. If a filename is provided, reads +the zip file. + +=item read( $filename ) + +Read a zip file. Returns AZ_OK on success. + +=item writeToFileNamed( $filename ) + +Write the archive to a file. Returns AZ_OK on success. + +=item writeToFileHandle( $fh ) + +Write the archive to a filehandle. Returns AZ_OK on success. + +=item members() + +Returns a list of Archive::Zip::Member objects. + +=item memberNames() + +Returns a list of member names. + +=item numberOfMembers() + +Returns the number of members. + +=item memberNamed( $name ) + +Returns the member with the given name, or undef. + +=item membersMatching( $regex ) + +Returns members whose names match the regex. + +=item addFile( $filename [, $memberName] ) + +Add a file to the archive. Returns the member object. + +=item addString( $content, $memberName ) + +Add a string as a member. Returns the member object. + +=item addDirectory( $dirname ) + +Add a directory entry. Returns the member object. + +=item extractMember( $memberName [, $destName] ) + +Extract a member to a file. Returns AZ_OK on success. + +=item extractMemberWithoutPaths( $member [, $destDir] ) + +Extract a member without path components. Returns AZ_OK on success. + +=item extractTree( [$root [, $dest]] ) + +Extract all members to a directory. Returns AZ_OK on success. + +=item removeMember( $member ) + +Remove a member from the archive. Returns the removed member. + +=back + +=head2 Member Methods + +=over 4 + +=item fileName() + +Returns the member's file name. + +=item contents() + +Returns the member's contents. + +=item isDirectory() + +Returns true if the member is a directory. + +=item uncompressedSize() + +Returns the uncompressed size. + +=item compressedSize() + +Returns the compressed size. + +=item compressionMethod() + +Returns the compression method (COMPRESSION_STORED or COMPRESSION_DEFLATED). + +=item lastModTime() + +Returns the last modification time (Unix timestamp). + +=item crc32() + +Returns the CRC32 checksum. + +=item externalFileName() + +Returns the external file name (for members added from files). + +=back + +=head1 CONSTANTS + +=head2 Error Codes + +=over 4 + +=item AZ_OK (0) + +Success. + +=item AZ_STREAM_END (1) + +End of stream. + +=item AZ_ERROR (2) + +Generic error. + +=item AZ_FORMAT_ERROR (3) + +Format error. + +=item AZ_IO_ERROR (4) + +I/O error. + +=back + +=head2 Compression Methods + +=over 4 + +=item COMPRESSION_STORED (0) + +No compression. + +=item COMPRESSION_DEFLATED (8) + +Deflate compression. + +=back + +=head2 Compression Levels + +=over 4 + +=item COMPRESSION_LEVEL_NONE (0) + +=item COMPRESSION_LEVEL_DEFAULT (-1) + +=item COMPRESSION_LEVEL_FASTEST (1) + +=item COMPRESSION_LEVEL_BEST_COMPRESSION (9) + +=back + +=head1 AUTHOR + +Original Author: Ned Konz, perl@bike-hierarchical.com + +This is a port for PerlOnJava using Java's java.util.zip package. + +=head1 COPYRIGHT AND LICENSE + +Original Archive::Zip Copyright (c) 2000-2017, Various Authors. + +This module is free software; you may distribute it under the same terms +as Perl itself. + +=cut