use anyhow::Result;
use flate2::bufread::{GzDecoder, GzEncoder};
use flate2::Compression;
use rusty_v8 as v8;
use std::io::{Cursor, Read};
fn compression_close_method(
scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
_retval: v8::ReturnValue,
) {
let this_obj: v8::Local<v8::Object> = args.this();
let writable_key = v8::String::new(scope, "writable").unwrap();
if let Some(writable_val) = this_obj.get(scope, writable_key.into()) {
if let Ok(writable) = v8::Local::<v8::Object>::try_from(writable_val) {
let close_key = v8::String::new(scope, "close").unwrap();
if let Some(close_fn) = writable.get(scope, close_key.into()) {
if let Ok(close_fn_local) = v8::Local::<v8::Function>::try_from(close_fn) {
let _ = close_fn_local.call(scope, writable.into(), &[]);
}
}
}
}
}
fn decompression_close_method(
scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
_retval: v8::ReturnValue,
) {
let this_obj: v8::Local<v8::Object> = args.this();
let writable_key = v8::String::new(scope, "writable").unwrap();
if let Some(writable_val) = this_obj.get(scope, writable_key.into()) {
if let Ok(writable) = v8::Local::<v8::Object>::try_from(writable_val) {
let close_key = v8::String::new(scope, "close").unwrap();
if let Some(close_fn) = writable.get(scope, close_key.into()) {
if let Ok(close_fn_local) = v8::Local::<v8::Function>::try_from(close_fn) {
let _ = close_fn_local.call(scope, writable.into(), &[]);
}
}
}
}
}
fn create_uint8_array<'a>(
scope: &mut v8::PinScope<'a, '_>,
data: &[u8],
) -> Option<v8::Local<'a, v8::Uint8Array>> {
let buffer = v8::ArrayBuffer::new(scope, data.len());
if data.len() > 0 {
let store = buffer.get_backing_store();
let ptr = store
.data()
.map(|p| p.as_ptr() as *mut u8)
.unwrap_or(std::ptr::null_mut());
unsafe {
std::slice::from_raw_parts_mut(ptr, data.len()).copy_from_slice(data);
}
}
v8::Uint8Array::new(scope, buffer, 0, data.len())
}
fn uint8_array_to_vec(
scope: &mut v8::PinScope,
data: v8::Local<v8::Uint8Array>,
) -> Option<Vec<u8>> {
let len = data.byte_length();
if len == 0 {
return Some(Vec::new());
}
let buffer = data.buffer(scope)?;
let store = buffer.get_backing_store();
let ptr = store
.data()
.map(|p| p.as_ptr() as *const u8)
.unwrap_or(std::ptr::null());
if ptr.is_null() {
return None;
}
let byte_offset = data.byte_offset();
Some(unsafe { std::slice::from_raw_parts(ptr.add(byte_offset), len).to_vec() })
}
fn attach_compression_close_method(scope: &mut v8::PinScope, this_obj: v8::Local<v8::Object>) {
let close_key = v8::String::new(scope, "close").unwrap();
let close_fn_template = v8::FunctionTemplate::new(scope, compression_close_method);
if let Some(close_fn) = close_fn_template.get_function(scope) {
this_obj.set(scope, close_key.into(), close_fn.into());
}
}
fn attach_decompression_close_method(scope: &mut v8::PinScope, this_obj: v8::Local<v8::Object>) {
let close_key = v8::String::new(scope, "close").unwrap();
let close_fn_template = v8::FunctionTemplate::new(scope, decompression_close_method);
if let Some(close_fn) = close_fn_template.get_function(scope) {
this_obj.set(scope, close_key.into(), close_fn.into());
}
}
pub fn setup_compression_api(
scope: &mut v8::ContextScope<v8::HandleScope>,
context: &v8::Local<v8::Context>,
) -> Result<()> {
let global: _ = context.global(scope);
let compression_template: _ = v8::FunctionTemplate::new(scope, compression_stream_constructor);
let compression_constructor: _ = compression_template.get_function(scope).unwrap();
let compression_key: _ = v8::String::new(scope, "CompressionStream").unwrap();
global.set(
scope,
compression_key.into(),
compression_constructor.into(),
);
let decompression_template: _ =
v8::FunctionTemplate::new(scope, decompression_stream_constructor);
let decompression_constructor: _ = decompression_template.get_function(scope).unwrap();
let decompression_key: _ = v8::String::new(scope, "DecompressionStream").unwrap();
global.set(
scope,
decompression_key.into(),
decompression_constructor.into(),
);
let compress_template = v8::FunctionTemplate::new(
scope,
|_scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
mut retval: v8::ReturnValue| {
if args.length() >= 2 {
let data_arg = args.get(0);
let format_arg = args.get(1);
if let Ok(data) = v8::Local::<v8::Uint8Array>::try_from(data_arg) {
let Some(bytes) = uint8_array_to_vec(_scope, data) else {
return;
};
let format_str = if format_arg.is_string() {
format_arg
.to_string(_scope)
.map(|s| s.to_rust_string_lossy(_scope))
.unwrap_or_else(|| "gzip".to_string())
} else {
"gzip".to_string()
};
let compressed = if format_str == "gzip" {
let cursor = Cursor::new(bytes);
let mut encoder = GzEncoder::new(cursor, Compression::default());
let mut output = Vec::new();
let _ = encoder.read_to_end(&mut output);
output
} else {
let mut encoder = GzEncoder::new(bytes.as_slice(), Compression::default());
let mut output = Vec::new();
let _ = encoder.read_to_end(&mut output);
output
};
if let Some(result_array) = create_uint8_array(_scope, &compressed) {
retval.set(result_array.into());
}
}
}
},
);
let compress_fn = compress_template.get_function(scope).unwrap();
let compress_key = v8::String::new(scope, "_compressData").unwrap();
global.set(scope, compress_key.into(), compress_fn.into());
let decompress_template = v8::FunctionTemplate::new(
scope,
|_scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
mut retval: v8::ReturnValue| {
if args.length() >= 2 {
let data_arg = args.get(0);
let format_arg = args.get(1);
if let Ok(data) = v8::Local::<v8::Uint8Array>::try_from(data_arg) {
let Some(bytes) = uint8_array_to_vec(_scope, data) else {
return;
};
let format_str = if format_arg.is_string() {
format_arg
.to_string(_scope)
.map(|s| s.to_rust_string_lossy(_scope))
.unwrap_or_else(|| "gzip".to_string())
} else {
"gzip".to_string()
};
let decompressed = if format_str == "gzip" {
let cursor = Cursor::new(bytes);
let mut decoder = GzDecoder::new(cursor);
let mut output = Vec::new();
let _ = decoder.read_to_end(&mut output);
output
} else {
let mut decoder = GzDecoder::new(bytes.as_slice());
let mut output = Vec::new();
let _ = decoder.read_to_end(&mut output);
output
};
if let Some(result_array) = create_uint8_array(_scope, &decompressed) {
retval.set(result_array.into());
}
}
}
},
);
let decompress_fn = decompress_template.get_function(scope).unwrap();
let decompress_key = v8::String::new(scope, "_decompressData").unwrap();
global.set(scope, decompress_key.into(), decompress_fn.into());
Ok(())
}
fn compression_stream_constructor(
scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
mut retval: v8::ReturnValue,
) {
let format_arg = args.get(0);
let format_str: String = if format_arg.is_string() {
format_arg
.to_string(scope)
.map(|s| s.to_rust_string_lossy(scope).to_lowercase())
.unwrap_or_else(|| "gzip".to_string())
} else {
"gzip".to_string()
};
match format_str.as_str() {
"gzip" | "deflate" => {}
_ => {
let error = v8::String::new(
scope,
&format!("CompressionStream: unsupported format '{}'", format_str),
)
.unwrap();
let error_obj = v8::Exception::range_error(scope, error);
scope.throw_exception(error_obj.into());
return;
}
};
let this_obj: v8::Local<v8::Object> = args.this();
let format_key = v8::String::new(scope, "format").unwrap();
let format_val = v8::String::new(scope, &format_str).unwrap();
this_obj.set(scope, format_key.into(), format_val.into());
let setup_key = v8::String::new(scope, "_compressionSetup").unwrap();
let setup_val = v8::Boolean::new(scope, true).into();
this_obj.set(scope, setup_key.into(), setup_val);
let factory_code = format!(
r#"
(function() {{
const format = '{}';
let readableController = null;
let writableController = null;
let state = 0; // 0=open, 1=closed, 2=errored
// Create readable stream with controller reference
const readable = new ReadableStream({{
start(controller) {{
readableController = controller;
}}
}});
// Create writable stream with compression
const writable = new WritableStream({{
start(controller) {{
writableController = controller;
}},
async write(chunk) {{
if (state !== 0 || !writableController || !readableController) return;
// Get bytes from chunk
let data;
if (chunk instanceof Uint8Array) {{
data = chunk;
}} else if (typeof chunk === 'string') {{
data = new TextEncoder().encode(chunk);
}} else {{
data = new Uint8Array(chunk);
}}
// Compress using the Rust helper function
const compressed = _compressData(data, format);
if (compressed && compressed.length > 0) {{
readableController.enqueue(compressed);
}}
}},
close() {{
state = 1;
if (readableController) {{
readableController.close();
}}
writableController = null;
readableController = null;
}},
abort(err) {{
state = 2;
if (readableController) {{
readableController.error(err);
}}
writableController = null;
readableController = null;
}}
}});
return {{ readable, writable }};
}})"#,
format_str
);
let factory_code_str = v8::String::new(scope, &factory_code).unwrap();
if let Some(factory_script) = v8::Script::compile(scope, factory_code_str, None) {
if let Some(factory_fn) = factory_script.run(scope) {
if let Ok(factory) = v8::Local::<v8::Function>::try_from(factory_fn) {
let undefined = v8::undefined(scope).into();
if let Some(result) = factory.call(scope, undefined, &[]) {
if let Ok(result_obj) = v8::Local::<v8::Object>::try_from(result) {
let readable_key = v8::String::new(scope, "readable").unwrap();
if let Some(readable_val) = result_obj.get(scope, readable_key.into()) {
this_obj.set(scope, readable_key.into(), readable_val);
}
let writable_key = v8::String::new(scope, "writable").unwrap();
if let Some(writable_val) = result_obj.get(scope, writable_key.into()) {
this_obj.set(scope, writable_key.into(), writable_val);
}
}
}
}
}
}
attach_compression_close_method(scope, this_obj);
retval.set(this_obj.into());
}
fn decompression_stream_constructor(
scope: &mut v8::PinScope,
args: v8::FunctionCallbackArguments,
mut retval: v8::ReturnValue,
) {
let format_arg = args.get(0);
let format_str: String = if format_arg.is_string() {
format_arg
.to_string(scope)
.map(|s| s.to_rust_string_lossy(scope).to_lowercase())
.unwrap_or_else(|| "gzip".to_string())
} else {
"gzip".to_string()
};
match format_str.as_str() {
"gzip" | "deflate" => {}
_ => {
let error = v8::String::new(
scope,
&format!("DecompressionStream: unsupported format '{}'", format_str),
)
.unwrap();
let error_obj = v8::Exception::range_error(scope, error);
scope.throw_exception(error_obj.into());
return;
}
};
let this_obj: v8::Local<v8::Object> = args.this();
let format_key = v8::String::new(scope, "format").unwrap();
let format_val = v8::String::new(scope, &format_str).unwrap();
this_obj.set(scope, format_key.into(), format_val.into());
let setup_key = v8::String::new(scope, "_decompressionSetup").unwrap();
let setup_val = v8::Boolean::new(scope, true).into();
this_obj.set(scope, setup_key.into(), setup_val);
let factory_code = format!(
r#"
(function() {{
const format = '{}';
let readableController = null;
let writableController = null;
let state = 0; // 0=open, 1=closed, 2=errored
// Create readable stream
const readable = new ReadableStream({{
start(controller) {{
readableController = controller;
}}
}});
// Create writable stream with decompression
const writable = new WritableStream({{
start(controller) {{
writableController = controller;
}},
async write(chunk) {{
if (state !== 0 || !writableController || !readableController) return;
// Get bytes from chunk
let data;
if (chunk instanceof Uint8Array) {{
data = chunk;
}} else if (typeof chunk === 'string') {{
data = new TextEncoder().encode(chunk);
}} else {{
data = new Uint8Array(chunk);
}}
// Decompress using the Rust helper function
const decompressed = _decompressData(data, format);
if (decompressed && decompressed.length > 0) {{
readableController.enqueue(decompressed);
}}
}},
close() {{
state = 1;
if (readableController) {{
readableController.close();
}}
writableController = null;
readableController = null;
}},
abort(err) {{
state = 2;
if (readableController) {{
readableController.error(err);
}}
writableController = null;
readableController = null;
}}
}});
return {{ readable, writable }};
}})"#,
format_str
);
let factory_code_str = v8::String::new(scope, &factory_code).unwrap();
if let Some(factory_script) = v8::Script::compile(scope, factory_code_str, None) {
if let Some(factory_fn) = factory_script.run(scope) {
if let Ok(factory) = v8::Local::<v8::Function>::try_from(factory_fn) {
let undefined = v8::undefined(scope).into();
if let Some(result) = factory.call(scope, undefined, &[]) {
if let Ok(result_obj) = v8::Local::<v8::Object>::try_from(result) {
let readable_key = v8::String::new(scope, "readable").unwrap();
if let Some(readable_val) = result_obj.get(scope, readable_key.into()) {
this_obj.set(scope, readable_key.into(), readable_val);
}
let writable_key = v8::String::new(scope, "writable").unwrap();
if let Some(writable_val) = result_obj.get(scope, writable_key.into()) {
this_obj.set(scope, writable_key.into(), writable_val);
}
}
}
}
}
}
attach_decompression_close_method(scope, this_obj);
retval.set(this_obj.into());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compression_format_validation() {
let valid_formats = vec!["gzip", "deflate"];
let invalid_formats = vec!["invalid", "lzma", ""];
for format in valid_formats {
assert!(
format == "gzip" || format == "deflate",
"Valid format: {}",
format
);
}
for format in invalid_formats {
assert!(
format != "gzip" && format != "deflate",
"Invalid format: {}",
format
);
}
}
#[test]
fn test_compression_level() {
let _fast = Compression::fast();
let _default = Compression::default();
let _best = Compression::best();
}
}