From 7062300a377054830f9cca05e0889835ed22d6ff Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 20 Sep 2026 22:15:00 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20define/create=20=E9=9D=9E=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E7=9B=AE=E6=A0=87=E6=8A=9B=E9=94=99=E4=B8=8E=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0=E7=AC=A6=E4=B8=A4=E9=98=B6=E6=AE=B5=E6=94=B6=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/error.rs | 5 + crates/oxide_builtins/src/object.rs | 84 ++++++++----- .../oxide_vm/tests/builtins_object_tests.rs | 115 ++++++++++++++++++ 3 files changed, 176 insertions(+), 28 deletions(-) diff --git a/crates/oxide_builtins/src/error.rs b/crates/oxide_builtins/src/error.rs index 3b6335df..7e1fd287 100644 --- a/crates/oxide_builtins/src/error.rs +++ b/crates/oxide_builtins/src/error.rs @@ -89,8 +89,13 @@ const KIND_PREFIXES: [(&str, &str); 7] = [ /// # 注意事项 /// - 仅在错误文本通道(kind 前缀约定)内使用;kind 名与创建入口的映射保持 /// 单向,调用方不得据此改写原始异常值。 +/// - 空消息错误的文本即裸 kind 名(如 `"uncaught RangeError"`,与 +/// `String(new RangeError())` 序列化一致),按原文恢复 kind。 pub fn split_kinded(text: &str) -> Option<(&'static str, &str)> { let text = text.strip_prefix("uncaught ").unwrap_or(text); + if let Some((kind, _)) = KIND_PREFIXES.iter().find(|(kind, _)| *kind == text) { + return Some((*kind, "")); + } KIND_PREFIXES .iter() .find_map(|(kind, prefix)| text.strip_prefix(prefix).map(|rest| (*kind, rest))) diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index e9cedd03..eb02e5d1 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -566,7 +566,11 @@ pub fn object_create(vm: &mut H, args: &[u8]) -> NativeResult { let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); if args.len() >= 3 && !vm.reg(args[2]).is_undefined() { if let Err(msg) = define_all_from_properties(vm, obj, vm.reg(args[2])) { - return NativeResult::Err(crate::error::create_type_error(vm, &msg)); + // 强转期用户异常原值重抛;define 失败文本按 kind 前缀恢复原类型。 + if let Some(exc) = vm.take_pending_length_exception() { + return NativeResult::Err(exc); + } + return NativeResult::Err(crate::error::create_define_failure(vm, &msg)); } } return NativeResult::Ok(JsValue::from_js_object(obj)); @@ -580,7 +584,11 @@ pub fn object_create(vm: &mut H, args: &[u8]) -> NativeResult { let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, proto_val)); if args.len() >= 3 && !vm.reg(args[2]).is_undefined() { if let Err(msg) = define_all_from_properties(vm, obj, vm.reg(args[2])) { - return NativeResult::Err(crate::error::create_type_error(vm, &msg)); + // 强转期用户异常原值重抛;define 失败文本按 kind 前缀恢复原类型。 + 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(JsValue::from_js_object(obj)) @@ -819,36 +827,54 @@ pub(crate) fn define_from_descriptor( Ok(()) } -/// 遍历对象自身的可枚举属性,把每个值当作描述符依次定义到目标对象上。 -/// `Object.defineProperties` 与 `Object.create(proto, properties)` 共用。 +/// 把 properties 实参按 ToObject 装箱后遍历其可枚举自身属性,把每个值当作 +/// 描述符定义到目标对象上。`Object.defineProperties` 与 +/// `Object.create(proto, properties)` 共用。 +/// +/// # 步骤 +/// 1. ToObject 装箱 properties(null/undefined 抛 TypeError,包装盒无键则零定义) +/// 2. 先对全部可枚举键完成描述符值收集(触发全部 getter),再逐键定义—— +/// 先取后写,避免前键定义影响后键读取 /// /// # 边界与前提 -/// - `props_val` 必须是对象,否则返回错误(null 触 ToObject 抛 TypeError) /// - 仅处理可枚举自身属性;描述符值经 ordinary_get 读取(触发访问器 getter) +/// - 描述符值的非对象校验由 define_from_descriptor 承担(ToPropertyDescriptor) +/// - String 盒的自身键是字符下标 0..len-1(String exotic,各键值为对应 +/// 字符),盒存储未落属性枚举面,此处按规范物化 /// /// # 副作用 /// - 修改 target 的 shape 链、属性表与 generation fn define_all_from_properties( vm: &mut H, target_ptr: *mut JsObject, props_val: JsValue, ) -> Result<(), String> { - if !props_val.is_object() { - return Err("Property description must be an object".to_string()); - } - let props_ptr = props_val.as_js_object_ptr(); - if props_ptr.is_null() { - return Err("Property description must be an object".to_string()); - } - let prop_keys: Vec<(u32, u32)> = { + // ToObject:原始值装箱;null/undefined 的 TypeError 文本经调用方 kind + // 前缀恢复后原类型抛出。 + let props_obj = oxide_runtime_api::to_object(props_val, vm)?; + let props_ptr = props_obj.as_js_object_ptr(); + let descriptors: Vec<(u32, JsValue)> = if unsafe { &*props_ptr }.is_string_obj() { + // String 盒:键为字符下标、值为字符本身;空串零键。 + let props = unsafe { &*props_ptr }; + let raw = props.boxed_value(); + let raw = if raw.is_undefined() { props.get_prop_at(0) } else { raw }; + let units = if raw.is_string() { vm.string_units(raw).to_vec() } else { Vec::new() }; + units + .iter() + .enumerate() + .map(|(i, unit)| (make_int_key(i as u32), vm.new_string_units(std::slice::from_ref(unit)))) + .collect() + } else { let props = unsafe { &*props_ptr }; walk_own_keys(vm, props) + .into_iter() + // 非可枚举自身属性跳过;无显式 meta 视为可枚举(普通字面量默认)。 + .filter(|(_si, offset)| !props.prop_meta_at(*offset).map(|m| !m.attributes.enumerable()).unwrap_or(false)) + .map(|(key_si, _offset)| { + let props = unsafe { &*props_ptr }; + vm.ordinary_get(props, key_si, props_obj).map(|desc_val| (key_si, desc_val)) + }) + .collect::, _>>()? }; - for (key_si, offset) in prop_keys { - let props = unsafe { &*props_ptr }; - // 非可枚举自身属性跳过;无显式 meta 视为可枚举(普通字面量默认)。 - if props.prop_meta_at(offset).map(|m| !m.attributes.enumerable()).unwrap_or(false) { - continue; - } - let desc_val = vm.ordinary_get(props, key_si, props_val)?; + for (key_si, desc_val) in descriptors { define_from_descriptor(vm, target_ptr, key_si, desc_val)?; } Ok(()) @@ -863,10 +889,11 @@ pub fn object_define_property(vm: &mut H, args: &[u8]) -> NativeResul "Object.defineProperty: expected at least 3 arguments", )); } - let obj_val = match oxide_runtime_api::to_object(vm.reg(args[1]), vm) { - Ok(v) => v, - Err(msg) => return NativeResult::Err(crate::error::create_type_error(vm, &msg)), - }; + // target 非对象直接抛 TypeError(不做 ToObject 装箱)。 + let obj_val = vm.reg(args[1]); + if !obj_val.is_object() { + return NativeResult::Err(crate::error::create_type_error(vm, "Object.defineProperty called on non-object")); + } let obj_ptr = obj_val.as_js_object_ptr(); // well-known symbol 等特殊键统一走 property_key_si(映射到各自的 Symbol 键), // 保证与计算属性访问、Reflect.defineProperty 等读键路径一致。 @@ -1306,11 +1333,12 @@ pub fn object_define_properties(vm: &mut H, args: &[u8]) -> NativeRes "Object.defineProperties: expected at least 2 arguments", )); } + // target 非对象直接抛 TypeError(不做 ToObject 装箱)。 let target_val = vm.reg(args[1]); - let target_ptr = match require_obj_arg(vm, args, "defineProperties") { - Ok(ptr) => ptr, - Err(err) => return NativeResult::Err(err), - }; + if !target_val.is_object() { + return NativeResult::Err(crate::error::create_type_error(vm, "Object.defineProperties called on non-object")); + } + let target_ptr = target_val.as_js_object_ptr(); if let Err(msg) = define_all_from_properties(vm, target_ptr, vm.reg(args[2])) { // 与单属性入口一致:强转期用户异常原值重抛,非法 length 以 RangeError // kind 穿透,其余 define 失败投影为 TypeError。 diff --git a/crates/oxide_vm/tests/builtins_object_tests.rs b/crates/oxide_vm/tests/builtins_object_tests.rs index 2c1f751f..ec500c68 100644 --- a/crates/oxide_vm/tests/builtins_object_tests.rs +++ b/crates/oxide_vm/tests/builtins_object_tests.rs @@ -432,3 +432,118 @@ fn object_get_own_property_descriptors_null_receiver_throws() { }; assert!(err.contains("TypeError"), "unexpected error: {err}"); } + +// -- defineProperties / create properties 实参语义 -- + +#[test] +fn object_define_properties_primitive_props_boxed() { + // properties 实参经 ToObject 装箱(布尔/数字/空串/Symbol/数字盒), + // 包装盒均无可枚举自身键:零定义,目标原样返回且自身键集不变。 + for arg in ["false", "-12", "''", "Symbol('s')", "42"] { + let (_vm, result) = eval(&format!( + "(function(){{ var t = {{m: 1}}; return Object.defineProperties(t, {arg}) === t }})()" + )) + .unwrap(); + assert!(result.is_bool() && result.as_bool(), "arg {arg}"); + let (_vm, result) = eval(&format!( + "(function(){{ var t = {{m: 1}}; Object.defineProperties(t, {arg}); return Object.keys(t).length }})()" + )) + .unwrap(); + assert_eq!(result.as_int(), 1, "arg {arg}"); + } +} + +#[test] +fn object_create_primitive_props_boxed() { + // Object.create 的 properties 实参同走 ToObject:数字盒零键,仅原型生效。 + let (_vm, result) = eval( + "(function(){ var p = {}; var o = Object.create(p, 42); \ + return Object.getPrototypeOf(o) === p && Object.keys(o).length === 0 })()", + ) + .unwrap(); + assert!(result.is_bool() && result.as_bool()); +} + +#[test] +fn object_create_bigint_props_boxed() { + // BigInt 实参装箱,BigInt 盒零可枚举自身键:仅原型生效。 + let (_vm, result) = eval( + "(function(){ var p = {}; var o = Object.create(p, 0n); \ + return Object.getPrototypeOf(o) === p && Object.keys(o).length === 0 })()", + ) + .unwrap(); + assert!(result.is_bool() && result.as_bool()); +} + +#[test] +fn object_define_properties_null_props_throws() { + // null 是 ToObject 拒绝的值:两入口均抛 TypeError。 + let err = match eval("Object.defineProperties({}, null)") { + Ok(_) => panic!("null properties should throw TypeError"), + Err(err) => err, + }; + assert!(err.contains("TypeError"), "unexpected error: {err}"); + let err = match eval("Object.create(null, null)") { + Ok(_) => panic!("null properties should throw TypeError"), + Err(err) => err, + }; + assert!(err.contains("TypeError"), "unexpected error: {err}"); +} + +#[test] +fn object_define_property_primitive_target_throws() { + // target 不做 ToObject 装箱:原始值直接抛 TypeError。 + for src in [ + "Object.defineProperty(0, 'a', {value: 1})", + "Object.defineProperty(true, 'a', {value: 1})", + "Object.defineProperty('abc', 'a', {value: 1})", + ] { + let err = match eval(src) { + Ok(_) => panic!("{src} should throw TypeError"), + Err(err) => err, + }; + assert!(err.contains("TypeError"), "unexpected error: {err}"); + } +} + +#[test] +fn object_define_properties_primitive_target_throws() { + // target 不做 ToObject 装箱:原始值直接抛 TypeError。 + for src in [ + "Object.defineProperties(0, {})", + "Object.defineProperties(true, {})", + "Object.defineProperties('abc', {})", + ] { + let err = match eval(src) { + Ok(_) => panic!("{src} should throw TypeError"), + Err(err) => err, + }; + assert!(err.contains("TypeError"), "unexpected error: {err}"); + } +} + +#[test] +fn object_create_getter_range_error_propagates() { + // 描述符收集期访问器 getter 抛出的用户异常原类型穿透,不投影为 TypeError。 + let err = match eval("Object.create(null, {get p(){ throw new RangeError('boom'); }})") { + Ok(_) => panic!("getter RangeError should propagate"), + Err(err) => err, + }; + assert!(err.contains("RangeError"), "unexpected error: {err}"); +} + +#[test] +fn object_define_properties_two_phase_get_before_define() { + // 两阶段语义:全部描述符 getter 先触发后逐键定义;后键 getter 抛出时 + // 前键尚未定义。 + let (_vm, result) = eval( + "(function(){ var t = {}; var n = 0; \ + Object.defineProperty(t, 'p1', { get: function(){ n++; return {value: 1}; }, enumerable: true }); \ + Object.defineProperty(t, 'p2', { get: function(){ if (n === 1) { n++; throw 42; } return {value: 2}; }, enumerable: true }); \ + var target = {}; \ + try { Object.defineProperties(target, t); } catch (e) {} \ + return n === 2 && target.hasOwnProperty('p1') === false && target.hasOwnProperty('p2') === false })()", + ) + .unwrap(); + assert!(result.is_bool() && result.as_bool()); +}