From b45f979db7ccab7f4ff2962e8c2b0c29c8c10d31 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 18 Sep 2026 02:42:11 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=95=B0=E7=BB=84=E9=95=BF=E5=BA=A6?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=BC=82=E5=B8=B8=E5=8E=9F=E5=80=BC=E6=8A=9B?= =?UTF-8?q?=E5=87=BA=E5=B9=B6=E4=BF=9D=E7=95=99=E9=94=99=E8=AF=AF=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/error.rs | 53 ++++++++++++------- crates/oxide_builtins/src/object.rs | 12 ++++- crates/oxide_builtins/src/reflect.rs | 20 ++++--- crates/oxide_runtime_api/src/host.rs | 7 +++ crates/oxide_vm/src/vm/mod.rs | 4 ++ crates/oxide_vm/src/vm/vmhost.rs | 3 ++ crates/oxide_vm/src/vm_props.rs | 34 +++++++++--- crates/oxide_vm/src/vm_support.rs | 3 ++ .../tests/array_length_define_tests.rs | 41 ++++++++++++++ 9 files changed, 144 insertions(+), 33 deletions(-) diff --git a/crates/oxide_builtins/src/error.rs b/crates/oxide_builtins/src/error.rs index ee146bde..3b6335df 100644 --- a/crates/oxide_builtins/src/error.rs +++ b/crates/oxide_builtins/src/error.rs @@ -71,6 +71,31 @@ pub fn create_error(host: &mut H, msg: &str) -> JsValue { create_kind_error(host, "Error", msg) } +/// 错误文本的 kind 前缀表:`(kind 名, 前缀)`,供文本恢复与 `define` 通道共用。 +/// `Error` 排在末位,避免在匹配更具体的子类前缀之前抢先命中。 +const KIND_PREFIXES: [(&str, &str); 7] = [ + ("TypeError", "TypeError: "), + ("ReferenceError", "ReferenceError: "), + ("RangeError", "RangeError: "), + ("SyntaxError", "SyntaxError: "), + ("URIError", "URIError: "), + ("EvalError", "EvalError: "), + ("Error", "Error: "), +]; + +/// 拆分错误文本的 kind 前缀:`"RangeError: msg"` 返回 `Some(("RangeError", "msg"))`, +/// 无已知前缀返回 `None`。文本可带 `"uncaught "` 包装。 +/// +/// # 注意事项 +/// - 仅在错误文本通道(kind 前缀约定)内使用;kind 名与创建入口的映射保持 +/// 单向,调用方不得据此改写原始异常值。 +pub fn split_kinded(text: &str) -> Option<(&'static str, &str)> { + let text = text.strip_prefix("uncaught ").unwrap_or(text); + KIND_PREFIXES + .iter() + .find_map(|(kind, prefix)| text.strip_prefix(prefix).map(|rest| (*kind, rest))) +} + /// 从错误文本(形如 `TypeError: msg` / `ReferenceError: msg`)解析类型前缀并创建 /// 对应 kind 的错误对象;无前缀时创建普通 Error。 /// @@ -78,23 +103,10 @@ pub fn create_error(host: &mut H, msg: &str) -> JsValue { /// 异常传播链中错误对象被降级为文本(error_text 输出、`last_uncaught_value` 被 /// 嵌套覆盖后的兜底路径)时,据此恢复错误类型,避免全部塌缩成普通 Error。 pub fn create_from_text(host: &mut H, text: &str) -> JsValue { - let text = text.strip_prefix("uncaught ").unwrap_or(text); - for (kind, prefix) in [ - ("TypeError", "TypeError: "), - ("ReferenceError", "ReferenceError: "), - ("RangeError", "RangeError: "), - ("SyntaxError", "SyntaxError: "), - ("URIError", "URIError: "), - ("EvalError", "EvalError: "), - ] { - if let Some(msg) = text.strip_prefix(prefix) { - return create_kind_error(host, kind, msg); - } + match split_kinded(text) { + Some((kind, msg)) => create_kind_error(host, kind, msg), + None => create_error(host, text.strip_prefix("uncaught ").unwrap_or(text)), } - if let Some(msg) = text.strip_prefix("Error: ") { - return create_kind_error(host, "Error", msg); - } - create_error(host, text) } /// 创建一个带指定 message 的 ReferenceError 对象。 @@ -110,11 +122,12 @@ pub fn create_range_error(host: &mut H, msg: &str) -> JsValue { /// 从 `defineProperty` 通道的错误文本恢复异常对象。 /// /// 数组 length 的 `ArraySetLength` 非法值以 `"RangeError: "` 前缀标记 kind, -/// 须保留为 RangeError;其余 define 失败(非可配置收窄、不可扩展、accessor -/// 冲突等)统一投影为 TypeError。 +/// 强转期 `ToPrimitive`/`ToNumber` 失败以 `"TypeError: "` 前缀标记,两者均须保留 +/// kind;其余 define 失败(非可配置收窄、不可扩展、accessor 冲突等)为无前缀 +/// 文本,统一投影为 TypeError。 pub fn create_define_failure(host: &mut H, msg: &str) -> JsValue { - match msg.strip_prefix("RangeError: ") { - Some(rest) => create_range_error(host, rest), + match split_kinded(msg) { + Some((kind, rest)) => create_kind_error(host, kind, rest), None => create_type_error(host, msg), } } diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 00a97852..6bd3e7e6 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -848,6 +848,11 @@ pub fn object_define_property(vm: &mut H, args: &[u8]) -> NativeResul let si = vm.property_key_si(vm.reg(args[2])); if let Err(msg) = define_from_descriptor(vm, obj_ptr, si, vm.reg(args[3])) { + // 强转期用户代码(valueOf / Symbol.toPrimitive)抛出的异常值转存专用槽, + // 须原值重抛而非改写成引擎 TypeError。 + if let Some(exc) = vm.take_pending_length_exception() { + return NativeResult::Err(exc); + } return NativeResult::Err(crate::error::create_define_failure(vm, &msg)); } NativeResult::Ok(obj_val) @@ -1217,7 +1222,12 @@ pub fn object_define_properties(vm: &mut H, args: &[u8]) -> NativeRes Err(err) => return NativeResult::Err(err), }; if let Err(msg) = define_all_from_properties(vm, target_ptr, vm.reg(args[2])) { - return NativeResult::Err(crate::error::create_type_error(vm, &msg)); + // 与单属性入口一致:强转期用户异常原值重抛,非法 length 以 RangeError + // kind 穿透,其余 define 失败投影为 TypeError。 + if let Some(exc) = vm.take_pending_length_exception() { + return NativeResult::Err(exc); + } + return NativeResult::Err(crate::error::create_define_failure(vm, &msg)); } NativeResult::Ok(target_val) } diff --git a/crates/oxide_builtins/src/reflect.rs b/crates/oxide_builtins/src/reflect.rs index 5f006d1c..12449fbf 100644 --- a/crates/oxide_builtins/src/reflect.rs +++ b/crates/oxide_builtins/src/reflect.rs @@ -74,8 +74,9 @@ pub fn reflect_construct(vm: &mut H, args: &[u8]) -> NativeResult { /// # 注意事项 /// - 与 `Object.defineProperty` 共用 `define_from_descriptor`:缺失字段回填现有 /// 属性、模块命名空间 exotic 收窄语义一处生效;普通失败投影为布尔 false。 -/// - 数组 length 的非法值(`"RangeError: "` 前缀)按规范抛 RangeError,不投影为 -/// false。 +/// - 数组 length 的非法值(`"RangeError: "` 前缀)与强转失败(`"TypeError: "` +/// 前缀)按规范抛对应错误,不投影为 false。 +/// - 强转期用户代码抛出的原始异常经 VM 专用槽原值重抛。 pub fn reflect_define_property(vm: &mut H, args: &[u8]) -> NativeResult { let target_val = arg(vm, args, 1); let Some(target_ptr) = object_ptr(target_val) else { @@ -89,10 +90,17 @@ pub fn reflect_define_property(vm: &mut H, args: &[u8]) -> NativeResu let key_si = vm.property_key_si(arg(vm, args, 2)); match crate::object::define_from_descriptor(vm, target_ptr, key_si, desc_val) { Ok(()) => NativeResult::Ok(JsValue::bool(true)), - Err(msg) => match msg.strip_prefix("RangeError: ") { - Some(rest) => NativeResult::Err(crate::error::create_range_error(vm, rest)), - None => NativeResult::Ok(JsValue::bool(false)), - }, + Err(msg) => { + // 强转期用户代码(valueOf / Symbol.toPrimitive)抛出的异常值转存专用槽, + // 须原值重抛,不得投影为 false 或改写成引擎错误。 + if let Some(exc) = vm.take_pending_length_exception() { + return NativeResult::Err(exc); + } + match crate::error::split_kinded(&msg) { + Some((kind, rest)) => NativeResult::Err(crate::error::create_kind_error(vm, kind, rest)), + None => NativeResult::Ok(JsValue::bool(false)), + } + } } } diff --git a/crates/oxide_runtime_api/src/host.rs b/crates/oxide_runtime_api/src/host.rs index 8961d3d6..937ae1f3 100644 --- a/crates/oxide_runtime_api/src/host.rs +++ b/crates/oxide_runtime_api/src/host.rs @@ -147,6 +147,13 @@ pub trait VmHost { /// 忽略调用(如 IteratorClose 的 `return()`)抛错时不得让自身值覆盖槽—— /// 调用前暂存、调用后恢复,保证在途异常值跨忽略调用存活。 fn restore_uncaught_value(&mut self, value: Option); + /// 取回数组 length define 强转期用户代码抛出的原始异常。 + /// + /// # 注意事项 + /// - 与 [`Self::take_uncaught_value`] 分离:后者可能残留其它操作忽略调用时 + /// 写入的值,本槽仅由 length 强转失败写入,入口据此区分「强转异常」与 + /// 「描述符收敛失败」。 + fn take_pending_length_exception(&mut self) -> Option; // 错误处理 fn checked_object_ptr(&mut self, val: JsValue, error_msg: &str) -> Result, String>; diff --git a/crates/oxide_vm/src/vm/mod.rs b/crates/oxide_vm/src/vm/mod.rs index 30757de1..758fb671 100644 --- a/crates/oxide_vm/src/vm/mod.rs +++ b/crates/oxide_vm/src/vm/mod.rs @@ -222,6 +222,10 @@ pub struct Vm { /// String 后,for-of 需要重新抛出原值。放在 VM 顶层字段(不在 InlineSyncState 中) /// 以便跨内联调用的恢复过程存活。 pub(crate) last_uncaught_value: Option, + /// 数组 length define 强转期用户代码抛出的原始异常:与 `last_uncaught_value` + /// 分离,避免入口误取其它操作忽略调用残留的值。仅 `define_array_length` 写入, + /// 由 Object/Reflect define 入口消费。 + pub(crate) pending_length_exception: Option, pub(crate) pending_exception: Option, pub(crate) pending_error_kind: Option<&'static str>, /// 控制流完成(break/continue/return)暂存,finally 执行后由 TRY_FINALLY_END 恢复。 diff --git a/crates/oxide_vm/src/vm/vmhost.rs b/crates/oxide_vm/src/vm/vmhost.rs index 0a8ba4bf..7d89ded4 100644 --- a/crates/oxide_vm/src/vm/vmhost.rs +++ b/crates/oxide_vm/src/vm/vmhost.rs @@ -52,6 +52,9 @@ impl oxide_runtime_api::VmHost for Vm { fn restore_uncaught_value(&mut self, value: Option) { self.last_uncaught_value = value; } + fn take_pending_length_exception(&mut self) -> Option { + self.pending_length_exception.take() + } fn property_key_si(&mut self, val: JsValue) -> u32 { // 对象键转换(to_string_full)失败时降级为空键:本 trait 路径供 // Reflect/Object 内置使用,这些内置按规范不传播键转换异常;计算属性 diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index 5aa3eb93..883cec49 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -1060,17 +1060,39 @@ impl Vm { /// 可写位、bump 世代。 /// /// # 注意事项 - /// - 非法长度以 `"RangeError: "` 前缀标记 kind,由 Object/Reflect 入口各自投影; - /// 其余失败投影为 TypeError。 + /// - 非法长度以 `"RangeError: "` 前缀标记 kind,强转失败以 `"TypeError: "` + /// 前缀标记;用户代码抛出的原始异常值转存 VM 专用槽,由 Object/Reflect + /// 入口取出并原值重抛。 fn define_array_length( &mut self, obj: &mut JsObject, val: JsValue, attributes: PropAttributes, ) -> Result<(), String> { + // BigInt 无 ToNumber 语义(ToNumber(BigInt) 抛 TypeError):在强转前拦截, + // 避免通用 to_number 的近似分支把 1n 当作 1.0 接受。 + if val.is_bigint() { + return Err(self.error_message_text("TypeError", "Cannot convert a BigInt value to a number")); + } + // [[Value]] 两次强转都要实际执行(可触发用户代码),且须早于描述符校验; - // 两次强转之间用户代码可能把 length 收窄为不可写。 - let new_len = self.coerce_uint32_bounded(val)?; - let number_len = self.coerce_number_bounded(val)?; + // 两次强转之间用户代码可能把 length 收窄为不可写。清空未捕获槽后执行, + // 槽内值必为本次强转产生;用户抛出时原值转存专用槽供入口原值重抛, + // 引擎转换失败(如 Symbol)留空由消息前缀重建 TypeError。 + self.last_uncaught_value = None; + let new_len = match self.coerce_uint32_bounded(val) { + Ok(v) => v, + Err(msg) => { + self.pending_length_exception = self.last_uncaught_value.take(); + return Err(msg); + } + }; + let number_len = match self.coerce_number_bounded(val) { + Ok(v) => v, + Err(msg) => { + self.pending_length_exception = self.last_uncaught_value.take(); + return Err(msg); + } + }; if new_len as f64 != number_len { - return Err("RangeError: Invalid array length".to_string()); + return Err(self.error_message_text("RangeError", "Invalid array length")); } let old_logical = obj.logical_len(); diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 8bb5287e..5266be2c 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -87,6 +87,7 @@ impl Vm { try_stack: Vec::new(), exception_value: None, last_uncaught_value: None, + pending_length_exception: None, pending_exception: None, pending_error_kind: None, pending_completion: None, @@ -216,6 +217,7 @@ impl Vm { try_stack: Vec::new(), exception_value: None, last_uncaught_value: None, + pending_length_exception: None, pending_exception: None, pending_error_kind: None, pending_completion: None, @@ -513,6 +515,7 @@ impl Vm { // 未捕获异常侧通道持原始 epoch 对象指针:执行期状态,跨 run/reset 不保留, // 池回收后残留将悬垂。 self.last_uncaught_value = None; + self.pending_length_exception = None; self.pending_exception = None; self.pending_error_kind = None; self.pending_completion = None; diff --git a/crates/oxide_vm/tests/array_length_define_tests.rs b/crates/oxide_vm/tests/array_length_define_tests.rs index c06cf7ee..eea6e310 100644 --- a/crates/oxide_vm/tests/array_length_define_tests.rs +++ b/crates/oxide_vm/tests/array_length_define_tests.rs @@ -146,6 +146,47 @@ fn define_length_shrink_partial_truncation_sets_writable() { ); } +#[test] +fn define_length_coercion_exception_propagates_original_value() { + // 强转期 valueOf 抛出的自定义 Error 在两个入口都须原值重抛(identity 不改写)。 + let obj_src = "var err=new Error('boom'); var v={valueOf:function(){throw err;}}; var c=null; try{Object.defineProperty([],'length',{value:v});}catch(e){c=e;} (c===err)+':'+c.name"; + let ref_src = "var err=new TypeError('boom'); var v={valueOf:function(){throw err;}}; var c=null; try{Reflect.defineProperty([],'length',{value:v});}catch(e){c=e;} (c===err)+':'+c.name"; + assert_eq!(eval_str(obj_src), "true:Error"); + assert_eq!(eval_str(ref_src), "true:TypeError"); + // Symbol.toPrimitive 抛出的异常同样原值重抛。 + assert_eq!( + eval_str("var err=new RangeError('sym'); var v={}; v[Symbol.toPrimitive]=function(h){throw err;}; var c=null; try{Reflect.defineProperty([],'length',{value:v});}catch(e){c=e;} (c===err)+':'+c.name"), + "true:RangeError" + ); +} + +#[test] +fn define_length_symbol_value_throws_type_error() { + // value=Symbol() 无 ToNumber 语义:两入口均抛 TypeError(Reflect 不得吞成 false)。 + assert_eq!( + eval_str("var r=[]; try{Object.defineProperty([],'length',{value:Symbol()});r.push('no-throw');}catch(e){r.push(e.name);} try{Reflect.defineProperty([],'length',{value:Symbol()});r.push('no-throw');}catch(e){r.push(e.name);} r.join(',')"), + "TypeError,TypeError" + ); +} + +#[test] +fn define_length_bigint_value_throws_type_error() { + // ToNumber(BigInt) 抛 TypeError:define 路径不得把 1n 近似为 1.0 接受。 + assert_eq!( + eval_str("var r=[]; try{Object.defineProperty([],'length',{value:1n});r.push('no-throw');}catch(e){r.push(e.name);} try{Reflect.defineProperty([],'length',{value:1n});r.push('no-throw');}catch(e){r.push(e.name);} r.join(',')"), + "TypeError,TypeError" + ); +} + +#[test] +fn define_properties_length_range_error_kind() { + // Object.defineProperties 须与单属性入口一致地保留 RangeError kind。 + assert_eq!( + eval_str("var r=[]; try{Object.defineProperties([],{length:{value:-1}});r.push('no-throw');}catch(e){r.push(e.name);} try{Object.defineProperties([],{length:{value:1.5}});r.push('no-throw');}catch(e){r.push(e.name);} r.join(',')"), + "RangeError,RangeError" + ); +} + #[test] fn define_length_configurable_true_error_leaves_elements() { // TypeError 后元素区不截断。