Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/libdeflate_sys/libdeflate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,15 @@ impl Default for Options {
}
}

/// Valid `compression_level` range for `libdeflate_alloc_compressor`. Values
/// outside this range make the allocator return NULL (indistinguishable from OOM),
/// so callers must range-check first.
pub const MIN_COMPRESSION_LEVEL: c_int = 0;
pub const MAX_COMPRESSION_LEVEL: c_int = 12;

unsafe extern "C" {
// Allocation: scalar arg, no preconditions; returns null on OOM.
// Allocation: scalar arg, no preconditions; returns null on OOM or
// compression_level outside MIN..=MAX_COMPRESSION_LEVEL.
pub(crate) safe fn libdeflate_alloc_compressor(compression_level: c_int) -> *mut Compressor;
// NOT safe: `Options` carries caller-supplied `malloc_func`/`free_func`
// callbacks that libdeflate will invoke and write through. A bogus callback
Expand Down Expand Up @@ -254,7 +261,8 @@ impl Compressor {
pub struct OwnedCompressor(NonNull<Compressor>);

impl OwnedCompressor {
/// Allocate a compressor at `level` (0..=12). Returns `None` on OOM.
/// Allocate a compressor at `level` ([`MIN_COMPRESSION_LEVEL`]..=[`MAX_COMPRESSION_LEVEL`]).
/// Returns `None` on OOM or if `level` is out of range.
#[inline]
pub fn new(level: c_int) -> Option<Self> {
NonNull::new(Compressor::alloc(level)).map(Self)
Expand Down
13 changes: 11 additions & 2 deletions src/runtime/api/BunObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2708,8 +2708,17 @@ pub mod JSZlib {
leak_list_into_uint8array(global_this, list)
}
Library::Libdeflate => {
let Some(mut compressor) = bun_libdeflate::OwnedCompressor::new(level.unwrap_or(6))
else {
let level = level.unwrap_or(6);
if !(bun_libdeflate::MIN_COMPRESSION_LEVEL..=bun_libdeflate::MAX_COMPRESSION_LEVEL)
.contains(&level)
{
return Err(global_this.throw_invalid_arguments(format_args!(
"Compression level must be between {} and {} for libdeflate",
bun_libdeflate::MIN_COMPRESSION_LEVEL,
bun_libdeflate::MAX_COMPRESSION_LEVEL,
)));
}
let Some(mut compressor) = bun_libdeflate::OwnedCompressor::new(level) else {
return Err(global_this.throw_out_of_memory());
};
let encoding = if is_gzip {
Expand Down
30 changes: 30 additions & 0 deletions test/js/node/zlib/zlib.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,36 @@ describe("zlib", () => {
const data = new TextEncoder().encode("Hello World!".repeat(1));
expect(() => gunzipSync(data, { library: "zlib" })).toThrow(new Error("incorrect header check"));
});

describe("libdeflate level validation", () => {
const data = Buffer.alloc(64, "a");
// libdeflate_alloc_compressor returns NULL for level outside [0, 12]; that NULL must
// surface as an invalid-argument error, not "Out of memory".
for (const fn of [gzipSync, deflateSync]) {
it(`${fn.name}: out-of-range level throws an argument error, not OOM`, () => {
for (const level of [-2, -1, 13, 100]) {
let err;
try {
fn(data, { library: "libdeflate", level });
} catch (e) {
err = e;
}
expect(err).toBeDefined();
expect(err.message).not.toContain("memory");
expect(err.message).toContain("Compression level must be between 0 and 12");
}
});

it(`${fn.name}: in-range levels 0..12 succeed and round-trip`, () => {
const decompress = fn === gzipSync ? gunzipSync : inflateSync;
for (const level of [0, 1, 6, 9, 12]) {
const out = fn(data, { library: "libdeflate", level });
expect(out.length).toBeGreaterThan(0);
expect(Buffer.from(decompress(out, { library: "libdeflate" }))).toEqual(data);
}
});
}
});
});

function* window(buffer, size, advance = size) {
Expand Down
Loading