From b97b52bcadbdf38057f4d82cd883da01ea54b143 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 27 Sep 2026 04:53:51 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20JSON.stringify=20=E9=A1=B6=E5=B1=82=20re?= =?UTF-8?q?placer=20=E5=BA=8F=E4=B8=8E=20space=20=E7=9A=84=20Infinity/?= =?UTF-8?q?=E7=A0=81=E5=85=83=E8=BE=B9=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/json.rs | 203 +++++++++--------- .../tests/builtins_json_tests.rs | 88 ++++++++ 2 files changed, 194 insertions(+), 97 deletions(-) diff --git a/crates/oxide_builtins/src/json.rs b/crates/oxide_builtins/src/json.rs index 92135e78..6a464fa8 100644 --- a/crates/oxide_builtins/src/json.rs +++ b/crates/oxide_builtins/src/json.rs @@ -1,5 +1,4 @@ use std::collections::HashSet; -use std::fmt::Write; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_types::object::{JsObject, PropAttributes}; @@ -703,21 +702,18 @@ fn create_wrapper(vm: &mut H, value: JsValue) -> JsValue { } /// space 参数文本化(Stringify 步 5):数字与装箱 Number 经 ToNumber → -/// ToIntegerOrInfinity 钳 10;字符串与装箱 String 取前 10 字符(装箱 String -/// 走完整 ToString,步 4c 同款语义,抛出值原样上抛);其余形态(装箱 Boolean、 -/// 普通对象、Symbol 等)gap 为空串。 -fn process_space(vm: &mut H, val: JsValue) -> Result { +/// ToIntegerOrInfinity 后 min(10, x) 个空格(+∞ → 10、−∞/NaN → 空);字符串与 +/// 装箱 String 取前 10 码元(码元口径,astral 字符可截在代理对边界,截断产物 +/// 保留孤立 surrogate;装箱 String 走完整 ToString,步 4c 同款语义,抛出值原样 +/// 上抛);其余形态(装箱 Boolean、普通对象、Symbol 等)gap 为空。 +fn process_space(vm: &mut H, val: JsValue) -> Result, JsValue> { 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 Ok(String::new()); - } - let clamped = (n as usize).min(10); - return Ok(" ".repeat(clamped)); + return Ok(number_space_units(n)); } if val.is_string() { - let s = unsafe { (*val.as_string_ptr()).to_owned_string() }; - return Ok(s.chars().take(10).collect()); + let units = vm.string_units(val); + return Ok(units.iter().take(10).copied().collect()); } if val.is_object() { let ptr = val.as_js_object_ptr(); @@ -735,11 +731,7 @@ fn process_space(vm: &mut H, val: JsValue) -> Result } }; let n = oxide_runtime_api::to_integer_or_infinity(JsValue::float(n)); - if n.is_nan() || n.is_infinite() || n <= 0.0 { - return Ok(String::new()); - } - let clamped = (n as usize).min(10); - return Ok(" ".repeat(clamped)); + return Ok(number_space_units(n)); } // [[StringData]]:完整 ToString。 if obj.is_string_obj() { @@ -751,12 +743,23 @@ fn process_space(vm: &mut H, val: JsValue) -> Result .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 units = vm.string_units(s); + return Ok(units.iter().take(10).copied().collect()); } } } - Ok(String::new()) + Ok(Vec::new()) +} + +/// 数字 space → min(10, ToIntegerOrInfinity(space)) 个空格码元:+∞ → 10、 +/// −∞/NaN/小于 1 → 空。 +fn number_space_units(n: f64) -> Vec { + let m = n.min(10.0); + if m.is_nan() || m < 1.0 { + Vec::new() + } else { + vec![0x20; m as usize] + } } /// 键 si 物化为单元序列:整数键 → ASCII 数字串,字符串键 → 码表键经 `decode_key` @@ -895,14 +898,13 @@ fn read_json_property_value( /// `JSON.stringify(value, replacer, space)`:将 JS 值序列化为 JSON 文本。 /// 支持 replacer 函数/属性白名单、toJSON 钩子、缩进与循环引用检测。 +/// 顶层值序为 Get → toJSON → replacer,undefined/函数/Symbol 早返在 replacer +/// 之后(replacer 可替换早返形态)。 pub fn json_stringify(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { return NativeResult::Ok(JsValue::undefined()); } let value = vm.reg(args[1]); - if value.is_undefined() { - return NativeResult::Ok(JsValue::undefined()); - } let mut replacer_fn: Option = None; let mut replacer_whitelist: Option> = None; @@ -942,7 +944,7 @@ pub fn json_stringify(vm: &mut H, args: &[u8]) -> NativeResult { Err(exc) => return NativeResult::Err(exc), } } else { - String::new() + Vec::new() }; let holder = create_wrapper(vm, value); @@ -967,7 +969,8 @@ pub fn json_stringify(vm: &mut H, args: &[u8]) -> NativeResult { value }; - // 顶层省略值:undefined、函数、Symbol 一律返回 undefined(非空串)。 + // 顶层省略值:replacer 之后的 undefined、函数、Symbol 一律返回 undefined + // (非空串)——replacer 先于本判定应用(SerializeJSONProperty 步 3 先于步 8/10)。 let top_is_fn = value.is_object() && { let p = value.as_js_object_ptr(); !p.is_null() && unsafe { (*p).is_function() } @@ -977,7 +980,7 @@ pub fn json_stringify(vm: &mut H, args: &[u8]) -> NativeResult { } let mut visited = HashSet::new(); - let mut output = String::new(); + let mut output: Vec = Vec::new(); let indent_level: usize = 0; match jsvalue_to_json( vm, @@ -989,7 +992,7 @@ pub fn json_stringify(vm: &mut H, args: &[u8]) -> NativeResult { &space, indent_level, ) { - Ok(()) => NativeResult::Ok(vm.new_string_owned(output)), + Ok(()) => NativeResult::Ok(vm.new_string_units_owned(output)), Err(exc) => NativeResult::Err(exc), } } @@ -1000,24 +1003,24 @@ pub fn json_stringify(vm: &mut H, args: &[u8]) -> NativeResult { /// 符合规范。`Err` 携带的原始异常值(含环检 TypeError)原样上抛。 #[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<&Vec>, space: &str, indent_level: usize, + vm: &mut H, val: JsValue, visited: &mut HashSet<*const JsObject>, out: &mut Vec, replacer_fn: Option, + replacer_whitelist: Option<&Vec>, space: &[u16], indent_level: usize, ) -> Result<(), JsValue> { if val.is_null() { - out.push_str("null"); + push_ascii(out, "null"); } else if val.is_undefined() { } else if val.is_symbol() { - out.push_str("null"); + push_ascii(out, "null"); } else if val.is_bool() { - out.push_str(if val.as_bool() { "true" } else { "false" }); + push_ascii(out, if val.as_bool() { "true" } else { "false" }); } else if val.is_int() { - let _ = write!(out, "{}", val.as_int()); + push_ascii(out, &val.as_int().to_string()); } else if val.is_double() { let n = val.as_double(); if !n.is_finite() { - out.push_str("null"); + push_ascii(out, "null"); } else { - oxide_runtime_api::write_number_into(n, out); + push_number(out, n); } } else if val.is_bigint() { // SerializeJSONProperty 步 10:BigInt 无 JSON 文本形态,抛 TypeError。 @@ -1029,7 +1032,7 @@ fn jsvalue_to_json( } else if val.is_object() { let obj_ptr = val.as_js_object_ptr(); if obj_ptr.is_null() { - out.push_str("null"); + push_ascii(out, "null"); return Ok(()); } @@ -1041,7 +1044,7 @@ fn jsvalue_to_json( let raw_val = obj.get_prop_at(0); // SAFETY: rawJSON 属性构造期即写入字符串值。 let raw_text = unsafe { (*raw_val.as_string_ptr()).to_owned_string() }; - out.push_str(&raw_text); + push_ascii(out, &raw_text); return Ok(()); } } @@ -1064,9 +1067,9 @@ fn jsvalue_to_json( } }; if n.is_finite() { - oxide_runtime_api::write_number_into(n, out); + push_number(out, n); } else { - out.push_str("null"); + push_ascii(out, "null"); } visited.remove(&(obj_ptr as *const JsObject)); return Ok(()); @@ -1074,7 +1077,7 @@ fn jsvalue_to_json( // 步 4b:装箱 Boolean → [[BooleanData]]。 if obj.is_boolean_obj() { let b = obj.boxed_value(); - out.push_str(if b.is_bool() && b.as_bool() { "true" } else { "false" }); + push_ascii(out, if b.is_bool() && b.as_bool() { "true" } else { "false" }); visited.remove(&(obj_ptr as *const JsObject)); return Ok(()); } @@ -1110,51 +1113,57 @@ fn jsvalue_to_json( Ok(()) } +/// 追加 ASCII 字面量到单元流(JSON 结构字符与转义序列恒为 ASCII)。 +fn push_ascii(out: &mut Vec, s: &str) { + out.extend(s.encode_utf16()); +} + +/// 追加数字规范文本到单元流(write_number_into 输出恒为 ASCII)。 +fn push_number(out: &mut Vec, n: f64) { + let mut s = String::new(); + oxide_runtime_api::write_number_into(n, &mut s); + push_ascii(out, &s); +} + /// JSON 字符串序列化(按码单元流):代理对原样输出 astral 字符,非配对孤立 -/// surrogate 输出 \uXXXX(well-formed JSON 要求,round-trip 经 JSON.parse 复原同值), -/// C0/C1 控制码输出 \uXXXX,其余按 JSON 转义规则。 -fn stringify_string_units(units: &[u16], out: &mut String) { - out.push('"'); +/// surrogate 输出 \uXXXX(well-formed JSON 要求,round-trip 经 JSON.parse 复原 +/// 同值),C0/C1 控制码输出 \uXXXX,其余按 JSON 转义规则。 +fn stringify_string_units(units: &[u16], out: &mut Vec) { + push_ascii(out, "\""); let mut i = 0; while i < units.len() { let u = units[i]; if (0xD800..=0xDBFF).contains(&u) && i + 1 < units.len() && (0xDC00..=0xDFFF).contains(&units[i + 1]) { - let cp = 0x10000 + ((u32::from(u) - 0xD800) << 10) + (u32::from(units[i + 1]) - 0xDC00); - if let Some(c) = char::from_u32(cp) { - out.push(c); - } + out.push(u); + out.push(units[i + 1]); i += 2; continue; } match u { - 0x22 => out.push_str("\\\""), - 0x5C => out.push_str("\\\\"), - 0x08 => out.push_str("\\b"), - 0x0C => out.push_str("\\f"), - 0x0A => out.push_str("\\n"), - 0x0D => out.push_str("\\r"), - 0x09 => out.push_str("\\t"), + 0x22 => push_ascii(out, "\\\""), + 0x5C => push_ascii(out, "\\"), + 0x08 => push_ascii(out, "\\b"), + 0x0C => push_ascii(out, "\\f"), + 0x0A => push_ascii(out, "\\n"), + 0x0D => push_ascii(out, "\\r"), + 0x09 => push_ascii(out, "\\t"), 0x00..=0x1F | 0x7F..=0x9F | 0xD800..=0xDFFF => { - let _ = write!(out, "\\u{:04x}", u); - } - _ => { - if let Some(c) = char::from_u32(u32::from(u)) { - out.push(c); - } + push_ascii(out, &format!("\\u{:04x}", u)); } + _ => out.push(u), } i += 1; } - out.push('"'); + push_ascii(out, "\""); } #[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<&Vec>, space: &str, indent_level: usize, + vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut Vec, + replacer_fn: Option, replacer_whitelist: Option<&Vec>, space: &[u16], indent_level: usize, ) -> Result<(), JsValue> { let has_space = !space.is_empty(); - out.push('{'); + push_ascii(out, "{"); // 键集:白名单给定时 K = P(列表序,不限自身可枚举,值读走 Get); // 无白名单走自身可枚举序(EnumerableOwnPropertyNames)。 @@ -1222,55 +1231,55 @@ fn stringify_object( } if !first { - out.push(','); + push_ascii(out, ","); } first = false; if has_space { - out.push('\n'); + push_ascii(out, "\n"); for _ in 0..indent_level + 1 { - out.push_str(space); + out.extend_from_slice(space); } stringify_string_units(&units, out); - out.push(':'); - out.push(' '); + push_ascii(out, ":"); + push_ascii(out, " "); } else { stringify_string_units(&units, out); - out.push(':'); + push_ascii(out, ":"); } jsvalue_to_json(vm, val, visited, out, replacer_fn, replacer_whitelist, space, indent_level + 1)?; } if has_space && !first { - out.push('\n'); + push_ascii(out, "\n"); for _ in 0..indent_level { - out.push_str(space); + out.extend_from_slice(space); } } - out.push('}'); + push_ascii(out, "}"); Ok(()) } #[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<&Vec>, space: &str, indent_level: usize, + vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut Vec, + replacer_fn: Option, replacer_whitelist: Option<&Vec>, space: &[u16], indent_level: usize, ) -> Result<(), JsValue> { let has_space = !space.is_empty(); - out.push('['); + push_ascii(out, "["); let obj_val = JsValue::from_js_object(obj as *const JsObject as *mut JsObject); let len = obj.prop_count() as usize; for i in 0..len { if i > 0 { - out.push(','); + push_ascii(out, ","); } if has_space { - out.push('\n'); + push_ascii(out, "\n"); for _ in 0..indent_level + 1 { - out.push_str(space); + out.extend_from_slice(space); } } @@ -1287,7 +1296,7 @@ fn stringify_array( match vm.call_function_sync(replacer, obj_val, &[key_val, val]) { Ok(v) => { if v.is_undefined() { - out.push_str("null"); + push_ascii(out, "null"); continue; } v @@ -1308,19 +1317,19 @@ fn stringify_array( !ptr.is_null() && unsafe { (*ptr).is_function() } }; if is_function || val.is_undefined() { - out.push_str("null"); + push_ascii(out, "null"); } else { jsvalue_to_json(vm, val, visited, out, replacer_fn, replacer_whitelist, space, indent_level + 1)?; } } if has_space && len > 0 { - out.push('\n'); + push_ascii(out, "\n"); for _ in 0..indent_level { - out.push_str(space); + out.extend_from_slice(space); } } - out.push(']'); + push_ascii(out, "]"); Ok(()) } @@ -1328,15 +1337,15 @@ fn stringify_array( /// 逐元素按对象臂规范序 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<&Vec>, space: &str, indent_level: usize, + vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut Vec, + replacer_fn: Option, replacer_whitelist: Option<&Vec>, space: &[u16], 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)?; // live 口径:buffer 收缩/detach 后按当前 live 长度枚举(detached 空对象形态)。 let len = crate::typed_array::ta_live_length(view); - out.push('{'); + push_ascii(out, "{"); // 键集:白名单给定时 K = P(列表序,整数索引名读元素、越界名跳过、 // 非索引名经 Get 读命名键如 length);无白名单走升序元素序。 @@ -1401,32 +1410,32 @@ fn stringify_typed_array( } if !first { - out.push(','); + push_ascii(out, ","); } first = false; if has_space { - out.push('\n'); + push_ascii(out, "\n"); for _ in 0..indent_level + 1 { - out.push_str(space); + out.extend_from_slice(space); } stringify_string_units(&index_units, out); - out.push(':'); - out.push(' '); + push_ascii(out, ":"); + push_ascii(out, " "); } else { stringify_string_units(&index_units, out); - out.push(':'); + push_ascii(out, ":"); } jsvalue_to_json(vm, val, visited, out, replacer_fn, replacer_whitelist, space, indent_level + 1)?; } if has_space && !first { - out.push('\n'); + push_ascii(out, "\n"); for _ in 0..indent_level { - out.push_str(space); + out.extend_from_slice(space); } } - out.push('}'); + push_ascii(out, "}"); Ok(()) } diff --git a/crates/oxide_builtins/tests/builtins_json_tests.rs b/crates/oxide_builtins/tests/builtins_json_tests.rs index 52ffa567..e11de5ae 100644 --- a/crates/oxide_builtins/tests/builtins_json_tests.rs +++ b/crates/oxide_builtins/tests/builtins_json_tests.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use oxide_compiler::compiler::Compiler; +use oxide_runtime_api::VmHost; use oxide_types::value::JsValue; use oxide_vm::vm::Vm; @@ -58,6 +59,33 @@ fn replacer_function_transform() { assert_eq!(stringify_val(&result), r#"{"a":2}"#); } +// --- 顶层 replacer 序(replacer 先于 undefined/函数/Symbol 早返)--- + +// 顶层 undefined:replacer 先被调用,返回值即序列化结果。 +#[test] +fn stringify_top_level_undefined_replacer_first() { + let out = eval_str("JSON.stringify(undefined, (k,v) => 42)").unwrap(); + assert_eq!(out, "42"); +} + +// 顶层函数/Symbol:replacer 可替换为可序列化值。 +#[test] +fn stringify_top_level_fn_symbol_replacer() { + let out = eval_str("JSON.stringify(function(){}, (k,v) => 'x')").unwrap(); + assert_eq!(out, r#""x""#); + let out = eval_str("JSON.stringify(Symbol(), (k,v) => 'y')").unwrap(); + assert_eq!(out, r#""y""#); +} + +// 顶层 undefined 无 replacer / replacer 透传:仍 undefined(守卫)。 +#[test] +fn stringify_top_level_undefined_still_undefined() { + let (_vm, result) = eval("JSON.stringify(undefined, (k,v) => v)").unwrap(); + assert!(result.is_undefined()); + let (_vm, result) = eval("JSON.stringify(undefined)").unwrap(); + assert!(result.is_undefined()); +} + // --- space --- #[test] @@ -74,6 +102,66 @@ fn space_negative_clamped() { assert_eq!(stringify_val(&result), r#"{"a":1}"#); } +// space = +∞ → 10 空格 gap(ToIntegerOrInfinity 后 min(10, x))。 +#[test] +fn stringify_space_infinity_ten_spaces() { + let out = eval_str("JSON.stringify({a:1}, null, Infinity)").unwrap(); + assert_eq!(out, "{\n \"a\": 1\n}"); +} + +// space = −∞/NaN → 空 gap。 +#[test] +fn stringify_space_neg_inf_nan_empty() { + let out = eval_str("JSON.stringify({a:1}, null, -Infinity)").unwrap(); + assert_eq!(out, r#"{"a":1}"#); + let out = eval_str("JSON.stringify({a:1}, null, NaN)").unwrap(); + assert_eq!(out, r#"{"a":1}"#); +} + +// space 小数截断(2.5 → 2 空格)、12 钳 10。 +#[test] +fn stringify_space_fractional_and_clamp() { + let out = eval_str("JSON.stringify({a:1}, null, 2.5)").unwrap(); + assert_eq!(out, "{\n \"a\": 1\n}"); + let out = eval_str("JSON.stringify({a:1}, null, 12)").unwrap(); + assert_eq!(out, "{\n \"a\": 1\n}"); +} + +// 装箱 Number space:ToNumber 后与素形同语义(+∞ → 10、−∞ → 空)。 +#[test] +fn stringify_space_boxed_number() { + let out = eval_str("JSON.stringify({a:1}, null, new Number(Infinity))").unwrap(); + assert_eq!(out, "{\n \"a\": 1\n}"); + let out = eval_str("JSON.stringify({a:1}, null, new Number(-Infinity))").unwrap(); + assert_eq!(out, r#"{"a":1}"#); +} + +// space 字符串 astral:按前 10 码元截断(12 码元 → 5 对,非标量值 6 口径)。 +#[test] +fn stringify_space_astral_truncated_units() { + let out = eval_str("JSON.stringify([1], null, '😀😀😀😀😀😀')").unwrap(); + assert_eq!(out, "[\n😀😀😀😀😀1\n]"); +} + +// 装箱 String space:同款码元口径。 +#[test] +fn stringify_space_astral_boxed_string() { + let out = eval_str("JSON.stringify([1], null, new String('😀😀😀😀😀😀'))").unwrap(); + assert_eq!(out, "[\n😀😀😀😀😀1\n]"); +} + +// 截断落在代理对边界:gap 保留原始孤立 surrogate(node 口径,非 \u 转义)。 +#[test] +fn stringify_space_astral_lone_surrogate() { + let (vm, result) = + eval("JSON.stringify([1], null, '\\uD83D\\uD83D\\uD83D\\uD83D\\uD83D\\uD83D\\uD83D\\uD83D')").unwrap(); + let units = vm.string_units(result); + let expected: Vec = vec![ + 0x5B, 0x0A, 0xD83D, 0xD83D, 0xD83D, 0xD83D, 0xD83D, 0xD83D, 0xD83D, 0xD83D, 0x31, 0x0A, 0x5D, + ]; + assert_eq!(units.as_ref(), expected.as_slice()); +} + // --- toJSON --- #[test]