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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 51 additions & 11 deletions crates/perry-api-manifest/src/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,17 @@ const fn p_any(name: &'static str) -> ParamSpec {
}
}

/// #1843 — every `zlib.create*` Transform-stream factory shares the same
/// shape: an optional `options` object in, a stream handle (`Any`) out.
const ZLIB_STREAM_OPTS: &[ParamSpec] = &[ParamSpec::Named {
name: "options",
ty: TypeSpec::Any,
optional: true,
}];
const fn zlib_stream_factory(name: &'static str) -> ApiEntry {
method_sig("zlib", name, false, None, ZLIB_STREAM_OPTS, TypeSpec::Any)
}

/// Source-of-truth manifest. See module-level docs for what feeds it.
pub static API_MANIFEST: &[ApiEntry] = &[
// ===========================================================
Expand Down Expand Up @@ -1271,23 +1282,52 @@ pub static API_MANIFEST: &[ApiEntry] = &[
// by axios for stream wiring. Values are resolved at runtime by
// `get_native_module_constant` in `perry-runtime/src/object.rs`.
property("zlib", "constants"),
// `zlib.createBrotliDecompress(options?)` — axios feature-checks this
// at module init (the typeof === 'function' shape). The native shim
// returns a registered Buffer-shaped handle; the real decode path is
// only reached when a server actually replies with
// `content-encoding: br`, which we leave for a follow-up.
// #1843 — Brotli one-shot compress/decompress (sync + async).
method_sig(
"zlib",
"createBrotliDecompress",
"brotliCompressSync",
false,
None,
&[ParamSpec::Named {
name: "options",
ty: TypeSpec::Any,
optional: true,
}],
&[p_str("p0")],
TypeSpec::String,
),
method_sig(
"zlib",
"brotliDecompressSync",
false,
None,
&[p_str("p0")],
TypeSpec::String,
),
method_sig(
"zlib",
"brotliCompress",
false,
None,
&[p_str("p0")],
TypeSpec::Any,
),
method_sig(
"zlib",
"brotliDecompress",
false,
None,
&[p_str("p0")],
TypeSpec::Any,
),
// #1843 — Transform-stream factories. Each returns a stream handle
// supporting `.write`/`.end`/`.on('data'|'end'|'error')`/`.pipe`.
zlib_stream_factory("createGzip"),
zlib_stream_factory("createGunzip"),
zlib_stream_factory("createDeflate"),
zlib_stream_factory("createInflate"),
zlib_stream_factory("createDeflateRaw"),
zlib_stream_factory("createInflateRaw"),
zlib_stream_factory("createUnzip"),
zlib_stream_factory("createBrotliCompress"),
// `zlib.createBrotliDecompress(options?)` — now a real Transform stream
// (still passes axios's `typeof === 'function'` module-init gate).
zlib_stream_factory("createBrotliDecompress"),
method_sig(
"cron",
"validate",
Expand Down
123 changes: 118 additions & 5 deletions crates/perry-codegen/src/lower_call/native_table/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,11 +322,124 @@ pub(super) const MEDIA_ROWS: &[NativeModSig] = &[
args: &[NA_STR],
ret: NR_PTR,
},
// `zlib.createBrotliDecompress(options?)` — axios feature-checks
// this at module init. The runtime stub returns a registered
// Buffer-shaped handle (NaN-boxed as a pointer) so callers see
// a truthy non-null object; the real Brotli decode path is a
// follow-up. `options` is NaN-boxed as f64.
// `zlib.brotli{Compress,Decompress}Sync(data)` — one-shot Brotli via the
// `brotli` crate (already a `compression`-feature dep). #1843 cluster 2.
NativeModSig {
module: "zlib",
has_receiver: false,
method: "brotliCompressSync",
class_filter: None,
runtime: "js_zlib_brotli_compress_sync",
args: &[NA_STR],
ret: NR_STR,
},
NativeModSig {
module: "zlib",
has_receiver: false,
method: "brotliDecompressSync",
class_filter: None,
runtime: "js_zlib_brotli_decompress_sync",
args: &[NA_STR],
ret: NR_STR,
},
NativeModSig {
module: "zlib",
has_receiver: false,
method: "brotliCompress",
class_filter: None,
runtime: "js_zlib_brotli_compress",
args: &[NA_STR],
ret: NR_PTR,
},
NativeModSig {
module: "zlib",
has_receiver: false,
method: "brotliDecompress",
class_filter: None,
runtime: "js_zlib_brotli_decompress",
args: &[NA_STR],
ret: NR_PTR,
},
// zlib Transform-stream factories (#1843 cluster 1). Each returns an i64
// stream handle (0x60000+ range) NaN-boxed with POINTER_TAG; subsequent
// `.write`/`.end`/`.on`/`.pipe`/`.flush`/`.close` lose their static type
// and route through HANDLE_METHOD_DISPATCH → `dispatch_zlib_stream`
// (crates/perry-stdlib/src/common/dispatch.rs). `options` is NaN-boxed f64.
NativeModSig {
module: "zlib",
has_receiver: false,
method: "createGzip",
class_filter: None,
runtime: "js_zlib_create_gzip",
args: &[NA_F64],
ret: NR_PTR,
},
NativeModSig {
module: "zlib",
has_receiver: false,
method: "createGunzip",
class_filter: None,
runtime: "js_zlib_create_gunzip",
args: &[NA_F64],
ret: NR_PTR,
},
NativeModSig {
module: "zlib",
has_receiver: false,
method: "createDeflate",
class_filter: None,
runtime: "js_zlib_create_deflate",
args: &[NA_F64],
ret: NR_PTR,
},
NativeModSig {
module: "zlib",
has_receiver: false,
method: "createInflate",
class_filter: None,
runtime: "js_zlib_create_inflate",
args: &[NA_F64],
ret: NR_PTR,
},
NativeModSig {
module: "zlib",
has_receiver: false,
method: "createDeflateRaw",
class_filter: None,
runtime: "js_zlib_create_deflate_raw",
args: &[NA_F64],
ret: NR_PTR,
},
NativeModSig {
module: "zlib",
has_receiver: false,
method: "createInflateRaw",
class_filter: None,
runtime: "js_zlib_create_inflate_raw",
args: &[NA_F64],
ret: NR_PTR,
},
NativeModSig {
module: "zlib",
has_receiver: false,
method: "createUnzip",
class_filter: None,
runtime: "js_zlib_create_unzip",
args: &[NA_F64],
ret: NR_PTR,
},
NativeModSig {
module: "zlib",
has_receiver: false,
method: "createBrotliCompress",
class_filter: None,
runtime: "js_zlib_create_brotli_compress",
args: &[NA_F64],
ret: NR_PTR,
},
// `zlib.createBrotliDecompress(options?)` — now a real Transform-stream
// handle (previously a feature-check Buffer stub; axios's
// `typeof createBrotliDecompress === 'function'` gate still passes).
NativeModSig {
module: "zlib",
has_receiver: false,
Expand Down
16 changes: 16 additions & 0 deletions crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,22 @@ pub fn declare_stdlib_ffi(module: &mut LlModule) {
module.declare_function("js_zlib_gzip", I64, &[I64]);
module.declare_function("js_zlib_gzip_sync", I64, &[I64]);
module.declare_function("js_zlib_inflate_sync", I64, &[I64]);
// #1843 — Brotli one-shots (StringHeader ptr in/out; async returns a Promise ptr).
module.declare_function("js_zlib_brotli_compress_sync", I64, &[I64]);
module.declare_function("js_zlib_brotli_decompress_sync", I64, &[I64]);
module.declare_function("js_zlib_brotli_compress", I64, &[I64]);
module.declare_function("js_zlib_brotli_decompress", I64, &[I64]);
// #1843 — Transform-stream factories: `_opts` (DOUBLE) in, i64 handle out.
// (`js_zlib_create_brotli_decompress` is declared alongside the other
// crypto/zlib helpers in runtime_decls/strings.rs.)
module.declare_function("js_zlib_create_gzip", I64, &[DOUBLE]);
module.declare_function("js_zlib_create_gunzip", I64, &[DOUBLE]);
module.declare_function("js_zlib_create_deflate", I64, &[DOUBLE]);
module.declare_function("js_zlib_create_inflate", I64, &[DOUBLE]);
module.declare_function("js_zlib_create_deflate_raw", I64, &[DOUBLE]);
module.declare_function("js_zlib_create_inflate_raw", I64, &[DOUBLE]);
module.declare_function("js_zlib_create_unzip", I64, &[DOUBLE]);
module.declare_function("js_zlib_create_brotli_compress", I64, &[DOUBLE]);

// ========== Buffer ==========
module.declare_function("js_buffer_alloc_unsafe", I64, &[I32]);
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-ext-zlib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ crate-type = ["staticlib", "rlib"]
[dependencies]
perry-ffi.workspace = true
flate2 = "1"
# Brotli stream + one-shot support (#1843). Matches the version the
# `compression` feature pulls into perry-stdlib.
brotli = "8.0.2"

[dev-dependencies]
perry-ffi = { workspace = true, features = ["runtime-link"] }
32 changes: 16 additions & 16 deletions crates/perry-ext-zlib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,22 @@

use flate2::read::{DeflateDecoder, DeflateEncoder, GzDecoder, GzEncoder};
use flate2::Compression;
use perry_ffi::{
alloc_buffer, alloc_bytes, read_bytes, spawn_blocking, BufferHeader, JsPromise, JsString,
JsValue, Promise, StringHeader,
};
use perry_ffi::{alloc_bytes, spawn_blocking, JsPromise, JsValue, Promise, StringHeader};
use std::io::Read;

// #1843 — Transform-stream objects (`createGzip`/`createDeflate`/… with
// `.write`/`.end`/`.on`/`.pipe`) and Brotli one-shots. Split into its own
// module to keep this file under the 2000-line size gate.
mod stream;
pub use stream::*;

unsafe fn read_input(ptr: *const StringHeader) -> Option<Vec<u8>> {
let handle = JsString::from_raw(ptr as *mut StringHeader);
read_bytes(handle).map(|b| b.to_vec())
// #1843: route through the buffer-aware reader so `gzipSync` / `gunzipSync`
// / `deflateSync` / `inflateSync` accept real Buffers/Uint8Arrays (e.g.
// `gunzipSync(Buffer.concat(chunks))`, `gunzipSync(fs.readFileSync(...))`),
// not just StringHeader-shaped inputs. Falls back to the StringHeader path
// for JS strings / our own `alloc_bytes` outputs.
stream::read_input_bytes(ptr)
}

fn gzip_bytes(data: &[u8]) -> std::io::Result<Vec<u8>> {
Expand Down Expand Up @@ -102,15 +109,8 @@ pub unsafe extern "C" fn js_zlib_inflate_sync(data_ptr: *const StringHeader) ->
}
}

/// `zlib.createBrotliDecompress(options?)`.
///
/// Minimal feature-check shim: return a truthy Buffer-shaped object so package
/// init paths that probe Brotli support can proceed. Real Brotli stream
/// decoding remains outside this wrapper's current surface.
#[no_mangle]
pub unsafe extern "C" fn js_zlib_create_brotli_decompress(_opts: f64) -> *mut BufferHeader {
alloc_buffer(&[])
}
// `zlib.createBrotliDecompress` and the other `create*` Transform-stream
// factories now live in `stream.rs` (returning real stream handles).

// ── async variants ────────────────────────────────────────────

Expand Down Expand Up @@ -185,7 +185,7 @@ pub unsafe extern "C" fn js_zlib_inflate(data_ptr: *const StringHeader) -> *mut
#[cfg(test)]
mod tests {
use super::*;
use perry_ffi::alloc_string;
use perry_ffi::{alloc_string, JsString};

#[test]
fn gzip_then_gunzip_round_trips_text() {
Expand Down
Loading