zipc

Typed, in-memory ZIP archive library for MoonBit (deflate via moonbit-community/flate)

zip
archive
compression
deflate
moon add moonbit-community/zipc@0.2.2
Download zip
Version
0.2.2
License
Apache-2.0
Last updated
2 months ago
Downloads
268

Dependencies

README

#zipc

A typed, in-memory ZIP archive library for MoonBit, ported in spirit from the OCaml zipc library.

Its focus is the archive model, not the codec: a value-oriented Archive / Member / File API that preserves per-entry Unix mode and modification time, produces deterministic (byte-stable) output, and verifies CRC-32 on every decode. The DEFLATE compression itself is delegated to moonbit-community/flate — a pure-MoonBit, io-free DEFLATE engine with real LZ77 and fixed/dynamic Huffman blocks.

#Scope

In scope

  • A pure, in-memory ZIP archive value type: Archive / Member / File with add / find / mem / remove / each / to_array.
  • Per-entry Unix mode and mtime preservation.
  • Deterministic serialization (members written in path order).
  • Stored and Deflate methods, produced and extracted natively, with CRC-32 verified on decode.
  • Byte-native throughout (Bytes in, Bytes out) — no lossy String round-trips.

Non-goals (use a dedicated library if you need these)

  • ZIP64 — archives are limited to 65535 members and ~2 GiB per entry (ZIP32). For larger archives, see hustcer/fzip.
  • Encryption, multi-part / split archives, and streaming (the whole archive is held in memory for encode/decode).
  • Tunable compression levels — the backend compresses at a single fast level. Choose per file between File::stored_from_bytes (no compression) and File::deflate_from_bytes (compressed).
  • Being a codec library. No standalone DEFLATE/zlib/gzip surface is exposed; compression is an internal detail of reading and writing entries. For raw DEFLATE, zlib, gzip, streaming or dictionaries, use moonbit-community/flate, moonbit-community/flate/zlib, moonbit-community/flate/gzip directly.

Entries that use methods other than Stored/Deflate round-trip as opaque Compression::Other payloads (they are preserved but not decoded).

#Archives — moonbit-community/zipc

Build an archive of members (files and directories), serialise it to ZIP bytes, and parse it back. Unix mode and modification time are preserved.

///|
test {
// build
let archive = @zipc.Archive::empty()
archive.add(
@zipc.Member::make(
"readme.txt",
File(@zipc.File::stored_from_bytes(b"hello")),
),
)
|> ignore
archive.add(
@zipc.Member::make(
"src/data.bin",
File(
@zipc.File::deflate_from_bytes(b"compress me, compress me, compress me"),
),
mode=0o644,
),
)
|> ignore
let bytes = archive.to_bytes()

// parse and extract
let parsed = @zipc.Archive::from_bytes(bytes)
guard parsed.find("readme.txt").unwrap().kind() is File(f)
assert_eq(f.to_bytes(), b"hello")
}

File::deflate_from_bytes automatically falls back to Stored when DEFLATE would not shrink the data. File::to_bytes verifies the entry's CRC-32 and size.

#Compared to other MoonBit options

  • moonbit-community/flate (with its gzip / zlib wrappers) is a codec only — no ZIP archive layer. This library builds on it rather than competing with it.
  • moonbitlang/async/gzip is a gzip stream transform for HTTP content-encoding; it is not a ZIP archive tool.
  • hustcer/fzip (an fflate port) is the broader toolkit: more formats, streaming, ZIP64 reading, more mature. If you just want zip_sync(files) -> bytes, prefer it.

What this library offers that the others don't is the typed, deterministic, metadata-preserving archive model — the right choice when you need reproducible archives or faithful filesystem round-tripping (permissions and timestamps), rather than just packing a bag of bytes.

#
ZipError

pub suberror ZipError {
ZipError(String)
}

impl Show for ZipError

#
Archive

pub struct Archive {
members : Map[String, Member]
}

An in-memory ZIP archive: a set of members keyed by path.

#
Archive::add

fn Archive::add(self : Archive, mem : Member) -> Archive

Add (or replace) a member, keyed by its (already normalised) path. Mutates and returns the archive for chaining.

#
Archive::each

fn Archive::each(self : Archive, f : (Member) -> Unit) -> Unit

Apply f to each member, in path order.

#
Archive::empty

fn Archive::empty() -> Archive

#
Archive::find

fn Archive::find(self : Archive, path : String) -> Member?

Look up the member at path.

#
Archive::from_bytes

fn Archive::from_bytes(data : Bytes) -> Archive raise ZipError

Parse ZIP bytes into an archive. Raises ZipError on malformed input.

#
Archive::is_empty

fn Archive::is_empty(self : Archive) -> Bool

#
Archive::mem

fn Archive::mem(self : Archive, path : String) -> Bool

Whether a member with path exists.

#
Archive::member_count

fn Archive::member_count(self : Archive) -> Int

#
Archive::remove

fn Archive::remove(self : Archive, path : String) -> Archive

Remove the member at path, if present.

#
Archive::to_array

fn Archive::to_array(self : Archive) -> Array[Member]

The members, sorted by path (so serialisation is deterministic).

#
Archive::to_bytes

fn Archive::to_bytes(self : Archive) -> Bytes raise ZipError

Serialise the archive to ZIP bytes. Members are written in path order, so the output is deterministic. Parse it back with Archive::from_bytes.

Example:

test {
let archive = @zipc.Archive::empty()
archive.add(
@zipc.Member::make("hi.txt", File(@zipc.File::stored_from_bytes(b"hi"))),
)
|> ignore
let bytes = archive.to_bytes()
let parsed = @zipc.Archive::from_bytes(bytes)
guard parsed.find("hi.txt").unwrap().kind() is File(f)
assert_eq(f.to_bytes(), b"hi")
}

#
Compression

pub enum Compression {
Stored
Deflate
Other(Int)
} derive(Eq)

impl Show for Compression

#
File

pub struct File {
compression : Compression
data : Bytes
decompressed_size : Int
decompressed_crc32 : Int
}

A file's data as it is stored in the archive.

data is the on-disk payload: the raw bytes for Stored, or a raw DEFLATE stream for Deflate. decompressed_size and decompressed_crc32 describe the original content.

#
File::can_extract

fn File::can_extract(self : File) -> Bool

Whether this file can be decompressed natively (Stored or Deflate).

#
File::compressed_size

fn File::compressed_size(self : File) -> Int

Size of the stored (possibly compressed) payload, in bytes.

#
File::compression

fn File::compression(self : File) -> Compression

#
File::decompressed_crc32

fn File::decompressed_crc32(self : File) -> Int

#
File::decompressed_size

fn File::decompressed_size(self : File) -> Int

#
File::deflate_from_bytes

fn File::deflate_from_bytes(data : Bytes) -> File

Create a Deflate-compressed file from data. If compression does not shrink the data (e.g. already-compressed or tiny inputs), the file is stored uncompressed instead.

Example:

test {
let file = @zipc.File::deflate_from_bytes(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaa")
inspect(file.compression(), content="Deflate")
assert_eq(file.to_bytes(), b"aaaaaaaaaaaaaaaaaaaaaaaaaaaa")
}

#
File::stored_from_bytes

fn File::stored_from_bytes(data : Bytes) -> File

Create a Stored (uncompressed) file from data.

#
File::to_bytes

fn File::to_bytes(self : File) -> Bytes raise ZipError

Decompress the file back to its original bytes, verifying the CRC-32. Raises ZipError for unsupported methods or on a CRC / size mismatch.

#
Member

pub struct Member {
path : String
mode : Int
mtime : Int
kind : MemberKind
}

An archive member: a path plus its kind, Unix mode and modification time (Unix seconds).

#
Member::is_dir

fn Member::is_dir(self : Member) -> Bool

Whether this member is a directory.

#
Member::kind

fn Member::kind(self : Member) -> MemberKind

#
Member::make

fn Member::make(path : String, kind : MemberKind, mode? : Int, mtime? : Int) -> Member raise ZipError

Build a member. mode defaults to 0o755 for directories and 0o644 for files; mtime defaults to the DOS epoch. Raises ZipError if the path is too long.

#
Member::mode

fn Member::mode(self : Member) -> Int

#
Member::mtime

fn Member::mtime(self : Member) -> Int

#
Member::path

fn Member::path(self : Member) -> String

#
MemberKind

pub(all) enum MemberKind {
Dir
File(File)
}

#
bytes_has_magic

fn bytes_has_magic(data : Bytes) -> Bool

Whether data starts with a ZIP local-file-header or empty-archive EOCD signature.

#
dos_epoch

let dos_epoch : Int

Unix timestamp of the DOS epoch (1980-01-01 00:00:00 UTC), the earliest time representable in a ZIP entry.