diff --git a/crates/oxide_builtins/src/json.rs b/crates/oxide_builtins/src/json.rs index bd9a2b47..3e11e390 100644 --- a/crates/oxide_builtins/src/json.rs +++ b/crates/oxide_builtins/src/json.rs @@ -195,13 +195,20 @@ fn key_si_to_units(vm: &H, si: u32) -> Vec { } fn call_to_json(vm: &mut H, obj_val: JsValue, key: &[u16]) -> Result { - if !obj_val.is_object() { - return Ok(obj_val); - } - let obj_ptr = obj_val.as_js_object_ptr(); - if obj_ptr.is_null() { + // SerializeJSONProperty 步 2:对象与 BigInt 原语均查 toJSON(Get 对原语 + // 自动装箱),BigInt 的查表对象取 %BigIntPrototype%。 + if !obj_val.is_object() && !obj_val.is_bigint() { return Ok(obj_val); } + let obj_ptr = if obj_val.is_object() { + let p = obj_val.as_js_object_ptr(); + if p.is_null() { + return Ok(obj_val); + } + p + } else { + 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 { @@ -349,6 +356,9 @@ fn jsvalue_to_json( } else { oxide_runtime_api::write_number_into(n, out); } + } else if val.is_bigint() { + // SerializeJSONProperty 步 10:BigInt 无 JSON 文本形态,抛 TypeError。 + return Err(crate::error::create_type_error(vm, "Do not know how to serialize a BigInt")); } else if val.is_string() { // SAFETY: val 已确认是字符串值;单元视图按码单元流序列化(见 stringify_string_units)。 let units = unsafe { (*val.as_string_ptr()).units() }; @@ -365,7 +375,13 @@ fn jsvalue_to_json( } let obj = unsafe { &*obj_ptr }; - if obj.is_array() { + // 步 4d:装箱 BigInt 无条件解包 [[BigIntData]],后续步 10 抛 TypeError。 + if obj.boxed_value().is_bigint() { + return Err(crate::error::create_type_error(vm, "Do not know how to serialize a BigInt")); + } + if obj.is_typed_array_obj() { + stringify_typed_array(vm, obj, visited, out, replacer_fn, replacer_whitelist, space, indent_level)?; + } else if obj.is_array() { stringify_array(vm, obj, visited, out, replacer_fn, replacer_whitelist, space, indent_level)?; } else { stringify_object(vm, obj, visited, out, replacer_fn, replacer_whitelist, space, indent_level)?; @@ -584,3 +600,89 @@ fn stringify_array( out.push(']'); Ok(()) } + +/// TypedArray 序列化:整数索引是可枚举自身属性(元素存于原生载荷,不在形状链), +/// 逐元素按对象臂规范序 Get → toJSON → replacer 处理,输出形态与普通对象一致。 +#[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, +) -> 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; + } + } + 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))?; + 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); + match vm.call_function_sync(replacer, this_val, &[key_val, val]) { + Ok(v) => { + if v.is_undefined() { + continue; + } + v + } + Err(msg) => { + let exc = vm + .take_uncaught_value() + .unwrap_or_else(|| crate::error::create_type_error(vm, &msg)); + return Err(exc); + } + } + } else { + val + }; + + let is_function = val.is_object() && { + let ptr = val.as_js_object_ptr(); + !ptr.is_null() && unsafe { (*ptr).is_function() } + }; + if val.is_undefined() || is_function { + continue; + } + + if !first && !has_space { + out.push(','); + } else if !first { + out.push_str(",\n"); + } + first = false; + + if has_space { + for _ in 0..indent_level + 1 { + out.push_str(space); + } + stringify_string_units(&index_units, out); + out.push(':'); + out.push(' '); + } else { + stringify_string_units(&index_units, out); + out.push(':'); + } + + jsvalue_to_json(vm, val, visited, out, replacer_fn, replacer_whitelist, space, indent_level + 1)?; + } + + if has_space && !first { + out.push('\n'); + for _ in 0..indent_level { + out.push_str(space); + } + } + out.push('}'); + Ok(()) +} diff --git a/crates/oxide_builtins/tests/builtins_json_tests.rs b/crates/oxide_builtins/tests/builtins_json_tests.rs index f88d5c53..6638893f 100644 --- a/crates/oxide_builtins/tests/builtins_json_tests.rs +++ b/crates/oxide_builtins/tests/builtins_json_tests.rs @@ -322,6 +322,113 @@ fn stringify_array_ignores_named_props() { assert_eq!(out, "[1,2]"); } +// --- BigInt 值位(SerializeJSONProperty 步 10 抛 TypeError;步 2 查 toJSON)--- + +fn assert_bigint_serialize_err(source: &str) { + let result = eval(source); + match result { + Err(e) => { + assert!( + e.to_lowercase().contains("do not know how to serialize a bigint"), + "expected BigInt serialize TypeError, got: {}", + e + ) + } + Ok(_) => panic!("expected BigInt serialize TypeError, got ok: {}", source), + } +} + +// 顶层原语四值位:1n / -1n / 0n / -0n 恒抛,无文本产出。 +#[test] +fn stringify_bigint_top_level_throws() { + assert_bigint_serialize_err("JSON.stringify(1n)"); + assert_bigint_serialize_err("JSON.stringify(-1n)"); + assert_bigint_serialize_err("JSON.stringify(0n)"); + assert_bigint_serialize_err("JSON.stringify(-0n)"); +} + +// 大位宽 BigInt 同样抛错(不产十进制文本)。 +#[test] +fn stringify_bigint_wide_bits_throws() { + assert_bigint_serialize_err("JSON.stringify(2n ** 32n + 1n)"); +} + +// 嵌套对象属性与数组元素位(含深嵌套、稀疏)均经值位臂抛错。 +#[test] +fn stringify_bigint_nested_throws() { + assert_bigint_serialize_err("JSON.stringify({a:1n})"); + assert_bigint_serialize_err("JSON.stringify([1n,2n,[3n]])"); + assert_bigint_serialize_err("JSON.stringify({a:{b:1n}})"); + assert_bigint_serialize_err("JSON.stringify([,1n])"); +} + +// 空格缩进不改变抛错路径。 +#[test] +fn stringify_bigint_space_indent_throws() { + assert_bigint_serialize_err("JSON.stringify({a:1n}, null, 2)"); +} + +// replacer 白名单命中键后仍走值位抛错。 +#[test] +fn stringify_bigint_whitelist_throws() { + assert_bigint_serialize_err("JSON.stringify({a:1n}, ['a'])"); +} + +// 装箱 BigInt(步 4d 无条件解包 [[BigIntData]])与裸值同抛。 +#[test] +fn stringify_bigint_boxed_throws() { + assert_bigint_serialize_err("JSON.stringify(Object(0n))"); + assert_bigint_serialize_err("JSON.stringify({a:Object(1n)})"); +} + +// BigInt64Array 元素位经对象臂读值后在值位臂抛错。 +#[test] +fn stringify_bigint64array_throws() { + assert_bigint_serialize_err("JSON.stringify(new BigInt64Array([1n,2n]))"); +} + +// replacer 透传不改变值位;replacer 结果再入值位臂同样抛错。 +#[test] +fn stringify_bigint_replacer_throws() { + assert_bigint_serialize_err("JSON.stringify(1n, (k,v) => v)"); + assert_bigint_serialize_err("JSON.stringify({a:1n}, (k,v) => v)"); + assert_bigint_serialize_err("JSON.stringify({a:1}, (k,v) => 2n)"); +} + +// BigInt.prototype 挂数据属性 toJSON:钩子被触发,返回串值正常序列化。 +#[test] +fn stringify_bigint_tojson_hook_top_level() { + let out = eval_str( + "(() => { BigInt.prototype.toJSON = function(){ return this.toString(); }; \ + const s = JSON.stringify(0n); \ + delete BigInt.prototype.toJSON; \ + return s; })()", + ) + .unwrap(); + assert_eq!(out, r#""0""#); +} + +// toJSON 钩子嵌套对象属性位:receiver 为 BigInt 原语,this.toString() 生效。 +#[test] +fn stringify_bigint_tojson_hook_nested() { + let out = eval_str( + "(() => { BigInt.prototype.toJSON = function(){ return this.toString(); }; \ + const s = JSON.stringify({a:1n}); \ + delete BigInt.prototype.toJSON; \ + return s; })()", + ) + .unwrap(); + assert_eq!(out, r#"{"a":"1"}"#); +} + +// 守卫:replacer 先于值位转换 BigInt → number,不抛错。 +#[test] +fn stringify_bigint_replacer_transform_guard() { + let out = eval_str("JSON.stringify({b:1n}, function(k,v){ if(typeof v==='bigint') return Number(v); return v; })") + .unwrap(); + assert_eq!(out, r#"{"b":1}"#); +} + fn eval_str(source: &str) -> Result { let (vm, result) = eval(source)?; vm.lookup_str(result)