Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,21 @@
|
||||
## Description
|
||||
|
||||
`compress` is a namespace for (multiple) compression algorithms supported by V.
|
||||
|
||||
At the moment, the following compression algorithms are implemented:
|
||||
|
||||
- `compress.bzip2`
|
||||
- `compress.deflate`
|
||||
- `compress.gzip`
|
||||
- `compress.lz` supporting the following variations:
|
||||
- lz4
|
||||
- lz77
|
||||
- lz78
|
||||
- lzjb
|
||||
- lzma
|
||||
- lzma2
|
||||
- lzss
|
||||
- lzw
|
||||
- `compress.snappy`
|
||||
- `compress.zlib`
|
||||
- `compress.zstd`
|
||||
@@ -0,0 +1,51 @@
|
||||
# compress.bzip2
|
||||
|
||||
Pure V bzip2 encoder/decoder.
|
||||
|
||||
## Features
|
||||
|
||||
- Pure V implementation (no C wrappers)
|
||||
- bzip2 stream compression (`BZh1`..`BZh9`)
|
||||
- bzip2 stream decompression
|
||||
- Block and stream CRC validation
|
||||
- Deterministic output for identical input/params
|
||||
|
||||
## API
|
||||
|
||||
```v
|
||||
import compress.bzip2
|
||||
|
||||
compressed := bzip2.compress('hello'.bytes())!
|
||||
plain := bzip2.decompress(compressed)!
|
||||
assert plain.bytestr() == 'hello'
|
||||
```
|
||||
|
||||
With params:
|
||||
|
||||
```v
|
||||
import compress.bzip2
|
||||
|
||||
data := 'hello'.bytes()
|
||||
compressed := bzip2.compress(data, block_size: 1)!
|
||||
plain := bzip2.decompress(compressed, verify_crc: true)!
|
||||
assert plain.bytestr() == 'hello'
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Randomized legacy bzip2 blocks are intentionally rejected.
|
||||
- API currently works on full byte slices (no streaming interface yet).
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
v test vlib/compress/bzip2/
|
||||
```
|
||||
|
||||
## Proof
|
||||
|
||||
- tested against the official bzip2 C library using a variety of inputs and parameters.
|
||||
- deterministic and produces identical output for identical input and parameters.
|
||||
- includes CRC validation to ensure data integrity during compression and decompression.
|
||||
- rejects randomized legacy bzip2 blocks, ensuring only valid bzip2 streams are processed.
|
||||
- tested with a variety of inputs, including edge cases, to ensure robustness and reliability.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,136 @@
|
||||
module bzip2
|
||||
|
||||
import encoding.hex
|
||||
|
||||
fn must_decode_hex(s string) []u8 {
|
||||
return hex.decode(s) or { panic(err) }
|
||||
}
|
||||
|
||||
fn test_roundtrip_empty_input() {
|
||||
src := []u8{}
|
||||
compressed := compress(src) or { panic(err) }
|
||||
decompressed := decompress(compressed) or { panic(err) }
|
||||
assert decompressed == src
|
||||
}
|
||||
|
||||
fn test_roundtrip_small_text() {
|
||||
src := 'hello world\n'.bytes()
|
||||
compressed := compress(src) or { panic(err) }
|
||||
decompressed := decompress(compressed) or { panic(err) }
|
||||
assert decompressed == src
|
||||
}
|
||||
|
||||
fn test_roundtrip_binary_data() {
|
||||
mut src := []u8{len: 4096}
|
||||
for i in 0 .. src.len {
|
||||
src[i] = u8((i * 17 + 13) & 0xff)
|
||||
}
|
||||
compressed := compress(src, block_size: 1) or { panic(err) }
|
||||
decompressed := decompress(compressed) or { panic(err) }
|
||||
assert decompressed == src
|
||||
}
|
||||
|
||||
fn test_roundtrip_long_runs() {
|
||||
src := 'a'.repeat(2000).bytes()
|
||||
compressed := compress(src) or { panic(err) }
|
||||
decompressed := decompress(compressed) or { panic(err) }
|
||||
assert decompressed == src
|
||||
}
|
||||
|
||||
fn test_decompress_known_python_vector_empty() {
|
||||
bz2 := must_decode_hex('425a683917724538509000000000')
|
||||
plain := decompress(bz2) or { panic(err) }
|
||||
assert plain == []u8{}
|
||||
}
|
||||
|
||||
fn test_decompress_known_python_vector_hello() {
|
||||
bz2 :=
|
||||
must_decode_hex('425a68393141592653594eece83600000251800010400006449080200031064c4101a7a9a580bb9431f8bb9229c28482776741b0')
|
||||
plain := decompress(bz2) or { panic(err) }
|
||||
assert plain.bytestr() == 'hello world\n'
|
||||
}
|
||||
|
||||
fn test_decompress_known_python_vector_text() {
|
||||
bz2 :=
|
||||
must_decode_hex('425a6839314159265359dc01b0d8000002d9800010410120080a00cc20200021a4d3688cd0806800e28a3de49f0b16b10d177245385090dc01b0d8')
|
||||
plain := decompress(bz2) or { panic(err) }
|
||||
assert plain.bytestr() == '1.test\ncopy ©\n'
|
||||
}
|
||||
|
||||
fn test_decompress_known_python_vector_repeated_a() {
|
||||
bz2 :=
|
||||
must_decode_hex('425a6839314159265359ca3d8dfb000000010420000400200021008283177245385090ca3d8dfb')
|
||||
plain := decompress(bz2) or { panic(err) }
|
||||
assert plain == 'a'.repeat(200).bytes()
|
||||
}
|
||||
|
||||
fn test_decompress_rejects_invalid_header() {
|
||||
_ := decompress('not-bzip2'.bytes()) or {
|
||||
assert err.msg().contains('invalid header')
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
|
||||
fn test_decompress_rejects_crc_mismatch() {
|
||||
mut bz2 :=
|
||||
must_decode_hex('425a68393141592653594eece83600000251800010400006449080200031064c4101a7a9a580bb9431f8bb9229c28482776741b0')
|
||||
// Corrupt the stored block CRC field (bytes 10..13 in a single-block stream).
|
||||
bz2[10] ^= 0x01
|
||||
_ := decompress(bz2) or {
|
||||
assert err.msg().contains('crc mismatch')
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
|
||||
fn test_selector_count_limit_boundaries() {
|
||||
below_limit := selector_count_from_symbol_count(900050) or { panic(err) }
|
||||
at_limit := selector_count_from_symbol_count(900100) or { panic(err) }
|
||||
assert below_limit == 18001
|
||||
assert at_limit == 18002
|
||||
|
||||
_ := selector_count_from_symbol_count(900101) or {
|
||||
assert err.msg().contains('invalid selector count')
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
|
||||
fn test_block_output_limit_guard() {
|
||||
ensure_block_output_limit(0, 100000, 100000) or { panic(err) }
|
||||
ensure_block_output_limit(99999, 1, 100000) or { panic(err) }
|
||||
|
||||
ensure_block_output_limit(100000, 1, 100000) or {
|
||||
assert err.msg().contains('block output exceeds declared block size')
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
|
||||
fn test_find_rle1_block_end_for_four_byte_runs() {
|
||||
mut src := []u8{cap: 100000}
|
||||
for i in 0 .. 25000 {
|
||||
b := u8(i & 0xff)
|
||||
for _ in 0 .. 4 {
|
||||
src << b
|
||||
}
|
||||
}
|
||||
end := find_rle1_block_end(src, 0, 100000)
|
||||
assert end == 80000
|
||||
assert rle1_encode(src[0..end]).len == 100000
|
||||
assert rle1_encode(src[0..end + 4]).len > 100000
|
||||
}
|
||||
|
||||
fn test_roundtrip_block_size_1_four_byte_runs() {
|
||||
mut src := []u8{cap: 100000}
|
||||
for i in 0 .. 25000 {
|
||||
b := u8(i & 0xff)
|
||||
for _ in 0 .. 4 {
|
||||
src << b
|
||||
}
|
||||
}
|
||||
compressed := compress(src, block_size: 1) or { panic(err) }
|
||||
decompressed := decompress(compressed) or { panic(err) }
|
||||
assert decompressed == src
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# bzip2 interop checker
|
||||
|
||||
This helper verifies interoperability between:
|
||||
|
||||
- V module: `compress.bzip2`
|
||||
- System CLI: `bzip2`
|
||||
- Python stdlib: `bz2`
|
||||
|
||||
It runs deterministic test vectors and compression levels, compresses each vector with all
|
||||
three producers, then cross-decompresses every produced stream with all three decoders.
|
||||
|
||||
A case passes only if every decompressed output is byte-identical to the original input.
|
||||
|
||||
Current levels are `1`, `6`, and `9` (see `compression_levels` in
|
||||
`vlib/compress/bzip2/interop/bzip2_interop_check.vsh`).
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
v run vlib/compress/bzip2/interop/bzip2_interop_check.vsh
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- `bzip2` available in `PATH`
|
||||
- `python3` with `bz2`
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env -S v
|
||||
|
||||
import compress.bzip2
|
||||
|
||||
const compression_levels = [1, 6, 9]!
|
||||
|
||||
struct TestVector {
|
||||
name string
|
||||
data []u8
|
||||
}
|
||||
|
||||
fn main() {
|
||||
ensure_tools() or {
|
||||
eprintln('SKIP: ${err.msg()}')
|
||||
exit(2)
|
||||
}
|
||||
vectors := make_test_vectors()
|
||||
mut total_checks := 0
|
||||
mut total_runs := 0
|
||||
tmp_root := join_path(temp_dir(), 'v_bzip2_interop_${getpid()}')
|
||||
mkdir_all(tmp_root) or { panic(err) }
|
||||
defer {
|
||||
rmdir_all(tmp_root) or {}
|
||||
}
|
||||
for level in compression_levels {
|
||||
for i, vec in vectors {
|
||||
total_checks += run_case(tmp_root, level, i, vec) or {
|
||||
eprintln('FAIL: ${vec.name} (level=${level}): ${err}')
|
||||
exit(1)
|
||||
0
|
||||
}
|
||||
total_runs++
|
||||
println('ok ${total_runs}/${vectors.len * compression_levels.len}: ${vec.name} (level=${level}, ${vec.data.len} bytes)')
|
||||
}
|
||||
}
|
||||
println('PASS: ${vectors.len} vectors x ${compression_levels.len} levels, ${total_checks} cross-checks')
|
||||
}
|
||||
|
||||
fn ensure_tools() ! {
|
||||
must_succeed('bzip2 --help >/dev/null 2>&1', 'system bzip2 command is not available')!
|
||||
must_succeed("python3 -c 'import bz2' >/dev/null 2>&1",
|
||||
'python3 with bz2 module is not available')!
|
||||
}
|
||||
|
||||
fn make_test_vectors() []TestVector {
|
||||
mut vectors := []TestVector{}
|
||||
vectors << TestVector{'empty', []u8{}}
|
||||
vectors << TestVector{'ascii_text', 'The quick brown fox jumps over the lazy dog.\n'.repeat(64).bytes()}
|
||||
vectors << TestVector{'repeated_byte', []u8{len: 10000, init: `A`}}
|
||||
vectors << TestVector{'all_bytes_x4', all_bytes_repeated(4)}
|
||||
vectors << TestVector{'lcg_64k', lcg_bytes(65536)}
|
||||
return vectors
|
||||
}
|
||||
|
||||
fn run_case(tmp_root string, level int, case_idx int, vec TestVector) !int {
|
||||
case_dir := join_path(tmp_root, 'case_l${level}_${case_idx:02}_${vec.name}')
|
||||
mkdir_all(case_dir)!
|
||||
|
||||
v_bz2 := bzip2.compress(vec.data, block_size: level)!
|
||||
cli_bz2 := cli_compress(case_dir, vec.data, level)!
|
||||
py_bz2 := py_compress(case_dir, vec.data, level)!
|
||||
|
||||
mut checks := 0
|
||||
|
||||
producers := {
|
||||
'v': v_bz2
|
||||
'cli': cli_bz2
|
||||
'py': py_bz2
|
||||
}
|
||||
for producer, compressed in producers {
|
||||
v_plain := bzip2.decompress(compressed)!
|
||||
assert_equal_bytes('v.decompress(${producer}.compress, level=${level})', vec.data, v_plain)!
|
||||
checks++
|
||||
|
||||
cli_plain := cli_decompress(case_dir, producer, compressed)!
|
||||
assert_equal_bytes('cli.decompress(${producer}.compress, level=${level})', vec.data,
|
||||
cli_plain)!
|
||||
checks++
|
||||
|
||||
py_plain := py_decompress(case_dir, producer, compressed)!
|
||||
assert_equal_bytes('py.decompress(${producer}.compress, level=${level})', vec.data,
|
||||
py_plain)!
|
||||
checks++
|
||||
}
|
||||
return checks
|
||||
}
|
||||
|
||||
fn cli_compress(case_dir string, plain []u8, level int) ![]u8 {
|
||||
in_path := join_path(case_dir, 'plain.in')
|
||||
out_path := join_path(case_dir, 'cli_l${level}.bz2')
|
||||
write_file_array(in_path, plain)!
|
||||
must_succeed('bzip2 -${level} -c -- ${shell_quote(in_path)} > ${shell_quote(out_path)}',
|
||||
'bzip2 compression failed')!
|
||||
return read_bytes(out_path)!
|
||||
}
|
||||
|
||||
fn cli_decompress(case_dir string, producer string, compressed []u8) ![]u8 {
|
||||
in_path := join_path(case_dir, '${producer}.for_cli.bz2')
|
||||
out_path := join_path(case_dir, '${producer}.from_cli.out')
|
||||
write_file_array(in_path, compressed)!
|
||||
must_succeed('bzip2 -d -c -- ${shell_quote(in_path)} > ${shell_quote(out_path)}',
|
||||
'bzip2 decompression failed')!
|
||||
return read_bytes(out_path)!
|
||||
}
|
||||
|
||||
fn py_compress(case_dir string, plain []u8, level int) ![]u8 {
|
||||
in_path := join_path(case_dir, 'plain_py.in')
|
||||
out_path := join_path(case_dir, 'py_l${level}.bz2')
|
||||
write_file_array(in_path, plain)!
|
||||
py_code := 'import bz2, pathlib, sys; p=pathlib.Path(sys.argv[1]); o=pathlib.Path(sys.argv[2]); l=int(sys.argv[3]); o.write_bytes(bz2.compress(p.read_bytes(), compresslevel=l))'
|
||||
must_succeed('python3 -c ${shell_quote(py_code)} ${shell_quote(in_path)} ${shell_quote(out_path)} ${level}',
|
||||
'python bz2 compression failed')!
|
||||
return read_bytes(out_path)!
|
||||
}
|
||||
|
||||
fn py_decompress(case_dir string, producer string, compressed []u8) ![]u8 {
|
||||
in_path := join_path(case_dir, '${producer}.for_py.bz2')
|
||||
out_path := join_path(case_dir, '${producer}.from_py.out')
|
||||
write_file_array(in_path, compressed)!
|
||||
py_code := 'import bz2, pathlib, sys; p=pathlib.Path(sys.argv[1]); o=pathlib.Path(sys.argv[2]); o.write_bytes(bz2.decompress(p.read_bytes()))'
|
||||
must_succeed('python3 -c ${shell_quote(py_code)} ${shell_quote(in_path)} ${shell_quote(out_path)}',
|
||||
'python bz2 decompression failed')!
|
||||
return read_bytes(out_path)!
|
||||
}
|
||||
|
||||
fn all_bytes_repeated(times int) []u8 {
|
||||
mut out := []u8{cap: 256 * times}
|
||||
for _ in 0 .. times {
|
||||
for i in 0 .. 256 {
|
||||
out << u8(i)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fn lcg_bytes(n int) []u8 {
|
||||
mut out := []u8{len: n}
|
||||
mut x := u32(0x12345678)
|
||||
for i in 0 .. n {
|
||||
x = x * u32(1664525) + u32(1013904223)
|
||||
out[i] = u8((x >> 16) & u32(0xff))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fn assert_equal_bytes(label string, expected []u8, got []u8) ! {
|
||||
if expected.len != got.len {
|
||||
return error('${label}: length mismatch expected=${expected.len} got=${got.len}')
|
||||
}
|
||||
for i in 0 .. expected.len {
|
||||
if expected[i] != got[i] {
|
||||
return error('${label}: byte mismatch at offset ${i}')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn must_succeed(command string, context string) ! {
|
||||
res := execute(command)
|
||||
if res.exit_code != 0 {
|
||||
return error('${context}\ncommand: ${command}\nexit_code: ${res.exit_code}\n${res.output}')
|
||||
}
|
||||
}
|
||||
|
||||
fn shell_quote(s string) string {
|
||||
return "'${s.replace("'", "'\\''")}'"
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
@[deprecated: 'use compress.deflate instead']
|
||||
@[deprecated_after: '2026-07-31']
|
||||
module compress
|
||||
|
||||
#flag -I @VEXEROOT/thirdparty/zip
|
||||
#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES
|
||||
#include "miniz.h"
|
||||
|
||||
@[deprecated: 'use max_i32 (from builtin) instead']
|
||||
@[deprecated_after: '2026-07-31']
|
||||
pub const max_size = u64(1 << 31)
|
||||
|
||||
fn C.tdefl_compress_mem_to_heap(source_buf voidptr, source_buf_len usize, out_len &usize, flags i32) voidptr
|
||||
fn C.tinfl_decompress_mem_to_heap(source_buf voidptr, source_buf_len usize, out_len &usize, flags i32) voidptr
|
||||
|
||||
// compresses an array of bytes based on providing flags and returns the compressed bytes in a new array
|
||||
// NB: this is a low level api, a high level implementation like zlib/gzip should be preferred
|
||||
@[deprecated: 'use compress.deflate.compress instead']
|
||||
@[deprecated_after: '2026-07-31']
|
||||
@[manualfree]
|
||||
pub fn compress(data []u8, flags int) ![]u8 {
|
||||
if u64(data.len) > max_size {
|
||||
return error('data too large (${data.len} > ${max_size})')
|
||||
}
|
||||
mut out_len := usize(0)
|
||||
|
||||
address := C.tdefl_compress_mem_to_heap(data.data, data.len, &out_len, flags)
|
||||
if address == 0 {
|
||||
return error('compression failed')
|
||||
}
|
||||
if u64(out_len) > max_size {
|
||||
return error('compressed data is too large (${out_len} > ${max_size})')
|
||||
}
|
||||
unsafe {
|
||||
ret := address.vbytes(int(out_len)).clone()
|
||||
C.free(address)
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
// decompresses an array of bytes based on providing flags and returns the decompressed bytes in a new array
|
||||
// NB: this is a low level api, a high level implementation like zlib/gzip should be preferred
|
||||
@[deprecated: 'use compress.deflate.decompress instead']
|
||||
@[deprecated_after: '2026-07-31']
|
||||
@[manualfree]
|
||||
pub fn decompress(data []u8, flags int) ![]u8 {
|
||||
mut out_len := usize(0)
|
||||
|
||||
address := C.tinfl_decompress_mem_to_heap(data.data, data.len, &out_len, flags)
|
||||
if address == 0 {
|
||||
return error('decompression failed')
|
||||
}
|
||||
if u64(out_len) > max_size {
|
||||
return error('decompressed data is too large (${out_len} > ${max_size})')
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ret := address.vbytes(int(out_len)).clone()
|
||||
C.free(address)
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
// ChunkCallback is used to receive decompressed chunks of maximum 32768 bytes.
|
||||
// After processing the chunk this function should return the chunk's length to indicate
|
||||
// the decompressor to send more chunks, otherwise the decompression stops.
|
||||
// The userdata parameter comes from the call to decompress_with_callback/4, and can be used
|
||||
// to pass arbitrary data, without having to create a closure.
|
||||
|
||||
@[deprecated: 'use compress.deflate.ChunkCallback instead']
|
||||
@[deprecated_after: '2026-07-31']
|
||||
pub type ChunkCallback = fn (chunk []u8, userdata voidptr) int
|
||||
|
||||
// decompress_with_callback decompresses an array of bytes, based on the provided flags, and a V fn callback to receive decompressed chunks, of at most 32 kilobytes each.
|
||||
// It returns the total decompressed length, or a decompression error.
|
||||
// NB: this is a low level api, a high level implementation like zlib/gzip should be preferred.
|
||||
@[deprecated: 'use compress.deflate.decompress_with_callback instead']
|
||||
@[deprecated_after: '2026-07-31']
|
||||
pub fn decompress_with_callback(data []u8, cb ChunkCallback, userdata voidptr, flags int) !u64 {
|
||||
cbdata := DecompressionCallBackData{
|
||||
data: data.data
|
||||
size: usize(data.len)
|
||||
cb: cb
|
||||
userdata: userdata
|
||||
}
|
||||
status := C.tinfl_decompress_mem_to_callback(cbdata.data, &cbdata.size,
|
||||
c_cb_for_decompress_mem, &cbdata, flags)
|
||||
if status == 0 {
|
||||
return error('decompression error')
|
||||
}
|
||||
return cbdata.decompressed_size
|
||||
}
|
||||
|
||||
struct DecompressionCallBackData {
|
||||
mut:
|
||||
data voidptr
|
||||
size usize
|
||||
decompressed_size u64
|
||||
userdata voidptr
|
||||
cb ChunkCallback = unsafe { nil }
|
||||
}
|
||||
|
||||
fn c_cb_for_decompress_mem(buf &char, len int, pdcbd voidptr) int {
|
||||
mut cbdata := unsafe { &DecompressionCallBackData(pdcbd) }
|
||||
if cbdata.cb(unsafe { voidptr(buf).vbytes(len) }, cbdata.userdata) == len {
|
||||
cbdata.decompressed_size += u64(len)
|
||||
return 1 // continue decompressing
|
||||
}
|
||||
return 0 // stop decompressing
|
||||
}
|
||||
|
||||
type DecompressCallback = fn (const_buffer voidptr, len int, userdata voidptr) int
|
||||
|
||||
fn C.tinfl_decompress_mem_to_callback(const_input_buffer voidptr, psize &usize, put_buf_cb DecompressCallback, userdata voidptr, flags i32) i32
|
||||
@@ -0,0 +1,37 @@
|
||||
## Description
|
||||
|
||||
`compress.deflate` is a pure V RFC-compliant DEFLATE module.
|
||||
|
||||
Compression output format is selected by `CompressFormat` via
|
||||
`compress(data, format: ...)`:
|
||||
|
||||
- `.zlib` (RFC 1950 wrapper)
|
||||
- `.gzip` (RFC 1952 wrapper)
|
||||
- `.raw_deflate` (RFC 1951 raw stream)
|
||||
|
||||
`compress` keeps default zlib behavior, and `decompress` auto-detects all three.
|
||||
|
||||
## Interop Validation
|
||||
|
||||
Cross-validation with C/zlib is kept separate from `v test` and can be run manually:
|
||||
|
||||
```bash
|
||||
./vnew run vlib/compress/deflate/interop/deflate_interop.v
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```v
|
||||
import compress.deflate
|
||||
|
||||
fn main() {
|
||||
uncompressed := 'Hello world!'
|
||||
zlib_stream := deflate.compress(uncompressed.bytes())!
|
||||
gzip_stream := deflate.compress(uncompressed.bytes(), format: .gzip)!
|
||||
raw_stream := deflate.compress(uncompressed.bytes(), format: .raw_deflate)!
|
||||
assert deflate.decompress(zlib_stream)! == uncompressed.bytes()
|
||||
assert deflate.decompress(gzip_stream)! == uncompressed.bytes()
|
||||
decompressed := deflate.decompress(raw_stream)!
|
||||
assert decompressed == uncompressed.bytes()
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,291 @@
|
||||
module deflate
|
||||
|
||||
import encoding.binary
|
||||
import hash.adler32
|
||||
import hash.crc32
|
||||
|
||||
pub type ChunkCallback = fn (chunk []u8, userdata voidptr) int
|
||||
|
||||
// CompressFormat selects the output container around the RFC 1951 payload.
|
||||
pub enum CompressFormat {
|
||||
zlib
|
||||
gzip
|
||||
raw_deflate
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct CompressParams {
|
||||
pub:
|
||||
format CompressFormat = .zlib
|
||||
}
|
||||
|
||||
pub struct RawInflateResult {
|
||||
pub:
|
||||
decoded []u8
|
||||
consumed int
|
||||
}
|
||||
|
||||
pub struct ZlibHeader {
|
||||
pub:
|
||||
payload_start int = 2
|
||||
}
|
||||
|
||||
pub struct GzipHeader {
|
||||
pub mut:
|
||||
flags u8
|
||||
payload_start int
|
||||
extra []u8
|
||||
filename []u8
|
||||
comment []u8
|
||||
modification_time u32
|
||||
operating_system u8
|
||||
}
|
||||
|
||||
// validate_zlib_header validates a RFC 1950 zlib header.
|
||||
@[direct_array_access]
|
||||
pub fn validate_zlib_header(data []u8) !ZlibHeader {
|
||||
if data.len < 6 {
|
||||
return error('invalid zlib stream: too short')
|
||||
}
|
||||
if data[0] & 0x0f != 8 {
|
||||
return error('invalid zlib stream: unsupported compression method')
|
||||
}
|
||||
if (u32(data[0]) * 256 + u32(data[1])) % 31 != 0 {
|
||||
return error('invalid zlib stream: bad header checksum')
|
||||
}
|
||||
if data[1] & 0x20 != 0 {
|
||||
return error('invalid zlib stream: preset dictionary not supported')
|
||||
}
|
||||
return ZlibHeader{}
|
||||
}
|
||||
|
||||
// validate_gzip_header validates a RFC 1952 gzip header and returns parsed fields.
|
||||
@[direct_array_access]
|
||||
pub fn validate_gzip_header(data []u8) !GzipHeader {
|
||||
if data.len < 18 {
|
||||
return error('invalid gzip stream: too short')
|
||||
}
|
||||
if data[0] != 0x1f || data[1] != 0x8b {
|
||||
return error('invalid gzip stream: bad magic')
|
||||
}
|
||||
if data[2] != 8 {
|
||||
return error('invalid gzip stream: unsupported compression method')
|
||||
}
|
||||
flg := data[3]
|
||||
if flg & 0xe0 != 0 {
|
||||
return error('invalid gzip stream: reserved flags set')
|
||||
}
|
||||
mut header := GzipHeader{
|
||||
flags: flg
|
||||
payload_start: 10
|
||||
modification_time: binary.little_endian_u32_at(data, 4)
|
||||
operating_system: data[9]
|
||||
}
|
||||
if flg & 0x04 != 0 {
|
||||
if header.payload_start + 2 > data.len {
|
||||
return error('invalid gzip stream: truncated extra')
|
||||
}
|
||||
xlen := int(u32(data[header.payload_start]) | u32(data[header.payload_start + 1]) << 8)
|
||||
header.payload_start += 2
|
||||
if header.payload_start + xlen > data.len {
|
||||
return error('invalid gzip stream: truncated extra')
|
||||
}
|
||||
header.extra = data[header.payload_start..header.payload_start + xlen]
|
||||
header.payload_start += xlen
|
||||
}
|
||||
if flg & 0x08 != 0 {
|
||||
for header.payload_start < data.len && data[header.payload_start] != 0 {
|
||||
header.filename << data[header.payload_start]
|
||||
header.payload_start++
|
||||
}
|
||||
header.payload_start++
|
||||
}
|
||||
if flg & 0x10 != 0 {
|
||||
for header.payload_start < data.len && data[header.payload_start] != 0 {
|
||||
header.comment << data[header.payload_start]
|
||||
header.payload_start++
|
||||
}
|
||||
header.payload_start++
|
||||
}
|
||||
if flg & 0x02 != 0 {
|
||||
if header.payload_start + 2 > data.len {
|
||||
return error('invalid gzip stream: truncated fhcrc')
|
||||
}
|
||||
expected_crc16 := u16(data[header.payload_start]) | (u16(data[header.payload_start + 1]) << 8)
|
||||
actual_crc16 := u16(crc32.sum(data[..header.payload_start]) & 0xffff)
|
||||
if actual_crc16 != expected_crc16 {
|
||||
return error('invalid gzip stream: header crc16 mismatch')
|
||||
}
|
||||
header.payload_start += 2
|
||||
}
|
||||
if header.payload_start + 8 > data.len {
|
||||
return error('invalid gzip stream: truncated payload')
|
||||
}
|
||||
return header
|
||||
}
|
||||
|
||||
// compress compresses data as zlib, gzip, or raw DEFLATE.
|
||||
pub fn compress(data []u8, format CompressParams) ![]u8 {
|
||||
return match format.format {
|
||||
.zlib { compress_zlib(data) }
|
||||
.gzip { compress_gzip(data) }
|
||||
.raw_deflate { deflate_compress_fixed(data)! }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compress_zlib(data []u8) ![]u8 {
|
||||
payload := deflate_compress_fixed(data)!
|
||||
cksum := adler32.sum(data)
|
||||
mut out := []u8{cap: 2 + payload.len + 4}
|
||||
out << u8(0x78) // CMF: CM=8 deflate, CINFO=7 (32K window)
|
||||
out << u8(0x9c) // FLG: default compression, FCHECK satisfies (CMF*256+FLG)%31==0
|
||||
out << payload
|
||||
out << binary.big_endian_get_u32(cksum)
|
||||
return out
|
||||
}
|
||||
|
||||
// compress_gzip compresses data into a gzip stream (RFC 1952).
|
||||
pub fn compress_gzip(data []u8) ![]u8 {
|
||||
payload := deflate_compress_fixed(data)!
|
||||
mut out := []u8{cap: 10 + payload.len + 8}
|
||||
// 10-byte gzip header: ID1 ID2 CM FLG MTIME(4) XFL OS
|
||||
out << [u8(0x1f), 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff]
|
||||
out << payload
|
||||
out << binary.little_endian_get_u32(crc32.sum(data))
|
||||
out << binary.little_endian_get_u32(u32(data.len))
|
||||
return out
|
||||
}
|
||||
|
||||
// compress_raw compresses data to a raw RFC 1951 DEFLATE stream.
|
||||
pub fn compress_raw(data []u8) ![]u8 {
|
||||
return deflate_compress_fixed(data)!
|
||||
}
|
||||
|
||||
// decompress decompresses a zlib (RFC 1950), gzip (RFC 1952), or raw DEFLATE (RFC 1951) stream.
|
||||
// The format is auto-detected.
|
||||
pub fn decompress(data []u8) ![]u8 {
|
||||
if data.len >= 2 {
|
||||
// gzip magic: 0x1f 0x8b
|
||||
if data[0] == 0x1f && data[1] == 0x8b {
|
||||
return decompress_gzip(data)
|
||||
}
|
||||
// zlib: CM=8 and header checksum passes
|
||||
if data[0] & 0x0f == 8 && (u32(data[0]) * 256 + u32(data[1])) % 31 == 0 {
|
||||
return decompress_zlib(data)
|
||||
}
|
||||
}
|
||||
// raw DEFLATE
|
||||
return inflate(data)
|
||||
}
|
||||
|
||||
// decompress_zlib decompresses a zlib stream (RFC 1950).
|
||||
// It returns the decompressed bytes in a new array.
|
||||
pub fn decompress_zlib(data []u8) ![]u8 {
|
||||
header := validate_zlib_header(data)!
|
||||
payload := data[header.payload_start..data.len - 4]
|
||||
expected := binary.big_endian_u32_at(data, data.len - 4)
|
||||
res := inflate_with_consumed(payload)!
|
||||
if res.consumed != payload.len {
|
||||
return error('invalid zlib stream: trailing data before adler32')
|
||||
}
|
||||
decoded := res.decoded
|
||||
if adler32.sum(decoded) != expected { return error('invalid zlib stream: adler32 mismatch') }
|
||||
return decoded
|
||||
}
|
||||
|
||||
// decompress_gzip decompresses a gzip stream (RFC 1952).
|
||||
// It returns the decompressed bytes in a new array.
|
||||
pub fn decompress_gzip(data []u8) ![]u8 {
|
||||
header := validate_gzip_header(data)!
|
||||
payload := data[header.payload_start..data.len - 8]
|
||||
expected_crc := binary.little_endian_u32_at(data, data.len - 8)
|
||||
expected_size := binary.little_endian_u32_at(data, data.len - 4)
|
||||
res := inflate_with_consumed(payload)!
|
||||
if res.consumed != payload.len {
|
||||
return error('invalid gzip stream: trailing data before trailer')
|
||||
}
|
||||
decoded := res.decoded
|
||||
if crc32.sum(decoded) != expected_crc {
|
||||
return error('invalid gzip stream: crc32 mismatch')
|
||||
}
|
||||
if u32(decoded.len) != expected_size {
|
||||
return error('invalid gzip stream: size mismatch')
|
||||
}
|
||||
return decoded
|
||||
}
|
||||
|
||||
// decompress_raw_with_consumed decompresses raw RFC 1951 DEFLATE data and tracks consumed bytes.
|
||||
pub fn decompress_raw_with_consumed(data []u8) !RawInflateResult {
|
||||
res := inflate_with_consumed(data)!
|
||||
return RawInflateResult{
|
||||
decoded: res.decoded
|
||||
consumed: res.consumed
|
||||
}
|
||||
}
|
||||
|
||||
// decompress_with_callback decompresses a zlib/gzip/raw stream (RFC 1950, RFC 1952) using a callback for chunked delivery.
|
||||
// The callback receives chunks of decompressed data and should return the chunk length to continue, or 0 to abort.
|
||||
// Returns the total decompressed length.
|
||||
pub fn decompress_with_callback(data []u8, cb ChunkCallback, userdata voidptr) !int {
|
||||
if data.len >= 2 {
|
||||
// gzip magic: 0x1f 0x8b
|
||||
if data[0] == 0x1f && data[1] == 0x8b {
|
||||
return decompress_gzip_with_callback(data, cb, userdata)
|
||||
}
|
||||
// zlib: CM=8 and header checksum passes
|
||||
if data[0] & 0x0f == 8 && (u32(data[0]) * 256 + u32(data[1])) % 31 == 0 {
|
||||
return decompress_zlib_with_callback(data, cb, userdata)
|
||||
}
|
||||
}
|
||||
// raw DEFLATE
|
||||
res := inflate_with_callback(data, cb, userdata)!
|
||||
return res.delivered
|
||||
}
|
||||
|
||||
fn decompress_zlib_with_callback(data []u8, cb ChunkCallback, userdata voidptr) !int {
|
||||
header := validate_zlib_header(data)!
|
||||
payload := data[header.payload_start..data.len - 4]
|
||||
expected := binary.big_endian_u32_at(data, data.len - 4)
|
||||
res := inflate_with_callback(payload, cb, userdata)!
|
||||
if res.aborted {
|
||||
return res.delivered
|
||||
}
|
||||
if res.consumed != payload.len {
|
||||
return error('invalid zlib stream: trailing data before adler32')
|
||||
}
|
||||
if adler32.sum(res.decoded) != expected {
|
||||
return error('invalid zlib stream: adler32 mismatch')
|
||||
}
|
||||
return res.delivered
|
||||
}
|
||||
|
||||
fn decompress_gzip_with_callback(data []u8, cb ChunkCallback, userdata voidptr) !int {
|
||||
header := validate_gzip_header(data)!
|
||||
payload := data[header.payload_start..data.len - 8]
|
||||
expected_crc := binary.little_endian_u32_at(data, data.len - 8)
|
||||
expected_size := binary.little_endian_u32_at(data, data.len - 4)
|
||||
res := inflate_with_callback(payload, cb, userdata)!
|
||||
if res.aborted {
|
||||
return res.delivered
|
||||
}
|
||||
if res.consumed != payload.len {
|
||||
return error('invalid gzip stream: trailing data before trailer')
|
||||
}
|
||||
if crc32.sum(res.decoded) != expected_crc {
|
||||
return error('invalid gzip stream: crc32 mismatch')
|
||||
}
|
||||
if u32(res.decoded.len) != expected_size {
|
||||
return error('invalid gzip stream: size mismatch')
|
||||
}
|
||||
return res.delivered
|
||||
}
|
||||
|
||||
fn bit_reverse(v u32, n int) u32 {
|
||||
mut r := u32(0)
|
||||
mut val := v
|
||||
for _ in 0 .. n {
|
||||
r = (r << 1) | (val & 1)
|
||||
val >>= 1
|
||||
}
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
module deflate
|
||||
|
||||
import hash.huffman
|
||||
|
||||
const deflate_hash_bits = 15
|
||||
const deflate_hash_size = 1 << deflate_hash_bits
|
||||
const deflate_max_chain = 64
|
||||
const deflate_min_match = 3
|
||||
const deflate_max_match = 258
|
||||
const deflate_window = 32768
|
||||
|
||||
// fixed_litlen_encode returns (reversed_codes, code_lengths) for fixed Huffman lit/len.
|
||||
// The LSB-first (bit-reversed) codes come straight from the shared canonical
|
||||
// builder, since the encoder writes bits LSB-first.
|
||||
fn fixed_litlen_encode() !([]u32, []int) {
|
||||
lens := fixed_litlen_lengths()
|
||||
t := huffman.build(lengths: lens, max_bits: 9, bit_order: .lsb_first)!
|
||||
return t.codes, lens
|
||||
}
|
||||
|
||||
// fixed_dist_encode returns (reversed_codes, code_lengths) for fixed Huffman distance.
|
||||
fn fixed_dist_encode() ([]u32, []int) {
|
||||
mut codes := []u32{len: 30}
|
||||
for i in 0 .. 30 {
|
||||
codes[i] = bit_reverse(u32(i), 5)
|
||||
}
|
||||
return codes, []int{len: 30, init: 5}
|
||||
}
|
||||
|
||||
fn length_code_info(length int) (int, int, int) {
|
||||
for i := length_bases.len - 1; i >= 0; i-- {
|
||||
if length >= length_bases[i] {
|
||||
return i, length - length_bases[i], length_extra_bits[i]
|
||||
}
|
||||
}
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
fn dist_code_info(distance int) (int, int, int) {
|
||||
for i := dist_bases.len - 1; i >= 0; i-- {
|
||||
if distance >= dist_bases[i] {
|
||||
return i, distance - dist_bases[i], dist_extra_bits[i]
|
||||
}
|
||||
}
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
fn hash3(data []u8, pos int) int {
|
||||
v := u32(data[pos]) | (u32(data[pos + 1]) << 8) | (u32(data[pos + 2]) << 16)
|
||||
return int((v * u32(2654435761)) >> u32(32 - deflate_hash_bits))
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn find_lz_match(data []u8, pos int, last []int, prev []int) (int, int) {
|
||||
if pos + deflate_min_match > data.len {
|
||||
return 0, 0
|
||||
}
|
||||
max_len := if pos + deflate_max_match < data.len { deflate_max_match } else { data.len - pos }
|
||||
mut best_len := 0
|
||||
mut best_off := 0
|
||||
mut i := last[hash3(data, pos)]
|
||||
mut chain := 0
|
||||
for i >= 0 && chain < deflate_max_chain {
|
||||
off := pos - i
|
||||
if off > deflate_window {
|
||||
break
|
||||
}
|
||||
mut l := 0
|
||||
for l < max_len && data[i + l] == data[pos + l] {
|
||||
l++
|
||||
}
|
||||
if l > best_len {
|
||||
best_len = l
|
||||
best_off = off
|
||||
if best_len == max_len {
|
||||
break
|
||||
}
|
||||
}
|
||||
i = prev[i]
|
||||
chain++
|
||||
}
|
||||
return best_off, best_len
|
||||
}
|
||||
|
||||
struct BitWriter {
|
||||
mut:
|
||||
buf []u8
|
||||
bits u32
|
||||
nbits int
|
||||
}
|
||||
|
||||
@[direct_array_access; inline]
|
||||
fn (mut w BitWriter) write_bits(value u32, nbits int) {
|
||||
if nbits == 0 {
|
||||
return
|
||||
}
|
||||
w.bits |= value << w.nbits
|
||||
w.nbits += nbits
|
||||
for w.nbits >= 8 {
|
||||
w.buf << u8(w.bits & 0xff)
|
||||
w.bits >>= 8
|
||||
w.nbits -= 8
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut w BitWriter) flush() {
|
||||
if w.nbits > 0 {
|
||||
w.buf << u8(w.bits & 0xff)
|
||||
w.bits = 0
|
||||
w.nbits = 0
|
||||
}
|
||||
}
|
||||
|
||||
// deflate_compress_fixed compresses data to RFC 1951 DEFLATE using fixed Huffman codes.
|
||||
@[direct_array_access]
|
||||
fn deflate_compress_fixed(data []u8) ![]u8 {
|
||||
ll_codes, ll_lens := fixed_litlen_encode()!
|
||||
d_codes, d_lens := fixed_dist_encode()
|
||||
mut w := BitWriter{}
|
||||
// BFINAL=1, BTYPE=01 (fixed Huffman)
|
||||
w.write_bits(1, 1)
|
||||
w.write_bits(1, 2)
|
||||
if data.len == 0 {
|
||||
w.write_bits(ll_codes[256], ll_lens[256])
|
||||
w.flush()
|
||||
return w.buf
|
||||
}
|
||||
mut last := []int{len: deflate_hash_size, init: -1}
|
||||
mut prev := []int{len: data.len, init: -1}
|
||||
mut pos := 0
|
||||
for pos < data.len {
|
||||
off, match_len := find_lz_match(data, pos, last, prev)
|
||||
if match_len >= deflate_min_match {
|
||||
li, lext, lext_bits := length_code_info(match_len)
|
||||
sym := 257 + li
|
||||
w.write_bits(ll_codes[sym], ll_lens[sym])
|
||||
w.write_bits(u32(lext), lext_bits)
|
||||
di, dext, dext_bits := dist_code_info(off)
|
||||
w.write_bits(d_codes[di], d_lens[di])
|
||||
w.write_bits(u32(dext), dext_bits)
|
||||
for i in pos .. pos + match_len {
|
||||
if i + deflate_min_match < data.len {
|
||||
h := hash3(data, i)
|
||||
prev[i] = last[h]
|
||||
last[h] = i
|
||||
}
|
||||
}
|
||||
pos += match_len
|
||||
} else {
|
||||
w.write_bits(ll_codes[int(data[pos])], ll_lens[int(data[pos])])
|
||||
if pos + deflate_min_match < data.len {
|
||||
h := hash3(data, pos)
|
||||
prev[pos] = last[h]
|
||||
last[h] = pos
|
||||
}
|
||||
pos++
|
||||
}
|
||||
}
|
||||
w.write_bits(ll_codes[256], ll_lens[256])
|
||||
w.flush()
|
||||
return w.buf
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
module deflate
|
||||
|
||||
import hash.huffman
|
||||
|
||||
// vfmt off
|
||||
// RFC 1951 length/distance decode tables
|
||||
const length_bases = [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59,
|
||||
67, 83, 99, 115, 131, 163, 195, 227, 258]
|
||||
const length_extra_bits = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4,
|
||||
4, 5, 5, 5, 5, 0]
|
||||
const dist_bases = [1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769,
|
||||
1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577]
|
||||
const dist_extra_bits = [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10,
|
||||
10, 11, 11, 12, 12, 13, 13]
|
||||
// code-length alphabet order (RFC 1951)
|
||||
const cl_order = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]
|
||||
// vmt on
|
||||
|
||||
// fixed_litlen_lengths returns code lengths for the fixed Huffman lit/len tree (RFC 1951 §3.2.6).
|
||||
fn fixed_litlen_lengths() []int {
|
||||
mut lens := []int{len: 288}
|
||||
for i in 0 .. 144 {
|
||||
lens[i] = 8
|
||||
}
|
||||
for i in 144 .. 256 {
|
||||
lens[i] = 9
|
||||
}
|
||||
for i in 256 .. 280 {
|
||||
lens[i] = 7
|
||||
}
|
||||
for i in 280 .. 288 {
|
||||
lens[i] = 8
|
||||
}
|
||||
return lens
|
||||
}
|
||||
|
||||
// HuffTree is a MSB-first Huffman lookup table for DEFLATE decoding.
|
||||
// Indexed by the next max_bits bits read LSB-first from the stream.
|
||||
struct HuffTree {
|
||||
table []u32 // entry: (symbol << 5) | code_length; 0xFFFF_FFFF = invalid
|
||||
max_bits int
|
||||
}
|
||||
|
||||
fn build_huff_tree(lengths []int) !HuffTree {
|
||||
mut max_bits := 0
|
||||
for l in lengths {
|
||||
if l > max_bits {
|
||||
max_bits = l
|
||||
}
|
||||
}
|
||||
if max_bits == 0 {
|
||||
return HuffTree{
|
||||
table: [u32(0)]
|
||||
max_bits: 0
|
||||
}
|
||||
}
|
||||
// Build the flat LSB-first lookup table the decode loop expects directly
|
||||
// from the lengths via the shared canonical builder: entry = (symbol << 5) |
|
||||
// length, 0xFFFF_FFFF for invalid. flat_table() allocates no intermediate
|
||||
// codes array, so this matches the hand-rolled loop's cost. Over-subscribed
|
||||
// lengths now surface as an error instead of a silently corrupt table.
|
||||
table := huffman.flat_table(lengths: lengths, max_bits: max_bits, bit_order: .lsb_first)!
|
||||
return HuffTree{
|
||||
table: table
|
||||
max_bits: max_bits
|
||||
}
|
||||
}
|
||||
|
||||
struct BitReader {
|
||||
buf []u8
|
||||
mut:
|
||||
pos int
|
||||
bits u32
|
||||
nbits int
|
||||
}
|
||||
|
||||
@[direct_array_access; inline]
|
||||
fn (mut r BitReader) read_bits(n int) !u32 {
|
||||
for r.nbits < n {
|
||||
if r.pos >= r.buf.len {
|
||||
return error('inflate: unexpected end of stream')
|
||||
}
|
||||
r.bits |= u32(r.buf[r.pos]) << r.nbits
|
||||
r.pos++
|
||||
r.nbits += 8
|
||||
}
|
||||
val := r.bits & ((u32(1) << n) - 1)
|
||||
r.bits >>= u32(n)
|
||||
r.nbits -= n
|
||||
return val
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn (mut r BitReader) align_byte() {
|
||||
r.bits = 0
|
||||
r.nbits = 0
|
||||
}
|
||||
|
||||
@[direct_array_access; inline]
|
||||
fn (mut r BitReader) read_byte_raw() !u8 {
|
||||
if r.pos >= r.buf.len {
|
||||
return error('inflate: unexpected end of stream')
|
||||
}
|
||||
b := r.buf[r.pos]
|
||||
r.pos++
|
||||
return b
|
||||
}
|
||||
|
||||
@[direct_array_access; inline]
|
||||
fn (mut r BitReader) huff_decode(t HuffTree) !u32 {
|
||||
for r.nbits < t.max_bits {
|
||||
if r.pos >= r.buf.len {
|
||||
break
|
||||
}
|
||||
r.bits |= u32(r.buf[r.pos]) << r.nbits
|
||||
r.pos++
|
||||
r.nbits += 8
|
||||
}
|
||||
idx := int(r.bits & ((u32(1) << t.max_bits) - 1))
|
||||
entry := t.table[idx]
|
||||
if entry == 0xffff_ffff {
|
||||
return error('inflate: invalid Huffman code')
|
||||
}
|
||||
len_ := int(entry & 0x1f)
|
||||
if r.nbits < len_ {
|
||||
return error('inflate: unexpected end of stream')
|
||||
}
|
||||
sym := entry >> 5
|
||||
r.bits >>= u32(len_)
|
||||
r.nbits -= len_
|
||||
return sym
|
||||
}
|
||||
|
||||
struct InflateResult {
|
||||
decoded []u8
|
||||
consumed int
|
||||
}
|
||||
|
||||
struct InflateStreamResult {
|
||||
decoded []u8
|
||||
consumed int
|
||||
delivered int
|
||||
aborted bool
|
||||
}
|
||||
|
||||
struct InflateStreamState {
|
||||
mut:
|
||||
delivered int
|
||||
}
|
||||
|
||||
const inflate_callback_chunk_size = 32768
|
||||
|
||||
@[direct_array_access; inline]
|
||||
fn flush_stream_chunks(out []u8, cb ChunkCallback, userdata voidptr, mut state InflateStreamState) bool {
|
||||
for out.len - state.delivered >= inflate_callback_chunk_size {
|
||||
end := state.delivered + inflate_callback_chunk_size
|
||||
chunk := out[state.delivered..end]
|
||||
if cb(chunk, userdata) != chunk.len {
|
||||
return false
|
||||
}
|
||||
state.delivered = end
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// inflate_with_consumed decompresses raw RFC 1951 DEFLATE data and reports
|
||||
// how many input bytes were consumed by the DEFLATE bitstream.
|
||||
fn inflate_with_consumed(data []u8) !InflateResult {
|
||||
mut r := BitReader{
|
||||
buf: data
|
||||
}
|
||||
mut out := []u8{}
|
||||
fixed_ll := build_huff_tree(fixed_litlen_lengths())!
|
||||
fixed_d := build_huff_tree([]int{len: 32, init: 5})!
|
||||
for {
|
||||
bfinal := r.read_bits(1)!
|
||||
btype := r.read_bits(2)!
|
||||
match btype {
|
||||
0 {
|
||||
r.align_byte()
|
||||
len_ := int(r.read_byte_raw()!) | (int(u32(r.read_byte_raw()!) << 8))
|
||||
nlen := int(r.read_byte_raw()!) | (int(u32(r.read_byte_raw()!) << 8))
|
||||
if len_ & 0xffff != (~nlen) & 0xffff {
|
||||
return error('inflate: bad stored block length')
|
||||
}
|
||||
for _ in 0 .. len_ {
|
||||
out << r.read_byte_raw()!
|
||||
}
|
||||
}
|
||||
1 {
|
||||
inflate_block(mut r, mut out, fixed_ll, fixed_d)!
|
||||
}
|
||||
2 {
|
||||
hlit := int(r.read_bits(5)!) + 257
|
||||
hdist := int(r.read_bits(5)!) + 1
|
||||
hclen := int(r.read_bits(4)!) + 4
|
||||
mut cl_lens := []int{len: 19}
|
||||
for i in 0 .. hclen {
|
||||
cl_lens[cl_order[i]] = int(r.read_bits(3)!)
|
||||
}
|
||||
cl_tree := build_huff_tree(cl_lens)!
|
||||
mut all_lens := []int{}
|
||||
for all_lens.len < hlit + hdist {
|
||||
sym := r.huff_decode(cl_tree)!
|
||||
if sym <= 15 {
|
||||
all_lens << int(sym)
|
||||
} else if sym == 16 {
|
||||
if all_lens.len == 0 {
|
||||
return error('inflate: repeat with empty history')
|
||||
}
|
||||
rep := int(r.read_bits(2)!) + 3
|
||||
last := all_lens[all_lens.len - 1]
|
||||
for _ in 0 .. rep {
|
||||
all_lens << last
|
||||
}
|
||||
} else if sym == 17 {
|
||||
rep := int(r.read_bits(3)!) + 3
|
||||
for _ in 0 .. rep {
|
||||
all_lens << 0
|
||||
}
|
||||
} else if sym == 18 {
|
||||
rep := int(r.read_bits(7)!) + 11
|
||||
for _ in 0 .. rep {
|
||||
all_lens << 0
|
||||
}
|
||||
} else {
|
||||
return error('inflate: bad code length symbol')
|
||||
}
|
||||
}
|
||||
ll_tree := build_huff_tree(all_lens[..hlit])!
|
||||
d_tree := build_huff_tree(all_lens[hlit..])!
|
||||
inflate_block(mut r, mut out, ll_tree, d_tree)!
|
||||
}
|
||||
else {
|
||||
return error('inflate: reserved block type')
|
||||
}
|
||||
}
|
||||
|
||||
if bfinal == 1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
consumed := r.pos - (r.nbits >> 3)
|
||||
return InflateResult{
|
||||
decoded: out
|
||||
consumed: consumed
|
||||
}
|
||||
}
|
||||
|
||||
// inflate decompresses raw RFC 1951 DEFLATE data.
|
||||
fn inflate(data []u8) ![]u8 {
|
||||
res := inflate_with_consumed(data)!
|
||||
return res.decoded
|
||||
}
|
||||
|
||||
// inflate_with_callback decompresses raw RFC 1951 DEFLATE data and streams output through `cb`.
|
||||
// It still tracks consumed input bytes for container-level validation.
|
||||
fn inflate_with_callback(data []u8, cb ChunkCallback, userdata voidptr) !InflateStreamResult {
|
||||
mut r := BitReader{
|
||||
buf: data
|
||||
}
|
||||
mut out := []u8{}
|
||||
mut state := InflateStreamState{}
|
||||
mut aborted := false
|
||||
fixed_ll := build_huff_tree(fixed_litlen_lengths())!
|
||||
fixed_d := build_huff_tree([]int{len: 32, init: 5})!
|
||||
for {
|
||||
bfinal := r.read_bits(1)!
|
||||
btype := r.read_bits(2)!
|
||||
match btype {
|
||||
0 {
|
||||
r.align_byte()
|
||||
len_ := int(r.read_byte_raw()!) | (int(u32(r.read_byte_raw()!) << 8))
|
||||
nlen := int(r.read_byte_raw()!) | (int(u32(r.read_byte_raw()!) << 8))
|
||||
if len_ & 0xffff != (~nlen) & 0xffff {
|
||||
return error('inflate: bad stored block length')
|
||||
}
|
||||
for _ in 0 .. len_ {
|
||||
out << r.read_byte_raw()!
|
||||
if !flush_stream_chunks(out, cb, userdata, mut state) {
|
||||
aborted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
1 {
|
||||
if !inflate_block_stream(mut r, mut out, fixed_ll, fixed_d, cb, userdata, mut state)! {
|
||||
aborted = true
|
||||
}
|
||||
}
|
||||
2 {
|
||||
hlit := int(r.read_bits(5)!) + 257
|
||||
hdist := int(r.read_bits(5)!) + 1
|
||||
hclen := int(r.read_bits(4)!) + 4
|
||||
mut cl_lens := []int{len: 19}
|
||||
for i in 0 .. hclen {
|
||||
cl_lens[cl_order[i]] = int(r.read_bits(3)!)
|
||||
}
|
||||
cl_tree := build_huff_tree(cl_lens)!
|
||||
mut all_lens := []int{}
|
||||
for all_lens.len < hlit + hdist {
|
||||
sym := r.huff_decode(cl_tree)!
|
||||
if sym <= 15 {
|
||||
all_lens << int(sym)
|
||||
} else if sym == 16 {
|
||||
if all_lens.len == 0 {
|
||||
return error('inflate: repeat with empty history')
|
||||
}
|
||||
rep := int(r.read_bits(2)!) + 3
|
||||
last := all_lens[all_lens.len - 1]
|
||||
for _ in 0 .. rep {
|
||||
all_lens << last
|
||||
}
|
||||
} else if sym == 17 {
|
||||
rep := int(r.read_bits(3)!) + 3
|
||||
for _ in 0 .. rep {
|
||||
all_lens << 0
|
||||
}
|
||||
} else if sym == 18 {
|
||||
rep := int(r.read_bits(7)!) + 11
|
||||
for _ in 0 .. rep {
|
||||
all_lens << 0
|
||||
}
|
||||
} else {
|
||||
return error('inflate: bad code length symbol')
|
||||
}
|
||||
}
|
||||
ll_tree := build_huff_tree(all_lens[..hlit])!
|
||||
d_tree := build_huff_tree(all_lens[hlit..])!
|
||||
if !inflate_block_stream(mut r, mut out, ll_tree, d_tree, cb, userdata, mut state)! {
|
||||
aborted = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
return error('inflate: reserved block type')
|
||||
}
|
||||
}
|
||||
|
||||
if aborted || bfinal == 1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !aborted && out.len > state.delivered {
|
||||
chunk := unsafe { out[state.delivered..] }
|
||||
if cb(chunk, userdata) != chunk.len {
|
||||
aborted = true
|
||||
} else {
|
||||
state.delivered = out.len
|
||||
}
|
||||
}
|
||||
consumed := r.pos - (r.nbits >> 3)
|
||||
return InflateStreamResult{
|
||||
decoded: out
|
||||
consumed: consumed
|
||||
delivered: state.delivered
|
||||
aborted: aborted
|
||||
}
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn inflate_block(mut r BitReader, mut out []u8, ll HuffTree, dist HuffTree) ! {
|
||||
for {
|
||||
sym := r.huff_decode(ll)!
|
||||
if sym == 256 {
|
||||
break
|
||||
}
|
||||
if sym < 256 {
|
||||
out << u8(sym)
|
||||
} else {
|
||||
li := int(sym) - 257
|
||||
if li < 0 || li >= length_bases.len {
|
||||
return error('inflate: invalid length symbol ${sym}')
|
||||
}
|
||||
length := length_bases[li] + int(r.read_bits(length_extra_bits[li])!)
|
||||
dsym := r.huff_decode(dist)!
|
||||
di := int(dsym)
|
||||
if di >= dist_bases.len {
|
||||
return error('inflate: invalid distance symbol ${dsym}')
|
||||
}
|
||||
distance := dist_bases[di] + int(r.read_bits(dist_extra_bits[di])!)
|
||||
if distance > out.len {
|
||||
return error('inflate: distance past output start')
|
||||
}
|
||||
base := out.len - distance
|
||||
for i in 0 .. length {
|
||||
out << out[base + i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn inflate_block_stream(mut r BitReader, mut out []u8, ll HuffTree, dist HuffTree, cb ChunkCallback, userdata voidptr, mut state InflateStreamState) !bool {
|
||||
for {
|
||||
sym := r.huff_decode(ll)!
|
||||
if sym == 256 {
|
||||
break
|
||||
}
|
||||
if sym < 256 {
|
||||
out << u8(sym)
|
||||
if !flush_stream_chunks(out, cb, userdata, mut state) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
li := int(sym) - 257
|
||||
if li < 0 || li >= length_bases.len {
|
||||
return error('inflate: invalid length symbol ${sym}')
|
||||
}
|
||||
length := length_bases[li] + int(r.read_bits(length_extra_bits[li])!)
|
||||
dsym := r.huff_decode(dist)!
|
||||
di := int(dsym)
|
||||
if di >= dist_bases.len {
|
||||
return error('inflate: invalid distance symbol ${dsym}')
|
||||
}
|
||||
distance := dist_bases[di] + int(r.read_bits(dist_extra_bits[di])!)
|
||||
if distance > out.len {
|
||||
return error('inflate: distance past output start')
|
||||
}
|
||||
base := out.len - distance
|
||||
for i in 0 .. length {
|
||||
out << out[base + i]
|
||||
if !flush_stream_chunks(out, cb, userdata, mut state) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
module deflate
|
||||
|
||||
fn test_zlib_roundtrip() {
|
||||
data := 'Hello world!'.bytes()
|
||||
compressed := compress(data)!
|
||||
assert compressed[0] == 0x78 && compressed[1] == 0x9c // zlib header
|
||||
assert decompress(compressed)! == data
|
||||
}
|
||||
|
||||
fn test_gzip_roundtrip() {
|
||||
data := 'Hello gzip!'.repeat(10).bytes()
|
||||
compressed := compress(data, format: .gzip)!
|
||||
assert compressed[0] == 0x1f && compressed[1] == 0x8b // gzip magic
|
||||
assert decompress(compressed)! == data
|
||||
}
|
||||
|
||||
fn test_raw_deflate_roundtrip() {
|
||||
data := 'raw deflate'.repeat(20).bytes()
|
||||
raw := compress(data, format: .raw_deflate)!
|
||||
decoded := decompress(raw)! // auto-detected as raw
|
||||
assert decoded == data
|
||||
}
|
||||
|
||||
fn test_decompress_auto_detects_all_formats() {
|
||||
data := 'multi-format detection test'.repeat(5).bytes()
|
||||
assert decompress(compress(data)!)! == data
|
||||
assert decompress(compress(data, format: .gzip)!)! == data
|
||||
assert decompress(compress(data, format: .raw_deflate)!)! == data
|
||||
}
|
||||
|
||||
fn test_wrapper_helpers_match_unified_api() {
|
||||
data := 'wrapper compatibility'.repeat(8).bytes()
|
||||
assert compress(data)! == compress(data, format: .zlib)!
|
||||
assert compress_gzip(data)! == compress(data, format: .gzip)!
|
||||
assert compress_raw(data)! == compress(data, format: .raw_deflate)!
|
||||
}
|
||||
|
||||
fn test_roundtrip_repeated() {
|
||||
data := 'abcabc'.repeat(100).bytes()
|
||||
compressed := compress(data)!
|
||||
assert compressed.len < data.len
|
||||
assert decompress(compressed)! == data
|
||||
}
|
||||
|
||||
fn test_bad_compression_method_fails() {
|
||||
bad := [u8(0x79), 0x18, 0x00, 0x00, 0x00, 0x00]
|
||||
decompress(bad) or {
|
||||
assert err.msg().len > 0
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
|
||||
fn test_corrupt_checksum_fails() {
|
||||
mut enc := compress(('hello world').repeat(10).bytes())!
|
||||
// flip a byte in the adler32 footer
|
||||
enc[enc.len - 1] ^= 0xff
|
||||
decompress(enc) or {
|
||||
assert err.msg().contains('adler32')
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
|
||||
fn test_truncated_zlib_payload_fails() {
|
||||
decompress([u8(0x78), 0x9c, 0x03, 0x00, 0x00, 0x00, 0x01]) or {
|
||||
assert err.msg().contains('unexpected end of stream')
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
|
||||
fn test_zlib_inserted_bytes_before_adler_fails() {
|
||||
enc := compress('zlib injected trailer bytes'.repeat(4).bytes())!
|
||||
mut bad := []u8{cap: enc.len + 2}
|
||||
bad << enc[..enc.len - 4]
|
||||
bad << [u8(0xaa), 0x55]
|
||||
bad << enc[enc.len - 4..]
|
||||
decompress(bad) or {
|
||||
assert err.msg() == 'invalid zlib stream: trailing data before adler32'
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
|
||||
fn test_gzip_inserted_bytes_before_trailer_fails() {
|
||||
enc := compress('gzip injected trailer bytes'.repeat(4).bytes(), format: .gzip)!
|
||||
mut bad := []u8{cap: enc.len + 1}
|
||||
bad << enc[..enc.len - 8]
|
||||
bad << u8(0x42)
|
||||
bad << enc[enc.len - 8..]
|
||||
decompress(bad) or {
|
||||
assert err.msg() == 'invalid gzip stream: trailing data before trailer'
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
module main
|
||||
|
||||
import compress.deflate
|
||||
import os
|
||||
|
||||
fn choose_cc() string {
|
||||
for cc in ['cc', 'gcc', 'clang'] {
|
||||
if os.execute('${cc} --version').exit_code == 0 {
|
||||
return cc
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
fn compile_c_ref(workdir string) !string {
|
||||
cc := choose_cc()
|
||||
if cc == '' {
|
||||
return error('no C compiler found')
|
||||
}
|
||||
src := os.join_path(@DIR, 'deflate_ref.c')
|
||||
bin := os.join_path(workdir, 'deflate_cross_validate')
|
||||
res := os.execute('${cc} -O2 ${os.quoted_path(src)} -lz -o ${os.quoted_path(bin)}')
|
||||
if res.exit_code != 0 {
|
||||
return error('C compile failed: ${res.output}')
|
||||
}
|
||||
return bin
|
||||
}
|
||||
|
||||
fn main() {
|
||||
base_tmp := os.join_path(os.temp_dir(), 'v_deflate_interop')
|
||||
os.mkdir_all(base_tmp) or {
|
||||
eprintln('could not create base temp dir: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
workdir := os.join_path(base_tmp, 'run_${os.getpid()}')
|
||||
os.mkdir_all(workdir) or {
|
||||
eprintln('could not create work dir: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
defer {
|
||||
os.rmdir_all(workdir) or {}
|
||||
}
|
||||
|
||||
bin := compile_c_ref(workdir) or {
|
||||
eprintln('Skipping C cross-validation: ${err.msg()}')
|
||||
return
|
||||
}
|
||||
|
||||
input := 'deflate C cross-validation payload'.repeat(50).bytes()
|
||||
ip := os.join_path(workdir, 'xval_in.bin')
|
||||
cp := os.join_path(workdir, 'xval_c_zlib.bin')
|
||||
gp := os.join_path(workdir, 'xval_c_gzip.bin')
|
||||
vp := os.join_path(workdir, 'xval_v_zlib.bin')
|
||||
vgp := os.join_path(workdir, 'xval_v_gzip.bin')
|
||||
dp := os.join_path(workdir, 'xval_dec.bin')
|
||||
|
||||
os.write_file_array(ip, input) or {
|
||||
eprintln('write input failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
res1 :=
|
||||
os.execute('${os.quoted_path(bin)} compress ${os.quoted_path(ip)} ${os.quoted_path(cp)}')
|
||||
if res1.exit_code != 0 {
|
||||
eprintln('C zlib compress failed: ${res1.output}')
|
||||
exit(1)
|
||||
}
|
||||
c_zlib := os.read_bytes(cp) or {
|
||||
eprintln('read C zlib stream failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
v_decoded_zlib := deflate.decompress(c_zlib) or {
|
||||
eprintln('V decompress of C zlib failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
if v_decoded_zlib != input {
|
||||
eprintln('C zlib -> V mismatch')
|
||||
exit(1)
|
||||
}
|
||||
println('OK: C zlib -> V decompress')
|
||||
|
||||
res2 := os.execute('${os.quoted_path(bin)} gzip ${os.quoted_path(ip)} ${os.quoted_path(gp)}')
|
||||
if res2.exit_code != 0 {
|
||||
eprintln('C gzip failed: ${res2.output}')
|
||||
exit(1)
|
||||
}
|
||||
c_gzip := os.read_bytes(gp) or {
|
||||
eprintln('read C gzip stream failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
v_decoded_gzip := deflate.decompress(c_gzip) or {
|
||||
eprintln('V decompress of C gzip failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
if v_decoded_gzip != input {
|
||||
eprintln('C gzip -> V mismatch')
|
||||
exit(1)
|
||||
}
|
||||
println('OK: C gzip -> V decompress')
|
||||
|
||||
v_zlib := deflate.compress(input) or {
|
||||
eprintln('V zlib compress failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
os.write_file_array(vp, v_zlib) or {
|
||||
eprintln('write V zlib failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
res3 :=
|
||||
os.execute('${os.quoted_path(bin)} decompress ${os.quoted_path(vp)} ${os.quoted_path(dp)}')
|
||||
if res3.exit_code != 0 {
|
||||
eprintln('C decompress of V zlib failed: ${res3.output}')
|
||||
exit(1)
|
||||
}
|
||||
c_unzlib := os.read_bytes(dp) or {
|
||||
eprintln('read C zlib decompressed output failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
if c_unzlib != input {
|
||||
eprintln('V zlib -> C mismatch')
|
||||
exit(1)
|
||||
}
|
||||
println('OK: V zlib -> C decompress')
|
||||
|
||||
v_gzip := deflate.compress(input, format: .gzip) or {
|
||||
eprintln('V gzip compress failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
os.write_file_array(vgp, v_gzip) or {
|
||||
eprintln('write V gzip failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
res4 := os.execute('${os.quoted_path(bin)} gunzip ${os.quoted_path(vgp)} ${os.quoted_path(dp)}')
|
||||
if res4.exit_code != 0 {
|
||||
eprintln('C gunzip of V gzip failed: ${res4.output}')
|
||||
exit(1)
|
||||
}
|
||||
c_ungzip := os.read_bytes(dp) or {
|
||||
eprintln('read C gzip decompressed output failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
if c_ungzip != input {
|
||||
eprintln('V gzip -> C mismatch')
|
||||
exit(1)
|
||||
}
|
||||
println('OK: V gzip -> C decompress')
|
||||
|
||||
if os.execute('python3 --version').exit_code == 0 {
|
||||
py_src := os.join_path(@DIR, 'deflate_ref.py')
|
||||
py_driver := os.join_path(workdir, 'deflate_ref.py')
|
||||
os.cp(py_src, py_driver) or {
|
||||
eprintln('copy Python reference script failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
py_zp := os.join_path(workdir, 'xval_py_zlib.bin')
|
||||
py_gp := os.join_path(workdir, 'xval_py_gzip.bin')
|
||||
|
||||
res5 :=
|
||||
os.execute('python3 ${os.quoted_path(py_driver)} compress ${os.quoted_path(ip)} ${os.quoted_path(py_zp)}')
|
||||
if res5.exit_code != 0 {
|
||||
eprintln('Python zlib compress failed: ${res5.output}')
|
||||
exit(1)
|
||||
}
|
||||
py_zlib := os.read_bytes(py_zp) or {
|
||||
eprintln('read Python zlib stream failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
if deflate.decompress(py_zlib) or { []u8{} } != input {
|
||||
eprintln('Python zlib -> V mismatch')
|
||||
exit(1)
|
||||
}
|
||||
println('OK: Python zlib -> V decompress')
|
||||
|
||||
res6 :=
|
||||
os.execute('python3 ${os.quoted_path(py_driver)} gzip ${os.quoted_path(ip)} ${os.quoted_path(py_gp)}')
|
||||
if res6.exit_code != 0 {
|
||||
eprintln('Python gzip failed: ${res6.output}')
|
||||
exit(1)
|
||||
}
|
||||
py_gzip := os.read_bytes(py_gp) or {
|
||||
eprintln('read Python gzip stream failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
if deflate.decompress(py_gzip) or { []u8{} } != input {
|
||||
eprintln('Python gzip -> V mismatch')
|
||||
exit(1)
|
||||
}
|
||||
println('OK: Python gzip -> V decompress')
|
||||
|
||||
py_unz := os.join_path(workdir, 'xval_py_unz.bin')
|
||||
res7 :=
|
||||
os.execute('python3 ${os.quoted_path(py_driver)} decompress ${os.quoted_path(vp)} ${os.quoted_path(py_unz)}')
|
||||
if res7.exit_code != 0 {
|
||||
eprintln('Python decompress of V zlib failed: ${res7.output}')
|
||||
exit(1)
|
||||
}
|
||||
if (os.read_bytes(py_unz) or { []u8{} }) != input {
|
||||
eprintln('V zlib -> Python mismatch')
|
||||
exit(1)
|
||||
}
|
||||
println('OK: V zlib -> Python decompress')
|
||||
|
||||
py_ungz := os.join_path(workdir, 'xval_py_ungz.bin')
|
||||
res8 :=
|
||||
os.execute('python3 ${os.quoted_path(py_driver)} gunzip ${os.quoted_path(vgp)} ${os.quoted_path(py_ungz)}')
|
||||
if res8.exit_code != 0 {
|
||||
eprintln('Python gunzip of V gzip failed: ${res8.output}')
|
||||
exit(1)
|
||||
}
|
||||
if (os.read_bytes(py_ungz) or { []u8{} }) != input {
|
||||
eprintln('V gzip -> Python mismatch')
|
||||
exit(1)
|
||||
}
|
||||
println('OK: V gzip -> Python decompress')
|
||||
} else {
|
||||
eprintln('Skipping Python cross-validation: python3 not found')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <zlib.h>
|
||||
|
||||
static int rf(const char *p, unsigned char **o, size_t *n) {
|
||||
FILE *f = fopen(p, "rb");
|
||||
if (!f) return 1;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long s = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
*o = malloc(*n = (size_t)s);
|
||||
if (!*o) {
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
if (fread(*o, 1, *n, f) != *n) {
|
||||
free(*o);
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int wf(const char *p, const unsigned char *b, size_t n) {
|
||||
FILE *f = fopen(p, "wb");
|
||||
if (!f) return 1;
|
||||
if (fwrite(b, 1, n, f) != n) {
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 4) {
|
||||
fputs("usage: xval compress|decompress|gzip|gunzip in out\n", stderr);
|
||||
return 2;
|
||||
}
|
||||
unsigned char *in;
|
||||
size_t in_n;
|
||||
if (rf(argv[2], &in, &in_n)) {
|
||||
fputs("read error\n", stderr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (strcmp(argv[1], "compress") == 0) {
|
||||
uLongf cap = compressBound((uLong)in_n);
|
||||
unsigned char *out = malloc(cap);
|
||||
if (!out) return 1;
|
||||
if (compress2(out, &cap, in, (uLong)in_n, Z_DEFAULT_COMPRESSION) != Z_OK) {
|
||||
fputs("compress2 failed\n", stderr);
|
||||
free(in);
|
||||
free(out);
|
||||
return 1;
|
||||
}
|
||||
wf(argv[3], out, (size_t)cap);
|
||||
free(out);
|
||||
} else if (strcmp(argv[1], "decompress") == 0) {
|
||||
uLongf cap = in_n * 8 + 65536;
|
||||
unsigned char *out = malloc(cap);
|
||||
if (!out) return 1;
|
||||
if (uncompress(out, &cap, in, (uLong)in_n) != Z_OK) {
|
||||
fputs("uncompress failed\n", stderr);
|
||||
free(in);
|
||||
free(out);
|
||||
return 1;
|
||||
}
|
||||
wf(argv[3], out, (size_t)cap);
|
||||
free(out);
|
||||
} else if (strcmp(argv[1], "gzip") == 0) {
|
||||
z_stream s;
|
||||
memset(&s, 0, sizeof(s));
|
||||
if (deflateInit2(&s, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 | 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
|
||||
free(in);
|
||||
return 1;
|
||||
}
|
||||
uLongf cap = deflateBound(&s, (uLong)in_n) + 32;
|
||||
unsigned char *out = malloc(cap);
|
||||
if (!out) {
|
||||
deflateEnd(&s);
|
||||
free(in);
|
||||
return 1;
|
||||
}
|
||||
s.next_in = in;
|
||||
s.avail_in = (uInt)in_n;
|
||||
s.next_out = out;
|
||||
s.avail_out = (uInt)cap;
|
||||
if (deflate(&s, Z_FINISH) != Z_STREAM_END) {
|
||||
fputs("gzip deflate failed\n", stderr);
|
||||
deflateEnd(&s);
|
||||
free(in);
|
||||
free(out);
|
||||
return 1;
|
||||
}
|
||||
wf(argv[3], out, (size_t)s.total_out);
|
||||
deflateEnd(&s);
|
||||
free(out);
|
||||
} else if (strcmp(argv[1], "gunzip") == 0) {
|
||||
z_stream s;
|
||||
memset(&s, 0, sizeof(s));
|
||||
if (inflateInit2(&s, 15 | 16) != Z_OK) {
|
||||
free(in);
|
||||
return 1;
|
||||
}
|
||||
uLongf cap = in_n * 8 + 65536;
|
||||
unsigned char *out = malloc(cap);
|
||||
if (!out) {
|
||||
inflateEnd(&s);
|
||||
free(in);
|
||||
return 1;
|
||||
}
|
||||
s.next_in = in;
|
||||
s.avail_in = (uInt)in_n;
|
||||
s.next_out = out;
|
||||
s.avail_out = (uInt)cap;
|
||||
if (inflate(&s, Z_FINISH) != Z_STREAM_END) {
|
||||
fputs("gunzip inflate failed\n", stderr);
|
||||
inflateEnd(&s);
|
||||
free(in);
|
||||
free(out);
|
||||
return 1;
|
||||
}
|
||||
wf(argv[3], out, (size_t)s.total_out);
|
||||
inflateEnd(&s);
|
||||
free(out);
|
||||
}
|
||||
|
||||
free(in);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import sys, zlib
|
||||
|
||||
def rf(p):
|
||||
with open(p, "rb") as f: return f.read()
|
||||
|
||||
def wf(p, b):
|
||||
with open(p, "wb") as f: f.write(b)
|
||||
|
||||
if len(sys.argv) != 4:
|
||||
print("usage: py_ref compress|decompress|gzip|gunzip in out", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
mode, inp, outp = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
data = rf(inp)
|
||||
if mode == "compress":
|
||||
wf(outp, zlib.compress(data))
|
||||
elif mode == "decompress":
|
||||
wf(outp, zlib.decompress(data))
|
||||
elif mode == "gzip":
|
||||
co = zlib.compressobj(level=6, wbits=16 + zlib.MAX_WBITS)
|
||||
wf(outp, co.compress(data) + co.flush())
|
||||
elif mode == "gunzip":
|
||||
wf(outp, zlib.decompress(data, 16 + zlib.MAX_WBITS))
|
||||
else:
|
||||
print("unknown mode", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
## Description
|
||||
|
||||
`compress.gzip` is a module that assists in the compression and
|
||||
decompression of binary data using `gzip` compression
|
||||
|
||||
|
||||
## Examples:
|
||||
|
||||
```v
|
||||
import compress.gzip
|
||||
|
||||
fn main() {
|
||||
uncompressed := 'Hello world!'
|
||||
compressed := gzip.compress(uncompressed.bytes())!
|
||||
decompressed := gzip.decompress(compressed)!
|
||||
assert decompressed == uncompressed.bytes()
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,98 @@
|
||||
// [rfc1952](https://datatracker.ietf.org/doc/html/rfc1952) compliant
|
||||
// gzip compression/decompression
|
||||
|
||||
module gzip
|
||||
|
||||
import compress.deflate
|
||||
|
||||
// CompressFlags
|
||||
@[deprecated: 'never used']
|
||||
@[deprecated_after: '2026-07-31']
|
||||
@[flag]
|
||||
pub enum CompressFlags {
|
||||
// The low 12 bits will be overwritten by `compression_level`
|
||||
compression_level_overwrite_flag01
|
||||
compression_level_overwrite_flag02
|
||||
compression_level_overwrite_flag03
|
||||
compression_level_overwrite_flag04
|
||||
compression_level_overwrite_flag05
|
||||
compression_level_overwrite_flag06
|
||||
compression_level_overwrite_flag07
|
||||
compression_level_overwrite_flag08
|
||||
compression_level_overwrite_flag09
|
||||
compression_level_overwrite_flag10
|
||||
compression_level_overwrite_flag11
|
||||
compression_level_overwrite_flag12
|
||||
write_zlib_header
|
||||
compute_adler32
|
||||
greedy_parsing_flag
|
||||
nondeterministic_parsing_flag
|
||||
rle_matches
|
||||
filter_matches
|
||||
force_all_static_blocks
|
||||
force_all_raw_blocks
|
||||
}
|
||||
|
||||
// CompressParams set compression_level for compression:
|
||||
@[deprecated: 'never used']
|
||||
@[deprecated_after: '2026-07-31']
|
||||
@[params]
|
||||
pub struct CompressParams {
|
||||
pub:
|
||||
compression_level int
|
||||
flags CompressFlags
|
||||
}
|
||||
|
||||
// compress compresses an array of bytes using gzip and returns the compressed bytes in a new array.
|
||||
pub fn compress(data []u8) ![]u8 {
|
||||
// Delegate to deflate.compress_gzip() which implements RFC 1952
|
||||
return deflate.compress_gzip(data)
|
||||
}
|
||||
|
||||
// DecompressFlags
|
||||
// N.B.: only retained for API compatibility.
|
||||
@[deprecated: 'never used']
|
||||
@[deprecated_after: '2026-07-31']
|
||||
@[flag]
|
||||
pub enum DecompressFlags {
|
||||
parse_zlib_header
|
||||
has_more_input
|
||||
using_non_wrapping_output_buf
|
||||
compute_adler32
|
||||
}
|
||||
|
||||
// DecompressParams controls gzip decompression behavior.
|
||||
// N.B.: only retained for API compatibility.
|
||||
@[deprecated: 'never used']
|
||||
@[deprecated_after: '2026-07-31']
|
||||
@[params]
|
||||
pub struct DecompressParams {
|
||||
pub:
|
||||
verify_header_checksum bool = true
|
||||
verify_length bool = true
|
||||
verify_checksum bool = true
|
||||
flags DecompressFlags
|
||||
}
|
||||
|
||||
// validate validates the gzip header of data and returns its parsed details if valid.
|
||||
// N.B.: only retained for API compatibility, all validation is now performed by the deflate backend.
|
||||
// The returned header details are not used by the decompressor.
|
||||
// TODO: remove after the deprecation period.
|
||||
@[deprecated: 'never used']
|
||||
@[deprecated_after: '2026-07-31']
|
||||
pub fn validate(data []u8, _ DecompressParams) !deflate.GzipHeader {
|
||||
return deflate.validate_gzip_header(data)!
|
||||
}
|
||||
|
||||
// decompress decompresses a gzip stream and returns the decompressed bytes in a new array.
|
||||
pub fn decompress(data []u8) ![]u8 {
|
||||
return deflate.decompress_gzip(data)
|
||||
}
|
||||
|
||||
// decompress_with_callback decompresses a gzip stream (RFC 1952) using a callback for chunked delivery.
|
||||
// The callback receives chunks of decompressed data and should return the chunk length to continue, or 0 to abort.
|
||||
// Returns the total decompressed length.
|
||||
pub fn decompress_with_callback(data []u8, cb deflate.ChunkCallback, userdata voidptr) !int {
|
||||
deflate.validate_gzip_header(data)!
|
||||
return deflate.decompress_with_callback(data, cb, userdata)
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
module gzip
|
||||
|
||||
import hash.crc32
|
||||
import os
|
||||
|
||||
const test_ftext = u8(0b0000_0001)
|
||||
const test_fhcrc = u8(0b0000_0010)
|
||||
const test_fextra = u8(0b0000_0100)
|
||||
const test_fname = u8(0b0000_1000)
|
||||
const test_fcomment = u8(0b0001_0000)
|
||||
const samples_folder = os.join_path(os.dir(@FILE), 'samples')
|
||||
|
||||
fn test_gzip() {
|
||||
uncompressed := 'Hello world!'
|
||||
compressed := compress(uncompressed.bytes())!
|
||||
decompressed := decompress(compressed)!
|
||||
assert decompressed == uncompressed.bytes()
|
||||
}
|
||||
|
||||
fn assert_decompress_error(data []u8, reason string) ! {
|
||||
decompress(data) or {
|
||||
assert err.msg() == reason
|
||||
return
|
||||
}
|
||||
return error('did not error')
|
||||
}
|
||||
|
||||
fn test_gzip_invalid_too_short() {
|
||||
assert_decompress_error([]u8{}, 'invalid gzip stream: too short')!
|
||||
}
|
||||
|
||||
fn test_gzip_invalid_magic_numbers() {
|
||||
assert_decompress_error([]u8{len: 100}, 'invalid gzip stream: bad magic')!
|
||||
}
|
||||
|
||||
fn test_gzip_invalid_compression() {
|
||||
mut data := []u8{len: 100}
|
||||
data[0] = 0x1f
|
||||
data[1] = 0x8b
|
||||
assert_decompress_error(data, 'invalid gzip stream: unsupported compression method')!
|
||||
}
|
||||
|
||||
fn test_gzip_with_ftext() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[3] |= test_ftext
|
||||
decompressed := decompress(compressed)!
|
||||
assert decompressed == uncompressed.bytes()
|
||||
}
|
||||
|
||||
fn test_gzip_with_fname() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[3] |= test_fname
|
||||
compressed.insert(10, `h`)
|
||||
compressed.insert(11, `i`)
|
||||
compressed.insert(12, 0x00)
|
||||
decompressed := decompress(compressed)!
|
||||
assert decompressed == uncompressed.bytes()
|
||||
}
|
||||
|
||||
fn test_gzip_with_fcomment() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[3] |= test_fcomment
|
||||
compressed.insert(10, `h`)
|
||||
compressed.insert(11, `i`)
|
||||
compressed.insert(12, 0x00)
|
||||
decompressed := decompress(compressed)!
|
||||
assert decompressed == uncompressed.bytes()
|
||||
}
|
||||
|
||||
fn test_gzip_with_fname_fcomment() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[3] |= (test_fname | test_fcomment)
|
||||
compressed.insert(10, `h`)
|
||||
compressed.insert(11, `i`)
|
||||
compressed.insert(12, 0x00)
|
||||
compressed.insert(10, `h`)
|
||||
compressed.insert(11, `i`)
|
||||
compressed.insert(12, 0x00)
|
||||
decompressed := decompress(compressed)!
|
||||
assert decompressed == uncompressed.bytes()
|
||||
}
|
||||
|
||||
fn test_gzip_with_fextra() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[3] |= test_fextra
|
||||
// XLEN is 2-byte little-endian value
|
||||
xlen := u16(2)
|
||||
compressed.insert(10, u8(xlen))
|
||||
compressed.insert(11, u8(xlen >> 8))
|
||||
compressed.insert(12, `h`)
|
||||
compressed.insert(13, `i`)
|
||||
decompressed := decompress(compressed)!
|
||||
assert decompressed == uncompressed.bytes()
|
||||
}
|
||||
|
||||
fn test_gzip_with_hcrc() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[3] |= test_fhcrc
|
||||
// FHCRC is 2-byte CRC-16 (low 16 bits of CRC32) in little-endian format
|
||||
checksum := crc32.sum(compressed[..10])
|
||||
crc16 := u16(checksum & 0xffff)
|
||||
compressed.insert(10, u8(crc16))
|
||||
compressed.insert(11, u8(crc16 >> 8))
|
||||
decompressed := decompress(compressed)!
|
||||
assert decompressed == uncompressed.bytes()
|
||||
}
|
||||
|
||||
fn test_gzip_with_invalid_hcrc() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[3] |= test_fhcrc
|
||||
// FHCRC is 2-byte CRC-16 (low 16 bits of CRC32) in little-endian format
|
||||
checksum := crc32.sum(compressed[..10])
|
||||
crc16 := u16(checksum & 0xffff)
|
||||
compressed.insert(10, u8(crc16))
|
||||
compressed.insert(11, u8((crc16 >> 8) + 1)) // corrupt high byte
|
||||
assert_decompress_error(compressed, 'invalid gzip stream: header crc16 mismatch')!
|
||||
}
|
||||
|
||||
fn test_gzip_with_invalid_checksum() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[compressed.len - 5] += 1
|
||||
assert_decompress_error(compressed, 'invalid gzip stream: crc32 mismatch')!
|
||||
}
|
||||
|
||||
fn test_gzip_with_invalid_length() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[compressed.len - 1] += 1
|
||||
assert_decompress_error(compressed, 'invalid gzip stream: size mismatch')!
|
||||
}
|
||||
|
||||
fn test_gzip_with_invalid_flags() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[3] |= 0b1000_0000
|
||||
assert_decompress_error(compressed, 'invalid gzip stream: reserved flags set')!
|
||||
}
|
||||
|
||||
fn test_gzip_decompress_callback() {
|
||||
uncompressed := '321323'.repeat(10_000)
|
||||
gz := compress(uncompressed.bytes())!
|
||||
mut size := 0
|
||||
mut ref := &size
|
||||
decoded := decompress_with_callback(gz, fn (chunk []u8, ref &int) int {
|
||||
unsafe {
|
||||
*ref += chunk.len
|
||||
}
|
||||
return chunk.len
|
||||
}, ref)!
|
||||
assert decoded == size
|
||||
assert decoded == uncompressed.len
|
||||
}
|
||||
|
||||
fn test_gzip_decompress_callback_rejects_non_gzip() {
|
||||
z := [u8(0x78), 0x9c, 0x03, 0x00, 0x00, 0x00, 0x01]
|
||||
decompress_with_callback(z, fn (chunk []u8, _ voidptr) int {
|
||||
return chunk.len
|
||||
}, unsafe { nil }) or {
|
||||
assert err.msg() == 'invalid gzip stream: too short'
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
|
||||
fn s(fname string) string {
|
||||
return os.join_path(samples_folder, fname)
|
||||
}
|
||||
|
||||
fn read_and_decode_file(fpath string) !([]u8, string) {
|
||||
compressed := os.read_bytes(fpath)!
|
||||
decoded := decompress(compressed)!
|
||||
content := decoded.bytestr()
|
||||
return compressed, content
|
||||
}
|
||||
|
||||
fn test_reading_and_decoding_a_known_gziped_file() {
|
||||
compressed, content := read_and_decode_file(s('known.gz'))!
|
||||
assert compressed#[0..3] == [u8(31), 139, 8]
|
||||
assert compressed#[-5..] == [u8(127), 115, 1, 0, 0]
|
||||
assert content.contains('## Description')
|
||||
assert content.contains('## Examples:')
|
||||
assert content.ends_with('```\n')
|
||||
}
|
||||
|
||||
fn test_decoding_all_samples_files() {
|
||||
for gz_file in os.walk_ext(samples_folder, '.gz') {
|
||||
_, content := read_and_decode_file(gz_file)!
|
||||
assert content.len > 0, 'decoded content should not be empty: `${content}`'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_reading_gzip_files_compressed_with_different_options() {
|
||||
_, content1 := read_and_decode_file(s('readme_level_1.gz'))!
|
||||
_, content5 := read_and_decode_file(s('readme_level_5.gz'))!
|
||||
_, content9 := read_and_decode_file(s('readme_level_9.gz'))!
|
||||
_, content9_rsyncable := read_and_decode_file(s('readme_level_9_rsyncable.gz'))!
|
||||
assert content9_rsyncable == content9
|
||||
assert content9 == content5
|
||||
assert content5 == content1
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,35 @@
|
||||
## Description
|
||||
|
||||
`compress.lz` provides pure V implementations of several LZ-family codecs.
|
||||
|
||||
Supported formats:
|
||||
|
||||
- `lz77`
|
||||
- `lz78`
|
||||
- `lzw`
|
||||
- `lz4`
|
||||
- `lzss`
|
||||
- `lzma`
|
||||
- `lzma2`
|
||||
- `lzjb`
|
||||
|
||||
Use the generic API when selecting a format dynamically:
|
||||
|
||||
```v
|
||||
import compress.lz
|
||||
|
||||
encoded := lz.compress('hello hello hello'.bytes(), .lz77)!
|
||||
decoded := lz.decompress(encoded, .lz77)!
|
||||
assert decoded.bytestr() == 'hello hello hello'
|
||||
```
|
||||
|
||||
Use the format-specific APIs for direct calls:
|
||||
|
||||
```v
|
||||
import compress.lz
|
||||
|
||||
encoded := lz.compress_lzw('banana banana'.bytes())!
|
||||
decoded := lz.decompress_lzw(encoded)!
|
||||
assert decoded.bytestr() == 'banana banana'
|
||||
```
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
module lz
|
||||
|
||||
const stream_magic = [u8(0x56), 0x4c, 0x5a, 0x31] // VLZ1
|
||||
|
||||
struct MatchProfile {
|
||||
window int
|
||||
min_match int
|
||||
max_match int
|
||||
max_literal int
|
||||
}
|
||||
|
||||
const match_hash_bits = 16
|
||||
const match_hash_size = 1 << match_hash_bits
|
||||
const max_match_candidates = 64
|
||||
|
||||
fn wrap_payload(format Format, source []u8, payload []u8) []u8 {
|
||||
mut out := []u8{cap: stream_magic.len + 8 + payload.len}
|
||||
out << stream_magic
|
||||
out << u8(format)
|
||||
encode_uvarint(mut out, u64(source.len))
|
||||
out << payload
|
||||
return out
|
||||
}
|
||||
|
||||
fn unwrap_payload(data []u8, format Format) !([]u8, i64) {
|
||||
if data.len < stream_magic.len + 2 {
|
||||
return error('invalid lz stream: too short')
|
||||
}
|
||||
if data[..stream_magic.len] != stream_magic {
|
||||
return error('invalid lz stream: bad magic')
|
||||
}
|
||||
wire_format := data[stream_magic.len]
|
||||
if wire_format != u8(format) {
|
||||
return error('invalid lz stream: format mismatch')
|
||||
}
|
||||
decoded_len_u64, mut pos, ok := decode_uvarint(data, stream_magic.len + 1)
|
||||
if !ok {
|
||||
return error('invalid lz stream: bad length')
|
||||
}
|
||||
if decoded_len_u64 > u64(max_int) {
|
||||
return error('invalid lz stream: decoded length too large')
|
||||
}
|
||||
decoded_len := i64(decoded_len_u64)
|
||||
if pos > data.len {
|
||||
return error('invalid lz stream: truncated payload')
|
||||
}
|
||||
return data[pos..], decoded_len
|
||||
}
|
||||
|
||||
fn compress_with_profile(data []u8, profile MatchProfile, format Format) []u8 {
|
||||
if data.len == 0 {
|
||||
return wrap_payload(format, data, []u8{})
|
||||
}
|
||||
mut payload := []u8{cap: data.len}
|
||||
mut literals := []u8{cap: profile.max_literal}
|
||||
mut last_match := []int{len: match_hash_size, init: -1}
|
||||
mut prev_match := []int{len: data.len, init: -1}
|
||||
mut pos := 0
|
||||
for pos < data.len {
|
||||
offset, length := find_best_match(data, pos, profile, last_match, prev_match)
|
||||
if length >= profile.min_match {
|
||||
flush_literals(mut payload, mut literals)
|
||||
emit_match(mut payload, offset, length, profile.min_match)
|
||||
for i := pos; i < pos + length; i++ {
|
||||
index_match_position(data, i, mut last_match, mut prev_match)
|
||||
}
|
||||
pos += length
|
||||
} else {
|
||||
literals << data[pos]
|
||||
if literals.len == profile.max_literal {
|
||||
flush_literals(mut payload, mut literals)
|
||||
}
|
||||
index_match_position(data, pos, mut last_match, mut prev_match)
|
||||
pos++
|
||||
}
|
||||
}
|
||||
flush_literals(mut payload, mut literals)
|
||||
return wrap_payload(format, data, payload)
|
||||
}
|
||||
|
||||
fn decompress_with_profile(data []u8, profile MatchProfile, format Format) ![]u8 {
|
||||
payload, expected_len := unwrap_payload(data, format)!
|
||||
mut out := []u8{cap: int(expected_len)}
|
||||
mut pos := 0
|
||||
for pos < payload.len {
|
||||
control := payload[pos]
|
||||
pos++
|
||||
if control & 0x80 == 0 {
|
||||
literal_len := int(control & 0x7f) + 1
|
||||
if pos + literal_len > payload.len {
|
||||
return error('invalid lz stream: truncated literal')
|
||||
}
|
||||
out << payload[pos..pos + literal_len]
|
||||
pos += literal_len
|
||||
continue
|
||||
}
|
||||
match_len := int(control & 0x7f) + profile.min_match
|
||||
offset, next_pos, ok := decode_uvarint(payload, pos)
|
||||
if !ok {
|
||||
return error('invalid lz stream: truncated match offset')
|
||||
}
|
||||
pos = next_pos
|
||||
if offset == 0 || offset > u64(max_i64) || i64(offset) > i64(out.len) {
|
||||
return error('invalid lz stream: bad match offset')
|
||||
}
|
||||
offset_int := int(offset)
|
||||
base := out.len - offset_int
|
||||
for i in 0 .. match_len {
|
||||
out << out[base + i]
|
||||
}
|
||||
}
|
||||
if i64(out.len) != expected_len {
|
||||
return error('invalid lz stream: length mismatch')
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fn find_best_match(data []u8, pos int, profile MatchProfile, last_match []int, prev_match []int) (int, int) {
|
||||
if pos + profile.min_match > data.len {
|
||||
return 0, 0
|
||||
}
|
||||
max_len := if pos + profile.max_match < data.len { profile.max_match } else { data.len - pos }
|
||||
mut best_len := 0
|
||||
mut best_offset := 0
|
||||
hash_idx := match_hash(data, pos)
|
||||
mut candidates_checked := 0
|
||||
mut i := last_match[hash_idx]
|
||||
for i >= 0 && candidates_checked < max_match_candidates {
|
||||
offset := pos - i
|
||||
if offset > profile.window {
|
||||
break
|
||||
}
|
||||
mut current_len := 0
|
||||
for current_len < max_len && data[i + current_len] == data[pos + current_len] {
|
||||
current_len++
|
||||
}
|
||||
if current_len > best_len {
|
||||
best_len = current_len
|
||||
best_offset = offset
|
||||
if best_len == max_len {
|
||||
break
|
||||
}
|
||||
}
|
||||
i = prev_match[i]
|
||||
candidates_checked++
|
||||
}
|
||||
return best_offset, best_len
|
||||
}
|
||||
|
||||
fn index_match_position(data []u8, pos int, mut last_match []int, mut prev_match []int) {
|
||||
if pos + 2 >= data.len {
|
||||
return
|
||||
}
|
||||
hash_idx := match_hash(data, pos)
|
||||
prev_match[pos] = last_match[hash_idx]
|
||||
last_match[hash_idx] = pos
|
||||
}
|
||||
|
||||
fn match_hash(data []u8, pos int) int {
|
||||
v := (u32(data[pos]) << 16) | (u32(data[pos + 1]) << 8) | u32(data[pos + 2])
|
||||
return int((v * u32(2654435761)) >> (32 - match_hash_bits))
|
||||
}
|
||||
|
||||
fn flush_literals(mut payload []u8, mut literals []u8) {
|
||||
if literals.len == 0 {
|
||||
return
|
||||
}
|
||||
payload << u8(literals.len - 1)
|
||||
payload << literals
|
||||
literals.clear()
|
||||
}
|
||||
|
||||
fn emit_match(mut payload []u8, offset int, length int, min_match int) {
|
||||
payload << u8(0x80 | u8(length - min_match))
|
||||
encode_uvarint(mut payload, u64(offset))
|
||||
}
|
||||
|
||||
fn encode_uvarint(mut out []u8, value u64) {
|
||||
mut v := value
|
||||
for v >= 0x80 {
|
||||
out << u8(v & 0x7f | 0x80)
|
||||
v >>= 7
|
||||
}
|
||||
out << u8(v)
|
||||
}
|
||||
|
||||
fn decode_uvarint(data []u8, start int) (u64, int, bool) {
|
||||
mut value := u64(0)
|
||||
mut shift := u32(0)
|
||||
mut pos := start
|
||||
for pos < data.len && shift <= 63 {
|
||||
b := data[pos]
|
||||
pos++
|
||||
value |= u64(b & 0x7f) << shift
|
||||
if b & 0x80 == 0 {
|
||||
return value, pos, true
|
||||
}
|
||||
shift += 7
|
||||
}
|
||||
return 0, start, false
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
## LZ Interop Validation (V, C, Python)
|
||||
|
||||
This tool validates:
|
||||
|
||||
- V compress/decompress roundtrips for all formats: `lz77`, `lz78`, `lzw`,
|
||||
`lz4`, `lzss`, `lzma`, `lzjb`
|
||||
- a C `lz77`-like reference implementation
|
||||
- a Python `lz77`-like reference implementation
|
||||
|
||||
The C/Python references are intentionally simple so the benchmark is easy to run
|
||||
without external dependencies.
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
cd /home/jalon/git/v
|
||||
./vnew run vlib/compress/lz/interop/lz_interop.v 2>&1
|
||||
```
|
||||
|
||||
Optional args:
|
||||
|
||||
1. validation rounds (default: `40`)
|
||||
2. input size in bytes (default: `524288`)
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
cd /home/jalon/git/v
|
||||
./vnew run vlib/compress/lz/interop/lz_interop.v 25 262144 2>&1
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
The tool prints validation status lines only (no timing output).
|
||||
|
||||
When helpers are available, it also cross-validates compression/expansion
|
||||
interoperability with V (`lz77`) in both directions:
|
||||
|
||||
- V compress -> C decompress
|
||||
- C compress -> V decompress
|
||||
- V compress -> Python decompress
|
||||
- Python compress -> V decompress
|
||||
|
||||
If `cc`/`gcc` or `python3` are missing, that row is skipped with a message.
|
||||
@@ -0,0 +1,256 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#define MIN_MATCH 3
|
||||
#define MAX_LITERAL 128
|
||||
|
||||
static const uint8_t STREAM_MAGIC[4] = {0x56, 0x4c, 0x5a, 0x31};
|
||||
static const uint8_t FORMAT_LZ77 = 0;
|
||||
|
||||
typedef struct {
|
||||
uint8_t *data;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
} Buffer;
|
||||
|
||||
static void die(const char *msg) {
|
||||
fprintf(stderr, "%s\n", msg);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
static void buf_init(Buffer *b, size_t cap) {
|
||||
b->data = (uint8_t *)malloc(cap > 0 ? cap : 1);
|
||||
if (!b->data) {
|
||||
die("allocation failed");
|
||||
}
|
||||
b->len = 0;
|
||||
b->cap = cap > 0 ? cap : 1;
|
||||
}
|
||||
|
||||
static void buf_push(Buffer *b, uint8_t v) {
|
||||
if (b->len >= b->cap) {
|
||||
size_t new_cap = b->cap * 2;
|
||||
uint8_t *n = (uint8_t *)realloc(b->data, new_cap);
|
||||
if (!n) {
|
||||
die("reallocation failed");
|
||||
}
|
||||
b->data = n;
|
||||
b->cap = new_cap;
|
||||
}
|
||||
b->data[b->len++] = v;
|
||||
}
|
||||
|
||||
static void buf_append(Buffer *b, const uint8_t *src, size_t len) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
buf_push(b, src[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static Buffer read_all(const char *path) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
die("could not open input file");
|
||||
}
|
||||
if (fseek(f, 0, SEEK_END) != 0) {
|
||||
fclose(f);
|
||||
die("could not seek input file");
|
||||
}
|
||||
long sz = ftell(f);
|
||||
if (sz < 0) {
|
||||
fclose(f);
|
||||
die("could not read input file size");
|
||||
}
|
||||
if (fseek(f, 0, SEEK_SET) != 0) {
|
||||
fclose(f);
|
||||
die("could not rewind input file");
|
||||
}
|
||||
Buffer in;
|
||||
buf_init(&in, (size_t)sz + 1);
|
||||
in.len = (size_t)sz;
|
||||
if (in.len > 0 && fread(in.data, 1, in.len, f) != in.len) {
|
||||
fclose(f);
|
||||
free(in.data);
|
||||
die("could not read input file");
|
||||
}
|
||||
fclose(f);
|
||||
return in;
|
||||
}
|
||||
|
||||
static void write_all(const char *path, const uint8_t *data, size_t len) {
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) {
|
||||
die("could not open output file");
|
||||
}
|
||||
if (len > 0 && fwrite(data, 1, len, f) != len) {
|
||||
fclose(f);
|
||||
die("could not write output file");
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static void write_uvarint(Buffer *out, uint64_t value) {
|
||||
uint64_t v = value;
|
||||
while (v >= 0x80) {
|
||||
buf_push(out, (uint8_t)(v & 0x7f) | 0x80);
|
||||
v >>= 7;
|
||||
}
|
||||
buf_push(out, (uint8_t)v);
|
||||
}
|
||||
|
||||
static int read_uvarint(const uint8_t *data, size_t len, size_t *pos, uint64_t *value) {
|
||||
uint64_t out = 0;
|
||||
uint32_t shift = 0;
|
||||
while (*pos < len && shift <= 63) {
|
||||
uint8_t b = data[*pos];
|
||||
(*pos)++;
|
||||
out |= ((uint64_t)(b & 0x7f)) << shift;
|
||||
if ((b & 0x80) == 0) {
|
||||
*value = out;
|
||||
return 1;
|
||||
}
|
||||
shift += 7;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Buffer compress_lz77(const uint8_t *in, size_t in_len) {
|
||||
Buffer out;
|
||||
buf_init(&out, in_len + 32);
|
||||
buf_append(&out, STREAM_MAGIC, 4);
|
||||
buf_push(&out, FORMAT_LZ77);
|
||||
write_uvarint(&out, (uint64_t)in_len);
|
||||
|
||||
for (size_t i = 0; i < in_len;) {
|
||||
size_t lit_len = in_len - i;
|
||||
if (lit_len > MAX_LITERAL) {
|
||||
lit_len = MAX_LITERAL;
|
||||
}
|
||||
buf_push(&out, (uint8_t)(lit_len - 1));
|
||||
buf_append(&out, in + i, lit_len);
|
||||
i += lit_len;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static Buffer decompress_lz77(const uint8_t *in, size_t in_len) {
|
||||
if (in_len < 6 || memcmp(in, STREAM_MAGIC, 4) != 0) {
|
||||
die("bad magic");
|
||||
}
|
||||
if (in[4] != FORMAT_LZ77) {
|
||||
die("format mismatch");
|
||||
}
|
||||
size_t pos = 5;
|
||||
uint64_t expected_len_u64 = 0;
|
||||
if (!read_uvarint(in, in_len, &pos, &expected_len_u64)) {
|
||||
die("bad length varint");
|
||||
}
|
||||
size_t expected_len = (size_t)expected_len_u64;
|
||||
|
||||
Buffer out;
|
||||
buf_init(&out, expected_len + 16);
|
||||
while (pos < in_len) {
|
||||
uint8_t control = in[pos++];
|
||||
if ((control & 0x80) == 0) {
|
||||
size_t lit_len = (size_t)(control & 0x7f) + 1;
|
||||
if (pos + lit_len > in_len) {
|
||||
die("truncated literal");
|
||||
}
|
||||
buf_append(&out, in + pos, lit_len);
|
||||
pos += lit_len;
|
||||
} else {
|
||||
size_t match_len = (size_t)(control & 0x7f) + MIN_MATCH;
|
||||
uint64_t offset_u64 = 0;
|
||||
if (!read_uvarint(in, in_len, &pos, &offset_u64)) {
|
||||
die("bad match offset");
|
||||
}
|
||||
size_t offset = (size_t)offset_u64;
|
||||
if (offset == 0 || offset > out.len) {
|
||||
die("bad offset");
|
||||
}
|
||||
size_t base = out.len - offset;
|
||||
for (size_t k = 0; k < match_len; k++) {
|
||||
buf_push(&out, out.data[base + k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out.len != expected_len) {
|
||||
die("length mismatch");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static int64_t now_ms(void) {
|
||||
return (int64_t)((double)clock() * 1000.0 / (double)CLOCKS_PER_SEC);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) {
|
||||
fprintf(stderr,
|
||||
"usage:\n"
|
||||
" %s bench <input.bin> <iterations>\n"
|
||||
" %s compress <input.bin> <output.bin>\n"
|
||||
" %s decompress <input.bin> <output.bin>\n",
|
||||
argv[0], argv[0], argv[0]);
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(argv[1], "bench") == 0) {
|
||||
if (argc < 4) {
|
||||
fprintf(stderr, "usage: %s bench <input.bin> <iterations>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
int iterations = atoi(argv[3]);
|
||||
if (iterations <= 0) {
|
||||
fprintf(stderr, "iterations must be > 0\n");
|
||||
return 1;
|
||||
}
|
||||
Buffer input = read_all(argv[2]);
|
||||
int64_t start = now_ms();
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
Buffer comp = compress_lz77(input.data, input.len);
|
||||
Buffer decomp = decompress_lz77(comp.data, comp.len);
|
||||
if (decomp.len != input.len || memcmp(decomp.data, input.data, input.len) != 0) {
|
||||
fprintf(stderr, "roundtrip mismatch\n");
|
||||
return 1;
|
||||
}
|
||||
free(comp.data);
|
||||
free(decomp.data);
|
||||
}
|
||||
int64_t elapsed = now_ms() - start;
|
||||
printf("ms=%lld\n", (long long)elapsed);
|
||||
free(input.data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(argv[1], "compress") == 0) {
|
||||
if (argc < 4) {
|
||||
fprintf(stderr, "usage: %s compress <input.bin> <output.bin>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
Buffer input = read_all(argv[2]);
|
||||
Buffer comp = compress_lz77(input.data, input.len);
|
||||
write_all(argv[3], comp.data, comp.len);
|
||||
free(input.data);
|
||||
free(comp.data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(argv[1], "decompress") == 0) {
|
||||
if (argc < 4) {
|
||||
fprintf(stderr, "usage: %s decompress <input.bin> <output.bin>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
Buffer input = read_all(argv[2]);
|
||||
Buffer dec = decompress_lz77(input.data, input.len);
|
||||
write_all(argv[3], dec.data, dec.len);
|
||||
free(input.data);
|
||||
free(dec.data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fprintf(stderr, "unknown mode: %s\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import time
|
||||
|
||||
MIN_MATCH = 3
|
||||
MAX_LITERAL = 128
|
||||
STREAM_MAGIC = b'VLZ1'
|
||||
FORMAT_LZ77 = 0
|
||||
|
||||
|
||||
def encode_uvarint(value: int) -> bytes:
|
||||
out = bytearray()
|
||||
v = value
|
||||
while v >= 0x80:
|
||||
out.append((v & 0x7F) | 0x80)
|
||||
v >>= 7
|
||||
out.append(v)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def decode_uvarint(data: bytes, pos: int):
|
||||
value = 0
|
||||
shift = 0
|
||||
i = pos
|
||||
while i < len(data) and shift <= 63:
|
||||
b = data[i]
|
||||
i += 1
|
||||
value |= (b & 0x7F) << shift
|
||||
if (b & 0x80) == 0:
|
||||
return value, i
|
||||
shift += 7
|
||||
raise ValueError('bad length varint')
|
||||
|
||||
|
||||
def compress_lz77(data: bytes) -> bytes:
|
||||
out = bytearray()
|
||||
out.extend(STREAM_MAGIC)
|
||||
out.append(FORMAT_LZ77)
|
||||
out.extend(encode_uvarint(len(data)))
|
||||
i = 0
|
||||
while i < len(data):
|
||||
lit_len = min(MAX_LITERAL, len(data) - i)
|
||||
out.append(lit_len - 1)
|
||||
out.extend(data[i : i + lit_len])
|
||||
i += lit_len
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def decompress_lz77(data: bytes) -> bytes:
|
||||
if len(data) < 6 or data[:4] != STREAM_MAGIC:
|
||||
raise ValueError('bad magic')
|
||||
if data[4] != FORMAT_LZ77:
|
||||
raise ValueError('format mismatch')
|
||||
|
||||
expected_len, pos = decode_uvarint(data, 5)
|
||||
out = bytearray()
|
||||
while pos < len(data):
|
||||
control = data[pos]
|
||||
pos += 1
|
||||
if (control & 0x80) == 0:
|
||||
literal_len = (control & 0x7F) + 1
|
||||
if pos + literal_len > len(data):
|
||||
raise ValueError('truncated literal')
|
||||
out.extend(data[pos : pos + literal_len])
|
||||
pos += literal_len
|
||||
else:
|
||||
length = (control & 0x7F) + MIN_MATCH
|
||||
off, pos = decode_uvarint(data, pos)
|
||||
if off == 0 or off > len(out):
|
||||
raise ValueError('bad offset')
|
||||
base = len(out) - off
|
||||
for k in range(length):
|
||||
out.append(out[base + k])
|
||||
if len(out) != expected_len:
|
||||
raise ValueError('length mismatch')
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print(
|
||||
f'usage:\n'
|
||||
f' {sys.argv[0]} bench <input.bin> <iterations>\n'
|
||||
f' {sys.argv[0]} compress <input.bin> <output.bin>\n'
|
||||
f' {sys.argv[0]} decompress <input.bin> <output.bin>',
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
mode = sys.argv[1]
|
||||
if mode == 'bench':
|
||||
if len(sys.argv) < 4:
|
||||
print(f'usage: {sys.argv[0]} bench <input.bin> <iterations>', file=sys.stderr)
|
||||
return 1
|
||||
input_path = sys.argv[2]
|
||||
iterations = int(sys.argv[3])
|
||||
if iterations <= 0:
|
||||
print('iterations must be > 0', file=sys.stderr)
|
||||
return 1
|
||||
with open(input_path, 'rb') as f:
|
||||
data = f.read()
|
||||
start = time.perf_counter()
|
||||
for _ in range(iterations):
|
||||
enc = compress_lz77(data)
|
||||
dec = decompress_lz77(enc)
|
||||
if dec != data:
|
||||
print('roundtrip mismatch', file=sys.stderr)
|
||||
return 1
|
||||
elapsed_ms = int((time.perf_counter() - start) * 1000)
|
||||
print(f'ms={elapsed_ms}')
|
||||
return 0
|
||||
|
||||
if mode == 'compress':
|
||||
if len(sys.argv) < 4:
|
||||
print(f'usage: {sys.argv[0]} compress <input.bin> <output.bin>', file=sys.stderr)
|
||||
return 1
|
||||
with open(sys.argv[2], 'rb') as f:
|
||||
data = f.read()
|
||||
out = compress_lz77(data)
|
||||
with open(sys.argv[3], 'wb') as f:
|
||||
f.write(out)
|
||||
return 0
|
||||
|
||||
if mode == 'decompress':
|
||||
if len(sys.argv) < 4:
|
||||
print(f'usage: {sys.argv[0]} decompress <input.bin> <output.bin>', file=sys.stderr)
|
||||
return 1
|
||||
with open(sys.argv[2], 'rb') as f:
|
||||
data = f.read()
|
||||
out = decompress_lz77(data)
|
||||
with open(sys.argv[3], 'wb') as f:
|
||||
f.write(out)
|
||||
return 0
|
||||
|
||||
print(f'unknown mode: {mode}', file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
module main
|
||||
|
||||
import compress.lz
|
||||
import os
|
||||
|
||||
const default_data_size = 512 * 1024
|
||||
|
||||
fn main() {
|
||||
mut data_size := default_data_size
|
||||
if os.args.len > 1 {
|
||||
parsed := int(os.args[1].i32())
|
||||
if parsed > 0 {
|
||||
data_size = parsed
|
||||
}
|
||||
}
|
||||
data := `z`.repeat(data_size).bytes()
|
||||
|
||||
println('LZ interop input: ${data.len} bytes')
|
||||
|
||||
for format in [lz.Format.lz77, .lz78, .lzw, .lz4, .lzss, .lzma, .lzma2, .lzjb] {
|
||||
validate_v_roundtrip(data, format) or {
|
||||
eprintln('V validation failed for ${format}: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
println('V roundtrip (${format}): OK')
|
||||
}
|
||||
|
||||
tmp_dir := os.join_path(os.temp_dir(), 'v_lz_interop')
|
||||
os.mkdir_all(tmp_dir) or {
|
||||
eprintln('Could not create temp directory ${tmp_dir}: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
defer {
|
||||
os.rmdir_all(tmp_dir) or {
|
||||
eprintln('Could not remove temp directory ${tmp_dir}: ${err.msg()}')
|
||||
}
|
||||
}
|
||||
input_path := os.join_path(tmp_dir, 'input.bin')
|
||||
os.write_file_array(input_path, data) or {
|
||||
eprintln('Could not write input file ${input_path}: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
mut c_bin := ''
|
||||
if bin := compile_c_runner() {
|
||||
c_bin = bin
|
||||
} else {
|
||||
eprintln('Skipping C benchmark: ${err.msg()}')
|
||||
}
|
||||
python_ok := has_python3()
|
||||
if !python_ok {
|
||||
eprintln('Skipping Python benchmark: python3 is not available')
|
||||
}
|
||||
|
||||
if c_bin.len > 0 {
|
||||
cross_validate_v_c(c_bin, data, input_path, tmp_dir) or {
|
||||
eprintln('Cross-validation V<->C failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
println('Cross-validation: V<->C compress/decompress OK')
|
||||
} else {
|
||||
println('Cross-validation: skipped V<->C (requires C compiler)')
|
||||
}
|
||||
|
||||
if python_ok {
|
||||
cross_validate_v_python(data, input_path, tmp_dir) or {
|
||||
eprintln('Cross-validation V<->Python failed: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
println('Cross-validation: V<->Python compress/decompress OK')
|
||||
} else {
|
||||
println('Cross-validation: skipped V<->Python (requires python3)')
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_v_roundtrip(data []u8, format lz.Format) ! {
|
||||
encoded := lz.compress(data, format)!
|
||||
decoded := lz.decompress(encoded, format)!
|
||||
if decoded != data {
|
||||
return error('roundtrip mismatch for ${format}')
|
||||
}
|
||||
}
|
||||
|
||||
fn compile_c_runner() !string {
|
||||
cc := choose_cc()
|
||||
if cc.len == 0 {
|
||||
return error('no C compiler found (tried cc, gcc, and clang)')
|
||||
}
|
||||
bin_path := os.join_path(os.temp_dir(), 'lz77_ref_bench')
|
||||
c_src := os.join_path(@DIR, 'lz77_ref.c')
|
||||
compile_cmd := '${cc} -O3 -std=c99 ${os.quoted_path(c_src)} -o ${os.quoted_path(bin_path)}'
|
||||
compile_res := os.execute(compile_cmd)
|
||||
if compile_res.exit_code != 0 {
|
||||
return error('C compile failed: ${compile_res.output.trim_space()}')
|
||||
}
|
||||
return bin_path
|
||||
}
|
||||
|
||||
fn choose_cc() string {
|
||||
for cc in ['cc', 'gcc', 'clang'] {
|
||||
if os.execute('${cc} --version').exit_code == 0 {
|
||||
return cc
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
fn has_python3() bool {
|
||||
return os.execute('python3 --version').exit_code == 0
|
||||
}
|
||||
|
||||
fn cross_validate_v_c(c_bin string, original []u8, input_path string, tmp_dir string) ! {
|
||||
v_encoded := os.join_path(tmp_dir, 'v_encoded.bin')
|
||||
c_decoded := os.join_path(tmp_dir, 'c_decoded.bin')
|
||||
c_encoded := os.join_path(tmp_dir, 'c_encoded.bin')
|
||||
|
||||
v_stream := lz.compress_lz77(original)!
|
||||
os.write_file_array(v_encoded, v_stream)!
|
||||
|
||||
mut res :=
|
||||
os.execute('${os.quoted_path(c_bin)} decompress ${os.quoted_path(v_encoded)} ${os.quoted_path(c_decoded)}')
|
||||
if res.exit_code != 0 {
|
||||
return error('C decompress(V output) failed: ${res.output.trim_space()}')
|
||||
}
|
||||
validate_equal_files(input_path, c_decoded, 'V->C')!
|
||||
|
||||
res =
|
||||
os.execute('${os.quoted_path(c_bin)} compress ${os.quoted_path(input_path)} ${os.quoted_path(c_encoded)}')
|
||||
if res.exit_code != 0 {
|
||||
return error('C compress failed: ${res.output.trim_space()}')
|
||||
}
|
||||
c_encoded_data := os.read_bytes(c_encoded)!
|
||||
v_decoded := lz.decompress_lz77(c_encoded_data)!
|
||||
if v_decoded != original {
|
||||
return error('C->V output mismatch')
|
||||
}
|
||||
}
|
||||
|
||||
fn cross_validate_v_python(original []u8, input_path string, tmp_dir string) ! {
|
||||
v_encoded := os.join_path(tmp_dir, 'v_encoded_for_py.bin')
|
||||
py_decoded := os.join_path(tmp_dir, 'py_decoded.bin')
|
||||
py_encoded := os.join_path(tmp_dir, 'py_encoded.bin')
|
||||
py_script := os.join_path(@DIR, 'lz77_ref.py')
|
||||
|
||||
v_stream := lz.compress_lz77(original)!
|
||||
os.write_file_array(v_encoded, v_stream)!
|
||||
|
||||
mut res :=
|
||||
os.execute('python3 ${os.quoted_path(py_script)} decompress ${os.quoted_path(v_encoded)} ${os.quoted_path(py_decoded)}')
|
||||
if res.exit_code != 0 {
|
||||
return error('Python decompress(V output) failed: ${res.output.trim_space()}')
|
||||
}
|
||||
validate_equal_files(input_path, py_decoded, 'V->Python')!
|
||||
|
||||
res =
|
||||
os.execute('python3 ${os.quoted_path(py_script)} compress ${os.quoted_path(input_path)} ${os.quoted_path(py_encoded)}')
|
||||
if res.exit_code != 0 {
|
||||
return error('Python compress failed: ${res.output.trim_space()}')
|
||||
}
|
||||
py_encoded_data := os.read_bytes(py_encoded)!
|
||||
v_decoded := lz.decompress_lz77(py_encoded_data)!
|
||||
if v_decoded != original {
|
||||
return error('Python->V output mismatch')
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_equal_files(expected_path string, actual_path string, tag string) ! {
|
||||
expected := os.read_bytes(expected_path)!
|
||||
actual := os.read_bytes(actual_path)!
|
||||
if expected != actual {
|
||||
return error('${tag} output mismatch')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
module lz
|
||||
|
||||
// Format identifies which LZ-family codec variant to use.
|
||||
pub enum Format {
|
||||
lz77
|
||||
lz78
|
||||
lzw
|
||||
lz4
|
||||
lzss
|
||||
lzma
|
||||
lzma2
|
||||
lzjb
|
||||
}
|
||||
|
||||
// format_from_string parses a case-insensitive format name.
|
||||
pub fn format_from_string(name string) !Format {
|
||||
key := name.to_lower()
|
||||
return match key {
|
||||
'lz77' { .lz77 }
|
||||
'lz78' { .lz78 }
|
||||
'lzw' { .lzw }
|
||||
'lz4' { .lz4 }
|
||||
'lzss' { .lzss }
|
||||
'lzma' { .lzma }
|
||||
'lzma2' { .lzma2 }
|
||||
'lzjb' { .lzjb }
|
||||
else { return error('unknown lz format: ${name}') }
|
||||
}
|
||||
}
|
||||
|
||||
// compress compresses data with the selected LZ format.
|
||||
pub fn compress(data []u8, format Format) ![]u8 {
|
||||
return match format {
|
||||
.lz77 { compress_lz77(data) }
|
||||
.lz78 { compress_lz78(data) }
|
||||
.lzw { compress_lzw(data) }
|
||||
.lz4 { compress_lz4(data) }
|
||||
.lzss { compress_lzss(data) }
|
||||
.lzma { compress_lzma(data) }
|
||||
.lzma2 { compress_lzma2(data) }
|
||||
.lzjb { compress_lzjb(data) }
|
||||
}
|
||||
}
|
||||
|
||||
// decompress decompresses data with the selected LZ format.
|
||||
pub fn decompress(data []u8, format Format) ![]u8 {
|
||||
return match format {
|
||||
.lz77 { decompress_lz77(data) }
|
||||
.lz78 { decompress_lz78(data) }
|
||||
.lzw { decompress_lzw(data) }
|
||||
.lz4 { decompress_lz4(data) }
|
||||
.lzss { decompress_lzss(data) }
|
||||
.lzma { decompress_lzma(data) }
|
||||
.lzma2 { decompress_lzma2(data) }
|
||||
.lzjb { decompress_lzjb(data) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
module lz
|
||||
|
||||
const lz4_profile = MatchProfile{
|
||||
window: 65535
|
||||
min_match: 4
|
||||
max_match: 130
|
||||
max_literal: 128
|
||||
}
|
||||
|
||||
// compress_lz4 compresses data using a pure-V LZ4-like stream.
|
||||
pub fn compress_lz4(data []u8) ![]u8 {
|
||||
return compress_with_profile(data, lz4_profile, .lz4)
|
||||
}
|
||||
|
||||
// decompress_lz4 decompresses data produced by compress_lz4.
|
||||
pub fn decompress_lz4(data []u8) ![]u8 {
|
||||
return decompress_with_profile(data, lz4_profile, .lz4)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
module lz
|
||||
|
||||
const lz77_profile = MatchProfile{
|
||||
window: 4096
|
||||
min_match: 3
|
||||
max_match: 130
|
||||
max_literal: 128
|
||||
}
|
||||
|
||||
// compress_lz77 compresses data using a pure-V LZ77 style stream.
|
||||
pub fn compress_lz77(data []u8) ![]u8 {
|
||||
return compress_with_profile(data, lz77_profile, .lz77)
|
||||
}
|
||||
|
||||
// decompress_lz77 decompresses data produced by compress_lz77.
|
||||
pub fn decompress_lz77(data []u8) ![]u8 {
|
||||
return decompress_with_profile(data, lz77_profile, .lz77)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
module lz
|
||||
|
||||
// compress_lz78 compresses data using a pure-V LZ78 dictionary stream.
|
||||
pub fn compress_lz78(data []u8) ![]u8 {
|
||||
mut payload := []u8{}
|
||||
mut dict := map[string]int{}
|
||||
mut next_index := 1
|
||||
mut word := []u8{}
|
||||
|
||||
for b in data {
|
||||
mut candidate := word.clone()
|
||||
candidate << b
|
||||
candidate_key := candidate.bytestr()
|
||||
if candidate_key in dict {
|
||||
word = candidate.clone()
|
||||
continue
|
||||
}
|
||||
|
||||
prefix_index := if word.len == 0 { 0 } else { dict[word.bytestr()] }
|
||||
encode_uvarint(mut payload, u64(prefix_index))
|
||||
payload << u8(1)
|
||||
payload << b
|
||||
dict[candidate_key] = next_index
|
||||
next_index++
|
||||
word.clear()
|
||||
}
|
||||
|
||||
if word.len > 0 {
|
||||
final_index := dict[word.bytestr()]
|
||||
encode_uvarint(mut payload, u64(final_index))
|
||||
payload << u8(0)
|
||||
}
|
||||
|
||||
return wrap_payload(.lz78, data, payload)
|
||||
}
|
||||
|
||||
// decompress_lz78 decompresses data produced by compress_lz78.
|
||||
pub fn decompress_lz78(data []u8) ![]u8 {
|
||||
payload, expected_len := unwrap_payload(data, .lz78)!
|
||||
mut out := []u8{cap: int(expected_len)}
|
||||
mut dict := map[int][]u8{}
|
||||
mut next_index := 1
|
||||
mut pos := 0
|
||||
|
||||
for pos < payload.len {
|
||||
prefix, next_pos, ok := decode_uvarint(payload, pos)
|
||||
if !ok {
|
||||
return error('invalid lz78 stream: bad prefix index')
|
||||
}
|
||||
pos = next_pos
|
||||
if pos >= payload.len {
|
||||
return error('invalid lz78 stream: missing suffix flag')
|
||||
}
|
||||
has_suffix := payload[pos]
|
||||
pos++
|
||||
|
||||
mut phrase := if prefix == 0 {
|
||||
[]u8{}
|
||||
} else {
|
||||
if int(prefix) !in dict {
|
||||
return error('invalid lz78 stream: unknown prefix index')
|
||||
}
|
||||
dict[int(prefix)].clone()
|
||||
}
|
||||
|
||||
if has_suffix == 1 {
|
||||
if pos >= payload.len {
|
||||
return error('invalid lz78 stream: missing suffix byte')
|
||||
}
|
||||
phrase << payload[pos]
|
||||
pos++
|
||||
} else if has_suffix != 0 {
|
||||
return error('invalid lz78 stream: bad suffix flag')
|
||||
}
|
||||
|
||||
out << phrase
|
||||
dict[next_index] = phrase
|
||||
next_index++
|
||||
}
|
||||
|
||||
if i64(out.len) != expected_len {
|
||||
return error('invalid lz78 stream: length mismatch')
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
module lz
|
||||
|
||||
const sample_data = ('The quick brown fox jumps over the lazy dog. '.repeat(12) +
|
||||
'aaaaaaaaabbbbbbbbbcccccccccdddddddddeeeeeeeee').bytes()
|
||||
|
||||
fn test_roundtrip_all_formats() {
|
||||
formats := [Format.lz77, .lz78, .lzw, .lz4, .lzss, .lzma, .lzma2, .lzjb]
|
||||
for format in formats {
|
||||
compressed := compress(sample_data, format)!
|
||||
decompressed := decompress(compressed, format)!
|
||||
assert decompressed == sample_data
|
||||
}
|
||||
}
|
||||
|
||||
fn test_format_specific_api_roundtrip() {
|
||||
lz77_data := compress_lz77(sample_data)!
|
||||
assert decompress_lz77(lz77_data)! == sample_data
|
||||
|
||||
lz78_data := compress_lz78(sample_data)!
|
||||
assert decompress_lz78(lz78_data)! == sample_data
|
||||
|
||||
lzw_data := compress_lzw(sample_data)!
|
||||
assert decompress_lzw(lzw_data)! == sample_data
|
||||
|
||||
lz4_data := compress_lz4(sample_data)!
|
||||
assert decompress_lz4(lz4_data)! == sample_data
|
||||
|
||||
lzss_data := compress_lzss(sample_data)!
|
||||
assert decompress_lzss(lzss_data)! == sample_data
|
||||
|
||||
lzma_data := compress_lzma(sample_data)!
|
||||
assert decompress_lzma(lzma_data)! == sample_data
|
||||
|
||||
lzma2_data := compress_lzma2(sample_data)!
|
||||
assert decompress_lzma2(lzma2_data)! == sample_data
|
||||
|
||||
lzjb_data := compress_lzjb(sample_data)!
|
||||
assert decompress_lzjb(lzjb_data)! == sample_data
|
||||
}
|
||||
|
||||
fn test_mismatched_format_fails() {
|
||||
compressed := compress(sample_data, .lz77)!
|
||||
decompress(compressed, .lz4) or {
|
||||
assert err.msg().contains('format mismatch')
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
|
||||
fn test_decoded_length_too_large_fails() {
|
||||
mut corrupt := []u8{}
|
||||
corrupt << stream_magic
|
||||
corrupt << u8(Format.lz77)
|
||||
encode_uvarint(mut corrupt, u64(1) << 31)
|
||||
|
||||
decompress_lz77(corrupt) or {
|
||||
assert err.msg().contains('decoded length too large')
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
|
||||
fn test_match_offset_too_large_fails() {
|
||||
mut corrupt := []u8{}
|
||||
corrupt << stream_magic
|
||||
corrupt << u8(Format.lz77)
|
||||
encode_uvarint(mut corrupt, u64(4))
|
||||
corrupt << u8(0)
|
||||
corrupt << `A`
|
||||
corrupt << u8(0x80)
|
||||
encode_uvarint(mut corrupt, u64(1) << 63)
|
||||
|
||||
decompress_lz77(corrupt) or {
|
||||
assert err.msg().contains('bad match offset')
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
|
||||
fn test_high_entropy_roundtrip_large_window_formats() {
|
||||
mut data := []u8{len: 128 * 1024}
|
||||
mut state := u32(0x9e3779b9)
|
||||
for i in 0 .. data.len {
|
||||
state = state * 1664525 + 1013904223
|
||||
data[i] = u8(state >> 24)
|
||||
}
|
||||
|
||||
for format in [Format.lz4, .lzma, .lzma2] {
|
||||
compressed := compress(data, format)!
|
||||
decompressed := decompress(compressed, format)!
|
||||
assert decompressed == data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
module lz
|
||||
|
||||
const lzjb_profile = MatchProfile{
|
||||
window: 1024
|
||||
min_match: 3
|
||||
max_match: 66
|
||||
max_literal: 128
|
||||
}
|
||||
|
||||
// compress_lzjb compresses data using a pure-V LZJB-like stream.
|
||||
pub fn compress_lzjb(data []u8) ![]u8 {
|
||||
return compress_with_profile(data, lzjb_profile, .lzjb)
|
||||
}
|
||||
|
||||
// decompress_lzjb decompresses data produced by compress_lzjb.
|
||||
pub fn decompress_lzjb(data []u8) ![]u8 {
|
||||
return decompress_with_profile(data, lzjb_profile, .lzjb)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
module lz
|
||||
|
||||
const lzma_profile = MatchProfile{
|
||||
window: 32768
|
||||
min_match: 3
|
||||
max_match: 130
|
||||
max_literal: 128
|
||||
}
|
||||
|
||||
// compress_lzma compresses data using a pure-V LZMA-like stream.
|
||||
pub fn compress_lzma(data []u8) ![]u8 {
|
||||
return compress_with_profile(data, lzma_profile, .lzma)
|
||||
}
|
||||
|
||||
// decompress_lzma decompresses data produced by compress_lzma.
|
||||
pub fn decompress_lzma(data []u8) ![]u8 {
|
||||
return decompress_with_profile(data, lzma_profile, .lzma)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
module lz
|
||||
|
||||
const lzma2_profile = MatchProfile{
|
||||
window: 65535
|
||||
min_match: 3
|
||||
max_match: 130
|
||||
max_literal: 128
|
||||
}
|
||||
|
||||
// compress_lzma2 compresses data using a pure-V LZMA2-like stream.
|
||||
pub fn compress_lzma2(data []u8) ![]u8 {
|
||||
return compress_with_profile(data, lzma2_profile, .lzma2)
|
||||
}
|
||||
|
||||
// decompress_lzma2 decompresses data produced by compress_lzma2.
|
||||
pub fn decompress_lzma2(data []u8) ![]u8 {
|
||||
return decompress_with_profile(data, lzma2_profile, .lzma2)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
module lz
|
||||
|
||||
const lzss_profile = MatchProfile{
|
||||
window: 4096
|
||||
min_match: 3
|
||||
max_match: 130
|
||||
max_literal: 128
|
||||
}
|
||||
|
||||
// compress_lzss compresses data using a pure-V LZSS style stream.
|
||||
pub fn compress_lzss(data []u8) ![]u8 {
|
||||
return compress_with_profile(data, lzss_profile, .lzss)
|
||||
}
|
||||
|
||||
// decompress_lzss decompresses data produced by compress_lzss.
|
||||
pub fn decompress_lzss(data []u8) ![]u8 {
|
||||
return decompress_with_profile(data, lzss_profile, .lzss)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
module lz
|
||||
|
||||
// compress_lzw compresses data using a pure-V LZW dictionary stream.
|
||||
pub fn compress_lzw(data []u8) ![]u8 {
|
||||
mut payload := []u8{}
|
||||
mut dict := map[string]int{}
|
||||
for i in 0 .. 256 {
|
||||
dict[[u8(i)].bytestr()] = i
|
||||
}
|
||||
mut next_code := 256
|
||||
mut word := ''
|
||||
|
||||
for b in data {
|
||||
symbol := [b].bytestr()
|
||||
candidate := word + symbol
|
||||
if candidate in dict {
|
||||
word = candidate
|
||||
continue
|
||||
}
|
||||
if word != '' {
|
||||
encode_uvarint(mut payload, u64(dict[word]))
|
||||
}
|
||||
dict[candidate] = next_code
|
||||
next_code++
|
||||
word = symbol
|
||||
}
|
||||
|
||||
if word != '' {
|
||||
encode_uvarint(mut payload, u64(dict[word]))
|
||||
}
|
||||
|
||||
return wrap_payload(.lzw, data, payload)
|
||||
}
|
||||
|
||||
// decompress_lzw decompresses data produced by compress_lzw.
|
||||
pub fn decompress_lzw(data []u8) ![]u8 {
|
||||
payload, expected_len := unwrap_payload(data, .lzw)!
|
||||
if payload.len == 0 {
|
||||
if expected_len == i64(0) {
|
||||
return []u8{}
|
||||
}
|
||||
return error('invalid lzw stream: missing codes')
|
||||
}
|
||||
|
||||
mut dict := map[int][]u8{}
|
||||
for i in 0 .. 256 {
|
||||
dict[i] = [u8(i)]
|
||||
}
|
||||
mut next_code := 256
|
||||
mut pos := 0
|
||||
|
||||
first_code, next_pos, ok := decode_uvarint(payload, pos)
|
||||
if !ok {
|
||||
return error('invalid lzw stream: bad initial code')
|
||||
}
|
||||
pos = next_pos
|
||||
if int(first_code) !in dict {
|
||||
return error('invalid lzw stream: unknown initial code')
|
||||
}
|
||||
mut word := dict[int(first_code)].clone()
|
||||
mut out := word.clone()
|
||||
|
||||
for pos < payload.len {
|
||||
code_u64, new_pos, ok_code := decode_uvarint(payload, pos)
|
||||
if !ok_code {
|
||||
return error('invalid lzw stream: bad code')
|
||||
}
|
||||
pos = new_pos
|
||||
code := int(code_u64)
|
||||
mut entry := []u8{}
|
||||
if code in dict {
|
||||
entry = dict[code].clone()
|
||||
} else if code == next_code {
|
||||
entry = word.clone()
|
||||
entry << word[0]
|
||||
} else {
|
||||
return error('invalid lzw stream: unknown code')
|
||||
}
|
||||
|
||||
out << entry
|
||||
mut new_entry := word.clone()
|
||||
new_entry << entry[0]
|
||||
dict[next_code] = new_entry
|
||||
next_code++
|
||||
word = entry.clone()
|
||||
}
|
||||
|
||||
if i64(out.len) != expected_len {
|
||||
return error('invalid lzw stream: length mismatch')
|
||||
}
|
||||
return out
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,457 @@
|
||||
// Implementation of Snappy compression fully in V.
|
||||
//
|
||||
// Compatible with the standard Snappy binary format. Implements level-1
|
||||
// (fast) compression using a hash-table approach over 64 KiB blocks.
|
||||
//
|
||||
// Public API:
|
||||
// compress(input []u8) []u8
|
||||
// decompress(input []u8) ![]u8
|
||||
|
||||
module snappy
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const block_size = 1 << 16 // 65 536 — max block processed at once
|
||||
const min_table_size = 1 << 8 // 256
|
||||
const max_table_size = 1 << 15 // 32 768 (fits in u16 alongside block_size-1)
|
||||
const input_margin = 15 // bytes of look-ahead the compressor needs
|
||||
const hash_magic = u32(0x1e35a7bd)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// max_compressed_length returns an upper bound on the compressed size for
|
||||
// `n` bytes of uncompressed input.
|
||||
fn max_compressed_length(n int) int {
|
||||
return 32 + n + n / 6
|
||||
}
|
||||
|
||||
// compress compresses `input` and returns the compressed bytes.
|
||||
pub fn compress(input []u8) []u8 {
|
||||
n := input.len
|
||||
mut out := []u8{cap: max_compressed_length(n)}
|
||||
|
||||
// Write the uncompressed length as a varint header.
|
||||
write_uvarint(mut out, u32(n))
|
||||
|
||||
if n == 0 {
|
||||
return out
|
||||
}
|
||||
|
||||
// Allocate a single hash table reused across every block.
|
||||
mut table := []u16{len: max_table_size}
|
||||
|
||||
mut pos := 0
|
||||
for pos < n {
|
||||
end := if pos + block_size < n { pos + block_size } else { n }
|
||||
block := input[pos..end]
|
||||
|
||||
// Zero only the slice of the table we will actually use.
|
||||
tsize := table_size_for(block.len)
|
||||
for i in 0 .. tsize {
|
||||
table[i] = 0
|
||||
}
|
||||
|
||||
compress_fragment(block, mut out, mut table, tsize)
|
||||
pos = end
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// decompress decompresses a Snappy-compressed slice.
|
||||
// It returns the original bytes, or an error if the data is malformed.
|
||||
pub fn decompress(input []u8) ![]u8 {
|
||||
// Read the varint-encoded uncompressed length from the header.
|
||||
ulen, hdr_end, ok := read_uvarint(input, 0)
|
||||
if !ok {
|
||||
return error('snappy: invalid header varint')
|
||||
}
|
||||
|
||||
// Pre-allocate the exact output size; reject obviously bogus lengths.
|
||||
ilen := int(ulen)
|
||||
if ilen < 0 {
|
||||
return error('snappy: decoded length overflow')
|
||||
}
|
||||
|
||||
// Use a pre-sized buffer with a write cursor instead of dynamic appends.
|
||||
// This eliminates per-byte reallocation checks on every literal and copy.
|
||||
mut out := []u8{len: ilen}
|
||||
mut op := 0
|
||||
mut pos := hdr_end
|
||||
|
||||
for pos < input.len {
|
||||
tag := input[pos]
|
||||
pos++
|
||||
|
||||
tag_type := tag & 0x03
|
||||
|
||||
if tag_type == 0 {
|
||||
// LITERAL
|
||||
lit_len, new_pos, lit_ok := decode_literal_len(input, tag, pos)
|
||||
if !lit_ok {
|
||||
return error('snappy: truncated literal length')
|
||||
}
|
||||
pos = new_pos
|
||||
if pos + lit_len > input.len {
|
||||
return error('snappy: literal extends past input')
|
||||
}
|
||||
if op + lit_len > ilen {
|
||||
return error('snappy: output exceeds declared length')
|
||||
}
|
||||
for i in 0 .. lit_len {
|
||||
out[op + i] = input[pos + i]
|
||||
}
|
||||
op += lit_len
|
||||
pos += lit_len
|
||||
} else if tag_type == 1 {
|
||||
// COPY_1 (2-byte offset)
|
||||
if pos >= input.len {
|
||||
return error('snappy: truncated copy-1 tag')
|
||||
}
|
||||
length := int((tag >> 2) & 0x07) + 4
|
||||
offset := int(u32(tag & 0xe0) << 3 | u32(input[pos]))
|
||||
pos++
|
||||
if offset == 0 {
|
||||
return error('snappy: zero offset in copy-1')
|
||||
}
|
||||
op = expand_copy_into(mut out, op, ilen, offset, length)!
|
||||
} else if tag_type == 2 {
|
||||
// COPY_2 (3-byte tag)
|
||||
if pos + 1 >= input.len {
|
||||
return error('snappy: truncated copy-2 tag')
|
||||
}
|
||||
length := int((tag >> 2) & 0x3f) + 1
|
||||
offset := int(u32(input[pos]) | u32(input[pos + 1]) << 8)
|
||||
pos += 2
|
||||
if offset == 0 {
|
||||
return error('snappy: zero offset in copy-2')
|
||||
}
|
||||
op = expand_copy_into(mut out, op, ilen, offset, length)!
|
||||
} else {
|
||||
// COPY_4 (5-byte tag)
|
||||
if pos + 3 >= input.len {
|
||||
return error('snappy: truncated copy-4 tag')
|
||||
}
|
||||
length := int((tag >> 2) & 0x3f) + 1
|
||||
offset := int(u32(input[pos]) | u32(input[pos + 1]) << 8 | u32(input[pos + 2]) << 16 | u32(input[
|
||||
pos + 3]) << 24)
|
||||
pos += 4
|
||||
if offset == 0 {
|
||||
return error('snappy: zero offset in copy-4')
|
||||
}
|
||||
op = expand_copy_into(mut out, op, ilen, offset, length)!
|
||||
}
|
||||
}
|
||||
|
||||
if op != ilen {
|
||||
return error('snappy: decompressed length mismatch: got ${op}, want ${ilen}')
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compression internals
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// compress_fragment compresses a single block (≤ 64 KiB) into `out`.
|
||||
// `table` must have at least `tsize` zeroed entries (tsize is a power of 2).
|
||||
fn compress_fragment(input []u8, mut out []u8, mut table []u16, tsize int) {
|
||||
n := input.len
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// tbits is the number of bits needed to index `tsize` entries; used
|
||||
// to right-shift a 32-bit hash down to the table index.
|
||||
tbits := ilog2(tsize)
|
||||
|
||||
// ip — current input position (index into `input`)
|
||||
// lit — start of current literal run
|
||||
mut ip := 0
|
||||
mut lit := 0
|
||||
|
||||
// We need at least `input_margin` bytes of look-ahead for the main loop.
|
||||
// Very short blocks go to the slow literal-only path.
|
||||
if n >= input_margin {
|
||||
// Emit one forced literal so we have a non-empty literal buffer,
|
||||
// then begin the hash loop one byte in.
|
||||
ip = 1
|
||||
|
||||
mut next_hash := mix32(load32le(input, ip), tbits)
|
||||
|
||||
outer: for {
|
||||
// skip grows when matches are not found — we scan with
|
||||
// increasing stride to amortize the cost.
|
||||
mut skip := 32
|
||||
mut candidate := 0
|
||||
mut next_ip := ip
|
||||
|
||||
// Inner probe loop: walk forward until we find a 4-byte match.
|
||||
for {
|
||||
ip = next_ip
|
||||
hash := next_hash
|
||||
bytes_until_skip := skip >> 5
|
||||
skip++
|
||||
next_ip = ip + bytes_until_skip
|
||||
if next_ip > n - input_margin {
|
||||
break outer
|
||||
}
|
||||
next_hash = mix32(load32le(input, next_ip), tbits)
|
||||
candidate = int(table[hash])
|
||||
table[hash] = u16(ip)
|
||||
if load32le(input, ip) == load32le(input, candidate) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// We have a 4-byte match at [ip] == [candidate].
|
||||
// Emit the literal bytes accumulated since `lit`.
|
||||
write_literal(mut out, input[lit..ip])
|
||||
|
||||
// Extend the match as far as possible.
|
||||
for {
|
||||
matched := 4 + find_match_length(input, candidate + 4, ip + 4, n)
|
||||
write_copy(mut out, ip - candidate, matched)
|
||||
ip += matched
|
||||
lit = ip
|
||||
|
||||
if ip >= n - input_margin {
|
||||
break outer
|
||||
}
|
||||
|
||||
// Look for a consecutive match immediately after the copy.
|
||||
// Note: mix32 already returns a value in [0, tsize), so the
|
||||
// redundant `& tmask` from the original code is removed.
|
||||
prev_hash := mix32(load32le(input, ip - 1), tbits)
|
||||
table[prev_hash] = u16(ip - 1)
|
||||
cur_hash := mix32(load32le(input, ip), tbits)
|
||||
candidate = int(table[cur_hash])
|
||||
table[cur_hash] = u16(ip)
|
||||
if load32le(input, ip) != load32le(input, candidate) {
|
||||
// seed next_hash for the outer loop's next iteration
|
||||
next_hash = mix32(load32le(input, ip + 1), tbits)
|
||||
ip++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit whatever literal bytes remain.
|
||||
write_literal(mut out, input[lit..])
|
||||
}
|
||||
|
||||
// write_literal emits a LITERAL tag followed by the literal bytes.
|
||||
@[inline]
|
||||
fn write_literal(mut out []u8, lit []u8) {
|
||||
n := lit.len
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
n1 := n - 1
|
||||
if n1 < 60 {
|
||||
out << u8(u32(n1) << 2) // tag byte encodes length-1 directly
|
||||
} else {
|
||||
w := byte_width(n1)
|
||||
out << u8(u32(59 + w) << 2) // tag signals 1..4 extra length bytes
|
||||
mut v := n1
|
||||
for _ in 0 .. w {
|
||||
out << u8(v & 0xff)
|
||||
v >>= 8
|
||||
}
|
||||
}
|
||||
out << lit
|
||||
}
|
||||
|
||||
// write_copy chooses and emits the most compact COPY tag for the given
|
||||
// (offset, length) back-reference.
|
||||
@[inline]
|
||||
fn write_copy(mut out []u8, offset int, length int) {
|
||||
mut remaining := length
|
||||
for remaining > 0 {
|
||||
// COPY_2 handles lengths 1..64; use it as the general case.
|
||||
// COPY_1 saves a byte when offset < 2048 and length is 4..11.
|
||||
chunk := if remaining > 64 { 64 } else { remaining }
|
||||
if offset < 2048 && chunk >= 4 && chunk <= 11 {
|
||||
// COPY_1: 2 bytes total
|
||||
out << u8(u32(1) | (u32(chunk - 4) << 2) | ((u32(offset) >> 3) & u32(0xe0)))
|
||||
out << u8(offset & 0xff)
|
||||
} else if offset < 65536 {
|
||||
// COPY_2: 3 bytes total
|
||||
out << u8(2 | (u32(chunk - 1) << 2))
|
||||
out << u8(offset & 0xff)
|
||||
out << u8((offset >> 8) & 0xff)
|
||||
} else {
|
||||
// COPY_4: 5 bytes total (never reached for 64 KiB blocks, left for completeness)
|
||||
out << u8(3 | (u32(chunk - 1) << 2))
|
||||
out << u8(offset & 0xff)
|
||||
out << u8((offset >> 8) & 0xff)
|
||||
out << u8((offset >> 16) & 0xff)
|
||||
out << u8((offset >> 24) & 0xff)
|
||||
}
|
||||
remaining -= chunk
|
||||
}
|
||||
}
|
||||
|
||||
// find_match_length returns how many bytes starting at s1 and s2 are equal,
|
||||
// stopping before `limit`. Uses 4-byte word comparisons when possible.
|
||||
@[inline]
|
||||
fn find_match_length(input []u8, s1 int, s2 int, limit int) int {
|
||||
mut i := 0
|
||||
// Compare 4 bytes at a time while there is room for a full word.
|
||||
for s2 + i + 4 <= limit && s1 + i + 4 <= limit {
|
||||
if load32le(input, s1 + i) != load32le(input, s2 + i) {
|
||||
// Mismatch somewhere in this word — find the exact byte.
|
||||
for s2 + i < limit && input[s1 + i] == input[s2 + i] {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
i += 4
|
||||
}
|
||||
// Handle the remaining 0..3 tail bytes.
|
||||
for s2 + i < limit && input[s1 + i] == input[s2 + i] {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Decompression internals
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// expand_copy_into copies `length` bytes from `offset` bytes before the
|
||||
// write cursor `op` in the pre-allocated `out` buffer. Returns the new
|
||||
// write cursor. Handles the overlapping (run-length) case correctly.
|
||||
@[inline]
|
||||
fn expand_copy_into(mut out []u8, op int, cap_ int, offset int, length int) !int {
|
||||
src := op - offset
|
||||
if src < 0 {
|
||||
return error('snappy: copy offset past beginning of output')
|
||||
}
|
||||
if op + length > cap_ {
|
||||
return error('snappy: output exceeds declared length')
|
||||
}
|
||||
// Both overlapping and non-overlapping copies read from indices
|
||||
// that were written in earlier iterations, so a forward byte loop
|
||||
// is correct in all cases (and required for the RLE / overlap case).
|
||||
for i in 0 .. length {
|
||||
out[op + i] = out[src + i]
|
||||
}
|
||||
return op + length
|
||||
}
|
||||
|
||||
// decode_literal_len decodes the length (in bytes) of a literal run from the
|
||||
// tag byte and returns (length, new_pos, ok). `pos` should point to the
|
||||
// byte immediately after the tag.
|
||||
@[inline]
|
||||
fn decode_literal_len(data []u8, tag u8, pos int) (int, int, bool) {
|
||||
n1 := int(tag >> 2) // length - 1, or 60..63 for extended form
|
||||
if n1 < 60 {
|
||||
return n1 + 1, pos, true
|
||||
}
|
||||
extra := n1 - 59 // number of extra bytes (1..4)
|
||||
if pos + extra > data.len {
|
||||
return 0, pos, false
|
||||
}
|
||||
mut val := 0
|
||||
for i in 0 .. extra {
|
||||
val |= int(u32(data[pos + i]) << u32(8 * i))
|
||||
}
|
||||
return val + 1, pos + extra, true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Varint (unsigned LEB-128, protobuf-style)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// write_uvarint appends `val` as a variable-length unsigned integer.
|
||||
@[inline]
|
||||
fn write_uvarint(mut out []u8, val u32) {
|
||||
mut v := val
|
||||
for v >= 0x80 {
|
||||
out << u8(v & 0x7f | 0x80)
|
||||
v >>= 7
|
||||
}
|
||||
out << u8(v)
|
||||
}
|
||||
|
||||
// read_uvarint decodes a varint from `data` starting at `pos`.
|
||||
// Returns (value, new_pos, ok).
|
||||
@[inline]
|
||||
fn read_uvarint(data []u8, pos int) (u32, int, bool) {
|
||||
mut val := u32(0)
|
||||
mut shift := u32(0)
|
||||
mut i := pos
|
||||
for i < data.len && i - pos < 5 {
|
||||
b := data[i]
|
||||
i++
|
||||
if shift == 28 && b > 0x0f {
|
||||
return 0, pos, false
|
||||
}
|
||||
val |= u32(b & 0x7f) << shift
|
||||
if b & 0x80 == 0 {
|
||||
return val, i, true
|
||||
}
|
||||
shift += 7
|
||||
}
|
||||
return 0, pos, false
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utilities
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// load32le reads four bytes from `data[pos..]` as a little-endian u32.
|
||||
@[inline]
|
||||
fn load32le(data []u8, pos int) u32 {
|
||||
return u32(data[pos]) | u32(data[pos + 1]) << 8 | u32(data[pos + 2]) << 16 | u32(data[pos + 3]) << 24
|
||||
}
|
||||
|
||||
// mix32 hashes a 32-bit word down to a `tbits`-bit index using a
|
||||
// multiplicative hash (same constant as the reference implementation).
|
||||
@[inline]
|
||||
fn mix32(v u32, tbits int) int {
|
||||
return int((v * hash_magic) >> u32(32 - tbits))
|
||||
}
|
||||
|
||||
// table_size_for returns the smallest power-of-two hash table size that
|
||||
// comfortably covers `n` bytes, clamped to [min_table_size, max_table_size].
|
||||
fn table_size_for(n int) int {
|
||||
mut size := min_table_size
|
||||
for size < n && size < max_table_size {
|
||||
size <<= 1
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// ilog2 returns floor(log2(n)) for n ≥ 1.
|
||||
@[inline]
|
||||
fn ilog2(n int) int {
|
||||
mut v := n
|
||||
mut r := 0
|
||||
for v > 1 {
|
||||
v >>= 1
|
||||
r++
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// byte_width returns the number of bytes needed to represent `n` (1..4).
|
||||
@[inline]
|
||||
fn byte_width(n int) int {
|
||||
if n < (1 << 8) {
|
||||
return 1
|
||||
}
|
||||
if n < (1 << 16) {
|
||||
return 2
|
||||
}
|
||||
if n < (1 << 24) {
|
||||
return 3
|
||||
}
|
||||
return 4
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
module snappy
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn round_trip(label string, input []u8) ! {
|
||||
compressed := compress(input)
|
||||
decompressed := decompress(compressed)!
|
||||
assert decompressed == input, '${label}: round-trip mismatch (input len=${input.len})'
|
||||
bound := max_compressed_length(input.len)
|
||||
assert compressed.len <= bound, '${label}: compressed size ${compressed.len} exceeds bound ${bound}'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn test_empty() {
|
||||
round_trip('empty', []u8{}) or { panic(err) }
|
||||
}
|
||||
|
||||
fn test_single_byte() {
|
||||
round_trip('single byte', [u8(0x42)]) or { panic(err) }
|
||||
}
|
||||
|
||||
fn test_two_bytes() {
|
||||
round_trip('two bytes', [u8(0xde), 0xad]) or { panic(err) }
|
||||
}
|
||||
|
||||
fn test_all_zeros() {
|
||||
// Highly compressible — long run of the same byte.
|
||||
input := []u8{len: 4096, init: 0}
|
||||
compressed := compress(input)
|
||||
decompressed := decompress(compressed) or { panic(err) }
|
||||
assert decompressed == input, 'all-zeros round-trip failed'
|
||||
// Should compress to much less than input.
|
||||
assert compressed.len < input.len / 4, 'all-zeros should compress well'
|
||||
}
|
||||
|
||||
fn test_all_same_nonzero() {
|
||||
input := []u8{len: 1024, init: u8(0xff)}
|
||||
round_trip('all-0xff', input) or { panic(err) }
|
||||
}
|
||||
|
||||
fn test_incompressible() {
|
||||
// Pseudo-random bytes — poor compressibility.
|
||||
mut input := []u8{len: 512}
|
||||
mut seed := u32(0xdeadbeef)
|
||||
for i in 0 .. input.len {
|
||||
seed = seed * 1664525 + 1013904223 // LCG
|
||||
input[i] = u8(seed >> 24)
|
||||
}
|
||||
round_trip('pseudo-random', input) or { panic(err) }
|
||||
}
|
||||
|
||||
fn test_repeated_pattern() {
|
||||
// Short repeating pattern — exercises the copy back-reference paths.
|
||||
pattern := 'abcdefgh'.bytes()
|
||||
mut input := []u8{cap: pattern.len * 200}
|
||||
for _ in 0 .. 200 {
|
||||
input << pattern
|
||||
}
|
||||
compressed := compress(input)
|
||||
decompressed := decompress(compressed) or { panic(err) }
|
||||
assert decompressed == input, 'repeated-pattern round-trip failed'
|
||||
assert compressed.len < input.len / 2, 'repeated pattern should compress well'
|
||||
}
|
||||
|
||||
fn test_lorem_ipsum() {
|
||||
lorem := 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' +
|
||||
'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' +
|
||||
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris ' +
|
||||
'nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in ' +
|
||||
'reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla ' +
|
||||
'pariatur. Excepteur sint occaecat cupidatat non proident, sunt in ' +
|
||||
'culpa qui officia deserunt mollit anim id est laborum.'
|
||||
mut input := []u8{cap: lorem.len * 50}
|
||||
for _ in 0 .. 50 {
|
||||
input << lorem.bytes()
|
||||
}
|
||||
round_trip('lorem-ipsum-x50', input) or { panic(err) }
|
||||
}
|
||||
|
||||
fn test_multi_block() {
|
||||
// Larger than one 64 KiB block to exercise the block-splitting path.
|
||||
mut input := []u8{len: 200_000, init: u8(index & 0xff)}
|
||||
round_trip('multi-block', input) or { panic(err) }
|
||||
}
|
||||
|
||||
fn test_max_compressed_length_bound() {
|
||||
for n in [0, 1, 100, 1000, 65536, 200_000] {
|
||||
bound := max_compressed_length(n)
|
||||
mut input := []u8{len: n, init: u8(index & 0xff)}
|
||||
compressed := compress(input)
|
||||
assert compressed.len <= bound, 'max_compressed_length(${n})=${bound} exceeded by ${compressed.len}'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_decompress_invalid_varint() {
|
||||
// A stream that never terminates the varint (all bytes have MSB set).
|
||||
bad := [u8(0xff), 0xff, 0xff, 0xff, 0xff, 0xff]
|
||||
decompress(bad) or { return } // expected — an error is correct
|
||||
panic('expected decompress to fail on invalid varint')
|
||||
}
|
||||
|
||||
fn test_decompress_truncated_literal() {
|
||||
// Header says 10 bytes uncompressed; literal tag claims 10 bytes
|
||||
// but the data is truncated.
|
||||
bad := [u8(10), u8(9 << 2), u8(0x41)] // varint(10) + tag + 1 byte
|
||||
decompress(bad) or { return }
|
||||
panic('expected decompress to fail on truncated literal')
|
||||
}
|
||||
|
||||
fn test_decompress_bad_offset() {
|
||||
// A COPY_2 that references before the start of output.
|
||||
// varint(5) tag=COPY_2(len=5, _) offset=999 (past output)
|
||||
bad := [u8(5), u8(2 | ((5 - 1) << 2)), u8(0xe7), u8(0x03)]
|
||||
decompress(bad) or { return }
|
||||
panic('expected decompress to fail on out-of-range offset')
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// CRC32C — Castagnoli variant used by the Snappy framing format.
|
||||
//
|
||||
// The lookup table is pre-computed at compile time (reflected polynomial
|
||||
// 0x82F63B78). No runtime initialisation is needed.
|
||||
|
||||
module snappy
|
||||
|
||||
// crc32c_table holds all 256 pre-computed CRC32C remainders.
|
||||
// Generated with polynomial 0x82F63B78 (reflected form of 0x1EDC6F41).
|
||||
const crc32c_table = [u32(0x00000000), u32(0xf26b8303), u32(0xe13b70f7), u32(0x1350f3f4),
|
||||
u32(0xc79a971f), u32(0x35f1141c), u32(0x26a1e7e8), u32(0xd4ca64eb), u32(0x8ad958cf),
|
||||
u32(0x78b2dbcc), u32(0x6be22838), u32(0x9989ab3b), u32(0x4d43cfd0), u32(0xbf284cd3),
|
||||
u32(0xac78bf27), u32(0x5e133c24), u32(0x105ec76f), u32(0xe235446c), u32(0xf165b798),
|
||||
u32(0x030e349b), u32(0xd7c45070), u32(0x25afd373), u32(0x36ff2087), u32(0xc494a384),
|
||||
u32(0x9a879fa0), u32(0x68ec1ca3), u32(0x7bbcef57), u32(0x89d76c54), u32(0x5d1d08bf),
|
||||
u32(0xaf768bbc), u32(0xbc267848), u32(0x4e4dfb4b), u32(0x20bd8ede), u32(0xd2d60ddd),
|
||||
u32(0xc186fe29), u32(0x33ed7d2a), u32(0xe72719c1), u32(0x154c9ac2), u32(0x061c6936),
|
||||
u32(0xf477ea35), u32(0xaa64d611), u32(0x580f5512), u32(0x4b5fa6e6), u32(0xb93425e5),
|
||||
u32(0x6dfe410e), u32(0x9f95c20d), u32(0x8cc531f9), u32(0x7eaeb2fa), u32(0x30e349b1),
|
||||
u32(0xc288cab2), u32(0xd1d83946), u32(0x23b3ba45), u32(0xf779deae), u32(0x05125dad),
|
||||
u32(0x1642ae59), u32(0xe4292d5a), u32(0xba3a117e), u32(0x4851927d), u32(0x5b016189),
|
||||
u32(0xa96ae28a), u32(0x7da08661), u32(0x8fcb0562), u32(0x9c9bf696), u32(0x6ef07595),
|
||||
u32(0x417b1dbc), u32(0xb3109ebf), u32(0xa0406d4b), u32(0x522bee48), u32(0x86e18aa3),
|
||||
u32(0x748a09a0), u32(0x67dafa54), u32(0x95b17957), u32(0xcba24573), u32(0x39c9c670),
|
||||
u32(0x2a993584), u32(0xd8f2b687), u32(0x0c38d26c), u32(0xfe53516f), u32(0xed03a29b),
|
||||
u32(0x1f682198), u32(0x5125dad3), u32(0xa34e59d0), u32(0xb01eaa24), u32(0x42752927),
|
||||
u32(0x96bf4dcc), u32(0x64d4cecf), u32(0x77843d3b), u32(0x85efbe38), u32(0xdbfc821c),
|
||||
u32(0x2997011f), u32(0x3ac7f2eb), u32(0xc8ac71e8), u32(0x1c661503), u32(0xee0d9600),
|
||||
u32(0xfd5d65f4), u32(0x0f36e6f7), u32(0x61c69362), u32(0x93ad1061), u32(0x80fde395),
|
||||
u32(0x72966096), u32(0xa65c047d), u32(0x5437877e), u32(0x4767748a), u32(0xb50cf789),
|
||||
u32(0xeb1fcbad), u32(0x197448ae), u32(0x0a24bb5a), u32(0xf84f3859), u32(0x2c855cb2),
|
||||
u32(0xdeeedfb1), u32(0xcdbe2c45), u32(0x3fd5af46), u32(0x7198540d), u32(0x83f3d70e),
|
||||
u32(0x90a324fa), u32(0x62c8a7f9), u32(0xb602c312), u32(0x44694011), u32(0x5739b3e5),
|
||||
u32(0xa55230e6), u32(0xfb410cc2), u32(0x092a8fc1), u32(0x1a7a7c35), u32(0xe811ff36),
|
||||
u32(0x3cdb9bdd), u32(0xceb018de), u32(0xdde0eb2a), u32(0x2f8b6829), u32(0x82f63b78),
|
||||
u32(0x709db87b), u32(0x63cd4b8f), u32(0x91a6c88c), u32(0x456cac67), u32(0xb7072f64),
|
||||
u32(0xa457dc90), u32(0x563c5f93), u32(0x082f63b7), u32(0xfa44e0b4), u32(0xe9141340),
|
||||
u32(0x1b7f9043), u32(0xcfb5f4a8), u32(0x3dde77ab), u32(0x2e8e845f), u32(0xdce5075c),
|
||||
u32(0x92a8fc17), u32(0x60c37f14), u32(0x73938ce0), u32(0x81f80fe3), u32(0x55326b08),
|
||||
u32(0xa759e80b), u32(0xb4091bff), u32(0x466298fc), u32(0x1871a4d8), u32(0xea1a27db),
|
||||
u32(0xf94ad42f), u32(0x0b21572c), u32(0xdfeb33c7), u32(0x2d80b0c4), u32(0x3ed04330),
|
||||
u32(0xccbbc033), u32(0xa24bb5a6), u32(0x502036a5), u32(0x4370c551), u32(0xb11b4652),
|
||||
u32(0x65d122b9), u32(0x97baa1ba), u32(0x84ea524e), u32(0x7681d14d), u32(0x2892ed69),
|
||||
u32(0xdaf96e6a), u32(0xc9a99d9e), u32(0x3bc21e9d), u32(0xef087a76), u32(0x1d63f975),
|
||||
u32(0x0e330a81), u32(0xfc588982), u32(0xb21572c9), u32(0x407ef1ca), u32(0x532e023e),
|
||||
u32(0xa145813d), u32(0x758fe5d6), u32(0x87e466d5), u32(0x94b49521), u32(0x66df1622),
|
||||
u32(0x38cc2a06), u32(0xcaa7a905), u32(0xd9f75af1), u32(0x2b9cd9f2), u32(0xff56bd19),
|
||||
u32(0x0d3d3e1a), u32(0x1e6dcdee), u32(0xec064eed), u32(0xc38d26c4), u32(0x31e6a5c7),
|
||||
u32(0x22b65633), u32(0xd0ddd530), u32(0x0417b1db), u32(0xf67c32d8), u32(0xe52cc12c),
|
||||
u32(0x1747422f), u32(0x49547e0b), u32(0xbb3ffd08), u32(0xa86f0efc), u32(0x5a048dff),
|
||||
u32(0x8ecee914), u32(0x7ca56a17), u32(0x6ff599e3), u32(0x9d9e1ae0), u32(0xd3d3e1ab),
|
||||
u32(0x21b862a8), u32(0x32e8915c), u32(0xc083125f), u32(0x144976b4), u32(0xe622f5b7),
|
||||
u32(0xf5720643), u32(0x07198540), u32(0x590ab964), u32(0xab613a67), u32(0xb831c993),
|
||||
u32(0x4a5a4a90), u32(0x9e902e7b), u32(0x6cfbad78), u32(0x7fab5e8c), u32(0x8dc0dd8f),
|
||||
u32(0xe330a81a), u32(0x115b2b19), u32(0x020bd8ed), u32(0xf0605bee), u32(0x24aa3f05),
|
||||
u32(0xd6c1bc06), u32(0xc5914ff2), u32(0x37faccf1), u32(0x69e9f0d5), u32(0x9b8273d6),
|
||||
u32(0x88d28022), u32(0x7ab90321), u32(0xae7367ca), u32(0x5c18e4c9), u32(0x4f48173d),
|
||||
u32(0xbd23943e), u32(0xf36e6f75), u32(0x0105ec76), u32(0x12551f82), u32(0xe03e9c81),
|
||||
u32(0x34f4f86a), u32(0xc69f7b69), u32(0xd5cf889d), u32(0x27a40b9e), u32(0x79b737ba),
|
||||
u32(0x8bdcb4b9), u32(0x988c474d), u32(0x6ae7c44e), u32(0xbe2da0a5), u32(0x4c4623a6),
|
||||
u32(0x5f16d052), u32(0xad7d5351)]!
|
||||
|
||||
// crc32c returns the CRC-32C checksum of `data`.
|
||||
pub fn crc32c(data []u8) u32 {
|
||||
mut crc := u32(0xffffffff)
|
||||
for b in data {
|
||||
crc = crc32c_table[(crc ^ u32(b)) & 0xff] ^ (crc >> 8)
|
||||
}
|
||||
return crc ^ 0xffffffff
|
||||
}
|
||||
|
||||
// mask_crc applies the Snappy framing mask to a raw CRC32C value:
|
||||
// ((crc >> 15) | (crc << 17)) + 0xa282ead8
|
||||
// This avoids accidental matches between the checksum and compressed data.
|
||||
@[inline]
|
||||
fn mask_crc(crc u32) u32 {
|
||||
return ((crc >> 15) | (crc << 17)) + u32(0xa282ead8)
|
||||
}
|
||||
|
||||
// unmask_crc reverses the masking applied by mask_crc.
|
||||
@[inline]
|
||||
fn unmask_crc(masked u32) u32 {
|
||||
rot := masked - u32(0xa282ead8)
|
||||
return (rot >> 17) | (rot << 15)
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
// Snappy framing format — streaming wrapper around raw Snappy blocks.
|
||||
//
|
||||
// Spec: https://github.com/google/snappy/blob/main/framing_format.txt
|
||||
//
|
||||
// A framed stream is a sequence of typed chunks, each with a 4-byte header:
|
||||
//
|
||||
// [type: 1 byte] [length: 3 bytes, little-endian]
|
||||
//
|
||||
// Chunk types:
|
||||
// 0xff Stream identifier — must be the first chunk; payload = "sNaPpY"
|
||||
// 0x00 Compressed data — 4-byte masked CRC32C + Snappy-compressed block
|
||||
// 0x01 Uncompressed data — 4-byte masked CRC32C + raw block
|
||||
// 0xfe Padding — payload is ignored
|
||||
// 0x80..0xcd Reserved skippable — may be silently ignored
|
||||
// 0x02..0x7f Reserved unskippable — must be treated as an error
|
||||
//
|
||||
// Public API:
|
||||
// encode_stream(input []u8) []u8
|
||||
// decode_stream(input []u8) ![]u8
|
||||
//
|
||||
// For incremental (push-based) I/O see StreamEncoder and StreamDecoder.
|
||||
|
||||
module snappy
|
||||
|
||||
import math
|
||||
import io
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const stream_identifier_magic = 'sNaPpY'.bytes()
|
||||
const chunk_type_stream_id = u8(0xff)
|
||||
const chunk_type_compressed = u8(0x00)
|
||||
const chunk_type_uncompressed = u8(0x01)
|
||||
const chunk_type_padding = u8(0xfe)
|
||||
|
||||
// Pre encoded stream header
|
||||
const stream_header = [
|
||||
chunk_type_stream_id,
|
||||
u8(stream_identifier_magic.len & 0xff),
|
||||
u8((stream_identifier_magic.len >> 8) & 0xff),
|
||||
u8((stream_identifier_magic.len >> 16) & 0xff),
|
||||
stream_identifier_magic[0],
|
||||
stream_identifier_magic[1],
|
||||
stream_identifier_magic[2],
|
||||
stream_identifier_magic[3],
|
||||
stream_identifier_magic[4],
|
||||
stream_identifier_magic[5],
|
||||
]
|
||||
|
||||
// max_chunk_data_size is the largest block of uncompressed data per chunk.
|
||||
const max_chunk_data_size = 65536
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// One-shot API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// encode_stream wraps `input` in the Snappy framing format.
|
||||
// It returns the complete byte stream. Input is split into <=64 KiB blocks automatically.
|
||||
pub fn encode_stream(input []u8) []u8 {
|
||||
// Upper bound: identifier chunk + one compressed chunk per block.
|
||||
// Each block: 4-byte header + 4-byte CRC + max_compressed_length.
|
||||
blocks := (input.len + max_chunk_data_size - 1) / max_chunk_data_size
|
||||
mut out := []u8{cap: 10 + blocks * (8 + max_compressed_length(max_chunk_data_size))}
|
||||
|
||||
out << stream_header
|
||||
|
||||
mut pos := 0
|
||||
for pos < input.len {
|
||||
end := math.min(pos + max_chunk_data_size, input.len)
|
||||
write_data_chunk(mut out, input[pos..end])
|
||||
pos = end
|
||||
}
|
||||
|
||||
// Always emit at least the identifier for empty input.
|
||||
return out
|
||||
}
|
||||
|
||||
// decode_stream decodes a Snappy framing stream.
|
||||
// It returns the concatenated uncompressed data, or an error if the stream is malformed.
|
||||
pub fn decode_stream(input []u8) ![]u8 {
|
||||
// The compressed stream is typically smaller than the uncompressed data,
|
||||
// so use a larger initial capacity estimate.
|
||||
mut out := []u8{cap: input.len * 2}
|
||||
mut pos := 0
|
||||
|
||||
// The very first chunk must be the stream identifier.
|
||||
pos = read_stream_identifier(input, pos)!
|
||||
|
||||
for pos < input.len {
|
||||
chunk_type, chunk_len, new_pos := read_chunk_header(input, pos)!
|
||||
pos = new_pos
|
||||
|
||||
if pos + chunk_len > input.len {
|
||||
return error('snappy framing: chunk body truncated at pos ${pos}')
|
||||
}
|
||||
chunk_data := input[pos..pos + chunk_len]
|
||||
pos += chunk_len
|
||||
|
||||
if chunk_type == chunk_type_compressed {
|
||||
uncompressed := decode_compressed_chunk(chunk_data)!
|
||||
out << uncompressed
|
||||
} else if chunk_type == chunk_type_uncompressed {
|
||||
raw := decode_uncompressed_chunk(chunk_data)!
|
||||
out << raw
|
||||
} else if chunk_type == chunk_type_padding || int(chunk_type) >= 0x80 {
|
||||
// 0xfe = padding, 0x80..0xfd = reserved skippable; ignore.
|
||||
continue
|
||||
} else {
|
||||
// 0x02..0x7f — reserved unskippable; must error.
|
||||
return error('snappy framing: unsupported unskippable chunk type 0x${chunk_type:02x}')
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Incremental encoder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// StreamEncoder holds state for incremental framing-format encoding.
|
||||
// Call write() one or more times, then close() to flush the final chunk.
|
||||
// Take the encoded bytes using read().
|
||||
pub struct StreamEncoder {
|
||||
mut:
|
||||
buf []u8 // pending uncompressed bytes
|
||||
out []u8 = stream_header // accumulated encoded output, initialized with stream identifier
|
||||
closed bool
|
||||
}
|
||||
|
||||
// write appends `buf` to the encoder, flushing complete 64 KiB chunks.
|
||||
pub fn (mut enc StreamEncoder) write(buf []u8) !int {
|
||||
if enc.closed {
|
||||
return error('snappy: write on a closed stream encoder')
|
||||
}
|
||||
|
||||
enc.buf << buf
|
||||
for enc.buf.len >= max_chunk_data_size {
|
||||
block := enc.buf[..max_chunk_data_size]
|
||||
write_data_chunk(mut enc.out, block)
|
||||
enc.buf = enc.buf[max_chunk_data_size..].clone()
|
||||
}
|
||||
|
||||
return buf.len
|
||||
}
|
||||
|
||||
pub fn (mut enc StreamEncoder) read(mut buf []u8) !int {
|
||||
if enc.out.len == 0 {
|
||||
if enc.closed {
|
||||
return io.Eof{}
|
||||
}
|
||||
return 0 // not finished yet, just no data ready, the caller should feed more
|
||||
}
|
||||
|
||||
n := copy(mut buf, enc.out)
|
||||
|
||||
if n == enc.out.len {
|
||||
enc.out = []u8{}
|
||||
} else {
|
||||
enc.out = enc.out[n..].clone()
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// close flushes any remaining buffered bytes and returns the complete stream.
|
||||
// The encoder must not be used after this call.
|
||||
pub fn (mut enc StreamEncoder) close() {
|
||||
if enc.closed {
|
||||
return
|
||||
}
|
||||
|
||||
if enc.buf.len > 0 {
|
||||
write_data_chunk(mut enc.out, enc.buf)
|
||||
enc.buf = []
|
||||
}
|
||||
enc.closed = true
|
||||
}
|
||||
|
||||
// peek returns the encoded bytes produced so far without flushing.
|
||||
pub fn (enc StreamEncoder) peek() []u8 {
|
||||
return enc.out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Incremental decoder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// StreamDecoder holds state for incremental framing-format decoding.
|
||||
// Call write() with successive chunks of compressed input, and call read() to read the decoded bytes.
|
||||
// The writer must call close() when it finishes writing the bytes.
|
||||
pub struct StreamDecoder {
|
||||
mut:
|
||||
buf []u8 // unparsed compressed bytes
|
||||
offset int // read position within buf (avoids repeated cloning)
|
||||
identified bool // have we seen the stream identifier chunk?
|
||||
err ?IError
|
||||
output []u8 // decoded bytes ready for the caller
|
||||
closed bool
|
||||
}
|
||||
|
||||
// write appends `buf` to the decoder's input buffer.
|
||||
// It processes as many complete chunks as possible and errors if the stream is malformed.
|
||||
pub fn (mut dec StreamDecoder) write(buf []u8) !int {
|
||||
if dec.closed {
|
||||
return error('snappy: write to closed stream decoder')
|
||||
}
|
||||
if err := dec.err {
|
||||
return err
|
||||
}
|
||||
dec.buf << buf
|
||||
|
||||
for {
|
||||
remaining := dec.buf.len - dec.offset
|
||||
// Need at least 4 bytes for the chunk header.
|
||||
if remaining < 4 {
|
||||
break
|
||||
}
|
||||
chunk_type, chunk_len, hdr_end := read_chunk_header(dec.buf, dec.offset) or {
|
||||
dec_err := error('snappy: failed while reading chunk header: ${err}')
|
||||
dec.err = dec_err
|
||||
return dec_err
|
||||
}
|
||||
if hdr_end + chunk_len > dec.buf.len {
|
||||
break // wait for more data
|
||||
}
|
||||
chunk_data := dec.buf[hdr_end..hdr_end + chunk_len]
|
||||
|
||||
if !dec.identified {
|
||||
if chunk_type != chunk_type_stream_id {
|
||||
dec_err := error('snappy framing: stream does not begin with identifier chunk')
|
||||
dec.err = dec_err
|
||||
return dec_err
|
||||
}
|
||||
if chunk_len != stream_identifier_magic.len {
|
||||
dec_err := error('snappy framing: bad identifier chunk length ${chunk_len}')
|
||||
dec.err = dec_err
|
||||
return dec_err
|
||||
}
|
||||
if chunk_data != stream_identifier_magic {
|
||||
dec_err := error('snappy framing: bad stream identifier payload')
|
||||
dec.err = dec_err
|
||||
return dec_err
|
||||
}
|
||||
dec.identified = true
|
||||
} else if chunk_type == chunk_type_compressed {
|
||||
uncompressed := decode_compressed_chunk(chunk_data) or {
|
||||
dec_err := error('snappy: failed while decoding compressed chunk: ${err}')
|
||||
dec.err = dec_err
|
||||
return dec_err
|
||||
}
|
||||
dec.output << uncompressed
|
||||
} else if chunk_type == chunk_type_uncompressed {
|
||||
raw := decode_uncompressed_chunk(chunk_data) or {
|
||||
dec_err := error('snappy: failed while decoding uncompressed chunk: ${err}')
|
||||
dec.err = dec_err
|
||||
return dec_err
|
||||
}
|
||||
dec.output << raw
|
||||
} else if chunk_type == chunk_type_padding || int(chunk_type) >= 0x80 {
|
||||
// skip
|
||||
} else {
|
||||
dec_err :=
|
||||
error('snappy framing: unsupported unskippable chunk type 0x${chunk_type:02x}')
|
||||
dec.err = dec_err
|
||||
return dec_err
|
||||
}
|
||||
|
||||
dec.offset = hdr_end + chunk_len
|
||||
}
|
||||
|
||||
// Compact the buffer: discard consumed bytes only when worthwhile
|
||||
// to avoid copying on every single chunk.
|
||||
if dec.offset > 0 {
|
||||
if dec.offset == dec.buf.len {
|
||||
dec.buf = []u8{}
|
||||
} else {
|
||||
dec.buf = dec.buf[dec.offset..].clone()
|
||||
}
|
||||
dec.offset = 0
|
||||
}
|
||||
|
||||
return buf.len
|
||||
}
|
||||
|
||||
// read removes and returns decoded bytes.
|
||||
// When the output buffer is empty it surfaces any error recorded during write()/close().
|
||||
// Only then does it return io.Eof{} for a cleanly terminated stream.
|
||||
pub fn (mut dec StreamDecoder) read(mut buf []u8) !int {
|
||||
// Always drain already-decoded output first so the caller never loses data.
|
||||
if dec.output.len > 0 {
|
||||
n := copy(mut buf, dec.output)
|
||||
dec.output = dec.output[n..]
|
||||
return n
|
||||
}
|
||||
|
||||
if dec.closed {
|
||||
// Surface truncation / malformed-stream errors before EOF.
|
||||
if err := dec.err {
|
||||
return err
|
||||
}
|
||||
return io.Eof{}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// close marks the stream finished and validates its terminal state.
|
||||
// Returns an error if the stream was truncated or the identifier was never received.
|
||||
// The error is also stored internally so read() will surface it after draining output.
|
||||
pub fn (mut dec StreamDecoder) close() ! {
|
||||
if dec.closed {
|
||||
return
|
||||
}
|
||||
dec.closed = true
|
||||
|
||||
remaining := dec.buf.len - dec.offset
|
||||
|
||||
if remaining > 0 {
|
||||
err :=
|
||||
error('snappy framing: stream closed with ${remaining} unprocessed bytes (truncated chunk)')
|
||||
dec.err = err
|
||||
return err
|
||||
}
|
||||
|
||||
if !dec.identified && dec.buf.len > 0 {
|
||||
err := error('snappy framing: stream closed before identifier chunk was received')
|
||||
dec.err = err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Chunk writers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// write_data_chunk compresses `block`, chooses the compact representation,
|
||||
// and appends the chunk (type + header + masked CRC + payload) to `out`.
|
||||
fn write_data_chunk(mut out []u8, block []u8) {
|
||||
crc := mask_crc(crc32c(block))
|
||||
compressed := compress(block)
|
||||
|
||||
// Use the compressed form only when it is strictly smaller.
|
||||
if compressed.len < block.len {
|
||||
// Compressed chunk: type=0x00, length=4+compressed.len
|
||||
out << chunk_type_compressed
|
||||
write_u24le(mut out, 4 + compressed.len)
|
||||
write_u32le(mut out, crc)
|
||||
out << compressed
|
||||
} else {
|
||||
// Uncompressed chunk: type=0x01, length=4+block.len
|
||||
out << chunk_type_uncompressed
|
||||
write_u24le(mut out, 4 + block.len)
|
||||
write_u32le(mut out, crc)
|
||||
out << block
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Chunk readers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// read_stream_identifier reads and validates the identifier chunk starting at
|
||||
// `pos` and returns the position immediately after it.
|
||||
fn read_stream_identifier(data []u8, pos int) !int {
|
||||
chunk_type, chunk_len, new_pos := read_chunk_header(data, pos)!
|
||||
if chunk_type != chunk_type_stream_id {
|
||||
return error('snappy framing: stream must begin with identifier chunk, got 0x${chunk_type:02x}')
|
||||
}
|
||||
if chunk_len != stream_identifier_magic.len {
|
||||
return error('snappy framing: bad identifier chunk length ${chunk_len}')
|
||||
}
|
||||
if new_pos + chunk_len > data.len {
|
||||
return error('snappy framing: identifier chunk truncated')
|
||||
}
|
||||
magic := data[new_pos..new_pos + chunk_len]
|
||||
if magic != stream_identifier_magic {
|
||||
return error('snappy framing: bad stream identifier magic')
|
||||
}
|
||||
return new_pos + chunk_len
|
||||
}
|
||||
|
||||
// read_chunk_header reads the 4-byte chunk header at `pos` and returns
|
||||
// (chunk_type, chunk_body_length, pos_after_header).
|
||||
@[inline]
|
||||
fn read_chunk_header(data []u8, pos int) !(u8, int, int) {
|
||||
if pos + 4 > data.len {
|
||||
return error('snappy framing: truncated chunk header at pos ${pos}')
|
||||
}
|
||||
chunk_type := data[pos]
|
||||
chunk_len := int(u32(data[pos + 1]) | u32(data[pos + 2]) << 8 | u32(data[pos + 3]) << 16)
|
||||
return chunk_type, chunk_len, pos + 4
|
||||
}
|
||||
|
||||
// decode_compressed_chunk decompresses a compressed data chunk payload
|
||||
// (CRC32C[4] + snappy_data[…]) and verifies the checksum.
|
||||
fn decode_compressed_chunk(payload []u8) ![]u8 {
|
||||
if payload.len < 4 {
|
||||
return error('snappy framing: compressed chunk too short')
|
||||
}
|
||||
stored_masked_crc := u32(payload[0]) | u32(payload[1]) << 8 | u32(payload[2]) << 16 | u32(payload[3]) << 24
|
||||
uncompressed := decompress(payload[4..])!
|
||||
actual_crc := mask_crc(crc32c(uncompressed))
|
||||
if actual_crc != stored_masked_crc {
|
||||
return error('snappy framing: CRC32C mismatch in compressed chunk')
|
||||
}
|
||||
return uncompressed
|
||||
}
|
||||
|
||||
// decode_uncompressed_chunk validates and returns the payload of an
|
||||
// uncompressed data chunk (CRC32C[4] + raw_data[…]).
|
||||
fn decode_uncompressed_chunk(payload []u8) ![]u8 {
|
||||
if payload.len < 4 {
|
||||
return error('snappy framing: uncompressed chunk too short')
|
||||
}
|
||||
stored_masked_crc := u32(payload[0]) | u32(payload[1]) << 8 | u32(payload[2]) << 16 | u32(payload[3]) << 24
|
||||
raw := payload[4..].clone()
|
||||
actual_crc := mask_crc(crc32c(raw))
|
||||
if actual_crc != stored_masked_crc {
|
||||
return error('snappy framing: CRC32C mismatch in uncompressed chunk')
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Little-endian integer writers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@[inline]
|
||||
fn write_u24le(mut out []u8, v int) {
|
||||
out << u8(v & 0xff)
|
||||
out << u8((v >> 8) & 0xff)
|
||||
out << u8((v >> 16) & 0xff)
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn write_u32le(mut out []u8, v u32) {
|
||||
out << u8(v & 0xff)
|
||||
out << u8((v >> 8) & 0xff)
|
||||
out << u8((v >> 16) & 0xff)
|
||||
out << u8((v >> 24) & 0xff)
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
module snappy
|
||||
|
||||
import arrays
|
||||
import io
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn stream_round_trip(label string, input []u8) ! {
|
||||
encoded := encode_stream(input)
|
||||
decoded := decode_stream(encoded)!
|
||||
|
||||
assert decoded == input, '${label}: stream round-trip mismatch (len=${input.len})'
|
||||
}
|
||||
|
||||
// drain_decoder closes dec and returns all decoded bytes accumulated so far.
|
||||
fn drain_decoder(mut dec StreamDecoder) []u8 {
|
||||
dec.close() or { panic(err) }
|
||||
mut collected := []u8{}
|
||||
mut chunk := []u8{len: 4096}
|
||||
for {
|
||||
n := dec.read(mut chunk) or { break }
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
collected << chunk[..n]
|
||||
}
|
||||
return collected
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CRC32C tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn test_crc32c_empty() {
|
||||
// Known value: CRC32C("") = 0x00000000
|
||||
assert crc32c([]) == u32(0x00000000)
|
||||
}
|
||||
|
||||
fn test_crc32c_known_vector() {
|
||||
// CRC32C("123456789") = 0xE3069283 — standard test vector.
|
||||
assert crc32c('123456789'.bytes()) == u32(0xe3069283)
|
||||
}
|
||||
|
||||
fn test_mask_unmask_roundtrip() {
|
||||
for v in [u32(0), u32(1), u32(0xdeadbeef), u32(0xffffffff)] {
|
||||
assert unmask_crc(mask_crc(v)) == v, 'mask/unmask failed for 0x${v:08x}'
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// One-shot encode / decode
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn test_stream_empty() {
|
||||
stream_round_trip('empty', []u8{}) or { panic(err) }
|
||||
}
|
||||
|
||||
fn test_stream_single_byte() {
|
||||
stream_round_trip('single byte', [u8(0x42)]) or { panic(err) }
|
||||
}
|
||||
|
||||
fn test_stream_small() {
|
||||
stream_round_trip('hello world', 'Hello, World!'.bytes()) or { panic(err) }
|
||||
}
|
||||
|
||||
fn test_stream_compressible() {
|
||||
input := []u8{len: 8192, init: u8(index % 7)}
|
||||
stream_round_trip('compressible', input) or { panic(err) }
|
||||
}
|
||||
|
||||
fn test_stream_incompressible() {
|
||||
mut input := []u8{len: 2048}
|
||||
mut seed := u32(0xc0ffee42)
|
||||
for i in 0 .. input.len {
|
||||
seed = seed * 1664525 + 1013904223
|
||||
input[i] = u8(seed >> 24)
|
||||
}
|
||||
stream_round_trip('incompressible', input) or { panic(err) }
|
||||
}
|
||||
|
||||
fn test_stream_exactly_one_block() {
|
||||
// Exactly max_chunk_data_size bytes — exercises the boundary condition.
|
||||
input := []u8{len: max_chunk_data_size, init: u8(index & 0xff)}
|
||||
stream_round_trip('one-full-block', input) or { panic(err) }
|
||||
}
|
||||
|
||||
fn test_stream_multi_block() {
|
||||
// Spans multiple 64 KiB blocks.
|
||||
input := []u8{len: max_chunk_data_size * 3 + 100, init: u8(index & 0xff)}
|
||||
stream_round_trip('multi-block', input) or { panic(err) }
|
||||
}
|
||||
|
||||
fn test_stream_identifier_present() {
|
||||
// The encoded stream must start with the identifier chunk.
|
||||
encoded := encode_stream('test'.bytes())
|
||||
assert encoded.len >= 10, 'stream too short to contain identifier'
|
||||
assert encoded[0] == chunk_type_stream_id, 'first byte must be stream identifier type'
|
||||
// The 6-byte magic must follow the 4-byte header.
|
||||
magic := encoded[4..10]
|
||||
assert magic == stream_identifier_magic, 'stream identifier magic mismatch'
|
||||
}
|
||||
|
||||
fn test_stream_crc_mismatch_detected() {
|
||||
mut encoded := encode_stream('hello'.bytes())
|
||||
// Corrupt a byte in the CRC field of the first data chunk (after the
|
||||
// 10-byte identifier: 4-byte header + 4-byte CRC starts at byte 14).
|
||||
if encoded.len > 14 {
|
||||
encoded[14] ^= 0xff
|
||||
}
|
||||
decode_stream(encoded) or { return } // expected error
|
||||
panic('expected decode_stream to fail on CRC mismatch')
|
||||
}
|
||||
|
||||
fn test_stream_missing_identifier() {
|
||||
// A stream that starts with a compressed chunk instead of the identifier.
|
||||
bad := [u8(chunk_type_compressed), 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||
decode_stream(bad) or { return }
|
||||
panic('expected decode_stream to fail without identifier chunk')
|
||||
}
|
||||
|
||||
fn test_stream_unskippable_reserved_chunk() {
|
||||
mut s := encode_stream('hello'.bytes())
|
||||
// Inject a reserved unskippable chunk (type=0x02) before the data.
|
||||
injected := [u8(0x02), 0x01, 0x00, 0x00, u8(0x00)] // type + 3-byte len=1 + 1 payload byte
|
||||
// Insert after the 10-byte stream identifier.
|
||||
s = arrays.reduce([s[..10], injected, s[10..]], fn (acc []u8, x []u8) []u8 {
|
||||
mut out := acc.clone()
|
||||
out << x
|
||||
return out
|
||||
})!
|
||||
decode_stream(s) or { return }
|
||||
panic('expected decode_stream to fail on unskippable reserved chunk')
|
||||
}
|
||||
|
||||
fn test_stream_padding_skipped() {
|
||||
// Build a stream manually: identifier + padding + compressed data.
|
||||
mut s := []u8{}
|
||||
// Stream identifier
|
||||
s << chunk_type_stream_id
|
||||
write_u24le(mut s, stream_identifier_magic.len)
|
||||
s << stream_identifier_magic
|
||||
// Padding chunk (type=0xfe, length=4, 4 zero bytes)
|
||||
s << chunk_type_padding
|
||||
write_u24le(mut s, 4)
|
||||
s << [u8(0), 0, 0, 0]
|
||||
// A real data chunk
|
||||
inner := encode_stream('after padding'.bytes())
|
||||
// Skip the inner identifier (first 10 bytes) and append the data chunk.
|
||||
s << inner[10..]
|
||||
|
||||
decoded := decode_stream(s) or { panic(err) }
|
||||
assert decoded == 'after padding'.bytes(), 'padding chunk should be silently skipped'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Incremental encoder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn test_incremental_encoder_matches_oneshot() {
|
||||
input := []u8{len: max_chunk_data_size + 500, init: u8(index & 0xff)}
|
||||
|
||||
// One-shot
|
||||
oneshot := encode_stream(input)
|
||||
|
||||
// Incremental: feed in 1000-byte pieces via write/close/peek.
|
||||
mut enc := StreamEncoder{}
|
||||
mut pos := 0
|
||||
for pos < input.len {
|
||||
end := if pos + 1000 < input.len { pos + 1000 } else { input.len }
|
||||
enc.write(input[pos..end]) or { panic(err) }
|
||||
pos = end
|
||||
}
|
||||
enc.close()
|
||||
incremental := enc.peek()
|
||||
|
||||
assert incremental == oneshot, 'incremental encoder output differs from one-shot'
|
||||
}
|
||||
|
||||
fn test_incremental_encoder_decodable() {
|
||||
input := 'Streaming is fun!'.bytes().repeat(3000)
|
||||
|
||||
mut enc := StreamEncoder{}
|
||||
enc.write(input[..input.len / 2]) or { panic(err) }
|
||||
enc.write(input[input.len / 2..]) or { panic(err) }
|
||||
enc.close()
|
||||
stream := enc.peek()
|
||||
|
||||
decoded := decode_stream(stream) or { panic(err) }
|
||||
assert decoded == input, 'incremental encoder produced undecodable stream'
|
||||
}
|
||||
|
||||
fn test_incremental_encoder_write_after_close_errors() {
|
||||
// write() on a closed encoder must return an error.
|
||||
mut enc := StreamEncoder{}
|
||||
enc.write('hello'.bytes()) or { panic(err) }
|
||||
enc.close()
|
||||
enc.write('world'.bytes()) or { return } // expected error
|
||||
panic('expected write on closed encoder to fail')
|
||||
}
|
||||
|
||||
fn test_incremental_encoder_read_drains_output() {
|
||||
// read() should drain bytes from the encoded output incrementally.
|
||||
input := []u8{len: max_chunk_data_size + 100, init: u8(index & 0xff)}
|
||||
mut enc := StreamEncoder{}
|
||||
enc.write(input) or { panic(err) }
|
||||
enc.close()
|
||||
|
||||
// Drain via read() in small pieces and reassemble.
|
||||
mut collected := []u8{}
|
||||
mut chunk := []u8{len: 256}
|
||||
for {
|
||||
n := enc.read(mut chunk) or { break }
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
collected << chunk[..n]
|
||||
}
|
||||
|
||||
expected := encode_stream(input)
|
||||
assert collected == expected, 'read() did not produce the same bytes as encode_stream'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Incremental decoder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn test_incremental_decoder_byte_by_byte() {
|
||||
input := 'Byte by byte delivery'.bytes().repeat(500)
|
||||
stream := encode_stream(input)
|
||||
|
||||
mut dec := StreamDecoder{}
|
||||
for b in stream {
|
||||
dec.write([b]) or { panic(err) }
|
||||
}
|
||||
assert drain_decoder(mut dec) == input, 'incremental decoder failed (byte-by-byte)'
|
||||
}
|
||||
|
||||
fn test_incremental_decoder_large_chunks() {
|
||||
input := []u8{len: max_chunk_data_size * 2 + 99, init: u8(index % 13)}
|
||||
stream := encode_stream(input)
|
||||
|
||||
mut dec := StreamDecoder{}
|
||||
mut pos := 0
|
||||
for pos < stream.len {
|
||||
end := if pos + 8192 < stream.len { pos + 8192 } else { stream.len }
|
||||
dec.write(stream[pos..end]) or { panic(err) }
|
||||
pos = end
|
||||
}
|
||||
assert drain_decoder(mut dec) == input, 'incremental decoder failed (large chunks)'
|
||||
}
|
||||
|
||||
fn test_incremental_decoder_read_drains_output() {
|
||||
// A first read() call should return all decoded bytes; a second call on a
|
||||
// closed decoder with no remaining output should return io.Eof.
|
||||
stream := encode_stream('hello world'.bytes())
|
||||
mut dec := StreamDecoder{}
|
||||
dec.write(stream) or { panic(err) }
|
||||
dec.close() or { panic(err) }
|
||||
|
||||
mut buf := []u8{len: 4096}
|
||||
|
||||
// First read — should yield the decoded payload.
|
||||
n := dec.read(mut buf) or { panic(err) }
|
||||
assert buf[..n] == 'hello world'.bytes(), 'first read should return decoded bytes'
|
||||
|
||||
// Second read — decoder is closed and output is empty; expect Eof.
|
||||
dec.read(mut buf) or {
|
||||
assert err is io.Eof, 'expected io.Eof after output is drained, got: ${err}'
|
||||
return
|
||||
}
|
||||
panic('expected io.Eof on second read after output is drained')
|
||||
}
|
||||
|
||||
fn test_incremental_decoder_write_after_close_errors() {
|
||||
// write() on a closed decoder must return an error.
|
||||
stream := encode_stream('hello'.bytes())
|
||||
mut dec := StreamDecoder{}
|
||||
dec.write(stream) or { panic(err) }
|
||||
dec.close() or { panic(err) }
|
||||
dec.write(stream) or { return } // expected error
|
||||
panic('expected write on closed decoder to fail')
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
module snappy
|
||||
|
||||
import os
|
||||
import benchmark
|
||||
|
||||
const samples_folder = os.join_path(os.dir(@FILE), 'samples')
|
||||
|
||||
fn s(fname string) string {
|
||||
return os.join_path(samples_folder, fname)
|
||||
}
|
||||
|
||||
fn test_framing_compress_decompress_alice() {
|
||||
alice := os.read_bytes(s('alice29.txt'))!
|
||||
|
||||
alice_compressed := encode_stream(alice)
|
||||
assert decode_stream(alice_compressed)! == alice, 'compressed alice should be the same as decompressed'
|
||||
}
|
||||
|
||||
fn test_precompressed_framing_alice() {
|
||||
alice_compressed := os.read_bytes(s('alice29_framing.snappy'))!
|
||||
alice := os.read_bytes(s('alice29.txt'))!
|
||||
|
||||
// Normalize line endings before comparing
|
||||
decompressed_str := decode_stream(alice_compressed)!.bytestr().replace('\r\n', '\n')
|
||||
alice_str := alice.bytestr().replace('\r\n', '\n')
|
||||
|
||||
assert decompressed_str == alice_str, 'compressed alice should be the same as decompressed'
|
||||
}
|
||||
|
||||
fn test_benchmark_framing_alice() {
|
||||
alice := os.read_bytes(s('alice29.txt'))!
|
||||
|
||||
mut b := benchmark.start()
|
||||
|
||||
encode_stream(alice)
|
||||
|
||||
b.measure('framing_alice')
|
||||
}
|
||||
|
||||
fn test_benchmark_block_alice() {
|
||||
alice := os.read_bytes(s('alice29.txt'))!
|
||||
|
||||
mut b := benchmark.start()
|
||||
|
||||
compress(alice)
|
||||
|
||||
b.measure('block_alice')
|
||||
}
|
||||
|
||||
fn test_block_compress_decompress_alice() {
|
||||
alice := os.read_bytes(s('alice29.txt'))!
|
||||
|
||||
alice_compressed := compress(alice)
|
||||
assert decompress(alice_compressed)! == alice, 'compressed alice should be the same as decompressed'
|
||||
}
|
||||
|
||||
fn test_precompressed_block_alice() {
|
||||
alice_compressed := os.read_bytes(s('alice29_block.snappy'))!
|
||||
alice := os.read_bytes(s('alice29.txt'))!
|
||||
|
||||
decompressed := decompress(alice_compressed)!
|
||||
|
||||
// Normalize line endings before comparing
|
||||
decompressed_str := decompressed.bytestr().replace('\r\n', '\n')
|
||||
alice_str := alice.bytestr().replace('\r\n', '\n')
|
||||
|
||||
assert decompressed_str == alice_str, 'compressed alice should be the same as decompressed'
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
## Description
|
||||
|
||||
`szip` is a thin wrapper around [miniz.h](https://github.com/richgel999/miniz),
|
||||
which in turn is "Single C source file zlib-replacement library,
|
||||
originally from code.google.com/p/miniz".
|
||||
It provides utility functions for reading/writing .zip files.
|
||||
@@ -0,0 +1,317 @@
|
||||
module szip
|
||||
|
||||
import os
|
||||
|
||||
#flag -I @VEXEROOT/thirdparty/zip
|
||||
#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES
|
||||
#include "zip.c"
|
||||
|
||||
@[params]
|
||||
pub struct ZipFolderOptions {
|
||||
pub:
|
||||
omit_empty_folders bool
|
||||
}
|
||||
|
||||
pub struct C.zip_t {
|
||||
}
|
||||
|
||||
pub type Zip = C.zip_t
|
||||
|
||||
pub type Fn_on_extract_entry = fn (&&char, &&char) int
|
||||
|
||||
fn C.zip_open(&char, i32, char) &Zip
|
||||
|
||||
fn C.zip_close(&Zip)
|
||||
|
||||
fn C.zip_entry_open(&Zip, &u8) i32
|
||||
|
||||
fn C.zip_entry_openbyindex(&Zip, usize) i32
|
||||
|
||||
fn C.zip_entry_close(&Zip) i32
|
||||
|
||||
fn C.zip_entry_name(&Zip) &u8
|
||||
|
||||
fn C.zip_entry_index(&Zip) i32
|
||||
|
||||
fn C.zip_entry_isdir(&Zip) i32
|
||||
|
||||
fn C.zip_entry_size(&Zip) u64
|
||||
|
||||
fn C.zip_entry_crc32(&Zip) u32
|
||||
|
||||
fn C.zip_entry_write(&Zip, voidptr, usize) i32
|
||||
|
||||
fn C.zip_entry_fwrite(&Zip, &char) i32
|
||||
|
||||
fn C.zip_entry_read(&Zip, &voidptr, &usize) i32
|
||||
|
||||
fn C.zip_entry_noallocread(&Zip, voidptr, usize) i32
|
||||
|
||||
fn C.zip_entry_fread(&Zip, &char) i32
|
||||
|
||||
fn C.zip_entries_total(&Zip) i32
|
||||
|
||||
fn C.zip_extract(&char, &char, Fn_on_extract_entry, voidptr) i32
|
||||
|
||||
fn cb_zip_extract(_filename &&char, _arg &&char) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
pub enum CompressionLevel {
|
||||
no_compression = C.MZ_NO_COMPRESSION
|
||||
best_speed = C.MZ_BEST_SPEED
|
||||
best_compression = C.MZ_BEST_COMPRESSION
|
||||
uber_compression = C.MZ_UBER_COMPRESSION
|
||||
default_level = C.MZ_DEFAULT_LEVEL
|
||||
default_compression = C.MZ_DEFAULT_COMPRESSION
|
||||
}
|
||||
|
||||
// OpenMode lists the opening modes.
|
||||
// .write: opens a file for reading/extracting (the file must exists).
|
||||
// .read_only: creates an empty file for writing.
|
||||
// .append: appends to an existing archive.
|
||||
pub enum OpenMode {
|
||||
write
|
||||
read_only
|
||||
append
|
||||
}
|
||||
|
||||
@[inline]
|
||||
fn (om OpenMode) to_u8() u8 {
|
||||
return match om {
|
||||
.write {
|
||||
`w`
|
||||
}
|
||||
.read_only {
|
||||
`r`
|
||||
}
|
||||
.append {
|
||||
`a`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// open opens zip archive with compression level using the given mode.
|
||||
// name: the name of the zip file to open.
|
||||
// level: can be any value of the CompressionLevel enum.
|
||||
// mode: can be any value of the OpenMode enum.
|
||||
pub fn open(name string, level CompressionLevel, mode OpenMode) !&Zip {
|
||||
if name == '' {
|
||||
return error('szip: name of file empty')
|
||||
}
|
||||
p_zip := unsafe { &Zip(C.zip_open(&char(name.str), int(level), char(mode.to_u8()))) }
|
||||
if isnil(p_zip) {
|
||||
return error('szip: cannot open/create/append new zip archive')
|
||||
}
|
||||
return p_zip
|
||||
}
|
||||
|
||||
// close closes the zip archive, releases resources - always finalize.
|
||||
@[inline]
|
||||
pub fn (mut z Zip) close() {
|
||||
C.zip_close(z)
|
||||
}
|
||||
|
||||
// open_entry opens an entry by name in the zip archive.
|
||||
// For zip archive opened in 'w' or 'a' mode the function will append
|
||||
// a new entry. In readonly mode the function tries to locate the entry
|
||||
// in global dictionary.
|
||||
pub fn (mut zentry Zip) open_entry(name string) ! {
|
||||
res := C.zip_entry_open(zentry, &char(name.str))
|
||||
if res == -1 {
|
||||
return error('szip: cannot open archive entry')
|
||||
}
|
||||
}
|
||||
|
||||
// open_entry_by_index opens an entry by index in the archive.
|
||||
pub fn (mut z Zip) open_entry_by_index(index int) ! {
|
||||
res := C.zip_entry_openbyindex(z, index)
|
||||
if res == -1 {
|
||||
return error('szip: cannot open archive entry at index ${index}')
|
||||
}
|
||||
}
|
||||
|
||||
// close_entry closes a zip entry, flushes buffer and releases resources.
|
||||
@[inline]
|
||||
pub fn (mut zentry Zip) close_entry() {
|
||||
C.zip_entry_close(zentry)
|
||||
}
|
||||
|
||||
// name returns a local name of the current zip entry.
|
||||
// The main difference between user's entry name and local entry name
|
||||
// is optional relative path.
|
||||
// Following .ZIP File Format Specification - the path stored MUST not contain
|
||||
// a drive or device letter, or a leading slash.
|
||||
// All slashes MUST be forward slashes '/' as opposed to backwards slashes '\'
|
||||
// for compatibility with Amiga and UNIX file systems etc.
|
||||
pub fn (mut zentry Zip) name() string {
|
||||
name := unsafe { &u8(C.zip_entry_name(zentry)) }
|
||||
if name == 0 {
|
||||
return ''
|
||||
}
|
||||
return unsafe { name.vstring() }
|
||||
}
|
||||
|
||||
// index returns an index of the current zip entry.
|
||||
pub fn (mut zentry Zip) index() !int {
|
||||
index := int(C.zip_entry_index(zentry))
|
||||
if index == -1 {
|
||||
return error('szip: cannot get current index of zip entry')
|
||||
}
|
||||
return index // must be check for INVALID_VALUE
|
||||
}
|
||||
|
||||
// is_dir determines if the current zip entry is a directory entry.
|
||||
pub fn (mut zentry Zip) is_dir() !bool {
|
||||
isdir := C.zip_entry_isdir(zentry)
|
||||
if isdir < 0 {
|
||||
return error('szip: cannot check entry type')
|
||||
}
|
||||
return isdir == 1
|
||||
}
|
||||
|
||||
// size returns an uncompressed size of the current zip entry.
|
||||
@[inline]
|
||||
pub fn (mut zentry Zip) size() u64 {
|
||||
return C.zip_entry_size(zentry)
|
||||
}
|
||||
|
||||
// crc32 returns CRC-32 checksum of the current zip entry.
|
||||
@[inline]
|
||||
pub fn (mut zentry Zip) crc32() u32 {
|
||||
return C.zip_entry_crc32(zentry)
|
||||
}
|
||||
|
||||
// write_entry compresses an input buffer for the current zip entry.
|
||||
pub fn (mut zentry Zip) write_entry(data []u8) ! {
|
||||
if data.len > 0 && int(data[0] & 0xff) == -1 {
|
||||
return error('szip: cannot write entry')
|
||||
}
|
||||
res := C.zip_entry_write(zentry, data.data, data.len)
|
||||
if res != 0 {
|
||||
return error('szip: failed to write entry')
|
||||
}
|
||||
}
|
||||
|
||||
// create_entry compresses a file for the current zip entry.
|
||||
pub fn (mut zentry Zip) create_entry(name string) ! {
|
||||
res := C.zip_entry_fwrite(zentry, &char(name.str))
|
||||
if res != 0 {
|
||||
return error('szip: failed to create entry')
|
||||
}
|
||||
}
|
||||
|
||||
// read_entry extracts the current zip entry into output buffer.
|
||||
// The function allocates sufficient memory for an output buffer.
|
||||
// NOTE: remember to release the memory allocated for an output buffer.
|
||||
// for large entries, please take a look at zip_entry_extract function.
|
||||
pub fn (mut zentry Zip) read_entry() !voidptr {
|
||||
mut buf := &u8(unsafe { nil })
|
||||
mut bsize := usize(0)
|
||||
res := C.zip_entry_read(zentry, unsafe { &voidptr(&buf) }, &bsize)
|
||||
if res == -1 {
|
||||
return error('szip: cannot read properly data from entry')
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// read_entry_buf extracts the current zip entry into user specified buffer.
|
||||
pub fn (mut zentry Zip) read_entry_buf(buf voidptr, in_bsize int) !int {
|
||||
bsize := usize(in_bsize)
|
||||
res := C.zip_entry_noallocread(zentry, buf, bsize)
|
||||
if res == -1 {
|
||||
return error('szip: cannot read properly data from entry')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// extract_entry extracts the current zip entry into output file.
|
||||
pub fn (mut zentry Zip) extract_entry(path string) ! {
|
||||
res := C.zip_entry_fread(zentry, &char(path.str))
|
||||
if res != 0 {
|
||||
return error('szip: failed to extract entry')
|
||||
}
|
||||
}
|
||||
|
||||
// extract zip file to directory.
|
||||
pub fn extract_zip_to_dir(file string, dir string) !bool {
|
||||
if C.access(&char(dir.str), 0) == -1 {
|
||||
return error('szip: cannot open directory for extracting, directory not exists')
|
||||
}
|
||||
res := C.zip_extract(&char(file.str), &char(dir.str), cb_zip_extract, 0)
|
||||
return res == 0
|
||||
}
|
||||
|
||||
// zip files (full path) to zip file.
|
||||
pub fn zip_files(path_to_file []string, path_to_export_zip string) ! {
|
||||
// open or create new zip
|
||||
mut zip := open(path_to_export_zip, .no_compression, .write) or { panic(err) }
|
||||
|
||||
// add all files from the directory to the archive
|
||||
for file in path_to_file {
|
||||
// add file to zip
|
||||
zip.open_entry(os.base(file)) or { panic(err) }
|
||||
file_as_byte := os.read_bytes(file) or { panic(err) }
|
||||
zip.write_entry(file_as_byte) or { panic(err) }
|
||||
|
||||
zip.close_entry()
|
||||
}
|
||||
|
||||
// close zip
|
||||
defer {
|
||||
zip.close()
|
||||
}
|
||||
}
|
||||
|
||||
// zip_folder zips all entries in `folder` *recursively* to the zip file at `zip_file`.
|
||||
// Empty folders will be included, unless specified otherwise in `opt`.
|
||||
pub fn zip_folder(folder string, zip_file string, opt ZipFolderOptions) ! {
|
||||
// get list of files from directory
|
||||
path := folder.trim_right(os.path_separator)
|
||||
mut files := []string{}
|
||||
os.walk_with_context(path, &files, fn (ctx voidptr, file string) {
|
||||
mut files := unsafe { &[]string(ctx) }
|
||||
files << file
|
||||
})
|
||||
|
||||
// open or create new zip
|
||||
mut zip := open(zip_file, .no_compression, .write)!
|
||||
// close zip
|
||||
defer {
|
||||
zip.close()
|
||||
}
|
||||
|
||||
// add all files from the directory to the archive
|
||||
for file in files {
|
||||
is_dir := os.is_dir(file)
|
||||
if opt.omit_empty_folders && is_dir {
|
||||
continue
|
||||
}
|
||||
// strip each zip entry for the path prefix - this way
|
||||
// all files in the archive can be made relative.
|
||||
mut zip_file_entry := file.trim_string_left(path + os.path_separator)
|
||||
// Normalize path on Windows \ -> /
|
||||
$if windows {
|
||||
zip_file_entry = zip_file_entry.replace(os.path_separator, '/')
|
||||
}
|
||||
if is_dir {
|
||||
zip_file_entry += '/' // Tells the implementation that the entry is a directory
|
||||
}
|
||||
// add file or directory (ends with "/") to zip
|
||||
zip.open_entry(zip_file_entry)!
|
||||
if !is_dir {
|
||||
file_as_byte := os.read_bytes(file)!
|
||||
zip.write_entry(file_as_byte)!
|
||||
}
|
||||
zip.close_entry()
|
||||
}
|
||||
}
|
||||
|
||||
// total returns the number of all entries (files and directories) in the zip archive.
|
||||
pub fn (mut zentry Zip) total() !int {
|
||||
tentry := int(C.zip_entries_total(zentry))
|
||||
if tentry == -1 {
|
||||
return error('szip: cannot count total entries')
|
||||
}
|
||||
return tentry
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import compress.szip
|
||||
import os
|
||||
|
||||
const test_out_zip = 'v_test_zip.zip'
|
||||
const test_dir_zip = 'v_test_dir_zip.zip'
|
||||
const test_path = 'zip files'
|
||||
const test_path2 = '.zip folder'
|
||||
const test_path3 = 'test zip folder'
|
||||
const test_path3_1 = os.join_path(test_path3, '1', '1')
|
||||
const test_path3_2 = os.join_path(test_path3, '2', '1')
|
||||
const test_path3_3 = os.join_path(test_path3, '3', '1')
|
||||
const test_path3_4 = os.join_path(test_path3, '4', '1')
|
||||
const fname1 = 'file_1.txt'
|
||||
const fpath1 = os.join_path(test_path, fname1)
|
||||
const fname2 = 'file_2.txt'
|
||||
const fpath2 = os.join_path(test_path, fname2)
|
||||
const fname3 = '.New Text Document.txt'
|
||||
const fpath3 = os.join_path(test_path2, fname3)
|
||||
const fname4 = 'file.txt'
|
||||
const fpath4 = os.join_path(test_path3_1, fname4)
|
||||
const fpath5 = os.join_path(test_path3_2, fname4)
|
||||
const fpath6 = os.join_path(test_path3_4, fname4)
|
||||
|
||||
fn cleanup() {
|
||||
os.chdir(os.temp_dir()) or {}
|
||||
os.rmdir_all(test_path) or {}
|
||||
os.rmdir_all(test_path2) or {}
|
||||
os.rmdir_all(test_path3) or {}
|
||||
os.rm(test_out_zip) or {}
|
||||
os.rm(test_dir_zip) or {}
|
||||
}
|
||||
|
||||
fn testsuite_begin() {
|
||||
cleanup()
|
||||
}
|
||||
|
||||
fn testsuite_end() {
|
||||
cleanup()
|
||||
}
|
||||
|
||||
fn test_szip_create_temp_files() {
|
||||
os.mkdir(test_path)!
|
||||
os.mkdir(test_path2)!
|
||||
os.write_file(fpath1, 'file one')!
|
||||
os.write_file(fpath2, 'file two')!
|
||||
os.write_file(fpath3, 'file three')!
|
||||
assert os.exists(fpath1)
|
||||
assert os.exists(fpath2)
|
||||
assert os.exists(fpath3)
|
||||
}
|
||||
|
||||
fn test_zipping_files() {
|
||||
mut files := (os.ls(test_path)!).map(os.join_path(test_path, it))
|
||||
files << (os.ls(test_path2)!).map(os.join_path(test_path2, it))
|
||||
szip.zip_files(files, test_out_zip)!
|
||||
assert os.exists(test_out_zip)
|
||||
os.rm(fpath1)!
|
||||
os.rm(fpath2)!
|
||||
os.rm(fpath3)!
|
||||
}
|
||||
|
||||
fn test_extract_zipped_files() {
|
||||
szip.extract_zip_to_dir(test_out_zip, test_path)!
|
||||
szip.extract_zip_to_dir(test_out_zip, test_path2)!
|
||||
assert os.exists(fpath1)
|
||||
assert os.exists(fpath2)
|
||||
assert os.exists(fpath3)
|
||||
assert (os.read_file(fpath1)!) == 'file one'
|
||||
assert (os.read_file(fpath2)!) == 'file two'
|
||||
assert (os.read_file(fpath3)!) == 'file three'
|
||||
cleanup()
|
||||
}
|
||||
|
||||
fn test_reading_zipping_files() {
|
||||
n_files := 2
|
||||
mut file_name_list := []string{}
|
||||
for i in 0 .. n_files {
|
||||
file_name_list << 'file_${i:02}.txt'
|
||||
}
|
||||
|
||||
cleanup()
|
||||
os.mkdir(test_path)!
|
||||
os.mkdir(test_path2)!
|
||||
os.write_file(fpath3, 'file three')!
|
||||
for c, f_name in file_name_list {
|
||||
tmp_path := os.join_path(test_path, f_name)
|
||||
os.write_file(tmp_path, 'file ${c:02}')!
|
||||
assert os.exists(tmp_path)
|
||||
}
|
||||
files := (os.ls(test_path)!).map(os.join_path(test_path, it))
|
||||
|
||||
szip.zip_files(files, test_out_zip)!
|
||||
assert os.exists(test_out_zip)
|
||||
|
||||
mut zp :=
|
||||
szip.open(test_out_zip, szip.CompressionLevel.no_compression, szip.OpenMode.read_only)!
|
||||
n_entries := zp.total()!
|
||||
assert n_entries == n_files
|
||||
|
||||
unsafe {
|
||||
data_len := 'file XX'.len
|
||||
buf_size := 32
|
||||
buf := malloc(data_len * 2)
|
||||
|
||||
for _ in 0 .. n_files {
|
||||
zp.open_entry_by_index(0)!
|
||||
name := zp.name()
|
||||
assert name in file_name_list
|
||||
|
||||
zp.read_entry_buf(buf, buf_size)!
|
||||
buf[data_len] = 0
|
||||
tmp_str := tos(buf, data_len)
|
||||
|
||||
assert tmp_str[0..4] == 'file'
|
||||
assert tmp_str[5..7] == name[5..7]
|
||||
|
||||
zp.close_entry()
|
||||
}
|
||||
|
||||
free(buf)
|
||||
}
|
||||
zp.close()
|
||||
}
|
||||
|
||||
fn test_zip_folder() {
|
||||
cleanup()
|
||||
os.mkdir_all(test_path3_1)!
|
||||
os.mkdir_all(test_path3_2)!
|
||||
os.mkdir_all(test_path3_3)!
|
||||
os.mkdir_all(test_path3_4)!
|
||||
os.write_file(fpath4, '4')!
|
||||
os.write_file(fpath5, '5')!
|
||||
os.write_file(fpath6, '6')!
|
||||
|
||||
szip.zip_folder(test_path3, test_dir_zip)!
|
||||
assert os.exists(test_dir_zip)
|
||||
|
||||
os.rmdir_all(test_path3)!
|
||||
os.mkdir_all(test_path3)!
|
||||
szip.extract_zip_to_dir(test_dir_zip, test_path3)!
|
||||
assert os.exists(test_path3_1)
|
||||
assert os.exists(test_path3_2)
|
||||
assert os.exists(test_path3_3) // This is the empty dir
|
||||
assert os.exists(test_path3_4)
|
||||
assert (os.read_file(fpath4)!) == '4'
|
||||
assert (os.read_file(fpath5)!) == '5'
|
||||
assert (os.read_file(fpath6)!) == '6'
|
||||
}
|
||||
|
||||
fn test_zip_folder_omit_empty_directories() {
|
||||
cleanup()
|
||||
os.mkdir_all(test_path3_1)!
|
||||
os.mkdir_all(test_path3_2)!
|
||||
os.mkdir_all(test_path3_3)!
|
||||
os.mkdir_all(test_path3_4)!
|
||||
os.write_file(fpath4, '4')!
|
||||
os.write_file(fpath5, '5')!
|
||||
os.write_file(fpath6, '6')!
|
||||
|
||||
szip.zip_folder(test_path3, test_dir_zip, omit_empty_folders: true)!
|
||||
assert os.exists(test_dir_zip)
|
||||
|
||||
os.rmdir_all(test_path3)!
|
||||
os.mkdir_all(test_path3)!
|
||||
szip.extract_zip_to_dir(test_dir_zip, test_path3)!
|
||||
assert os.exists(test_path3_1)
|
||||
assert os.exists(test_path3_2)
|
||||
assert !os.exists(test_path3_3) // This is the empty dir, should be omitted with `omit_empty_folders`
|
||||
assert os.exists(test_path3_4)
|
||||
assert (os.read_file(fpath4)!) == '4'
|
||||
assert (os.read_file(fpath5)!) == '5'
|
||||
assert (os.read_file(fpath6)!) == '6'
|
||||
}
|
||||
|
||||
fn test_zip_folder_empty_file() {
|
||||
cleanup()
|
||||
os.mkdir_all(test_path)!
|
||||
os.write_file('${test_path}/test.txt', '')! // Empty file
|
||||
szip.zip_folder(test_path, test_dir_zip)!
|
||||
assert os.exists(test_dir_zip)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
## Description
|
||||
|
||||
`compress.zlib` is a small wrapper around `compress.deflate` for working with zlib streams
|
||||
(RFC 1950).
|
||||
|
||||
## Examples
|
||||
|
||||
```v
|
||||
import compress.zlib
|
||||
|
||||
fn main() {
|
||||
data := 'Hello world!'.bytes()
|
||||
compressed := zlib.compress(data) or { panic(err) }
|
||||
decompressed := zlib.decompress(compressed) or { panic(err) }
|
||||
assert decompressed == data
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
# zlib interop checker
|
||||
This helper verifies interoperability between:
|
||||
- V module: `compress.zlib`
|
||||
- C helper linked with `libz`
|
||||
- Python stdlib: `zlib`
|
||||
It runs deterministic test vectors, compresses each vector with all three producers, then
|
||||
cross-decompresses every produced stream with all three consumers.
|
||||
A case passes only if every decompressed output is byte-identical to the original input.
|
||||
## Run
|
||||
```bash
|
||||
./vnew run vlib/compress/zlib/interop/zlib_interop.vsh
|
||||
```
|
||||
## Requirements
|
||||
- `python3` with the stdlib `zlib` module
|
||||
- a C compiler (`cc`, `gcc`, or `clang`)
|
||||
- `libz` development headers and linker support (`-lz`)
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env -S v
|
||||
|
||||
import compress.zlib
|
||||
|
||||
struct TestVector {
|
||||
name string
|
||||
data []u8
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tmp_root := join_path(temp_dir(), 'v_zlib_interop_${getpid()}')
|
||||
mkdir_all(tmp_root) or { panic(err) }
|
||||
defer {
|
||||
rmdir_all(tmp_root) or {}
|
||||
}
|
||||
|
||||
c_bin := compile_c_helper(tmp_root) or {
|
||||
eprintln('SKIP: ${err.msg()}')
|
||||
exit(2)
|
||||
}
|
||||
py_script := join_path(@DIR, 'zlib_ref.py')
|
||||
ensure_python(py_script) or {
|
||||
eprintln('SKIP: ${err.msg()}')
|
||||
exit(2)
|
||||
}
|
||||
|
||||
vectors := make_test_vectors()
|
||||
mut total_checks := 0
|
||||
for i, vec in vectors {
|
||||
total_checks += run_case(tmp_root, c_bin, py_script, i, vec) or {
|
||||
eprintln('FAIL: ${vec.name}: ${err.msg()}')
|
||||
exit(1)
|
||||
0
|
||||
}
|
||||
println('ok ${i + 1}/${vectors.len}: ${vec.name} (${vec.data.len} bytes)')
|
||||
}
|
||||
println('PASS: ${vectors.len} vectors, ${total_checks} cross-checks')
|
||||
}
|
||||
|
||||
fn compile_c_helper(tmp_root string) !string {
|
||||
cc := choose_cc()
|
||||
if cc == '' {
|
||||
return error('no C compiler found (tried cc, gcc, clang)')
|
||||
}
|
||||
src := join_path(@DIR, 'zlib_ref.c')
|
||||
bin := join_path(tmp_root, 'zlib_interop_ref')
|
||||
must_succeed('${cc} -O2 ${shell_quote(src)} -lz -o ${shell_quote(bin)}',
|
||||
'C zlib helper build failed')!
|
||||
return bin
|
||||
}
|
||||
|
||||
fn choose_cc() string {
|
||||
for cc in ['cc', 'gcc', 'clang'] {
|
||||
if execute('${cc} --version >/dev/null 2>&1').exit_code == 0 {
|
||||
return cc
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
fn ensure_python(py_script string) ! {
|
||||
must_succeed("python3 -c 'import zlib' >/dev/null 2>&1",
|
||||
'python3 with zlib module is not available')!
|
||||
if !exists(py_script) {
|
||||
return error('missing Python helper: ${py_script}')
|
||||
}
|
||||
}
|
||||
|
||||
fn make_test_vectors() []TestVector {
|
||||
mut vectors := []TestVector{}
|
||||
vectors << TestVector{'empty', []u8{}}
|
||||
vectors << TestVector{'ascii_text', 'The quick brown fox jumps over the lazy dog.\n'.repeat(64).bytes()}
|
||||
vectors << TestVector{'repeated_byte', []u8{len: 10000, init: `A`}}
|
||||
vectors << TestVector{'all_bytes_x4', all_bytes_repeated(4)}
|
||||
vectors << TestVector{'lcg_64k', lcg_bytes(65536)}
|
||||
return vectors
|
||||
}
|
||||
|
||||
fn run_case(tmp_root string, c_bin string, py_script string, case_idx int, vec TestVector) !int {
|
||||
case_dir := join_path(tmp_root, 'case_${case_idx:02}_${vec.name}')
|
||||
mkdir_all(case_dir)!
|
||||
|
||||
v_z := zlib.compress(vec.data)!
|
||||
c_z := c_compress(case_dir, c_bin, vec.data)!
|
||||
py_z := py_compress(case_dir, py_script, vec.data)!
|
||||
|
||||
mut checks := 0
|
||||
producers := {
|
||||
'v': v_z
|
||||
'c': c_z
|
||||
'py': py_z
|
||||
}
|
||||
for producer, compressed in producers {
|
||||
v_plain := zlib.decompress(compressed)!
|
||||
assert_equal_bytes('v.decompress(${producer}.compress)', vec.data, v_plain)!
|
||||
checks++
|
||||
|
||||
c_plain := c_decompress(case_dir, c_bin, producer, compressed)!
|
||||
assert_equal_bytes('c.decompress(${producer}.compress)', vec.data, c_plain)!
|
||||
checks++
|
||||
|
||||
py_plain := py_decompress(case_dir, py_script, producer, compressed)!
|
||||
assert_equal_bytes('python.decompress(${producer}.compress)', vec.data, py_plain)!
|
||||
checks++
|
||||
}
|
||||
return checks
|
||||
}
|
||||
|
||||
fn c_compress(case_dir string, c_bin string, plain []u8) ![]u8 {
|
||||
in_path := join_path(case_dir, 'plain.in')
|
||||
out_path := join_path(case_dir, 'c.zlib')
|
||||
write_file_array(in_path, plain)!
|
||||
must_succeed('${shell_quote(c_bin)} compress ${shell_quote(in_path)} ${shell_quote(out_path)}',
|
||||
'C zlib compression failed')!
|
||||
return read_bytes(out_path)!
|
||||
}
|
||||
|
||||
fn c_decompress(case_dir string, c_bin string, producer string, compressed []u8) ![]u8 {
|
||||
in_path := join_path(case_dir, '${producer}.for_c.zlib')
|
||||
out_path := join_path(case_dir, '${producer}.from_c.out')
|
||||
write_file_array(in_path, compressed)!
|
||||
must_succeed('${shell_quote(c_bin)} decompress ${shell_quote(in_path)} ${shell_quote(out_path)}',
|
||||
'C zlib decompression failed')!
|
||||
return read_bytes(out_path)!
|
||||
}
|
||||
|
||||
fn py_compress(case_dir string, py_script string, plain []u8) ![]u8 {
|
||||
in_path := join_path(case_dir, 'plain_py.in')
|
||||
out_path := join_path(case_dir, 'py.zlib')
|
||||
write_file_array(in_path, plain)!
|
||||
must_succeed('python3 ${shell_quote(py_script)} compress ${shell_quote(in_path)} ${shell_quote(out_path)}',
|
||||
'Python zlib compression failed')!
|
||||
return read_bytes(out_path)!
|
||||
}
|
||||
|
||||
fn py_decompress(case_dir string, py_script string, producer string, compressed []u8) ![]u8 {
|
||||
in_path := join_path(case_dir, '${producer}.for_py.zlib')
|
||||
out_path := join_path(case_dir, '${producer}.from_py.out')
|
||||
write_file_array(in_path, compressed)!
|
||||
must_succeed('python3 ${shell_quote(py_script)} decompress ${shell_quote(in_path)} ${shell_quote(out_path)}',
|
||||
'Python zlib decompression failed')!
|
||||
return read_bytes(out_path)!
|
||||
}
|
||||
|
||||
fn all_bytes_repeated(times int) []u8 {
|
||||
mut out := []u8{cap: 256 * times}
|
||||
for _ in 0 .. times {
|
||||
for i in 0 .. 256 {
|
||||
out << u8(i)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fn lcg_bytes(n int) []u8 {
|
||||
mut out := []u8{len: n}
|
||||
mut x := u32(0x12345678)
|
||||
for i in 0 .. n {
|
||||
x = x * u32(1664525) + u32(1013904223)
|
||||
out[i] = u8((x >> 16) & u32(0xff))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fn assert_equal_bytes(label string, expected []u8, got []u8) ! {
|
||||
if expected.len != got.len {
|
||||
return error('${label}: length mismatch expected=${expected.len} got=${got.len}')
|
||||
}
|
||||
for i in 0 .. expected.len {
|
||||
if expected[i] != got[i] {
|
||||
return error('${label}: byte mismatch at offset ${i}')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn must_succeed(command string, context string) ! {
|
||||
res := execute(command)
|
||||
if res.exit_code != 0 {
|
||||
return error('${context}\ncommand: ${command}\nexit_code: ${res.exit_code}\n${res.output}')
|
||||
}
|
||||
}
|
||||
|
||||
fn shell_quote(s string) string {
|
||||
return "'${s.replace("'", "'\\''")}'"
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <zlib.h>
|
||||
|
||||
static int rf(const char* p, unsigned char** o, size_t* n)
|
||||
{
|
||||
FILE* f = fopen(p, "rb");
|
||||
if (!f) return 1;
|
||||
if (fseek(f, 0, SEEK_END) != 0)
|
||||
{
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
long s = ftell(f);
|
||||
if (s < 0)
|
||||
{
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
if (fseek(f, 0, SEEK_SET) != 0)
|
||||
{
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
*n = (size_t)s;
|
||||
*o = *n ? (unsigned char*)malloc(*n) : NULL;
|
||||
if (*n && !*o)
|
||||
{
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
if (*n && fread(*o, 1, *n, f) != *n)
|
||||
{
|
||||
free(*o);
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int wf(const char* p, const unsigned char* b, size_t n)
|
||||
{
|
||||
FILE* f = fopen(p, "wb");
|
||||
if (!f) return 1;
|
||||
if (n && fwrite(b, 1, n, f) != n)
|
||||
{
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
static const unsigned char dummy = 0;
|
||||
if (argc != 4)
|
||||
{
|
||||
fputs("usage: zlib_ref compress|decompress in out\n", stderr);
|
||||
return 2;
|
||||
}
|
||||
unsigned char* in = NULL;
|
||||
size_t in_n = 0;
|
||||
if (rf(argv[2], &in, &in_n))
|
||||
{
|
||||
fputs("read error\n", stderr);
|
||||
return 1;
|
||||
}
|
||||
const unsigned char* in_ptr = in_n ? in : &dummy;
|
||||
if (strcmp(argv[1], "compress") == 0)
|
||||
{
|
||||
uLongf out_n = compressBound((uLong)in_n);
|
||||
unsigned char* out = (unsigned char*)malloc(out_n ? out_n : 1);
|
||||
if (!out)
|
||||
{
|
||||
free(in);
|
||||
return 1;
|
||||
}
|
||||
if (compress2(out, &out_n, in_ptr, (uLong)in_n, Z_DEFAULT_COMPRESSION) != Z_OK)
|
||||
{
|
||||
fputs("compress2 failed\n", stderr);
|
||||
free(in);
|
||||
free(out);
|
||||
return 1;
|
||||
}
|
||||
if (wf(argv[3], out, (size_t)out_n))
|
||||
{
|
||||
fputs("write error\n", stderr);
|
||||
free(in);
|
||||
free(out);
|
||||
return 1;
|
||||
}
|
||||
free(out);
|
||||
}
|
||||
else if (strcmp(argv[1], "decompress") == 0)
|
||||
{
|
||||
uLongf out_n = in_n * 8 + 64;
|
||||
if (out_n < 256) out_n = 256;
|
||||
unsigned char* out = NULL;
|
||||
int rc = Z_BUF_ERROR;
|
||||
while (rc == Z_BUF_ERROR)
|
||||
{
|
||||
unsigned char* next = (unsigned char*)realloc(out, out_n);
|
||||
if (!next)
|
||||
{
|
||||
free(in);
|
||||
free(out);
|
||||
return 1;
|
||||
}
|
||||
out = next;
|
||||
uLongf cap = out_n;
|
||||
rc = uncompress(out, &cap, in_ptr, (uLong)in_n);
|
||||
if (rc == Z_OK)
|
||||
{
|
||||
out_n = cap;
|
||||
break;
|
||||
}
|
||||
if (rc == Z_BUF_ERROR)
|
||||
{
|
||||
out_n *= 2;
|
||||
if (out_n < 256) out_n = 256;
|
||||
}
|
||||
}
|
||||
if (rc != Z_OK)
|
||||
{
|
||||
fputs("uncompress failed\n", stderr);
|
||||
free(in);
|
||||
free(out);
|
||||
return 1;
|
||||
}
|
||||
if (wf(argv[3], out, (size_t)out_n))
|
||||
{
|
||||
fputs("write error\n", stderr);
|
||||
free(in);
|
||||
free(out);
|
||||
return 1;
|
||||
}
|
||||
free(out);
|
||||
}
|
||||
else
|
||||
{
|
||||
fputs("unknown mode\n", stderr);
|
||||
free(in);
|
||||
return 2;
|
||||
}
|
||||
free(in);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import sys, zlib
|
||||
def rf(path):
|
||||
with open(path, "rb") as f:
|
||||
return f.read()
|
||||
def wf(path, data):
|
||||
with open(path, "wb") as f:
|
||||
f.write(data)
|
||||
if len(sys.argv) != 4:
|
||||
print("usage: zlib_ref.py compress|decompress in out", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
mode, inp, outp = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
data = rf(inp)
|
||||
if mode == "compress":
|
||||
wf(outp, zlib.compress(data))
|
||||
elif mode == "decompress":
|
||||
wf(outp, zlib.decompress(data))
|
||||
else:
|
||||
print("unknown mode", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
@@ -0,0 +1,21 @@
|
||||
module zlib
|
||||
|
||||
import compress.deflate
|
||||
|
||||
// compress compresses data using the zlib container format.
|
||||
pub fn compress(data []u8) ![]u8 {
|
||||
return deflate.compress_zlib(data)
|
||||
}
|
||||
|
||||
// decompress decompresses zlib-compressed data.
|
||||
pub fn decompress(data []u8) ![]u8 {
|
||||
return deflate.decompress_zlib(data)
|
||||
}
|
||||
|
||||
// decompress_with_callback decompresses a zlib stream (RFC 1950) using a callback for chunked delivery.
|
||||
// The callback receives chunks of decompressed data and should return the chunk length to continue, or 0 to abort.
|
||||
// Returns the total decompressed length.
|
||||
pub fn decompress_with_callback(data []u8, cb deflate.ChunkCallback, userdata voidptr) !int {
|
||||
deflate.validate_zlib_header(data)!
|
||||
return deflate.decompress_with_callback(data, cb, userdata)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
module zlib
|
||||
|
||||
import encoding.hex
|
||||
|
||||
fn must_decode_hex(s string) []u8 {
|
||||
return hex.decode(s) or { panic(err) }
|
||||
}
|
||||
|
||||
fn assert_decompress_error(data []u8, reason string) ! {
|
||||
decompress(data) or {
|
||||
assert err.msg() == reason
|
||||
return
|
||||
}
|
||||
return error('did not error')
|
||||
}
|
||||
|
||||
fn test_zlib_roundtrip_text() {
|
||||
data := 'Hello world!'.bytes()
|
||||
compressed := compress(data)!
|
||||
decompressed := decompress(compressed)!
|
||||
assert decompressed == data
|
||||
}
|
||||
|
||||
fn test_zlib_roundtrip_empty() {
|
||||
data := []u8{}
|
||||
compressed := compress(data)!
|
||||
decompressed := decompress(compressed)!
|
||||
assert decompressed == data
|
||||
}
|
||||
|
||||
fn test_zlib_roundtrip_binary() {
|
||||
data := [u8(0), 1, 2, 3, 127, 128, 254, 255]
|
||||
compressed := compress(data)!
|
||||
decompressed := decompress(compressed)!
|
||||
assert decompressed == data
|
||||
}
|
||||
|
||||
fn test_zlib_roundtrip_large() {
|
||||
data := 'abcdefgh'.repeat(1000).bytes()
|
||||
compressed := compress(data)!
|
||||
assert compressed.len < data.len
|
||||
decompressed := decompress(compressed)!
|
||||
assert decompressed == data
|
||||
}
|
||||
|
||||
fn test_zlib_decompress_known_python_vector() {
|
||||
compressed := must_decode_hex('789ccb48cdc9c95728cf2fca49e102001e720467')
|
||||
decompressed := decompress(compressed)!
|
||||
assert decompressed == 'hello world\n'.bytes()
|
||||
}
|
||||
|
||||
fn test_zlib_invalid_too_short() {
|
||||
assert_decompress_error([]u8{}, 'invalid zlib stream: too short')!
|
||||
}
|
||||
|
||||
fn test_zlib_invalid_header_checksum() {
|
||||
assert_decompress_error([u8(0x78), 0x9d, 0x00, 0x00, 0x00, 0x01],
|
||||
'invalid zlib stream: bad header checksum')!
|
||||
}
|
||||
|
||||
fn test_zlib_invalid_truncated_payload() {
|
||||
decompress([u8(0x78), 0x9c, 0x03, 0x00, 0x00, 0x00, 0x01]) or {
|
||||
assert err.msg().contains('unexpected end of stream')
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
|
||||
fn test_zlib_invalid_inserted_bytes_before_adler() {
|
||||
enc := compress('zlib edge-case regression'.repeat(5).bytes())!
|
||||
mut bad := []u8{cap: enc.len + 1}
|
||||
bad << enc[..enc.len - 4]
|
||||
bad << u8(0x7f)
|
||||
bad << enc[enc.len - 4..]
|
||||
assert_decompress_error(bad, 'invalid zlib stream: trailing data before adler32')!
|
||||
}
|
||||
|
||||
fn test_zlib_decompress_callback() {
|
||||
uncompressed := '321323'.repeat(10_000)
|
||||
gz := compress(uncompressed.bytes())!
|
||||
mut size := 0
|
||||
mut ref := &size
|
||||
decoded := decompress_with_callback(gz, fn (chunk []u8, ref &int) int {
|
||||
unsafe {
|
||||
*ref += chunk.len
|
||||
}
|
||||
return chunk.len
|
||||
}, ref)!
|
||||
assert decoded == size
|
||||
assert decoded == uncompressed.len
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
## Description
|
||||
|
||||
`compress.zstd` is a module that assists in the compression and
|
||||
decompression of binary data using `zstd` compression.
|
||||
|
||||
## Examples
|
||||
|
||||
Decode a `.zst` file from disk:
|
||||
|
||||
```v
|
||||
import os
|
||||
import compress.zstd
|
||||
|
||||
fn main() {
|
||||
compressed := os.read_bytes('document.zst') or { panic(err) }
|
||||
decompressed := zstd.decompress(compressed) or { panic(err) }
|
||||
println(decompressed.bytestr())
|
||||
}
|
||||
```
|
||||
|
||||
For a runnable example in this repository, see
|
||||
[examples/read_zstd_file.v](examples/read_zstd_file.v).
|
||||
|
||||
`read_zstd_files_test.v` is a test file. Run it with
|
||||
`v test read_zstd_files_test.v`, not with `v read_zstd_files_test.v`.
|
||||
@@ -0,0 +1,13 @@
|
||||
module main
|
||||
|
||||
import os
|
||||
import compress.zstd
|
||||
|
||||
const sample_file = os.join_path(os.dir(@FILE), '..', 'samples', 'known.zst')
|
||||
|
||||
fn main() {
|
||||
compressed := os.read_bytes(sample_file) or { panic(err) }
|
||||
decompressed := zstd.decompress(compressed) or { panic(err) }
|
||||
println('decoded `${sample_file}`:')
|
||||
println(decompressed.bytestr())
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import os
|
||||
import compress.zstd
|
||||
|
||||
const samples_folder = os.join_path(os.dir(@FILE), 'samples')
|
||||
|
||||
fn s(fname string) string {
|
||||
return os.join_path(samples_folder, fname)
|
||||
}
|
||||
|
||||
fn read_and_decode_file(fpath string) !([]u8, string) {
|
||||
compressed := os.read_bytes(fpath)!
|
||||
decoded := zstd.decompress(compressed)!
|
||||
content := decoded.bytestr()
|
||||
return compressed, content
|
||||
}
|
||||
|
||||
fn test_reading_and_decoding_a_known_zstded_file() {
|
||||
compressed, content := read_and_decode_file(s('known.zst'))!
|
||||
assert compressed#[0..3] == [u8(40), 181, 47]
|
||||
assert compressed#[-5..] == [u8(10), 78, 32, 170, 44]
|
||||
assert content.contains('## Description')
|
||||
assert content.contains('## Examples:')
|
||||
assert content.ends_with('```')
|
||||
}
|
||||
|
||||
fn test_decoding_all_samples_files() {
|
||||
for zstd_file in os.walk_ext(samples_folder, '.zst') {
|
||||
_, content := read_and_decode_file(zstd_file)!
|
||||
assert content.len > 0, 'decoded content should not be empty: `${content}`'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_reading_zstd_files_compressed_with_different_compress_level() {
|
||||
_, content1 := read_and_decode_file(s('readme_level_1.zst'))!
|
||||
_, content5 := read_and_decode_file(s('readme_level_5.zst'))!
|
||||
_, content9 := read_and_decode_file(s('readme_level_9.zst'))!
|
||||
_, content19 := read_and_decode_file(s('readme_level_19.zst'))!
|
||||
assert content19 == content9
|
||||
assert content9 == content5
|
||||
assert content5 == content1
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,649 @@
|
||||
// zstd(https://github.com/facebook/zstd) is a fast real-time compression algorithm developed by Facebook.
|
||||
// zstd compression/decompression
|
||||
module zstd
|
||||
|
||||
import os
|
||||
import encoding.binary
|
||||
|
||||
#flag -I @VMODROOT/thirdparty/zstd
|
||||
#include "zstd.c" // msvc can't compile multiple source files, so included
|
||||
|
||||
const frame_header_size_max = 18
|
||||
const content_size_unknown = u64(-1)
|
||||
const content_size_error = u64(-2)
|
||||
|
||||
const buf_in_size = 1024 * 1024
|
||||
const buf_out_size = 1024 * 1024
|
||||
|
||||
fn C.ZSTD_versionNumber() u32
|
||||
fn C.ZSTD_versionString() charptr
|
||||
|
||||
fn C.ZSTD_compress(voidptr, usize, voidptr, usize, i32) usize
|
||||
fn C.ZSTD_decompress(voidptr, usize, voidptr, usize) usize
|
||||
fn C.ZSTD_getFrameContentSize(voidptr, usize) u64
|
||||
fn C.ZSTD_findFrameCompressedSize(voidptr, usize) usize
|
||||
fn C.ZSTD_compressBound(usize) usize
|
||||
fn C.ZSTD_isError(usize) u32
|
||||
fn C.ZSTD_getErrorName(usize) charptr
|
||||
fn C.ZSTD_minCLevel() i32
|
||||
fn C.ZSTD_maxCLevel() i32
|
||||
fn C.ZSTD_defaultCLevel() i32
|
||||
fn C.ZSTD_createCCtx() &C.ZSTD_CCtx_s
|
||||
fn C.ZSTD_freeCCtx(voidptr) usize
|
||||
fn C.ZSTD_compressCCtx(voidptr, voidptr, usize, voidptr, usize, i32) usize
|
||||
fn C.ZSTD_createDCtx() &C.ZSTD_DCtx_s
|
||||
fn C.ZSTD_freeDCtx(voidptr) usize
|
||||
fn C.ZSTD_decompressDCtx(voidptr, voidptr, usize, voidptr, usize) usize
|
||||
|
||||
// note : new strategies _might_ be added in the future. Only the order (from fast to strong) is guaranteed
|
||||
pub enum Strategy {
|
||||
default = 0
|
||||
fast = 1
|
||||
dfast = 2
|
||||
greedy = 3
|
||||
lazy = 4
|
||||
lazy2 = 5
|
||||
btlazy2 = 6
|
||||
btopt = 7
|
||||
btultra = 8
|
||||
btultra2 = 9
|
||||
}
|
||||
|
||||
pub enum CParameter {
|
||||
// compression parameters
|
||||
// Note: When compressing with a ZSTD_CDict these parameters are superseded
|
||||
// by the parameters used to construct the ZSTD_CDict.
|
||||
// See ZSTD_CCtx_refCDict() for more info (superseded-by-cdict).
|
||||
//
|
||||
// Set compression parameters according to pre-defined cLevel table.
|
||||
// Note that exact compression parameters are dynamically determined,
|
||||
// depending on both compression level and srcSize (when known).
|
||||
// Default level is ZSTD_CLEVEL_DEFAULT==3.
|
||||
// Special: value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT.
|
||||
// Note 1 : it's possible to pass a negative compression level.
|
||||
// Note 2 : setting a level does not automatically set all other compression parameters
|
||||
// to default. Setting this will however eventually dynamically impact the compression
|
||||
// parameters which have not been manually set. The manually set
|
||||
// ones will 'stick'.
|
||||
compression_level = 100
|
||||
// Advanced compression parameters :
|
||||
// It's possible to pin down compression parameters to some specific values.
|
||||
// In which case, these values are no longer dynamically selected by the compressor
|
||||
//
|
||||
// Maximum allowed back-reference distance, expressed as power of 2.
|
||||
// This will set a memory budget for streaming decompression,
|
||||
// with larger values requiring more memory
|
||||
// and typically compressing more.
|
||||
// Must be clamped between ZSTD_WINDOWLOG_MIN and ZSTD_WINDOWLOG_MAX.
|
||||
// Special: value 0 means "use default windowLog".
|
||||
// Note: Using a windowLog greater than ZSTD_WINDOWLOG_LIMIT_DEFAULT
|
||||
// requires explicitly allowing such size at streaming decompression stage.
|
||||
window_log = 101
|
||||
// Size of the initial probe table, as a power of 2.
|
||||
// Resulting memory usage is (1 << (hashLog+2)).
|
||||
// Must be clamped between ZSTD_HASHLOG_MIN and ZSTD_HASHLOG_MAX.
|
||||
// Larger tables improve compression ratio of strategies <= dFast,
|
||||
// and improve speed of strategies > dFast.
|
||||
// Special: value 0 means "use default hashLog".
|
||||
hash_log = 102
|
||||
// Size of the multi-probe search table, as a power of 2.
|
||||
// Resulting memory usage is (1 << (chainLog+2)).
|
||||
// Must be clamped between ZSTD_CHAINLOG_MIN and ZSTD_CHAINLOG_MAX.
|
||||
// Larger tables result in better and slower compression.
|
||||
// This parameter is useless for "fast" strategy.
|
||||
// It's still useful when using "dfast" strategy,
|
||||
// in which case it defines a secondary probe table.
|
||||
// Special: value 0 means "use default chainLog".
|
||||
chain_log = 103
|
||||
// Number of search attempts, as a power of 2.
|
||||
// More attempts result in better and slower compression.
|
||||
// This parameter is useless for "fast" and "dFast" strategies.
|
||||
// Special: value 0 means "use default searchLog".
|
||||
search_log = 104
|
||||
// Minimum size of searched matches.
|
||||
// Note that Zstandard can still find matches of smaller size,
|
||||
// it just tweaks its search algorithm to look for this size and larger.
|
||||
// Larger values increase compression and decompression speed, but decrease ratio.
|
||||
// Must be clamped between ZSTD_MINMATCH_MIN and ZSTD_MINMATCH_MAX.
|
||||
// Note that currently, for all strategies < btopt, effective minimum is 4.
|
||||
// , for all strategies > fast, effective maximum is 6.
|
||||
// Special: value 0 means "use default minMatchLength".
|
||||
min_match = 105
|
||||
// Impact of this field depends on strategy.
|
||||
// For strategies btopt, btultra & btultra2:
|
||||
// Length of Match considered "good enough" to stop search.
|
||||
// Larger values make compression stronger, and slower.
|
||||
// For strategy fast:
|
||||
// Distance between match sampling.
|
||||
// Larger values make compression faster, and weaker.
|
||||
// Special: value 0 means "use default targetLength".
|
||||
target_length = 106
|
||||
// See ZSTD_strategy enum definition.
|
||||
// The higher the value of selected strategy, the more complex it is,
|
||||
// resulting in stronger and slower compression.
|
||||
// Special: value 0 means "use default strategy".
|
||||
strategy = 107
|
||||
// v1.5.6+
|
||||
// Attempts to fit compressed block size into approximately targetCBlockSize.
|
||||
// Bound by ZSTD_TARGETCBLOCKSIZE_MIN and ZSTD_TARGETCBLOCKSIZE_MAX.
|
||||
// Note that it's not a guarantee, just a convergence target (default:0).
|
||||
// No target when targetCBlockSize == 0.
|
||||
// This is helpful in low bandwidth streaming environments to improve end-to-end latency,
|
||||
// when a client can make use of partial documents (a prominent example being Chrome).
|
||||
// Note: this parameter is stable since v1.5.6.
|
||||
// It was present as an experimental parameter in earlier versions,
|
||||
// but it's not recommended using it with earlier library versions
|
||||
// due to massive performance regressions.
|
||||
target_c_block_size = 130
|
||||
// LDM mode parameters
|
||||
// Enable long distance matching.
|
||||
// This parameter is designed to improve compression ratio
|
||||
// for large inputs, by finding large matches at long distance.
|
||||
// It increases memory usage and window size.
|
||||
// Note: enabling this parameter increases default ZSTD_c_windowLog to 128 MB
|
||||
// except when expressly set to a different value.
|
||||
// Note: will be enabled by default if ZSTD_c_windowLog >= 128 MB and
|
||||
// compression strategy >= ZSTD_btopt (== compression level 16+)
|
||||
enable_long_distance_matching = 160
|
||||
// Size of the table for long distance matching, as a power of 2.
|
||||
// Larger values increase memory usage and compression ratio,
|
||||
// but decrease compression speed.
|
||||
// Must be clamped between ZSTD_HASHLOG_MIN and ZSTD_HASHLOG_MAX
|
||||
// default: windowlog - 7.
|
||||
// Special: value 0 means "automatically determine hashlog".
|
||||
ldm_hash_log = 161
|
||||
// Minimum match size for long distance matcher.
|
||||
// Larger/too small values usually decrease compression ratio.
|
||||
// Must be clamped between ZSTD_LDM_MINMATCH_MIN and ZSTD_LDM_MINMATCH_MAX.
|
||||
// Special: value 0 means "use default value" (default: 64).
|
||||
ldm_min_match = 162
|
||||
// log size of each bucket in the ldm hash table for collision resolution.
|
||||
// Larger values improve collision resolution but decrease compression speed.
|
||||
// The maximum value is ZSTD_LDM_BUCKETSIZELOG_MAX.
|
||||
// Special: value 0 means "use default value" (default: 3).
|
||||
ldm_bucket_size_log = 163
|
||||
// Frequency of inserting/looking up entries into the LDM hash table.
|
||||
// Must be clamped between 0 and (ZSTD_WINDOWLOG_MAX - ZSTD_HASHLOG_MIN).
|
||||
// Default is MAX(0, (windowLog - ldmHashLog)), optimizing hash table usage.
|
||||
// Larger values improve compression speed.
|
||||
// Deviating far from default value will likely result in a compression ratio decrease.
|
||||
// Special: value 0 means "automatically determine hashRateLog".
|
||||
ldm_hash_rate_log = 164
|
||||
// frame parameters
|
||||
// Content size will be written into frame header _whenever known_ (default:1)
|
||||
// Content size must be known at the beginning of compression.
|
||||
// This is automatically the case when using ZSTD_compress2(),
|
||||
// For streaming scenarios, content size must be provided with ZSTD_CCtx_setPledgedSrcSize()
|
||||
content_size_flag = 200
|
||||
// A 32-bits checksum of content is written at end of frame (default:0)
|
||||
checksum_flag = 201
|
||||
// When applicable, dictionary's ID is written into frame header (default:1)
|
||||
dict_id_flag = 202
|
||||
// multi-threading parameters
|
||||
// These parameters are only active if multi-threading is enabled (compiled with build macro ZSTD_MULTITHREAD).
|
||||
// Otherwise, trying to set any other value than default (0) will be a no-op and return an error.
|
||||
// In a situation where it's unknown if the linked library supports multi-threading or not,
|
||||
// setting ZSTD_c_nbWorkers to any value >= 1 and consulting the return value provides a quick way to check this property.
|
||||
//
|
||||
// Select how many threads will be spawned to compress in parallel.
|
||||
// When nbWorkers >= 1, triggers asynchronous mode when invoking ZSTD_compressStream*() :
|
||||
// ZSTD_compressStream*() consumes input and flush output if possible, but immediately gives back control to caller,
|
||||
// while compression is performed in parallel, within worker thread(s).
|
||||
// (note : a strong exception to this rule is when first invocation of ZSTD_compressStream2() sets ZSTD_e_end :
|
||||
// in which case, ZSTD_compressStream2() delegates to ZSTD_compress2(), which is always a blocking call).
|
||||
// More workers improve speed, but also increase memory usage.
|
||||
// Default value is `0`, aka "single-threaded mode" : no worker is spawned,
|
||||
// compression is performed inside Caller's thread, and all invocations are blocking
|
||||
nb_workers = 400
|
||||
// Size of a compression job. This value is enforced only when nbWorkers >= 1.
|
||||
// Each compression job is completed in parallel, so this value can indirectly impact the nb of active threads.
|
||||
// 0 means default, which is dynamically determined based on compression parameters.
|
||||
// Job size must be a minimum of overlap size, or ZSTDMT_JOBSIZE_MIN (= 512 KB), whichever is largest.
|
||||
// The minimum size is automatically and transparently enforced.
|
||||
job_size = 401
|
||||
// Control the overlap size, as a fraction of window size.
|
||||
// The overlap size is an amount of data reloaded from previous job at the beginning of a new job.
|
||||
// It helps preserve compression ratio, while each job is compressed in parallel.
|
||||
// This value is enforced only when nbWorkers >= 1.
|
||||
// Larger values increase compression ratio, but decrease speed.
|
||||
// Possible values range from 0 to 9 :
|
||||
// - 0 means "default" : value will be determined by the library, depending on strategy
|
||||
// - 1 means "no overlap"
|
||||
// - 9 means "full overlap", using a full window size.
|
||||
// Each intermediate rank increases/decreases load size by a factor 2 :
|
||||
// 9: full window; 8: w/2; 7: w/4; 6: w/8; 5:w/16; 4: w/32; 3:w/64; 2:w/128; 1:no overlap; 0:default
|
||||
// default value varies between 6 and 9, depending on strategy
|
||||
overlap_log = 402
|
||||
// note : additional experimental parameters are also available
|
||||
// within the experimental section of the API.
|
||||
// At the time of this writing, they include :
|
||||
// zstd_c_rsyncable
|
||||
// zstd_c_format
|
||||
// zstd_c_force_max_window
|
||||
// zstd_c_force_attach_dict
|
||||
// zstd_c_literal_compression_mode
|
||||
// zstd_c_target_c_block_size
|
||||
// zstd_c_src_size_hint
|
||||
// zstd_c_enable_dedicated_dict_search
|
||||
// zstd_c_stable_in_buffer
|
||||
// zstd_c_stable_out_buffer
|
||||
// zstd_c_block_delimiters
|
||||
// zstd_c_validate_sequences
|
||||
// zstd_c_use_block_splitter
|
||||
// zstd_c_use_row_match_finder
|
||||
// zstd_c_prefetch_c_dict_tables
|
||||
// zstd_c_enable_seq_producer_fallback
|
||||
// zstd_c_max_block_size
|
||||
// Because they are not stable, it's necessary to define ZSTD_STATIC_LINKING_ONLY to access them.
|
||||
// note : never ever use experimentalParam? names directly;
|
||||
// also, the enums values themselves are unstable and can still change.
|
||||
//
|
||||
experimental_param1 = 500
|
||||
experimental_param2 = 10
|
||||
experimental_param3 = 1000
|
||||
experimental_param4 = 1001
|
||||
experimental_param5 = 1002
|
||||
// experimental_param6 = 1003 is now ZSTD_c_targetCBlockSize
|
||||
experimental_param7 = 1004
|
||||
experimental_param8 = 1005
|
||||
experimental_param9 = 1006
|
||||
experimental_param10 = 1007
|
||||
experimental_param11 = 1008
|
||||
experimental_param12 = 1009
|
||||
experimental_param13 = 1010
|
||||
experimental_param14 = 1011
|
||||
experimental_param15 = 1012
|
||||
experimental_param16 = 1013
|
||||
experimental_param17 = 1014
|
||||
experimental_param18 = 1015
|
||||
experimental_param19 = 1016
|
||||
experimental_param20 = 1017
|
||||
}
|
||||
|
||||
pub struct Bounds {
|
||||
pub:
|
||||
error usize
|
||||
lower_bound int
|
||||
upper_bound int
|
||||
}
|
||||
|
||||
fn C.ZSTD_cParam_getBounds(CParameter) Bounds
|
||||
fn C.ZSTD_CCtx_setParameter(voidptr, CParameter, i32) usize
|
||||
fn C.ZSTD_CCtx_setPledgedSrcSize(voidptr, u64) usize
|
||||
|
||||
pub enum ResetDirective {
|
||||
session_only = 1
|
||||
parameters = 2
|
||||
session_and_parameters = 3
|
||||
}
|
||||
|
||||
fn C.ZSTD_CCtx_reset(voidptr, ResetDirective) usize
|
||||
fn C.ZSTD_compress2(voidptr, voidptr, usize, voidptr, usize) usize
|
||||
|
||||
pub enum DParameter {
|
||||
// Select a size limit (in power of 2) beyond which
|
||||
// the streaming API will refuse to allocate memory buffer
|
||||
// in order to protect the host from unreasonable memory requirements.
|
||||
// This parameter is only useful in streaming mode, since no internal buffer is allocated in single-pass mode.
|
||||
// By default, a decompression context accepts window sizes <= (1 << ZSTD_WINDOWLOG_LIMIT_DEFAULT).
|
||||
// Special: value 0 means "use default maximum windowLog".
|
||||
window_log_max = 100
|
||||
// note : additional experimental parameters are also available
|
||||
// within the experimental section of the API.
|
||||
// At the time of this writing, they include :
|
||||
// ZSTD_d_format
|
||||
// zstd_d_stable_out_buffer
|
||||
// zstd_d_force_ignore_checksum
|
||||
// zstd_d_ref_multipled_dicts
|
||||
// zstd_d_disable_huffman_assembly
|
||||
// Because they are not stable, it's necessary to define ZSTD_STATIC_LINKING_ONLY to access them.
|
||||
// note : never ever use experimentalParam? names directly
|
||||
experimental_param1 = 1000
|
||||
experimental_param2 = 1001
|
||||
experimental_param3 = 1002
|
||||
experimental_param4 = 1003
|
||||
experimental_param5 = 1004
|
||||
experimental_param6 = 1005
|
||||
}
|
||||
|
||||
fn C.ZSTD_dParam_getBounds(DParameter) Bounds
|
||||
fn C.ZSTD_DCtx_setParameter(voidptr, DParameter, i32) usize
|
||||
fn C.ZSTD_DCtx_reset(voidptr, ResetDirective) usize
|
||||
|
||||
// streaming compression
|
||||
pub struct InBuffer {
|
||||
pub mut:
|
||||
src voidptr
|
||||
size usize
|
||||
pos usize
|
||||
}
|
||||
|
||||
pub struct OutBuffer {
|
||||
pub mut:
|
||||
dst voidptr
|
||||
size usize
|
||||
pos usize
|
||||
}
|
||||
|
||||
fn C.ZSTD_createCStream() voidptr
|
||||
fn C.ZSTD_freeCStream(voidptr) usize
|
||||
|
||||
pub enum EndDirective {
|
||||
// collect more data, encoder decides when to output compressed result, for optimal compression ratio
|
||||
continue = 0
|
||||
// flush any data provided so far,
|
||||
// it creates (at least) one new block, that can be decoded immediately on reception;
|
||||
// frame will continue: any future data can still reference previously compressed data, improving compression.
|
||||
// note : multithreaded compression will block to flush as much output as possible.
|
||||
flush = 1
|
||||
// flush any remaining data _and_ close current frame.
|
||||
// note that frame is only closed after compressed data is fully flushed (return value == 0).
|
||||
// After that point, any additional data starts a new frame.
|
||||
// note : each frame is independent (does not reference any content from previous frame).
|
||||
// note : multithreaded compression will block to flush as much output as possible.
|
||||
end = 2
|
||||
}
|
||||
|
||||
fn C.ZSTD_compressStream2(voidptr, &OutBuffer, &InBuffer, EndDirective) usize
|
||||
fn C.ZSTD_CStreamInSize() usize
|
||||
fn C.ZSTD_CStreamOutSize() usize
|
||||
fn C.ZSTD_initCStream(voidptr, i32) usize
|
||||
fn C.ZSTD_compressStream(voidptr, &OutBuffer, &InBuffer) usize
|
||||
fn C.ZSTD_flushStream(voidptr, &OutBuffer) usize
|
||||
fn C.ZSTD_endStream(voidptr, &OutBuffer) usize
|
||||
|
||||
// streaming decompression
|
||||
fn C.ZSTD_createDStream() voidptr
|
||||
fn C.ZSTD_freeDStream(voidptr) usize
|
||||
fn C.ZSTD_initDStream(voidptr) usize
|
||||
fn C.ZSTD_decompressStream(voidptr, &OutBuffer, &InBuffer) usize
|
||||
fn C.ZSTD_DStreamInSize() usize
|
||||
fn C.ZSTD_DStreamOutSize() usize
|
||||
|
||||
// version_number returns runtime library version, the value is (MAJOR*100*100 + MINOR*100 + RELEASE).
|
||||
pub fn version_number() u32 {
|
||||
return C.ZSTD_versionNumber()
|
||||
}
|
||||
|
||||
// version_string returns runtime library version, like "1.5.7".
|
||||
pub fn version_string() string {
|
||||
return unsafe { tos_clone(C.ZSTD_versionString()) }
|
||||
}
|
||||
|
||||
// is_error tells if a `usize` function result is an error code.
|
||||
pub fn is_error(code usize) bool {
|
||||
return C.ZSTD_isError(code) == 1
|
||||
}
|
||||
|
||||
// get_error_name provides readable string from an error code.
|
||||
pub fn get_error_name(code usize) string {
|
||||
return unsafe { tos_clone(&u8(C.ZSTD_getErrorName(code))) }
|
||||
}
|
||||
|
||||
// check_zstd checks the zstd error code, and return a error string.
|
||||
pub fn check_error(code usize) ! {
|
||||
if is_error(code) {
|
||||
return error(get_error_name(code))
|
||||
}
|
||||
}
|
||||
|
||||
// min_c_level returns minimum negative compression level allowed.
|
||||
pub fn min_c_level() int {
|
||||
return C.ZSTD_minCLevel()
|
||||
}
|
||||
|
||||
// max_c_level returns maximum compression level available.
|
||||
pub fn max_c_level() int {
|
||||
return C.ZSTD_maxCLevel()
|
||||
}
|
||||
|
||||
// default_c_level returns default compression level.
|
||||
pub fn default_c_level() int {
|
||||
return C.ZSTD_defaultCLevel()
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct CompressParams {
|
||||
pub:
|
||||
// 1~22
|
||||
compression_level int = default_c_level()
|
||||
// how many threads will be spawned to compress in parallel
|
||||
nb_threads int = 1
|
||||
checksum_flag bool = true
|
||||
strategy Strategy = .default
|
||||
}
|
||||
|
||||
// compresses an array of bytes using zstd and returns the compressed bytes in a new array
|
||||
// extra compression parameters can be set by `params`
|
||||
// Example: b := 'abcdef'.repeat(1000).bytes(); cmpr := zstd.compress(b, compression_level: 10)!; assert cmpr.len < b.len; dc := zstd.decompress(cmpr)!; assert b == dc
|
||||
pub fn compress(data []u8, params CompressParams) ![]u8 {
|
||||
dst_capacity := C.ZSTD_compressBound(data.len)
|
||||
check_error(dst_capacity)!
|
||||
mut dst := []u8{len: int(dst_capacity)}
|
||||
mut cctx := new_cctx(params)!
|
||||
defer {
|
||||
cctx.free_cctx()
|
||||
}
|
||||
size := C.ZSTD_compress2(cctx.ctx, dst.data, dst.len, data.data, data.len)
|
||||
check_error(size)!
|
||||
return dst[..int(size)]
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct DecompressParams {
|
||||
pub:
|
||||
window_log_max int
|
||||
}
|
||||
|
||||
// decompresses an array of bytes using zstd and returns the decompressed bytes in a new array
|
||||
// extra decompression parameters can be set by `params`
|
||||
// Example: b := 'abcdef'.repeat(1000).bytes(); cmpr := zstd.compress(b, compression_level: 10)!; assert cmpr.len < b.len; dc := zstd.decompress(cmpr)!; assert b == dc
|
||||
pub fn decompress(data []u8, params DecompressParams) ![]u8 {
|
||||
dst_capacity := C.ZSTD_getFrameContentSize(data.data, frame_header_size_max)
|
||||
if dst_capacity == content_size_unknown {
|
||||
return error('The size cannot be determined, try use streaming mode to decompress data?')
|
||||
} else if dst_capacity == content_size_error {
|
||||
return error('An error occurred (e.g. invalid magic number, srcSize too small)')
|
||||
} else if dst_capacity == 0 {
|
||||
return error('The frame is valid but empty')
|
||||
}
|
||||
mut dst := []u8{len: int(dst_capacity)}
|
||||
decompressed_size := C.ZSTD_decompress(dst.data, dst.len, data.data, data.len)
|
||||
check_error(decompressed_size)!
|
||||
return dst[..int(decompressed_size)]
|
||||
}
|
||||
|
||||
pub struct CCtx {
|
||||
mut:
|
||||
ctx &C.ZSTD_CCtx_s
|
||||
}
|
||||
|
||||
// new_cctx create a compression context.
|
||||
// extra compression parameters can be set by `params`
|
||||
pub fn new_cctx(params CompressParams) !&CCtx {
|
||||
mut ctx := C.ZSTD_createCCtx()
|
||||
if isnil(ctx) {
|
||||
return error('new_cctx() failed!')
|
||||
}
|
||||
mut cctx := &CCtx{ctx}
|
||||
cctx.set(.compression_level, params.compression_level)!
|
||||
$if !(tinyc && windows) {
|
||||
// TODO: tinyc on windows doesn't support multiple thread
|
||||
cctx.set(.nb_workers, params.nb_threads)!
|
||||
}
|
||||
cctx.set(.checksum_flag, if params.checksum_flag { 1 } else { 0 })!
|
||||
cctx.set(.strategy, int(params.strategy))!
|
||||
return cctx
|
||||
}
|
||||
|
||||
// set_parameter set compression parameter `c_param` to value `val`
|
||||
pub fn (mut c CCtx) set(c_param CParameter, val int) ! {
|
||||
check_error(C.ZSTD_CCtx_setParameter(c.ctx, c_param, val))!
|
||||
}
|
||||
|
||||
// compress_stream2 do stream compress on `input`, and store compressed data in `output`.
|
||||
// `mode`:
|
||||
// .zstd_e_continue => continue stream compression.
|
||||
// .zstd_e_flush => flush data
|
||||
// .zstd_e_end => it is the last frame
|
||||
pub fn (mut c CCtx) compress_stream2(output &OutBuffer, input &InBuffer, mode EndDirective) !usize {
|
||||
res := C.ZSTD_compressStream2(c.ctx, output, input, mode)
|
||||
check_error(res)!
|
||||
return res
|
||||
}
|
||||
|
||||
// free_cctx free a compression context.
|
||||
pub fn (mut c CCtx) free_cctx() usize {
|
||||
return C.ZSTD_freeCCtx(c.ctx)
|
||||
}
|
||||
|
||||
struct C.ZSTD_CCtx_s {}
|
||||
|
||||
struct C.ZSTD_DCtx_s {}
|
||||
|
||||
pub struct DCtx {
|
||||
mut:
|
||||
ctx &C.ZSTD_DCtx_s
|
||||
}
|
||||
|
||||
// new_dctx creates a decompression context.
|
||||
// extra decompression parameters can be set by `params`
|
||||
pub fn new_dctx(params DecompressParams) !&DCtx {
|
||||
mut ctx := C.ZSTD_createDCtx()
|
||||
if isnil(ctx) {
|
||||
return error('new_dctx() failed!')
|
||||
}
|
||||
mut dctx := &DCtx{ctx}
|
||||
dctx.set(.window_log_max, params.window_log_max)!
|
||||
return dctx
|
||||
}
|
||||
|
||||
// set_parameter sets decompression parameter `d_param` to value `val`
|
||||
pub fn (mut d DCtx) set(d_param DParameter, val int) ! {
|
||||
check_error(C.ZSTD_DCtx_setParameter(d.ctx, d_param, val))!
|
||||
}
|
||||
|
||||
// decompress_stream do stream decompress on `input`, and store decompressed data in `output`.
|
||||
// return remaining bytes in `input` stream
|
||||
pub fn (mut d DCtx) decompress_stream(output &OutBuffer, input &InBuffer) !usize {
|
||||
res := C.ZSTD_decompressStream(d.ctx, output, input)
|
||||
check_error(res)!
|
||||
return res
|
||||
}
|
||||
|
||||
// free_cctx free a compression context
|
||||
pub fn (mut d DCtx) free_dctx() usize {
|
||||
return C.ZSTD_freeDCtx(d.ctx)
|
||||
}
|
||||
|
||||
// store_array compress an `array`'s data, and store it to file `fname`.
|
||||
// extra compression parameters can be set by `params`
|
||||
// WARNING: Because struct padding, some data in struct may be marked unused.
|
||||
// So, when `store_array`, it will cause memory fsanitize fail with 'use-of-uninitialized-value'.
|
||||
// It can be safely ignored.
|
||||
// For example, following struct may cause memory fsanitize fail:
|
||||
// struct MemoryTrace {
|
||||
// operation u8
|
||||
// address u64
|
||||
// size u8
|
||||
// }
|
||||
// By changing it into following , you can pass the memory fsanitize check :
|
||||
// struct MemoryTrace {
|
||||
// operation u64
|
||||
// address u64
|
||||
// size u64
|
||||
// }
|
||||
pub fn store_array[T](fname string, array []T, params CompressParams) ! {
|
||||
mut fout := os.open_file(fname, 'wb')!
|
||||
mut cctx := new_cctx(params)!
|
||||
defer {
|
||||
cctx.free_cctx()
|
||||
fout.close()
|
||||
}
|
||||
|
||||
mut buf_out := []u8{len: buf_out_size}
|
||||
mut input := &InBuffer{}
|
||||
mut output := &OutBuffer{}
|
||||
// first, write the array.len to file
|
||||
mut len_buf := []u8{len: 8}
|
||||
binary.little_endian_put_u64(mut len_buf, u64(array.len))
|
||||
input.src = len_buf.data
|
||||
input.size = 8
|
||||
input.pos = 0
|
||||
output.dst = buf_out.data
|
||||
output.size = buf_out_size
|
||||
output.pos = 0
|
||||
mut remaining := cctx.compress_stream2(output, input, .flush)!
|
||||
fout.write(buf_out[..int(output.pos)])!
|
||||
// then, write the array.data to file
|
||||
input.src = array.data
|
||||
input.size = usize(array.len * int(sizeof(T)))
|
||||
input.pos = 0
|
||||
output.dst = buf_out.data
|
||||
output.size = buf_out_size
|
||||
output.pos = 0
|
||||
for {
|
||||
output.dst = buf_out.data
|
||||
output.size = buf_out_size
|
||||
output.pos = 0
|
||||
remaining = cctx.compress_stream2(output, input, .end)!
|
||||
fout.write(buf_out[..int(output.pos)])!
|
||||
if remaining == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// load_array return an array which data is decompressed from a file `fname`.
|
||||
// extra decompression parameters can be set by `params`
|
||||
pub fn load_array[T](fname string, params DecompressParams) ![]T {
|
||||
mut fin := os.open_file(fname, 'rb')!
|
||||
mut dctx := new_dctx(params)!
|
||||
defer {
|
||||
dctx.free_dctx()
|
||||
fin.close()
|
||||
}
|
||||
|
||||
mut buf_in := []u8{len: buf_in_size}
|
||||
mut len_buf := []u8{len: 8}
|
||||
mut input := &InBuffer{}
|
||||
mut output := &OutBuffer{}
|
||||
mut last_ret := usize(0)
|
||||
mut last_chunk := false
|
||||
// first, read the array.len from file
|
||||
mut read_len := fin.read(mut buf_in)!
|
||||
last_chunk = read_len < buf_in.len
|
||||
input.src = buf_in.data
|
||||
input.size = usize(read_len)
|
||||
input.pos = 0
|
||||
output.dst = len_buf.data
|
||||
output.size = usize(len_buf.len)
|
||||
output.pos = 0
|
||||
last_ret = dctx.decompress_stream(output, input)!
|
||||
len := binary.little_endian_u64(len_buf)
|
||||
// then, read the array.data from file
|
||||
mut result := []T{len: int(len)}
|
||||
output.dst = result.data
|
||||
output.size = usize(result.len) * sizeof(T)
|
||||
output.pos = 0
|
||||
last_ret = dctx.decompress_stream(output, input)!
|
||||
for !last_chunk {
|
||||
read_len = fin.read(mut buf_in)!
|
||||
last_chunk = read_len < buf_in.len
|
||||
input.size = usize(read_len)
|
||||
input.pos = 0
|
||||
for input.pos < input.size {
|
||||
last_ret = dctx.decompress_stream(output, input)!
|
||||
}
|
||||
if read_len < buf_in.len {
|
||||
break
|
||||
}
|
||||
}
|
||||
if last_ret != 0 {
|
||||
// The last return value from ZSTD_decompressStream did not end on a
|
||||
// frame, but we reached the end of the file! We assume this is an
|
||||
// error, and the input was truncated.
|
||||
return error('EOF before end of stream: ${last_ret}')
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
module zstd
|
||||
|
||||
// As MUSL has no `qsort_r()` as GLIBC, so fallback to `qsort()`
|
||||
#flag -DZSTD_USE_C90_QSORT
|
||||
@@ -0,0 +1,364 @@
|
||||
module zstd
|
||||
|
||||
import os
|
||||
|
||||
const samples_folder = os.join_path(os.dir(@FILE), 'samples')
|
||||
|
||||
fn s(fname string) string {
|
||||
return os.join_path(samples_folder, fname)
|
||||
}
|
||||
|
||||
fn test_zstd() {
|
||||
assert version_number() >= 10505
|
||||
|
||||
uncompressed := 'Hello world!'.repeat(10000)
|
||||
compressed := compress(uncompressed.bytes())!
|
||||
decompressed := decompress(compressed)!
|
||||
assert decompressed == uncompressed.bytes()
|
||||
}
|
||||
|
||||
fn test_zstd_deferent_compression_level() {
|
||||
uncompressed := 'Hello world!'.repeat(10000)
|
||||
|
||||
compressed_1000 := compress(uncompressed.bytes(), compression_level: 1000)!
|
||||
decompressed_1000 := decompress(compressed_1000)!
|
||||
assert decompressed_1000 == uncompressed.bytes()
|
||||
|
||||
compressed_0 := compress(uncompressed.bytes(), compression_level: 0)!
|
||||
decompressed_0 := decompress(compressed_0)!
|
||||
assert decompressed_0 == uncompressed.bytes()
|
||||
|
||||
compressed_1 := compress(uncompressed.bytes(), compression_level: 1)!
|
||||
decompressed_1 := decompress(compressed_1)!
|
||||
assert decompressed_1 == uncompressed.bytes()
|
||||
|
||||
compressed_15 := compress(uncompressed.bytes(), compression_level: 15)!
|
||||
decompressed_15 := decompress(compressed_15)!
|
||||
assert decompressed_15 == uncompressed.bytes()
|
||||
}
|
||||
|
||||
fn test_zstd_nb_threads() {
|
||||
uncompressed := 'Hello world!'.repeat(10000)
|
||||
|
||||
compressed_0 := compress(uncompressed.bytes(), nb_threads: 0)!
|
||||
decompressed_0 := decompress(compressed_0)!
|
||||
assert decompressed_0 == uncompressed.bytes()
|
||||
|
||||
compressed_1 := compress(uncompressed.bytes(), nb_threads: 1)!
|
||||
decompressed_1 := decompress(compressed_1)!
|
||||
assert decompressed_1 == uncompressed.bytes()
|
||||
|
||||
compressed_15 := compress(uncompressed.bytes(), nb_threads: 15)!
|
||||
decompressed_15 := decompress(compressed_15)!
|
||||
assert decompressed_15 == uncompressed.bytes()
|
||||
}
|
||||
|
||||
fn test_zstd_checksum_flag() {
|
||||
uncompressed := 'Hello world!'.repeat(10000)
|
||||
|
||||
compressed_true := compress(uncompressed.bytes(), checksum_flag: true)!
|
||||
decompressed_true := decompress(compressed_true)!
|
||||
assert decompressed_true == uncompressed.bytes()
|
||||
|
||||
compressed_false := compress(uncompressed.bytes(), checksum_flag: false)!
|
||||
decompressed_false := decompress(compressed_false)!
|
||||
assert decompressed_false == uncompressed.bytes()
|
||||
}
|
||||
|
||||
fn test_zstd_deferent_strategy() {
|
||||
uncompressed := 'Hello world!'.repeat(10000)
|
||||
|
||||
compressed_default := compress(uncompressed.bytes(), strategy: .default)!
|
||||
decompressed_default := decompress(compressed_default)!
|
||||
assert decompressed_default == uncompressed.bytes()
|
||||
|
||||
compressed_fast := compress(uncompressed.bytes(), strategy: .fast)!
|
||||
decompressed_fast := decompress(compressed_fast)!
|
||||
assert decompressed_fast == uncompressed.bytes()
|
||||
|
||||
compressed_dfast := compress(uncompressed.bytes(), strategy: .dfast)!
|
||||
decompressed_dfast := decompress(compressed_dfast)!
|
||||
assert decompressed_dfast == uncompressed.bytes()
|
||||
|
||||
compressed_greedy := compress(uncompressed.bytes(), strategy: .greedy)!
|
||||
decompressed_greedy := decompress(compressed_greedy)!
|
||||
assert decompressed_greedy == uncompressed.bytes()
|
||||
|
||||
compressed_lazy := compress(uncompressed.bytes(), strategy: .lazy)!
|
||||
decompressed_lazy := decompress(compressed_lazy)!
|
||||
assert decompressed_lazy == uncompressed.bytes()
|
||||
|
||||
compressed_lazy2 := compress(uncompressed.bytes(), strategy: .lazy2)!
|
||||
decompressed_lazy2 := decompress(compressed_lazy2)!
|
||||
assert decompressed_lazy2 == uncompressed.bytes()
|
||||
|
||||
compressed_btlazy2 := compress(uncompressed.bytes(), strategy: .btlazy2)!
|
||||
decompressed_btlazy2 := decompress(compressed_btlazy2)!
|
||||
assert decompressed_btlazy2 == uncompressed.bytes()
|
||||
|
||||
compressed_btopt := compress(uncompressed.bytes(), strategy: .btopt)!
|
||||
decompressed_btopt := decompress(compressed_btopt)!
|
||||
assert decompressed_btopt == uncompressed.bytes()
|
||||
|
||||
compressed_btultra := compress(uncompressed.bytes(), strategy: .btultra)!
|
||||
decompressed_btultra := decompress(compressed_btultra)!
|
||||
assert decompressed_btultra == uncompressed.bytes()
|
||||
|
||||
compressed_btultra2 := compress(uncompressed.bytes(), strategy: .btultra2)!
|
||||
decompressed_btultra2 := decompress(compressed_btultra2)!
|
||||
assert decompressed_btultra2 == uncompressed.bytes()
|
||||
}
|
||||
|
||||
fn compress_file(fname string, oname string, params CompressParams) ! {
|
||||
mut fin := os.open_file(fname, 'rb')!
|
||||
mut fout := os.open_file(oname, 'wb')!
|
||||
defer {
|
||||
fin.close()
|
||||
fout.close()
|
||||
}
|
||||
|
||||
mut buf_in := []u8{len: buf_in_size}
|
||||
mut buf_out := []u8{len: buf_out_size}
|
||||
|
||||
mut cctx := new_cctx(params)!
|
||||
defer {
|
||||
cctx.free_cctx()
|
||||
}
|
||||
|
||||
mut last_chunk := false
|
||||
mut input := &InBuffer{
|
||||
src: buf_in.data
|
||||
size: 0
|
||||
pos: 0
|
||||
}
|
||||
mut output := &OutBuffer{
|
||||
dst: buf_out.data
|
||||
size: 0
|
||||
pos: 0
|
||||
}
|
||||
for !last_chunk {
|
||||
read_len := fin.read(mut buf_in)!
|
||||
last_chunk = read_len < buf_in_size
|
||||
mode := if last_chunk {
|
||||
EndDirective.end
|
||||
} else {
|
||||
EndDirective.continue
|
||||
}
|
||||
|
||||
mut finished := false
|
||||
input.src = buf_in.data
|
||||
input.size = usize(read_len)
|
||||
input.pos = 0
|
||||
for !finished {
|
||||
output.dst = buf_out.data
|
||||
output.size = buf_out_size
|
||||
output.pos = 0
|
||||
remaining := cctx.compress_stream2(output, input, mode)!
|
||||
fout.write(buf_out[..int(output.pos)])!
|
||||
finished = if last_chunk { remaining == 0 } else { input.pos == input.size }
|
||||
}
|
||||
|
||||
if input.pos != input.size {
|
||||
return error('Impossible: zstd only returns 0 when the input is completely consumed!')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decompress_file(fname string, oname string, params DecompressParams) ! {
|
||||
mut fin := os.open_file(fname, 'rb')!
|
||||
mut fout := os.open_file(oname, 'wb')!
|
||||
defer {
|
||||
fin.close()
|
||||
fout.close()
|
||||
}
|
||||
|
||||
mut buf_in := []u8{len: buf_in_size}
|
||||
mut buf_out := []u8{len: buf_out_size}
|
||||
|
||||
mut dctx := new_dctx(params)!
|
||||
defer {
|
||||
dctx.free_dctx()
|
||||
}
|
||||
|
||||
mut input := &InBuffer{
|
||||
src: buf_in.data
|
||||
size: 0
|
||||
pos: 0
|
||||
}
|
||||
mut output := &OutBuffer{
|
||||
dst: buf_out.data
|
||||
size: 0
|
||||
pos: 0
|
||||
}
|
||||
|
||||
mut last_ret := usize(0)
|
||||
for {
|
||||
read_len := fin.read(mut buf_in)!
|
||||
input.src = buf_in.data
|
||||
input.size = usize(read_len)
|
||||
input.pos = 0
|
||||
for input.pos < input.size {
|
||||
output.dst = buf_out.data
|
||||
output.size = buf_out_size
|
||||
output.pos = 0
|
||||
ret := dctx.decompress_stream(output, input)!
|
||||
fout.write(buf_out[..int(output.pos)])!
|
||||
last_ret = ret
|
||||
}
|
||||
if read_len < buf_in.len {
|
||||
break
|
||||
}
|
||||
}
|
||||
if last_ret != 0 {
|
||||
/* The last return value from DecompressStream did not end on a
|
||||
* frame, but we reached the end of the file! We assume this is an
|
||||
* error, and the input was truncated.
|
||||
*/
|
||||
return error('EOF before end of stream: ${last_ret}')
|
||||
}
|
||||
}
|
||||
|
||||
// zstd stream mode test
|
||||
fn test_zstd_stream() {
|
||||
decompress_file(s('readme_level_19.zst'), s('tmp_file1'))!
|
||||
compress_file(s('tmp_file1'), s('tmp_file.zstd'),
|
||||
compression_level: 6
|
||||
nb_threads: 1
|
||||
checksum_flag: true
|
||||
)!
|
||||
decompress_file(s('tmp_file.zstd'), s('tmp_file2'))!
|
||||
file1 := os.read_file(s('tmp_file1'))!
|
||||
assert file1.contains('## Acknowledgement')
|
||||
assert file1.contains('## Troubleshooting')
|
||||
file2 := os.read_file(s('tmp_file2'))!
|
||||
assert file1 == file2
|
||||
os.rm(s('tmp_file1'))!
|
||||
os.rm(s('tmp_file2'))!
|
||||
os.rm(s('tmp_file.zstd'))!
|
||||
}
|
||||
|
||||
// store_array compress an `array`'s data, and store it to file `fname`.
|
||||
// extra compression parameters can be set by `params`
|
||||
// WARNING: Because struct padding, some data in struct may be marked unused.
|
||||
// So, when `store_array`, it will cause memory fsanitize fail with 'use-of-uninitialized-value'.
|
||||
// It can be safely ignored.
|
||||
// For example, following struct may cause memory fsanitize fail:
|
||||
// struct MemoryTrace {
|
||||
// operation u8
|
||||
// address u64
|
||||
// size u8
|
||||
// }
|
||||
// By changing it into following , you can pass the memory fsanitize check :
|
||||
// struct MemoryTrace {
|
||||
// operation u64
|
||||
// address u64
|
||||
// size u64
|
||||
// }
|
||||
struct MemoryTrace {
|
||||
operation u64
|
||||
address u64
|
||||
size u64
|
||||
}
|
||||
|
||||
fn store_array_test(fname string) ! {
|
||||
// Create a big array
|
||||
mut store_memory_trace := []MemoryTrace{cap: 1000}
|
||||
for i in 0 .. 1000 {
|
||||
store_memory_trace << MemoryTrace{
|
||||
operation: u64(`L`)
|
||||
address: u64(i)
|
||||
size: u8(i % 8)
|
||||
}
|
||||
}
|
||||
store_array[MemoryTrace](fname, store_memory_trace, compression_level: 8)!
|
||||
}
|
||||
|
||||
fn load_array_test(fname string) ! {
|
||||
load_memory_trace := load_array[MemoryTrace](fname)!
|
||||
for i in 0 .. 1000 {
|
||||
assert load_memory_trace[i].operation == `L`
|
||||
assert load_memory_trace[i].address == i
|
||||
assert load_memory_trace[i].size == u8(i % 8)
|
||||
}
|
||||
}
|
||||
|
||||
fn test_zstd_store_load_array() {
|
||||
store_array_test(s('mem_trace.zstd'))!
|
||||
load_array_test(s('mem_trace.zstd'))!
|
||||
os.rm(s('mem_trace.zstd'))!
|
||||
}
|
||||
|
||||
fn assert_decompress_error(data []u8, reason string) ! {
|
||||
decompress(data) or {
|
||||
assert err.msg() == reason
|
||||
return
|
||||
}
|
||||
return error('did not error')
|
||||
}
|
||||
|
||||
fn test_zstd_invalid_too_small() {
|
||||
assert_decompress_error([]u8{},
|
||||
'An error occurred (e.g. invalid magic number, srcSize too small)')!
|
||||
}
|
||||
|
||||
fn test_zstd_invalid_magic_numbers() {
|
||||
assert_decompress_error([]u8{len: 100},
|
||||
'An error occurred (e.g. invalid magic number, srcSize too small)')!
|
||||
}
|
||||
|
||||
fn test_zstd_invalid_compression() {
|
||||
mut data := []u8{len: 100}
|
||||
data[0] = 0x1f
|
||||
data[1] = 0x8b
|
||||
assert_decompress_error(data,
|
||||
'An error occurred (e.g. invalid magic number, srcSize too small)')!
|
||||
}
|
||||
|
||||
fn test_zstd_with_corruption1() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[5] = u8(0x7A)
|
||||
assert_decompress_error(compressed, 'Data corruption detected')!
|
||||
}
|
||||
|
||||
fn test_zstd_with_corruption2() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[6] = u8(0x7A)
|
||||
assert_decompress_error(compressed, 'Destination buffer is too small')!
|
||||
}
|
||||
|
||||
fn test_zstd_with_corruption3() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[7] = u8(0x7A)
|
||||
assert_decompress_error(compressed, 'Src size is incorrect')!
|
||||
}
|
||||
|
||||
fn test_zstd_with_corruption4() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[8] = u8(0x7A)
|
||||
assert_decompress_error(compressed, 'Src size is incorrect')!
|
||||
}
|
||||
|
||||
fn test_zstd_with_corruption5() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[9] = u8(0x7A)
|
||||
assert_decompress_error(compressed, "Restored data doesn't match checksum")!
|
||||
}
|
||||
|
||||
fn test_zstd_with_corruption6() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[10] = u8(0x7A)
|
||||
assert_decompress_error(compressed, "Restored data doesn't match checksum")!
|
||||
}
|
||||
|
||||
fn test_zstd_with_corruption7() {
|
||||
uncompressed := 'Hello world!'
|
||||
mut compressed := compress(uncompressed.bytes())!
|
||||
compressed[compressed.len - 1] += 1
|
||||
assert_decompress_error(compressed, "Restored data doesn't match checksum")!
|
||||
}
|
||||
Reference in new issue
Block a user