diff --git a/crates/oxide_builtins/src/json.rs b/crates/oxide_builtins/src/json.rs index e0196236..82265130 100644 --- a/crates/oxide_builtins/src/json.rs +++ b/crates/oxide_builtins/src/json.rs @@ -185,21 +185,39 @@ fn create_wrapper(vm: &mut H, value: JsValue) -> JsValue { JsValue::from_js_object(obj_ptr) } -fn process_space(val: JsValue) -> String { +/// space 参数文本化(Stringify 步 5):数字经 ToIntegerOrInfinity 钳 10, +/// 字符串取前 10 字符;装箱 String 走完整 ToString(步 4c 同款语义, +/// 抛出值原样上抛),其余形态维持占位行为。 +fn process_space(vm: &mut H, val: JsValue) -> Result { if val.is_int() || val.is_double() { let n = oxide_runtime_api::to_integer_or_infinity(val); if n.is_nan() || n.is_infinite() || n <= 0.0 { - return String::new(); + return Ok(String::new()); } let clamped = (n as usize).min(10); - " ".repeat(clamped) - } else if val.is_string() { - let s = unsafe { (*val.as_string_ptr()).to_owned_string() }; - s.chars().take(10).collect() - } else { - let s = oxide_runtime_api::to_string(val); - s.chars().take(10).collect() + return Ok(" ".repeat(clamped)); } + if val.is_string() { + let s = unsafe { (*val.as_string_ptr()).to_owned_string() }; + return Ok(s.chars().take(10).collect()); + } + if val.is_object() { + let ptr = val.as_js_object_ptr(); + if !ptr.is_null() && unsafe { (*ptr).is_string_obj() } { + let s = match oxide_runtime_api::to_string_value_full(val, vm) { + Ok(s) => s, + Err(msg) => { + return Err(vm + .take_uncaught_value() + .unwrap_or_else(|| crate::error::create_type_error(vm, &msg))); + } + }; + let s = unsafe { (*s.as_string_ptr()).to_owned_string() }; + return Ok(s.chars().take(10).collect()); + } + } + let s = oxide_runtime_api::to_string(val); + Ok(s.chars().take(10).collect()) } /// 键 si 物化为单元序列:整数键 → ASCII 数字串,字符串键 → 码表键经 `decode_key` @@ -379,7 +397,14 @@ pub fn json_stringify(vm: &mut H, args: &[u8]) -> NativeResult { } } - let space = if args.len() > 3 { process_space(vm.reg(args[3])) } else { String::new() }; + let space = if args.len() > 3 { + match process_space(vm, vm.reg(args[3])) { + Ok(s) => s, + Err(exc) => return NativeResult::Err(exc), + } + } else { + String::new() + }; let holder = create_wrapper(vm, value); @@ -467,7 +492,22 @@ fn jsvalue_to_json( 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() { + // 步 4c:装箱 String 走完整 ToString([[StringData]] 臂):尊重 + // toString/Symbol.toPrimitive 覆盖、不直读载荷,抛出值原样传播。 + if obj.is_string_obj() { + let s = match oxide_runtime_api::to_string_value_full(val, vm) { + Ok(s) => s, + Err(msg) => { + let exc = vm + .take_uncaught_value() + .unwrap_or_else(|| crate::error::create_type_error(vm, &msg)); + visited.remove(&(obj_ptr as *const JsObject)); + return Err(exc); + } + }; + let units = vm.string_units(s); + stringify_string_units(&units, out); + } else 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)?; diff --git a/crates/oxide_vm/tests/json_tests.rs b/crates/oxide_vm/tests/json_tests.rs index 69a83825..e6853002 100644 --- a/crates/oxide_vm/tests/json_tests.rs +++ b/crates/oxide_vm/tests/json_tests.rs @@ -340,6 +340,114 @@ fn json_stringify_surrogate_pair_raw() { assert!(r2.as_bool(), "unpaired units around a pair must each be escaped"); } +// -- JSON.stringify 装箱 String([[StringData]] 臂) -- + +#[test] +fn json_stringify_boxed_string_top_level() { + // 顶层装箱 String 序列化为载荷文本([[StringData]] 臂 ToString,非索引枚举)。 + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify(new String('x'))").unwrap(); + assert_eq!(string_value(&vm, result), "\"x\""); +} + +#[test] +fn json_stringify_boxed_string_object_property() { + // 对象属性位同臂:{"a":"x"}。 + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify({a: new String('x')})").unwrap(); + assert_eq!(string_value(&vm, result), "{\"a\":\"x\"}"); +} + +#[test] +fn json_stringify_boxed_string_array_elements() { + // 数组元素位:["x","yz"]。 + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify([new String('x'), new String('yz')])").unwrap(); + assert_eq!(string_value(&vm, result), "[\"x\",\"yz\"]"); +} + +#[test] +fn json_stringify_boxed_string_respects_tostring_override() { + // 完整 ToString 语义:toString 覆盖生效,valueOf 不在 string hint 路径被调用。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var s = new String('x'); s.toString = function() { return 'y'; }; \ + s.valueOf = function() { throw new Error('no'); }; JSON.stringify(s)", + ) + .unwrap(); + assert_eq!(string_value(&vm, result), "\"y\""); +} + +#[test] +fn json_stringify_boxed_string_abrupt_tostring_propagates() { + // 值位 abrupt toString 抛原始值(原样传播,不折叠为环检 TypeError)。 + let mut vm = Vm::new(); + let err = eval( + &mut vm, + "var s = new String('x'); s.toString = function() { throw new RangeError('SE'); }; \ + JSON.stringify(s)", + ); + assert!(err.is_err()); +} + +#[test] +fn json_stringify_string_prototype_is_empty_string() { + // String.prototype 是规范 String exotic 对象([[StringData]] 空串)→ ""。 + let mut vm = Vm::new(); + let result = eval(&mut vm, "JSON.stringify(String.prototype)").unwrap(); + assert_eq!(string_value(&vm, result), "\"\""); +} + +#[test] +fn json_stringify_boxed_string_space_argument() { + // space 位装箱 String 与同文本原语串缩进同形。 + let mut vm = Vm::new(); + let r1 = eval(&mut vm, "JSON.stringify({a: 1}, undefined, new String('xxx'))").unwrap(); + let r2 = eval(&mut vm, "JSON.stringify({a: 1}, undefined, 'xxx')").unwrap(); + assert_eq!(string_value(&vm, r1), string_value(&vm, r2)); +} + +#[test] +fn json_stringify_boxed_string_space_tostring_override() { + // space 位 toString 覆盖生效(完整 ToString,非直读载荷)。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var s = new String('zzz'); s.toString = function() { return '---'; }; \ + JSON.stringify({a: 1}, undefined, s)", + ) + .unwrap(); + let plain = eval(&mut vm, "JSON.stringify({a: 1}, undefined, '---')").unwrap(); + assert_eq!(string_value(&vm, result), string_value(&vm, plain)); +} + +#[test] +fn json_stringify_boxed_string_space_abrupt_throws() { + // space 位 abrupt toString 抛原始值。 + let mut vm = Vm::new(); + let err = eval( + &mut vm, + "var s = new String('x'); s.toString = function() { throw new RangeError('SE'); }; \ + JSON.stringify({a: 1}, undefined, s)", + ); + assert!(err.is_err()); +} + +#[test] +fn json_stringify_boxed_string_tostring_result_units_escaped() { + // 完整 ToString 产物经单元流序列化:孤立 surrogate → \uXXXX 转义(well-formed JSON)。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var s = new String('x'); \ + s.toString = function() { return String.fromCharCode(0xD800); }; \ + JSON.stringify(s) === '\"\\\\ud800\"'", + ) + .unwrap(); + assert!(result.as_bool()); +} + #[test] fn json_stringify_key_units_real() { // 键序列化用真实键串(孤立 surrogate 转义),toJSON/replacer 键参数同口径。