From 3fef0036006f47459edb1c0af652f53cf5fd0aab Mon Sep 17 00:00:00 2001 From: Andrew DiZenzo Date: Thu, 4 Jun 2026 05:03:03 +0000 Subject: [PATCH] fix(buffer): align global File identity --- crates/perry-codegen/src/expr/new_dynamic.rs | 5 ++-- crates/perry-hir/src/lower/expr_new.rs | 11 +++++++ .../src/closure/dynamic_props.rs | 3 ++ .../src/object/class_registry.rs | 30 +++++++++++++++++++ .../perry-runtime/src/object/global_fetch.rs | 18 +++++++++++ .../perry-runtime/src/object/global_this.rs | 20 +++++++++++++ crates/perry-stdlib/src/common/dispatch.rs | 2 ++ crates/perry-stdlib/src/fetch/dispatch.rs | 6 ++++ .../buffer/blob-file/global-file.ts | 8 +++++ 9 files changed, 101 insertions(+), 2 deletions(-) diff --git a/crates/perry-codegen/src/expr/new_dynamic.rs b/crates/perry-codegen/src/expr/new_dynamic.rs index 2fa187536e..ca2016038f 100644 --- a/crates/perry-codegen/src/expr/new_dynamic.rs +++ b/crates/perry-codegen/src/expr/new_dynamic.rs @@ -455,10 +455,10 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // also supported because the helper falls back to a // class_id=0 empty-object allocation when no synthetic id // exists (preserves the pre-fix baseline). - // Also route PropertyGet callees through `js_new_function_construct`: + // Also route PropertyGet / IndexGet callees through `js_new_function_construct`: // covers `new date.constructor(value)` (date-fns // `constructFrom`) and generic `new obj.factory(...)` shapes - // where `obj.factory` resolves to a closure pointer at + // where `obj.factory` or `ctors[i]` resolves to a closure pointer at // runtime. The runtime helper detects the global Date / // Array / Object thunks and dispatches into the matching // real factory; non-matching closures still get the @@ -468,6 +468,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { Expr::FuncRef(_) | Expr::LocalGet(_) | Expr::PropertyGet { .. } + | Expr::IndexGet { .. } | Expr::Closure { .. } ); if routes_through_function_construct { diff --git a/crates/perry-hir/src/lower/expr_new.rs b/crates/perry-hir/src/lower/expr_new.rs index 78e895a7a0..b6333e686e 100644 --- a/crates/perry-hir/src/lower/expr_new.rs +++ b/crates/perry-hir/src/lower/expr_new.rs @@ -326,6 +326,17 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R return Ok(expr); } } + if obj_name == "globalThis" + && ctx.lookup_local("globalThis").is_none() + && is_fetch_constructor_name(prop_ident.sym.as_ref()) + { + ctx.uses_fetch = true; + return Ok(Expr::New { + class_name: prop_ident.sym.to_string(), + args: lower_optional_args(ctx, new_expr.args.as_deref())?, + type_args: Vec::new(), + }); + } let is_net_module = obj_name == "net" || ctx.lookup_builtin_module_alias(obj_name) == Some("net"); diff --git a/crates/perry-runtime/src/closure/dynamic_props.rs b/crates/perry-runtime/src/closure/dynamic_props.rs index 17b50d9e22..5c28c38372 100644 --- a/crates/perry-runtime/src/closure/dynamic_props.rs +++ b/crates/perry-runtime/src/closure/dynamic_props.rs @@ -274,6 +274,9 @@ pub fn is_closure_ptr(ptr: usize) -> bool { if ptr < 0x10000 { return false; } + if ptr % std::mem::align_of::() != 0 { + return false; + } unsafe { let type_tag = *((ptr as *const u8).add(12) as *const u32); type_tag == CLOSURE_MAGIC diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index bea798421c..d7aa1c3b36 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -672,6 +672,9 @@ pub(super) fn identify_global_builtin_constructor(func_value: f64) -> Option<&'s if ptr.is_null() { return None; } + if (ptr as usize) % std::mem::align_of::() != 0 { + return None; + } if !is_valid_obj_ptr(ptr as *const u8) { return None; } @@ -699,6 +702,7 @@ pub(super) fn identify_global_builtin_constructor(func_value: f64) -> Option<&'s || func_ptr == global_this_object_thunk as *const u8 as usize || func_ptr == global_this_date_thunk as *const u8 as usize || func_ptr == global_this_blob_thunk as *const u8 as usize + || func_ptr == global_this_file_thunk as *const u8 as usize || func_ptr == global_this_headers_thunk as *const u8 as usize || func_ptr == global_this_request_thunk as *const u8 as usize || func_ptr == global_this_response_thunk as *const u8 as usize @@ -1399,6 +1403,26 @@ pub unsafe extern "C" fn js_new_function_construct( .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); return crate::object::global_this_blob_thunk(std::ptr::null(), parts, options); } + "File" => { + let parts = args + .first() + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + let name = args + .get(1) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + let options = args + .get(2) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + return crate::object::global_this_file_thunk( + std::ptr::null(), + parts, + name, + options, + ); + } "Headers" => { let init = args .first() @@ -1867,6 +1891,9 @@ fn is_callable_function_value(value: f64) -> bool { if ptr.is_null() { return false; } + if (ptr as usize) % std::mem::align_of::() != 0 { + return false; + } if !is_valid_obj_ptr(ptr as *const u8) { return false; } @@ -1880,6 +1907,9 @@ fn is_arrow_function_value(value: f64) -> bool { return false; } let ptr = jv.as_pointer() as *const crate::closure::ClosureHeader; + if (ptr as usize) % std::mem::align_of::() != 0 { + return false; + } if ptr.is_null() || !is_valid_obj_ptr(ptr as *const u8) { return false; } diff --git a/crates/perry-runtime/src/object/global_fetch.rs b/crates/perry-runtime/src/object/global_fetch.rs index d95038b75e..d3ab8f3ffd 100644 --- a/crates/perry-runtime/src/object/global_fetch.rs +++ b/crates/perry-runtime/src/object/global_fetch.rs @@ -18,6 +18,7 @@ type FetchWithOptionsFn = unsafe extern "C" fn( ) -> *mut crate::promise::Promise; type FetchBlobNewFn = unsafe extern "C" fn(f64, f64) -> f64; +type FetchFileNewFn = unsafe extern "C" fn(f64, f64, f64, f64) -> f64; type FetchHeadersNewFn = extern "C" fn() -> f64; type FetchHeadersInitFromValueFn = unsafe extern "C" fn(f64, f64) -> f64; type FetchRequestNewFn = unsafe extern "C" fn( @@ -45,6 +46,7 @@ type FetchResponseStaticErrorFn = extern "C" fn() -> f64; static GLOBAL_FETCH_WITH_OPTIONS: AtomicPtr<()> = AtomicPtr::new(null_mut()); static GLOBAL_FETCH_BLOB_NEW: AtomicPtr<()> = AtomicPtr::new(null_mut()); +static GLOBAL_FETCH_FILE_NEW: AtomicPtr<()> = AtomicPtr::new(null_mut()); static GLOBAL_FETCH_HEADERS_NEW: AtomicPtr<()> = AtomicPtr::new(null_mut()); static GLOBAL_FETCH_HEADERS_INIT_FROM_VALUE: AtomicPtr<()> = AtomicPtr::new(null_mut()); static GLOBAL_FETCH_REQUEST_NEW: AtomicPtr<()> = AtomicPtr::new(null_mut()); @@ -61,6 +63,7 @@ pub extern "C" fn js_register_global_fetch_with_options(f: FetchWithOptionsFn) { #[no_mangle] pub extern "C" fn js_register_global_fetch_constructors( blob_new: FetchBlobNewFn, + file_new: FetchFileNewFn, headers_new: FetchHeadersNewFn, headers_init_from_value: FetchHeadersInitFromValueFn, request_new: FetchRequestNewFn, @@ -70,6 +73,7 @@ pub extern "C" fn js_register_global_fetch_constructors( response_static_error: FetchResponseStaticErrorFn, ) { GLOBAL_FETCH_BLOB_NEW.store(blob_new as *mut (), Ordering::Release); + GLOBAL_FETCH_FILE_NEW.store(file_new as *mut (), Ordering::Release); GLOBAL_FETCH_HEADERS_NEW.store(headers_new as *mut (), Ordering::Release); GLOBAL_FETCH_HEADERS_INIT_FROM_VALUE .store(headers_init_from_value as *mut (), Ordering::Release); @@ -166,6 +170,20 @@ pub(super) fn call_global_blob_new(parts: f64, type_value: f64) -> f64 { warn_unregistered_fetch_symbol("js_blob_new") } +pub(super) fn call_global_file_new( + parts: f64, + name: f64, + type_value: f64, + last_modified: f64, +) -> f64 { + let f = GLOBAL_FETCH_FILE_NEW.load(Ordering::Acquire); + if !f.is_null() { + let func: FetchFileNewFn = unsafe { std::mem::transmute(f) }; + return unsafe { func(parts, name, type_value, last_modified) }; + } + warn_unregistered_fetch_symbol("js_file_new") +} + pub(super) fn call_global_headers_new() -> f64 { let f = GLOBAL_FETCH_HEADERS_NEW.load(Ordering::Acquire); if !f.is_null() { diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 5751bfa438..fd2e046475 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -164,6 +164,22 @@ pub(crate) extern "C" fn global_this_blob_thunk( super::global_fetch::call_global_blob_new(parts, type_value) } +pub(crate) extern "C" fn global_this_file_thunk( + _closure: *const crate::closure::ClosureHeader, + parts: f64, + name: f64, + options: f64, +) -> f64 { + let type_value = global_this_fetch_option(options, b"type"); + let last_modified = global_this_fetch_option(options, b"lastModified"); + let last_modified = if last_modified.to_bits() == crate::value::TAG_UNDEFINED { + f64::NAN + } else { + last_modified + }; + super::global_fetch::call_global_file_new(parts, name, type_value, last_modified) +} + pub(crate) extern "C" fn global_this_headers_thunk( _closure: *const crate::closure::ClosureHeader, init: f64, @@ -2228,6 +2244,7 @@ pub(crate) fn populate_global_this_builtins(singleton: *mut ObjectHeader) { } "Date" => global_this_date_thunk as *const u8, "Blob" => global_this_blob_thunk as *const u8, + "File" => global_this_file_thunk as *const u8, "Headers" => global_this_headers_thunk as *const u8, "Request" => global_this_request_thunk as *const u8, "Response" => global_this_response_thunk as *const u8, @@ -2261,6 +2278,9 @@ pub(crate) fn populate_global_this_builtins(singleton: *mut ObjectHeader) { "Blob" | "Request" | "Response" => { crate::closure::js_register_closure_arity(func_ptr, 2); } + "File" => { + crate::closure::js_register_closure_arity(func_ptr, 3); + } "Error" | "TypeError" | "RangeError" | "ReferenceError" | "SyntaxError" | "EvalError" | "URIError" => { crate::closure::js_register_closure_arity(func_ptr, 1); diff --git a/crates/perry-stdlib/src/common/dispatch.rs b/crates/perry-stdlib/src/common/dispatch.rs index 3c84bf0ef5..b27730e32a 100644 --- a/crates/perry-stdlib/src/common/dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch.rs @@ -2616,6 +2616,7 @@ pub unsafe extern "C" fn js_stdlib_init_dispatch() { #[cfg(feature = "http-client")] fn js_register_global_fetch_constructors( blob_new: unsafe extern "C" fn(f64, f64) -> f64, + file_new: unsafe extern "C" fn(f64, f64, f64, f64) -> f64, headers_new: extern "C" fn() -> f64, headers_init_from_value: unsafe extern "C" fn(f64, f64) -> f64, request_new: unsafe extern "C" fn( @@ -2675,6 +2676,7 @@ pub unsafe extern "C" fn js_stdlib_init_dispatch() { #[cfg(feature = "http-client")] js_register_global_fetch_constructors( crate::fetch_blob::js_blob_new, + crate::fetch_blob::js_file_new, crate::fetch::js_headers_new, crate::fetch::js_headers_init_from_value, crate::fetch::js_request_new, diff --git a/crates/perry-stdlib/src/fetch/dispatch.rs b/crates/perry-stdlib/src/fetch/dispatch.rs index 1485da1d3a..7d3bfa448e 100644 --- a/crates/perry-stdlib/src/fetch/dispatch.rs +++ b/crates/perry-stdlib/src/fetch/dispatch.rs @@ -663,6 +663,12 @@ pub fn dispatch_blob_property(blob_id: usize, prop: &str) -> Option { js_string_from_bytes(blob.content_type.as_ptr(), blob.content_type.len() as u32); JSValue::string_ptr(p).bits() } + "name" => { + let name = blob.file_name.as_ref()?; + let p = js_string_from_bytes(name.as_ptr(), name.len() as u32); + JSValue::string_ptr(p).bits() + } + "lastModified" => return blob.last_modified_ms, _ => return None, }; Some(f64::from_bits(bits)) diff --git a/test-parity/node-suite/buffer/blob-file/global-file.ts b/test-parity/node-suite/buffer/blob-file/global-file.ts index 362eadd72a..3c55e9b6f2 100644 --- a/test-parity/node-suite/buffer/blob-file/global-file.ts +++ b/test-parity/node-suite/buffer/blob-file/global-file.ts @@ -2,6 +2,7 @@ import { File as BufferFile } from "node:buffer"; console.log("typeof File:", typeof File); console.log("File.name:", File.name); +console.log("File.length:", File.length); console.log("typeof globalThis.File:", typeof globalThis.File); console.log("global File identity:", globalThis.File === File); console.log("buffer File identity:", BufferFile === File, BufferFile === globalThis.File); @@ -20,3 +21,10 @@ const rebound = new FileCtor(["xy"], "y.txt", { }); console.log("rebound fields:", rebound.name, rebound.type, rebound.size, rebound.lastModified); console.log("rebound text:", await rebound.text()); + +const dynamic = new ([globalThis.File][0])(["zz"], "z.txt", { + type: "text/dynamic", + lastModified: 789, +}); +console.log("dynamic fields:", dynamic.name, dynamic.type, dynamic.size, dynamic.lastModified); +console.log("dynamic text:", await dynamic.text());