///|
/// The version this build reports.
///
/// The banner has gone stale twice (0.4.0 while the module was 0.4.2, then
/// 0.4.2 while it was 0.4.4) because the string is written by hand in a second
/// place. It is named here so `version_test.mbt` can check it and so a version
/// bump has one obvious place to change in this package.
/// `scripts/self_audit.py` checks this constant against `moon.mod`.
const CLI_VERSION : String = "0.4.6"
///|
fn usage() -> Unit {
println("MoonXZ \{CLI_VERSION}")
println("usage:")
println(" moonxz demo")
println(" moonxz compress [none|crc32|crc64|sha256]")
println(" moonxz decompress ")
println(" moonxz roundtrip ")
println(" moonxz lzma-decompress ")
println(" moonxz lzma-roundtrip ")
println(
" moonxz compress-file [none|crc32|crc64|sha256] [none|x86|delta]",
)
println(" moonxz decompress-file ")
println(" moonxz lzma-compress-file ")
println(" moonxz lzma-decompress-file ")
}
///|
fn run_demo() -> Unit raise {
let input = Bytes::repeat(b"MoonXZ compressed LZMA2 stream demo.\n", 200)
let encoded = @moonxz.compress(input)
let decoded = @moonxz.decompress(encoded)
println(
"input_bytes=\{input.length()} xz_bytes=\{encoded.length()} percent=\{encoded.length() * 100 / input.length()}",
)
println("roundtrip=\{decoded == input}")
println("xz_hex=\{@hex.encode(encoded)}")
}
///|
fn parse_check(name : String) -> @moonxz.CheckKind? {
match name {
"none" => Some(@moonxz.CheckKind::None)
"crc32" => Some(@moonxz.CheckKind::Crc32)
"crc64" => Some(@moonxz.CheckKind::Crc64)
"sha256" => Some(@moonxz.CheckKind::Sha256)
_ => None
}
}
///|
/// Parses a filter name. Only the filters MoonXZ implements are accepted; the
/// rest are reported as unknown so the CLI never implies support it does not have.
fn parse_filter(name : String) -> @moonxz.FilterMode? {
match name {
"none" => Some(@moonxz.FilterMode::None)
"x86" => Some(@moonxz.FilterMode::X86)
"delta" => Some(@moonxz.FilterMode::Delta(1))
_ => None
}
}
///|
fn run_compress(hex : String, check_name : String) -> Unit raise {
let input = @hex.decode(hex)
let check_kind = match parse_check(check_name) {
Some(value) => value
None => raise Failure::Failure("unknown check type: \{check_name}")
}
let encoded = @moonxz.compress_with_check(input, check_kind)
println(@hex.encode(encoded))
}
///|
fn run_decompress(hex : String) -> Unit raise {
let input = @hex.decode(hex)
let decoded = @moonxz.decompress(input)
println(@hex.encode(decoded))
}
///|
fn run_roundtrip(hex : String) -> Unit raise {
let input = @hex.decode(hex)
let decoded = @moonxz.decompress(@moonxz.compress(input))
println(if decoded == input { "true" } else { "false" })
}
///|
fn run_lzma_decompress(hex : String) -> Unit raise {
let input = @hex.decode(hex)
let decoded = @moonxz.decompress_lzma1(input)
println(@hex.encode(decoded))
}
///|
fn run_lzma_roundtrip(hex : String) -> Unit raise {
let input = @hex.decode(hex)
let encoded = @moonxz.compress_lzma1(input)
let decoded = @moonxz.decompress_lzma1(encoded)
println(if decoded == input { "true" } else { "false" })
}
///|
fn run_lzma_compress_file(
input_path : String,
output_path : String,
) -> Unit raise {
let input = @fs.read_file_to_bytes(input_path)
let output = @moonxz.compress_lzma1(input)
@fs.write_bytes_to_file(output_path, output)
println(
"lzma-compressed input_bytes=\{input.length()} output_bytes=\{output.length()}",
)
}
///|
fn run_lzma_decompress_file(
input_path : String,
output_path : String,
) -> Unit raise {
let input = @fs.read_file_to_bytes(input_path)
let decoded = @moonxz.decompress_lzma1(input)
@fs.write_bytes_to_file(output_path, decoded)
println(
"lzma-decompressed input_bytes=\{input.length()} output_bytes=\{decoded.length()}",
)
}
///|
fn run_compress_file(
input_path : String,
output_path : String,
check_name : String,
filter_name : String,
) -> Unit raise {
let check_kind = match parse_check(check_name) {
Some(value) => value
None => raise Failure::Failure("unknown check type: \{check_name}")
}
let filter = match parse_filter(filter_name) {
Some(value) => value
None => raise Failure::Failure("unknown filter: \{filter_name}")
}
let input = @fs.read_file_to_bytes(input_path)
let options = @moonxz.CompressionOptions::new(filter~)
let writer = @moonxz.XzWriter::new(check_kind~, options~)
let output = writer.write(input) + writer.finish()
@fs.write_bytes_to_file(output_path, output)
println(
"compressed input_bytes=\{input.length()} output_bytes=\{output.length()}",
)
}
///|
/// Decompresses a file using the chunk-fed decoder.
///
/// `moonbitlang/x/fs` exposes only whole-file reads and writes, so this cannot
/// write each block straight to disk: doing so would need an append primitive
/// the dependency does not have, and rewriting the file per block would be
/// quadratic. What the streaming decoder does buy is that the parser no longer
/// needs the entire stream up front, and decoded blocks are appended into one
/// buffer instead of being staged as a per-block array first - so there is a
/// single growing destination rather than many intermediate ones.
///
/// Measured peak memory is reported by `scripts/memcheck.py`, which is how the
/// remaining gap (output is still buffered before the single write) is kept
/// visible rather than assumed away.
fn run_decompress_file(input_path : String, output_path : String) -> Unit raise {
let input = @fs.read_file_to_bytes(input_path)
let decoder = @moonxz.XzStreamDecoder::new()
// Feeding in slices keeps the decoder's own window bounded rather than
// letting it hold every byte ever supplied.
let feed_chunk = 1 << 16
let output : Array[Byte] = []
let mut offset = 0
while offset < input.length() {
let end = if offset + feed_chunk > input.length() {
input.length()
} else {
offset + feed_chunk
}
decoder.supply(input[offset:end].to_owned())
offset = end
while true {
match decoder.next() {
Some(block) =>
for byte in block {
output.push(byte)
}
None => break
}
}
}
decoder.finish_input()
while true {
match decoder.next() {
Some(block) =>
for byte in block {
output.push(byte)
}
None => break
}
}
let decoded = Bytes::from_array(output)
@fs.write_bytes_to_file(output_path, decoded)
println(
"decompressed input_bytes=\{input.length()} output_bytes=\{decoded.length()}",
)
}
///|
fn main {
let args = @env.args()
if args.length() < 2 {
usage()
return
}
let command = args[1]
try {
match command {
"demo" => run_demo()
"compress" if args.length() == 3 => run_compress(args[2], "crc64")
"compress" if args.length() == 4 => run_compress(args[2], args[3])
"decompress" if args.length() == 3 => run_decompress(args[2])
"roundtrip" if args.length() == 3 => run_roundtrip(args[2])
"lzma-decompress" if args.length() == 3 => run_lzma_decompress(args[2])
"lzma-roundtrip" if args.length() == 3 => run_lzma_roundtrip(args[2])
"lzma-compress-file" if args.length() == 4 =>
run_lzma_compress_file(args[2], args[3])
"lzma-decompress-file" if args.length() == 4 =>
run_lzma_decompress_file(args[2], args[3])
"compress-file" if args.length() == 4 =>
run_compress_file(args[2], args[3], "crc64", "none")
"compress-file" if args.length() == 5 =>
run_compress_file(args[2], args[3], args[4], "none")
"compress-file" if args.length() == 6 =>
run_compress_file(args[2], args[3], args[4], args[5])
"decompress-file" if args.length() == 4 =>
run_decompress_file(args[2], args[3])
_ => usage()
}
} catch {
error => println("error: \{@debug.to_string(error)}")
}
}