// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Build archives and read them back.
///
/// moon run src/examples/archive
///
/// Both containers are directories that are files, and both are written here the
/// same way: hand the writer members, take the bytes it produces, hand them back
/// to a reader that was told nothing about them.
fn members() -> Array[@zip.Entry] {
[
@zip.Entry::new(
"readme.txt",
b"a member of an archive\n",
mtime=1700000000L,
),
@zip.Entry::new("data/table.csv", b"name,size\nreadme,24\n"),
@zip.Entry::new("data/already-gzipped.png", Bytes::make(600, b'\x89')),
]
}
///|
fn main raise {
let entries = members()
let plain = @zip.zip(entries[:])
println(
"zip: \{plain.length()} bytes, \{@zip.names(plain[:]).length()} members",
)
// The same archive in the wide form: sixty-four-bit sizes and offsets, for a
// member or an archive that outgrew the thirty-two-bit fields.
let wide = @zip.zip(entries[:], zip64=true)
println("zip64: \{wide.length()} bytes")
// With a data descriptor: each member's sizes follow its data rather than
// preceding it, which is what a writer that has not finished reading must do.
let streamed = @zip.zip(entries[:], descriptor=true)
println("with descriptors: \{streamed.length()} bytes")
// And encrypted, with the cipher the format called encryption for fifteen
// years. Not a security boundary; see the `zip` documentation.
let locked = @zip.zip(entries[:], password="moonzip")
let opened = @zip.unzip(locked[:], password="moonzip")
println(
"with a password: \{locked.length()} bytes, readable: \{opened[0].data == entries[0].data}",
)
let refused = try {
@zip.unzip(locked[:], password="wrong") |> ignore
"no"
} catch {
_ => "yes"
}
println("the wrong password is refused: \{refused}")
// Reading from the front, which is all a reader receiving an archive can do.
let reader = @zip.Reader::new(plain[:])
let seen : Array[String] = []
while true {
match reader.next() {
None => break
Some(entry) => seen.push(entry.name)
}
}
let walked = StringBuilder()
let mut first_one = true
for name in seen {
if !first_one {
walked.write_string(", ")
}
first_one = false
walked.write_string(name)
}
println("walked from the front: \{walked}")
// 7z is the third: a directory of coders, where the compression method is a
// property of the folder rather than of the archive.
let solid = [
@sevenz.Entry::new(
"readme.txt",
b"a member of an archive\n",
mtime=1700000000L,
),
@sevenz.Entry::new("data/table.csv", b"name,size\nreadme,24\n"),
@sevenz.Entry::directory("empty"),
]
for how in [@sevenz.Method::Copy, @sevenz.Method::Lzma, @sevenz.Method::Lzma2] {
let archive = @sevenz.sevenz(solid[:], how~)
let members_back = @sevenz.un7z(archive[:])
println(
"7z/\{how.label()}: \{archive.length()} bytes, \{members_back.length()} members, matches: \{members_back[0].data == solid[0].data}",
)
}
// The same archive written a member at a time. The bytes each member produced
// leave as it is added; only the directory and the header wait for the end,
// because the header's first field is how much packed data there is.
let writer = @sevenz.Writer::new(how=@sevenz.Method::Lzma)
let body : Array[Byte] = []
for entry in solid {
for b in writer.add(entry) {
body.push(b)
}
}
let directory = writer.finish()
let start = writer.signature()
let assembled : Array[Byte] = []
for b in start {
assembled.push(b)
}
for b in body {
assembled.push(b)
}
for b in directory {
assembled.push(b)
}
let streamed = Bytes::from_array(assembled)
let streaming = @sevenz.Reader::new()
streaming.push(streamed[:])
let read_back = streaming.finish()
println(
"7z streamed: \{streamed.length()} bytes, \{read_back.length()} members, matches: \{read_back[0].data == solid[0].data}",
)
// tar is the other half of the same idea, without the compressed members: one
// header per file, then the bytes. A tarball is that archive through gzip.
let files = [
@tar.Entry::new(
"readme.txt",
b"a member of an archive\n",
mtime=1700000000L,
),
@tar.Entry::new("data/table.csv", b"name,size\nreadme,24\n"),
@tar.Entry::directory("empty"),
@tar.Entry::symlink("latest", "readme.txt"),
]
let tarball = @tar.tar(files[:])
println("tar: \{tarball.length()} bytes")
let gzipped = @tar.targz(files[:])
println("tar.gz: \{gzipped.length()} bytes")
let back = @tar.untargz(gzipped[:])
let listed = StringBuilder()
let mut leading = true
for entry in back {
if !leading {
listed.write_string(", ")
}
leading = false
listed.write_string(entry.name)
}
println("read back: \{listed}")
println("the symlink survived: \{back[3].link}")
}