diff --git a/crates/oxide_builtins/src/json.rs b/crates/oxide_builtins/src/json.rs index 3e11e390..6dfe2242 100644 --- a/crates/oxide_builtins/src/json.rs +++ b/crates/oxide_builtins/src/json.rs @@ -102,7 +102,13 @@ fn walk_reviver( } Ok(()) } - Err(msg) => Err(crate::error::create_type_error(vm, &msg)), + Err(msg) => { + // reviver 内抛出的原始值原样传播(不折叠为 TypeError 文本)。 + let exc = vm + .take_uncaught_value() + .unwrap_or_else(|| crate::error::create_type_error(vm, &msg)); + Err(exc) + } } } @@ -194,6 +200,62 @@ fn key_si_to_units(vm: &H, si: u32) -> Vec { } } +/// 白名单元素 → 键名提取(Stringify 步 4):String/Number 原语直取;装箱 +/// String/Number 经 ToPrimitive string hint(toString 优先、valueOf 兜底、 +/// 两法皆不可调用落盒值);其余形态(装箱 Boolean、普通对象、Symbol 等) +/// 返回 None 跳过。方法读取与调用异常传播原始抛出值。 +fn whitelist_element_name(vm: &mut H, elem: JsValue) -> Result, JsValue> { + if elem.is_undefined() { + return Ok(None); + } + if elem.is_string() || elem.is_int() || elem.is_double() { + return Ok(Some(oxide_runtime_api::to_string(elem))); + } + if !elem.is_object() { + return Ok(None); + } + let ptr = elem.as_js_object_ptr(); + if ptr.is_null() { + return Ok(None); + } + let obj = unsafe { &*ptr }; + // 装箱 String 的盒值存 prop 槽 0(boxed_value 槽不存)。 + let boxed = obj.boxed_value(); + let boxed = if boxed.is_undefined() && obj.is_string_obj() { + obj.get_prop_at(0) + } else { + boxed + }; + if !boxed.is_string() && !boxed.is_int() && !boxed.is_double() { + return Ok(None); + } + + // ToPrimitive string hint:toString 优先,valueOf 兜底。 + let to_string_si = vm.kernel_core().perm_interner().intern("toString").0; + let value_of_si = vm.kernel_core().perm_interner().intern("valueOf").0; + for m_si in [to_string_si, value_of_si] { + let m = vm.ordinary_get(obj, m_si, elem).map_err(|msg| { + vm.take_uncaught_value() + .unwrap_or_else(|| crate::error::create_type_error(vm, &msg)) + })?; + if !m.is_object() { + continue; + } + let fptr = m.as_js_object_ptr(); + if fptr.is_null() || !unsafe { (*fptr).is_function() } { + continue; + } + let r = vm.call_function_sync(m, elem, &[]).map_err(|msg| { + vm.take_uncaught_value() + .unwrap_or_else(|| crate::error::create_type_error(vm, &msg)) + })?; + return Ok(Some(oxide_runtime_api::to_string(r))); + } + + // 装箱原型面理论不可达兜底:盒值直取。 + Ok(Some(oxide_runtime_api::to_string(boxed))) +} + fn call_to_json(vm: &mut H, obj_val: JsValue, key: &[u16]) -> Result { // SerializeJSONProperty 步 2:对象与 BigInt 原语均查 toJSON(Get 对原语 // 自动装箱),BigInt 的查表对象取 %BigIntPrototype%。 @@ -210,27 +272,36 @@ fn call_to_json(vm: &mut H, obj_val: JsValue, key: &[u16]) -> Result< vm.session().builtin_world().bigint_proto.as_ptr() as *mut JsObject }; let tojson_si = vm.kernel_core().perm_interner().intern("toJSON").0; - let resolved = vm.resolve_property(unsafe { &*obj_ptr }, tojson_si); - match resolved { - Some(fn_val) if fn_val.is_object() => { - let fn_ptr = fn_val.as_js_object_ptr(); - if !fn_ptr.is_null() && unsafe { (*fn_ptr).is_function() } { - let key_val = vm.new_string_units_owned(key.to_vec()); - match vm.call_function_sync(fn_val, obj_val, &[key_val]) { - Ok(v) => Ok(v), - Err(msg) => { - // 用户抛出的原始值原样传播(toJSON 抛非 Error 值不降级为 TypeError)。 - let exc = vm - .take_uncaught_value() - .unwrap_or_else(|| crate::error::create_type_error(vm, &msg)); - Err(exc) - } - } - } else { - Ok(obj_val) - } + // Get 语义查表:访问器形 toJSON 触发 getter(receiver = 原值,BigInt 原语 + // 以自身作 this),getter 异常传播原始抛出值。 + let fn_val = match vm.ordinary_get(unsafe { &*obj_ptr }, tojson_si, obj_val) { + Ok(v) => v, + Err(msg) => { + let exc = vm + .take_uncaught_value() + .unwrap_or_else(|| crate::error::create_type_error(vm, &msg)); + return Err(exc); } - _ => Ok(obj_val), + }; + if fn_val.is_object() { + let fn_ptr = fn_val.as_js_object_ptr(); + if !fn_ptr.is_null() && unsafe { (*fn_ptr).is_function() } { + let key_val = vm.new_string_units_owned(key.to_vec()); + match vm.call_function_sync(fn_val, obj_val, &[key_val]) { + Ok(v) => Ok(v), + Err(msg) => { + // 用户抛出的原始值原样传播(toJSON 抛非 Error 值不降级为 TypeError)。 + let exc = vm + .take_uncaught_value() + .unwrap_or_else(|| crate::error::create_type_error(vm, &msg)); + Err(exc) + } + } + } else { + Ok(obj_val) + } + } else { + Ok(obj_val) } } @@ -267,7 +338,7 @@ pub fn json_stringify(vm: &mut H, args: &[u8]) -> NativeResult { } let mut replacer_fn: Option = None; - let mut replacer_whitelist: Option> = None; + let mut replacer_whitelist: Option> = None; if args.len() > 2 { let replacer_val = vm.reg(args[2]); if replacer_val.is_object() { @@ -278,11 +349,18 @@ pub fn json_stringify(vm: &mut H, args: &[u8]) -> NativeResult { replacer_fn = Some(replacer_val); } else if robj.is_array() { let len = robj.prop_count() as usize; - let mut whitelist = HashSet::new(); + let mut whitelist: Vec = Vec::new(); for i in 0..len { let elem = robj.get_prop_at(i); - if !elem.is_undefined() { - whitelist.insert(oxide_runtime_api::to_string(elem)); + match whitelist_element_name(vm, elem) { + Ok(Some(name)) => { + // 有序去重:重复项不入列表。 + if !whitelist.contains(&name) { + whitelist.push(name); + } + } + Ok(None) => {} + Err(exc) => return NativeResult::Err(exc), } } replacer_whitelist = Some(whitelist); @@ -340,7 +418,7 @@ pub fn json_stringify(vm: &mut H, args: &[u8]) -> NativeResult { #[allow(clippy::too_many_arguments)] fn jsvalue_to_json( vm: &mut H, val: JsValue, visited: &mut HashSet<*const JsObject>, out: &mut String, replacer_fn: Option, - replacer_whitelist: Option<&HashSet>, space: &str, indent_level: usize, + replacer_whitelist: Option<&Vec>, space: &str, indent_level: usize, ) -> Result<(), JsValue> { if val.is_null() { out.push_str("null"); @@ -431,39 +509,45 @@ fn stringify_string_units(units: &[u16], out: &mut String) { #[allow(clippy::too_many_arguments)] fn stringify_object( vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut String, replacer_fn: Option, - replacer_whitelist: Option<&HashSet>, space: &str, indent_level: usize, + replacer_whitelist: Option<&Vec>, space: &str, indent_level: usize, ) -> Result<(), JsValue> { let has_space = !space.is_empty(); out.push('{'); - let keys = walk_own_keys(vm, obj); - // 仅序列化可枚举自身属性(规范 EnumerableOwnPropertyNames)。数组 replacer - // 白名单判定在 Get 之前(SerializeJSONObject 步 4a),非白名单键不触发 getter。 - let entries: Vec<(Vec, String, u32, u32)> = keys - .into_iter() - .filter(|(_si, pos)| { - obj.prop_meta_at(*pos) - .map(|m| m.attributes.enumerable()) - .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable()) - }) - .filter_map(|(si, pos)| { - let units = key_si_to_units(vm, si); - // 白名单匹配走 lossy 串形态(与白名单构造侧 to_string 同口径)。 - let name = String::from_utf16_lossy(&units); - if let Some(whitelist) = replacer_whitelist { - if !whitelist.contains(&name) { - return None; - } - } - Some((units, name, pos, si)) - }) - .collect(); + // 键集:白名单给定时 K = P(列表序,不限自身可枚举,值读走 Get); + // 无白名单走自身可枚举序(EnumerableOwnPropertyNames)。 + let entries: Vec<(Vec, u32, Option)> = if let Some(whitelist) = replacer_whitelist { + whitelist + .iter() + .map(|name| { + let si = vm.string_key_si(name); + (key_si_to_units(vm, si), si, None) + }) + .collect() + } else { + let keys = walk_own_keys(vm, obj); + keys.into_iter() + .filter(|(_si, pos)| { + obj.prop_meta_at(*pos) + .map(|m| m.attributes.enumerable()) + .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable()) + }) + .map(|(si, pos)| (key_si_to_units(vm, si), si, Some(pos))) + .collect() + }; let obj_val = JsValue::from_js_object(obj as *const JsObject as *mut JsObject); let mut first = true; - for (units, _name, pos, si) in entries { + for (units, si, read_pos) in entries { // 规范序 Get → toJSON → replacer(SerializeJSONProperty 步 1、2a、2b)。 - let val = read_json_property_value(vm, obj, obj_val, si, pos)?; + // K = P 时值读经 Get(原型链 + 访问器),无白名单自身槽直读。 + let val = match read_pos { + Some(pos) => read_json_property_value(vm, obj, obj_val, si, pos)?, + None => vm.ordinary_get(obj, si, obj_val).map_err(|msg| { + vm.take_uncaught_value() + .unwrap_or_else(|| crate::error::create_type_error(vm, &msg)) + })?, + }; let val = call_to_json(vm, val, &units)?; // replacer 函数回调。 @@ -530,7 +614,7 @@ fn stringify_object( #[allow(clippy::too_many_arguments)] fn stringify_array( vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut String, replacer_fn: Option, - replacer_whitelist: Option<&HashSet>, space: &str, indent_level: usize, + replacer_whitelist: Option<&Vec>, space: &str, indent_level: usize, ) -> Result<(), JsValue> { let has_space = !space.is_empty(); out.push('['); @@ -606,29 +690,49 @@ fn stringify_array( #[allow(clippy::too_many_arguments)] fn stringify_typed_array( vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut String, replacer_fn: Option, - replacer_whitelist: Option<&HashSet>, space: &str, indent_level: usize, + replacer_whitelist: Option<&Vec>, space: &str, indent_level: usize, ) -> Result<(), JsValue> { let has_space = !space.is_empty(); let this_val = JsValue::from_js_object(obj as *const JsObject as *mut JsObject); let view = crate::typed_array::get_typed_array_data(vm, this_val)?; out.push('{'); - let mut first = true; - for i in 0..view.length { - let index_str = i.to_string(); - // 白名单判定先于 Get(SerializeJSONObject 步 4a),非白名单键不读元素。 - if let Some(whitelist) = replacer_whitelist { - if !whitelist.contains(&index_str) { - continue; + + // 键集:白名单给定时 K = P(列表序,整数索引名读元素、越界名跳过、 + // 非索引名经 Get 读命名键如 length);无白名单走升序元素序。 + let entries: Vec<(Vec, JsValue)> = if let Some(whitelist) = replacer_whitelist { + let mut list: Vec<(Vec, JsValue)> = Vec::new(); + for name in whitelist { + let si = vm.string_key_si(name); + let val = if is_int_key(si) { + let idx = int_key_value(si); + if idx as usize >= view.length { + continue; + } + crate::typed_array::typed_array_element_get(vm, obj, idx) + } else { + vm.ordinary_get(obj, si, this_val) } - } - let index_units: Vec = index_str.encode_utf16().collect(); - let val = crate::typed_array::typed_array_element_get(vm, obj, i as u32) .map_err(|msg| crate::error::create_type_error(vm, &msg))?; + list.push((key_si_to_units(vm, si), val)); + } + list + } else { + let mut list: Vec<(Vec, JsValue)> = Vec::with_capacity(view.length); + for i in 0..view.length { + let val = crate::typed_array::typed_array_element_get(vm, obj, i as u32) + .map_err(|msg| crate::error::create_type_error(vm, &msg))?; + list.push((i.to_string().encode_utf16().collect(), val)); + } + list + }; + + let mut first = true; + for (index_units, val) in entries { let val = call_to_json(vm, val, &index_units)?; // replacer 函数回调。 let val = if let Some(replacer) = replacer_fn { - let key_val = vm.new_string(&index_str); + let key_val = vm.new_string_units_owned(index_units.clone()); match vm.call_function_sync(replacer, this_val, &[key_val, val]) { Ok(v) => { if v.is_undefined() { diff --git a/crates/oxide_builtins/tests/builtins_json_tests.rs b/crates/oxide_builtins/tests/builtins_json_tests.rs index 6638893f..1a702398 100644 --- a/crates/oxide_builtins/tests/builtins_json_tests.rs +++ b/crates/oxide_builtins/tests/builtins_json_tests.rs @@ -429,6 +429,184 @@ fn stringify_bigint_replacer_transform_guard() { assert_eq!(out, r#"{"b":1}"#); } +// --- reviver 抛错原值传播(InternalizeJSONProperty 步 3)--- + +// reviver 抛非 Error 原语:原值传播(捕获侧见 number 42,非 TypeError 对象)。 +#[test] +fn parse_reviver_throws_original_value() { + let out = eval_str( + "(() => { try { JSON.parse('0', function(){ throw 42; }); } \ + catch (e) { return typeof e + '|' + e; } })()", + ) + .unwrap(); + assert_eq!(out, "number|42"); +} + +// reviver 抛字符串:原始串传播。 +#[test] +fn parse_reviver_throws_string() { + let out = eval_str( + "(() => { try { JSON.parse('0', function(){ throw 'boom'; }); } \ + catch (e) { return typeof e + '|' + e; } })()", + ) + .unwrap(); + assert_eq!(out, "string|boom"); +} + +// reviver 抛 Error 对象:原错误对象传播(种类与消息保真)。 +#[test] +fn parse_reviver_throws_range_error() { + let out = eval_str( + "(() => { try { JSON.parse('0', function(){ throw new RangeError('re'); }); } \ + catch (e) { return e.constructor.name + '|' + e.message; } })()", + ) + .unwrap(); + assert_eq!(out, "RangeError|re"); +} + +// --- PropertyList 列表序(SerializeJSONObject 步 5 K = P)--- + +// 顶层对象:输出序 = 白名单列表序(非自身枚举序)。 +#[test] +fn replacer_array_list_order() { + let out = eval_str(r#"JSON.stringify({b:1,a:2,c:3}, ['c','b','a'])"#).unwrap(); + assert_eq!(out, r#"{"c":3,"b":1,"a":2}"#); +} + +// 嵌套层级每级同用列表序。 +#[test] +fn replacer_array_nested_list_order() { + let out = eval_str(r#"JSON.stringify({a:{b:2,c:3}}, ['c','b','a'])"#).unwrap(); + assert_eq!(out, r#"{"a":{"c":3,"b":2}}"#); +} + +// TypedArray:列表序定输出序,整数索引名读元素。 +#[test] +fn replacer_array_typed_array_list_order() { + let out = eval_str(r#"JSON.stringify(new Float64Array([1,2,3]), ['2','0','1'])"#).unwrap(); + assert_eq!(out, r#"{"2":3,"0":1,"1":2}"#); +} + +// --- 白名单元素强转(Stringify 步 4:装箱 String/Number 走 ToPrimitive)--- + +// 装箱 Number 自定义 toString:string hint 调用结果作键。 +#[test] +fn replacer_boxed_number_tostring() { + let out = eval_str( + "(() => { const n = new Number(10); \ + n.toString = function(){ return 'toString'; }; \ + return JSON.stringify({10:1,toString:2}, [n]); })()", + ) + .unwrap(); + assert_eq!(out, r#"{"toString":2}"#); +} + +// 素形装箱 Number:盒值 → "10"。 +#[test] +fn replacer_boxed_number_plain() { + let out = eval_str(r#"JSON.stringify({10:1}, [new Number(10)])"#).unwrap(); + assert_eq!(out, r#"{"10":1}"#); +} + +// 素形装箱 String:盒值 → "str"。 +#[test] +fn replacer_boxed_string_plain() { + let out = eval_str(r#"JSON.stringify({str:4}, [new String('str')])"#).unwrap(); + assert_eq!(out, r#"{"str":4}"#); +} + +// --- 白名单键 Get 语义(原型链 + 不可枚举自身 + 访问器)--- + +// 原型链 getter 键在白名单给定时命中(getter 触发)。 +#[test] +fn replacer_whitelist_proto_getter() { + let out = eval_str( + "(() => { const base = {get b(){ return 9; }}; \ + const o = Object.create(base); o.a = 1; \ + return JSON.stringify(o, ['a','b']); })()", + ) + .unwrap(); + assert_eq!(out, r#"{"a":1,"b":9}"#); +} + +// 不可枚举自身键在白名单给定时同样序列化。 +#[test] +fn replacer_whitelist_non_enumerable_own() { + let out = eval_str( + "(() => { const o = {a:1}; \ + Object.defineProperty(o,'b',{value:2,enumerable:false,configurable:true}); \ + return JSON.stringify(o, ['a','b']); })()", + ) + .unwrap(); + assert_eq!(out, r#"{"a":1,"b":2}"#); +} + +// 嵌套层级原型链 getter 同样命中。 +#[test] +fn replacer_whitelist_nested_proto_getter() { + let out = eval_str( + "(() => { const base = {get x(){ return 7; }}; \ + const o = Object.create(base); o.y = 1; \ + return JSON.stringify({o}, ['o','x','y']); })()", + ) + .unwrap(); + assert_eq!(out, r#"{"o":{"x":7,"y":1}}"#); +} + +// --- toJSON 查表 Get 语义(accessor 形 toJSON 触发)--- + +// BigInt.prototype toJSON accessor 形:getter 触发,receiver = BigInt 原语。 +#[test] +fn stringify_bigint_tojson_accessor() { + let out = eval_str( + "(() => { \"use strict\"; \ + Object.defineProperty(BigInt.prototype, 'toJSON', \ + {get(){ return () => typeof this; }, configurable:true}); \ + const s = JSON.stringify(1n); \ + delete BigInt.prototype.toJSON; \ + return s; })()", + ) + .unwrap(); + assert_eq!(out, r#""bigint""#); +} + +// --- 守卫(修后零漂移)--- + +// 白名单缺失名:空对象输出。 +#[test] +fn replacer_whitelist_missing_name() { + let out = eval_str(r#"JSON.stringify({b:1}, ['zzz'])"#).unwrap(); + assert_eq!(out, "{}"); +} + +// TA 白名单非索引名(length)经 Get 读命名键。 +#[test] +fn replacer_whitelist_typed_array_length() { + let out = eval_str(r#"JSON.stringify(new Float64Array(2), ['length'])"#).unwrap(); + assert_eq!(out, r#"{"length":2}"#); +} + +// 数组容器不查白名单。 +#[test] +fn replacer_whitelist_array_container_ignores() { + let out = eval_str(r#"JSON.stringify([1,2], ['1'])"#).unwrap(); + assert_eq!(out, "[1,2]"); +} + +// 装箱 Boolean / 普通对象元素跳过(不产键)。 +#[test] +fn replacer_whitelist_skip_object_elements() { + let out = eval_str(r#"JSON.stringify({b:5}, [new Boolean(true), {custom:1}])"#).unwrap(); + assert_eq!(out, "{}"); +} + +// 白名单命中键值为 undefined:跳键。 +#[test] +fn replacer_whitelist_undefined_value_skipped() { + let out = eval_str(r#"JSON.stringify({a:1,b:undefined}, ['a','b'])"#).unwrap(); + assert_eq!(out, r#"{"a":1}"#); +} + fn eval_str(source: &str) -> Result { let (vm, result) = eval(source)?; vm.lookup_str(result)