use crate::host::with_host;
use flate2::read::{DeflateDecoder, GzDecoder, ZlibDecoder};
use flate2::write::{DeflateEncoder, GzEncoder, ZlibEncoder};
use flate2::Compression;
use fusevm::Value;
use std::io::{Read, Write};
use super::buffer;
pub const MODULE_METHODS: &[&str] = &[
"gzipSync",
"gunzipSync",
"deflateSync",
"inflateSync",
"deflateRawSync",
"inflateRawSync",
"unzipSync",
"brotliCompressSync",
"brotliDecompressSync",
"zstdCompressSync",
"zstdDecompressSync",
"gzip",
"gunzip",
"deflate",
"inflate",
"deflateRaw",
"inflateRaw",
"unzip",
"brotliCompress",
"brotliDecompress",
"zstdCompress",
"zstdDecompress",
"crc32",
"createDeflate",
"createInflate",
"createGzip",
"createGunzip",
"createDeflateRaw",
"createInflateRaw",
"createUnzip",
"createBrotliCompress",
"createBrotliDecompress",
"createZstdCompress",
"createZstdDecompress",
];
pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
if method.starts_with("create") {
return Some(Err(crate::host::type_error(&format!(
"zlib.{method} is not supported in node-js (no streaming backend)"
))));
}
if method == "crc32" {
let data = input_bytes(args);
let init = {
let n = super::arg_num(args, 1);
if n.is_nan() {
0
} else {
n as i64 as u32
}
};
return Some(Ok(Value::Float(crc32(&data, init) as f64)));
}
if is_async(method) {
return Some(run_async(method, args));
}
let base = method.strip_suffix("Sync")?;
let out = oneshot(base, &input_bytes(args));
Some(out.map(|bytes| buffer::from_bytes(&bytes)))
}
fn is_async(method: &str) -> bool {
matches!(
method,
"gzip"
| "gunzip"
| "deflate"
| "inflate"
| "deflateRaw"
| "inflateRaw"
| "unzip"
| "brotliCompress"
| "brotliDecompress"
| "zstdCompress"
| "zstdDecompress"
)
}
fn run_async(op: &str, args: &[Value]) -> Result<Value, String> {
let Some(cb) = args.last().cloned() else {
return Ok(Value::Undef);
};
let input = input_bytes(args);
let (err, buf) = match oneshot(op, &input) {
Ok(bytes) => (with_host(|h| h.null()), buffer::from_bytes(&bytes)),
Err(e) => (
with_host(|h| {
h.exc
.take()
.unwrap_or_else(|| crate::builtins::synth_error(h, &e))
}),
Value::Undef,
),
};
with_host(|h| h.queue_micro(cb, vec![err, buf]));
Ok(Value::Undef)
}
fn oneshot(op: &str, input: &[u8]) -> Result<Vec<u8>, String> {
match op {
"gzip" => gzip(input),
"gunzip" => gunzip(input),
"deflate" => deflate(input),
"inflate" => inflate(input),
"deflateRaw" => deflate_raw(input),
"inflateRaw" => inflate_raw(input),
"unzip" => unzip(input),
"brotliCompress" => brotli_compress(input),
"brotliDecompress" => brotli_decompress(input),
"zstdCompress" => zstd_compress(input),
"zstdDecompress" => zstd_decompress(input),
_ => Err(format!("Error: unknown zlib op '{op}'")),
}
}
fn input_bytes(args: &[Value]) -> Vec<u8> {
let v = args.first().cloned().unwrap_or(Value::Undef);
match super::buffer::view_bytes(&v) {
Some(b) => b,
None => with_host(|h| h.str_of(&v)).into_bytes(),
}
}
fn io_err(e: std::io::Error) -> String {
format!("Error: {e}")
}
fn decode_err(truncated: bool) -> String {
let (code, errno, msg) = if truncated {
("Z_BUF_ERROR", -5, "unexpected end of file")
} else {
("Z_DATA_ERROR", -3, "incorrect header check")
};
let e = crate::builtins::make_error_pub("Error", msg);
for (k, v) in [
("errno", Value::Float(errno as f64)),
("code", with_host(|h| h.new_str(code.to_string()))),
] {
let _ = crate::builtins::set_property_pub(&e, k, v);
}
with_host(|h| h.exc = Some(e));
format!("Error: {msg}")
}
fn header_ok(kind: &str, input: &[u8]) -> bool {
match kind {
"gzip" => input.len() >= 2 && input[0] == 0x1f && input[1] == 0x8b,
"zlib" => {
input.len() >= 2
&& input[0] & 0x0f == 8
&& (u16::from(input[0]) * 256 + u16::from(input[1])) % 31 == 0
}
_ => true,
}
}
fn gzip(input: &[u8]) -> Result<Vec<u8>, String> {
let mut enc = GzEncoder::new(Vec::new(), Compression::default());
enc.write_all(input).map_err(io_err)?;
enc.finish().map_err(io_err)
}
fn gunzip(input: &[u8]) -> Result<Vec<u8>, String> {
let mut out = Vec::new();
GzDecoder::new(input)
.read_to_end(&mut out)
.map_err(|_| decode_err(header_ok("gzip", input)))?;
Ok(out)
}
fn deflate(input: &[u8]) -> Result<Vec<u8>, String> {
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(input).map_err(io_err)?;
enc.finish().map_err(io_err)
}
fn inflate(input: &[u8]) -> Result<Vec<u8>, String> {
let mut out = Vec::new();
ZlibDecoder::new(input)
.read_to_end(&mut out)
.map_err(|_| decode_err(header_ok("zlib", input)))?;
Ok(out)
}
fn deflate_raw(input: &[u8]) -> Result<Vec<u8>, String> {
let mut enc = DeflateEncoder::new(Vec::new(), Compression::default());
enc.write_all(input).map_err(io_err)?;
enc.finish().map_err(io_err)
}
fn inflate_raw(input: &[u8]) -> Result<Vec<u8>, String> {
let mut out = Vec::new();
DeflateDecoder::new(input)
.read_to_end(&mut out)
.map_err(io_err)?;
Ok(out)
}
fn unzip(input: &[u8]) -> Result<Vec<u8>, String> {
if input.starts_with(&[0x1f, 0x8b]) {
gunzip(input)
} else {
inflate(input)
}
}
fn brotli_compress(input: &[u8]) -> Result<Vec<u8>, String> {
let mut out = Vec::new();
{
let mut enc = brotli::CompressorWriter::new(&mut out, 4096, 11, 22);
enc.write_all(input).map_err(io_err)?;
}
Ok(out)
}
fn brotli_decompress(input: &[u8]) -> Result<Vec<u8>, String> {
let mut out = Vec::new();
brotli::Decompressor::new(input, 4096)
.read_to_end(&mut out)
.map_err(io_err)?;
Ok(out)
}
fn zstd_compress(input: &[u8]) -> Result<Vec<u8>, String> {
zstd::encode_all(input, 3).map_err(io_err)
}
fn zstd_decompress(input: &[u8]) -> Result<Vec<u8>, String> {
zstd::decode_all(input).map_err(io_err)
}
fn crc32(data: &[u8], init: u32) -> u32 {
let mut h = crc32fast::Hasher::new_with_initial(init);
h.update(data);
h.finalize()
}