use crate::ir::{BufferDecl, DataType, Expr, Node, Program};
use crate::ops::compression::deflate_core;
use crate::ops::{AlgebraicLaw, OpSpec, BYTES_TO_BYTES_INPUTS, BYTES_TO_BYTES_OUTPUTS};
pub const LAWS: &[AlgebraicLaw] = &[AlgebraicLaw::Bounded { lo: 0, hi: 255 }];
pub const FHCRC: u8 = 0x02;
pub const FEXTRA: u8 = 0x04;
pub const FNAME: u8 = 0x08;
pub const FCOMMENT: u8 = 0x10;
pub const MAX_OUTPUT_RATIO: usize = 1024;
#[derive(Debug, Clone, Copy, Default)]
pub struct GzipDecompress;
impl GzipDecompress {
pub const SPEC: OpSpec = OpSpec::composition(
"compression.gzip_decompress",
BYTES_TO_BYTES_INPUTS,
BYTES_TO_BYTES_OUTPUTS,
LAWS,
Self::program,
);
#[must_use]
pub fn program() -> Program {
Program::new(
vec![
BufferDecl::read("input", 0, DataType::Bytes),
BufferDecl::output("out", 1, DataType::Bytes),
],
[1, 1, 1],
vec![Node::if_then(
Expr::gt(
Expr::buf_len("out"),
Expr::mul(Expr::buf_len("input"), Expr::u32(1024)),
),
vec![Node::Return],
)],
)
}
}
pub fn decompress_bytes(input: &[u8]) -> Result<Vec<u8>, String> {
if input.len() < 18 {
return Err(
"Fix: provide a complete gzip member with 10-byte header and 8-byte trailer.".into(),
);
}
if input[0] != 0x1f || input[1] != 0x8b || input[2] != 8 {
return Err("Fix: gzip header must use magic 1f8b and DEFLATE method 8.".into());
}
let flags = input[3];
if flags & 0xe0 != 0 {
return Err("Fix: gzip reserved flag bits must be zero.".into());
}
let declared_size = usize::try_from(u32::from_le_bytes([
input[input.len() - 4],
input[input.len() - 3],
input[input.len() - 2],
input[input.len() - 1],
]))
.map_err(|error| format!("Fix: gzip ISIZE must fit usize: {error}"))?;
let max_output = input.len().checked_mul(MAX_OUTPUT_RATIO).ok_or_else(|| {
"Fix: reject gzip input whose max_output_ratio multiplication overflows.".to_string()
})?;
if declared_size > max_output {
return Err("Fix: gzip decompression bomb from O(1) ISIZE trailer check.".into());
}
let start = payload_start(input, flags)?;
let payload_end = input.len() - 8;
if start > payload_end {
return Err("Fix: gzip optional header fields overrun compressed payload.".into());
}
let output = deflate_core::decompress(&input[start..payload_end], max_output)?;
if output.len() != declared_size {
return Err("Fix: gzip ISIZE trailer does not match decompressed length.".into());
}
let expected_crc = u32::from_le_bytes([
input[payload_end],
input[payload_end + 1],
input[payload_end + 2],
input[payload_end + 3],
]);
if crc32(&output) != expected_crc {
return Err("Fix: gzip CRC-32 trailer does not match decompressed bytes.".into());
}
Ok(output)
}
pub fn payload_start(input: &[u8], flags: u8) -> Result<usize, String> {
let mut pos = 10_usize;
if flags & FEXTRA != 0 {
let len = input
.get(pos..pos + 2)
.ok_or_else(|| "Fix: gzip FEXTRA length must be present before payload.".to_string())?;
pos += 2 + usize::from(u16::from_le_bytes([len[0], len[1]]));
}
if flags & FNAME != 0 {
pos = nul_terminated_end(input, pos, "FNAME")?;
}
if flags & FCOMMENT != 0 {
pos = nul_terminated_end(input, pos, "FCOMMENT")?;
}
if flags & FHCRC != 0 {
pos += 2;
}
Ok(pos)
}
pub fn nul_terminated_end(input: &[u8], start: usize, field: &str) -> Result<usize, String> {
input[start..]
.iter()
.position(|&byte| byte == 0)
.map(|offset| start + offset + 1)
.ok_or_else(|| format!("Fix: gzip {field} must be NUL terminated."))
}
pub fn crc32(bytes: &[u8]) -> u32 {
let mut crc = 0xffff_ffff_u32;
for &byte in bytes {
crc ^= u32::from(byte);
for _ in 0..8 {
crc = if crc & 1 != 0 {
(crc >> 1) ^ 0xedb8_8320
} else {
crc >> 1
};
}
}
crc ^ 0xffff_ffff
}