fix: define/create 非对象目标抛错与描述符两阶段收集

This commit is contained in:
2026-09-20 22:15:00 +00:00
parent 59feec79d0
commit 7062300a37
3 changed files with 176 additions and 28 deletions
+5
View File
@@ -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)))
+56 -28
View File
@@ -566,7 +566,11 @@ pub fn object_create<H: VmHost>(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<H: VmHost>(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<H: VmHost>(
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<H: VmHost>(
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)
};
for (key_si, offset) in prop_keys {
let props = unsafe { &*props_ptr };
.into_iter()
// 非可枚举自身属性跳过;无显式 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)?;
.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::<Result<Vec<_>, _>>()?
};
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<H: VmHost>(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<H: VmHost>(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。
@@ -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());
}