Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+21
View File
@@ -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`
+51
View File
@@ -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
+136
View File
@@ -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("'", "'\\''")}'"
}
+114
View File
@@ -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
+37
View File
@@ -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()
}
```
+291
View File
@@ -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)
+18
View File
@@ -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()
}
```
+98
View File
@@ -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)
}
+208
View File
@@ -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.
+35
View File
@@ -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'
```
+201
View File
@@ -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.
+256
View File
@@ -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')
}
}
+57
View File
@@ -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) }
}
}
+18
View File
@@ -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)
}
+18
View File
@@ -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)
}
+85
View File
@@ -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
}
+93
View File
@@ -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
}
}
+18
View File
@@ -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)
}
+18
View File
@@ -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)
}
+18
View File
@@ -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)
}
+18
View File
@@ -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)
}
+92
View File
@@ -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
+457
View File
@@ -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'
}
+6
View File
@@ -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.
+317
View File
@@ -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
}
+181
View File
@@ -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)
}
+17
View File
@@ -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)
+21
View File
@@ -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)
}
+91
View File
@@ -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
}
+25
View File
@@ -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.
+649
View File
@@ -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
+364
View File
@@ -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")!
}