builtins/vm: JSON.stringify 序列化与键面改单元域——孤立 surrogate 串值不再被 lossy 串值打碎

jsvalue_to_json 串值分支原经 to_owned_string(lossy)后按 char 流序列化:孤立
surrogate 单元被打成 U+FFFD 且输出裸非法码单元(非 well-formed JSON,
JSON.parse 无法复原同值)。现按单元流序列化(stringify_string_units):良配
代理对原样输出 astral 字符,非配对孤立 surrogate 输出 \\uXXXX(well-formed
JSON 要求,round-trip 复原同值),C0/C1 控制码输出 \\uXXXX,其余按 JSON
转义规则。

toJSON/replacer 键参数同改:键 si 物化为单元序列(整数键 → ASCII 数字串,
字符串键 → 码表键经 decode_key 还原含孤立 surrogate 单元),new_string_units_owned
建参——与 0c750fa 键物化面(Object.keys/ownKeys/entries/Reflect.ownKeys/json
reviver 键同源)口径一致,replace/reviver 键参数不再走 lossy 串值。

三并钉:json_tests 增补 stringify 孤立 surrogate round-trip / 代理对 astral
直出 / 键面单元传递三则;JSON 套件 37/0,node 13 用例对照一致。
This commit is contained in:
2026-09-14 07:33:51 +00:00
parent 6f70d7d09f
commit eb6bc412d9
2 changed files with 115 additions and 34 deletions
+65 -34
View File
@@ -3,10 +3,10 @@ use std::fmt::Write;
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_types::object::{JsObject, PropAttributes};
use oxide_types::private_key::make_int_key;
use oxide_types::private_key::{int_key_value, is_int_key, make_int_key};
use oxide_types::value::JsValue;
use crate::object::{key_si_to_string, walk_own_keys};
use crate::object::walk_own_keys;
use oxide_runtime_api::{NativeResult, VmHost};
@@ -91,7 +91,7 @@ fn walk_reviver<H: VmHost>(
}
}
// 对当前值调用 reviver,用返回值覆盖属性槽(键物化为 JS 可见串值,含孤立 surrogate 单元)。
// 对当前值调用 reviver,用返回值覆盖属性槽。
let key_val = crate::object::key_si_to_js_value(vm, key_si);
let holder_val = JsValue::from_js_object(holder_ptr);
match vm.call_function_sync(reviver, holder_val, &[key_val, val]) {
@@ -179,7 +179,21 @@ fn process_space(val: JsValue) -> String {
}
}
fn call_to_json<H: VmHost>(vm: &mut H, obj_val: JsValue, key: &str) -> Result<JsValue, JsValue> {
/// 键 si 物化为单元序列:整数键 → ASCII 数字串,字符串键 → 码表键经 `decode_key`
/// 还原(含孤立 surrogate 单元)。序列化文本与 toJSON/replacer 键参数均用此真实串。
fn key_si_to_units<H: VmHost>(vm: &H, si: u32) -> Vec<u16> {
if is_int_key(si) {
int_key_value(si).to_string().encode_utf16().collect()
} else {
vm.kernel_core()
.perm_interner()
.lookup(si)
.map(oxide_kernel::string_forge::decode_key)
.unwrap_or_default()
}
}
fn call_to_json<H: VmHost>(vm: &mut H, obj_val: JsValue, key: &[u16]) -> Result<JsValue, JsValue> {
if !obj_val.is_object() {
return Ok(obj_val);
}
@@ -193,7 +207,7 @@ fn call_to_json<H: VmHost>(vm: &mut H, obj_val: JsValue, key: &str) -> Result<Js
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(key);
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) => Err(crate::error::create_type_error(vm, &msg)),
@@ -246,7 +260,7 @@ pub fn json_stringify<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let holder = create_wrapper(vm, value);
let value = match call_to_json(vm, value, "") {
let value = match call_to_json(vm, value, &[]) {
Ok(v) => v,
Err(e) => return NativeResult::Err(e),
};
@@ -264,7 +278,6 @@ pub fn json_stringify<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let mut visited = HashSet::new();
let mut output = String::new();
let indent_level: usize = 0;
let key = "";
if jsvalue_to_json(
vm,
value,
@@ -274,7 +287,7 @@ pub fn json_stringify<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
replacer_whitelist.as_ref(),
&space,
indent_level,
key,
&[],
)
.is_err()
{
@@ -286,7 +299,7 @@ pub fn json_stringify<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
#[allow(clippy::too_many_arguments)]
fn jsvalue_to_json<H: VmHost>(
vm: &mut H, val: JsValue, visited: &mut HashSet<*const JsObject>, out: &mut String, replacer_fn: Option<JsValue>,
replacer_whitelist: Option<&HashSet<String>>, space: &str, indent_level: usize, key: &str,
replacer_whitelist: Option<&HashSet<String>>, space: &str, indent_level: usize, key: &[u16],
) -> Result<(), ()> {
let val = call_to_json(vm, val, key).map_err(|_| ())?;
if val.is_null() {
@@ -304,8 +317,9 @@ fn jsvalue_to_json<H: VmHost>(
oxide_runtime_api::write_number_into(n, out);
}
} else if val.is_string() {
let s = unsafe { (*val.as_string_ptr()).to_owned_string() };
stringify_string(&s, out);
// SAFETY: val 已确认是字符串值;单元视图按码单元流序列化(见 stringify_string_units)。
let units = unsafe { (*val.as_string_ptr()).units() };
stringify_string_units(&units, out);
} else if val.is_object() {
let obj_ptr = val.as_js_object_ptr();
if obj_ptr.is_null() {
@@ -329,24 +343,38 @@ fn jsvalue_to_json<H: VmHost>(
Ok(())
}
fn stringify_string(s: &str, out: &mut String) {
/// 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('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c.is_control() => {
let mut buf = [0u16; 2];
let encoded = c.encode_utf16(&mut buf);
for unit in encoded {
out.push_str(&format!("\\u{:04x}", unit));
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);
}
i += 2;
continue;
}
match u {
0x22 => out.push_str("\\\""),
0x5C => out.push_str("\\\\"),
0x0A => out.push_str("\\n"),
0x0D => out.push_str("\\r"),
0x09 => out.push_str("\\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);
}
}
_ => out.push(c),
}
i += 1;
}
out.push('"');
}
@@ -361,7 +389,7 @@ fn stringify_object<H: VmHost>(
let keys = walk_own_keys(vm, obj);
// 仅序列化可枚举自身属性(规范 EnumerableOwnPropertyNames)。
let entries: Vec<(String, u32)> = keys
let entries: Vec<(Vec<u16>, String, u32)> = keys
.into_iter()
.filter(|(_si, pos)| {
obj.prop_meta_at(*pos)
@@ -369,23 +397,25 @@ fn stringify_object<H: VmHost>(
.unwrap_or(PropAttributes::DEFAULT_DATA.enumerable())
})
.filter_map(|(si, pos)| {
let name = key_si_to_string(vm, si);
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((name, pos))
Some((units, name, pos))
})
.collect();
let mut first = true;
for (name, pos) in entries {
for (units, _name, pos) in entries {
let val = obj.get_prop_at(pos);
// replacer 函数回调(toJSON 已在 jsvalue_to_json 中处理)。
let val = if let Some(replacer) = replacer_fn {
let key_val = vm.new_string(&name);
let key_val = vm.new_string_units_owned(units.clone());
let holder = JsValue::from_js_object(obj as *const JsObject as *mut JsObject);
match vm.call_function_sync(replacer, holder, &[key_val, val]) {
Ok(v) => {
@@ -419,15 +449,15 @@ fn stringify_object<H: VmHost>(
for _ in 0..indent_level + 1 {
out.push_str(space);
}
stringify_string(&name, out);
stringify_string_units(&units, out);
out.push(':');
out.push(' ');
} else {
stringify_string(&name, out);
stringify_string_units(&units, out);
out.push(':');
}
jsvalue_to_json(vm, val, visited, out, replacer_fn, replacer_whitelist, space, indent_level + 1, &name)?;
jsvalue_to_json(vm, val, visited, out, replacer_fn, replacer_whitelist, space, indent_level + 1, &units)?;
}
if has_space && !first {
@@ -466,6 +496,7 @@ fn stringify_array<H: VmHost>(
let val = obj.get_prop_at(i);
let index_str = i.to_string();
let index_units: Vec<u16> = index_str.encode_utf16().collect();
// replacer 函数回调(toJSON 已在 jsvalue_to_json 中处理)。
let val = if let Some(replacer) = replacer_fn {
@@ -501,7 +532,7 @@ fn stringify_array<H: VmHost>(
replacer_whitelist,
space,
indent_level + 1,
&index_str,
&index_units,
)?;
}
}
+50
View File
@@ -313,3 +313,53 @@ fn json_parse_numeric_keys_stringify_roundtrip() {
let s = string_value(&vm, result);
assert_eq!(s, "{\"5\":1}");
}
// -- JSON.stringify 单元语义(孤立 surrogate 转义 / 代理对原样 / 真实键串) --
#[test]
fn json_stringify_unpaired_surrogate_escaped() {
// 非配对孤立 surrogate 输出 \uXXXX(well-formed JSON 要求)。
let mut vm = Vm::new();
let r1 = eval(&mut vm, "JSON.stringify(String.fromCharCode(0xD800)) === '\"\\\\ud800\"'").unwrap();
assert!(r1.as_bool(), "unpaired high surrogate must be \\ud800-escaped");
let r2 = eval(&mut vm, "JSON.stringify(String.fromCharCode(0xDF06)) === '\"\\\\udf06\"'").unwrap();
assert!(r2.as_bool(), "unpaired low surrogate must be \\udf06-escaped");
}
#[test]
fn json_stringify_surrogate_pair_raw() {
// 配对 surrogate 原样输出 astral 字符;混串按 spec 逐单元处置。
let mut vm = Vm::new();
let r1 = eval(&mut vm, "JSON.stringify(String.fromCharCode(0xD834, 0xDF06)) === '\"\\u{1D306}\"'").unwrap();
assert!(r1.as_bool(), "paired surrogates must serialize as the raw astral char");
let r2 = eval(
&mut vm,
"JSON.stringify(String.fromCharCode(0xD834, 0xD834, 0xDF06, 0xD834)) === '\"\\\\ud834\\u{1D306}\\\\ud834\"'",
)
.unwrap();
assert!(r2.as_bool(), "unpaired units around a pair must each be escaped");
}
#[test]
fn json_stringify_key_units_real() {
// 键序列化用真实键串(孤立 surrogate 转义),toJSON/replacer 键参数同口径。
let mut vm = Vm::new();
let r1 = eval(
&mut vm,
"var o = {}; o[String.fromCharCode(0xD800)] = 1; JSON.stringify(o) === '{\"\\\\ud800\":1}'",
)
.unwrap();
assert!(r1.as_bool(), "object key with lone surrogate must serialize escaped");
let r2 = eval(
&mut vm,
"var ks = []; JSON.stringify({[String.fromCharCode(0xD800)]: 1}, function(k, v) { if (k !== '') ks.push(k.charCodeAt(0)); return v; }); ks.length === 1 && ks[0] === 0xD800",
)
.unwrap();
assert!(r2.as_bool(), "replacer must receive the real key string (lone unit, not FFFD)");
let r3 = eval(
&mut vm,
"var tk = -1; var inner = { toJSON: function(k) { tk = k.charCodeAt(0); return 'x'; } }; var outer = {}; outer[String.fromCharCode(0xD800)] = inner; JSON.stringify(outer); tk === 0xD800",
)
.unwrap();
assert!(r3.as_bool(), "toJSON must receive the real key string (lone unit, not FFFD)");
}