///|
/// Write GZIP header
fn gzh(c : FixedArray[Byte], o : GzipOptions) -> Unit {
c[0] = b'\x1F' // magic 1
c[1] = b'\x8B' // magic 2
c[2] = b'\x08' // CM = deflate
let xfl : Byte = if o.level < 2 {
b'\x04'
} else if o.level == 9 {
b'\x02'
} else {
b'\x00'
}
c[8] = xfl
c[9] = b'\x03' // OS = Unix
if o.mtime != 0 {
wbytes(c, 4, o.mtime)
}
if o.filename.length() > 0 {
c[3] = b'\x08' // FNAME flag
let fn_bytes = o.filename
for i in 0.. Int raise FzipError {
if d.length() < 10 {
raise fzip_err(InvalidHeader, msg="invalid gzip data")
}
if d[0] != b'\x1F' || d[1] != b'\x8B' || d[2] != b'\x08' {
raise fzip_err(InvalidHeader, msg="invalid gzip data")
}
let flg = d[3].to_int()
if (flg & 0xE0) != 0 {
raise fzip_err(InvalidHeader, msg="invalid gzip data")
}
if flg == 0 {
return 10
}
let mut st = 10
if (flg & 4) != 0 {
if st > d.length() - 2 {
raise fzip_err(InvalidHeader, msg="invalid gzip data")
}
let extra_len = d[st].to_int() | (d[st + 1].to_int() << 8)
st += extra_len + 2
if st > d.length() {
raise fzip_err(InvalidHeader, msg="invalid gzip data")
}
}
// skip FNAME and FCOMMENT
let mut zs = ((flg >> 3) & 1) + ((flg >> 4) & 1)
while zs > 0 {
if st >= d.length() {
raise fzip_err(InvalidHeader, msg="invalid gzip data")
}
if d[st] == b'\x00' {
zs -= 1
}
st += 1
}
// skip FHCRC
st + (flg & 2)
}
///|
/// Read a sync-API-safe GZIP ISIZE footer value.
fn gzl(d : FixedArray[Byte]) -> Int raise FzipError {
let l = d.length()
let raw = d[l - 4].to_uint() |
(d[l - 3].to_uint() << 8) |
(d[l - 2].to_uint() << 16) |
(d[l - 1].to_uint() << 24)
if raw > max_int_val().reinterpret_as_uint() {
raise fzip_err(InvalidZipData, msg="gzip ISIZE exceeds Int range")
}
raw.reinterpret_as_int()
}
///|
/// Calculate GZIP header length
fn gzhl(o : GzipOptions) -> Int {
10 + (if o.filename.length() > 0 { o.filename.length() + 1 } else { 0 })
}
///|
/// Compress data into a GZIP stream.
///
/// The output contains a GZIP header, a DEFLATE payload, and a footer with the
/// CRC-32 checksum and original input size. `GzipOptions` controls compression
/// level, optional dictionary use, and header metadata such as timestamp and
/// original filename. When a dictionary is used, callers must pass the same
/// dictionary to `gunzip_sync`; the GZIP format does not carry a dictionary ID.
pub fn gzip_sync(
data : FixedArray[Byte],
opts? : GzipOptions = GzipOptions::default(),
) -> FixedArray[Byte] {
let c = CRC32State::new()
let l = data.length()
let (d, len) = dopt(
data,
{ level: opts.level, mem: opts.mem, dictionary: opts.dictionary },
gzhl(opts),
8,
None,
crc_state=Some(c),
)
gzh(d, opts)
wbytes(d, len - 8, c.digest().reinterpret_as_int())
wbytes(d, len - 4, l)
trim_buf(d, len)
}
///|
/// Decompress a GZIP stream.
///
/// The GZIP header is parsed, the inner DEFLATE payload is inflated, and the
/// CRC-32 footer is verified by default. Set `verify_checksum` to `false` in
/// `GunzipOptions` only when checksum validation is handled elsewhere. If no
/// output buffer is provided, fzip allocates one using the GZIP ISIZE footer.
pub fn gunzip_sync(
data : FixedArray[Byte],
opts? : GunzipOptions = GunzipOptions::default(),
) -> FixedArray[Byte] raise FzipError {
let st = gzs(data)
if st + 8 > data.length() {
raise fzip_err(InvalidHeader, msg="invalid gzip data")
}
let isize = gzl(data)
if isize > opts.max_output_size {
raise fzip_err(InvalidZipData, msg="gzip ISIZE exceeds max_output_size")
}
let out = match opts.out {
Some(o) => o
None => FixedArray::make(isize, b'\x00')
}
let crc_st : CRC32State? = if opts.verify_checksum {
Some(CRC32State::new())
} else {
None
}
let (buf, len) = inflt(
data,
InflateState::new(2),
Some(out),
opts.dictionary,
opts.max_input_size,
opts.max_output_size,
dat_off=st,
dat_end=data.length() - 8,
crc_state=crc_st,
)
match crc_st {
Some(cs) => {
let expected_crc = b4(data, data.length() - 8)
if cs.digest() != expected_crc {
raise fzip_err(InvalidChecksum, msg="gzip CRC-32 mismatch")
}
}
None => ()
}
if len != isize {
raise fzip_err(InvalidZipData, msg="gzip ISIZE mismatch")
}
trim_buf(buf, len)
}