fix: JSON 白名单按列表序与 Get 语义取值并透传 reviver 抛错原值

This commit is contained in:
2026-09-20 09:57:56 +00:00
parent 1433d47329
commit 055ecf3022
2 changed files with 346 additions and 64 deletions
+168 -64
View File
@@ -102,7 +102,13 @@ fn walk_reviver<H: VmHost>(
}
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<H: VmHost>(vm: &H, si: u32) -> Vec<u16> {
}
}
/// 白名单元素 → 键名提取(Stringify 步 4):String/Number 原语直取;装箱
/// String/Number 经 ToPrimitive string hint(toString 优先、valueOf 兜底、
/// 两法皆不可调用落盒值);其余形态(装箱 Boolean、普通对象、Symbol 等)
/// 返回 None 跳过。方法读取与调用异常传播原始抛出值。
fn whitelist_element_name<H: VmHost>(vm: &mut H, elem: JsValue) -> Result<Option<String>, 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<H: VmHost>(vm: &mut H, obj_val: JsValue, key: &[u16]) -> Result<JsValue, JsValue> {
// SerializeJSONProperty 步 2:对象与 BigInt 原语均查 toJSON(Get 对原语
// 自动装箱),BigInt 的查表对象取 %BigIntPrototype%。
@@ -210,27 +272,36 @@ fn call_to_json<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
let mut replacer_fn: Option<JsValue> = None;
let mut replacer_whitelist: Option<HashSet<String>> = None;
let mut replacer_whitelist: Option<Vec<String>> = 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<H: VmHost>(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<String> = 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<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,
replacer_whitelist: Option<&Vec<String>>, 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<H: VmHost>(
vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut String, replacer_fn: Option<JsValue>,
replacer_whitelist: Option<&HashSet<String>>, space: &str, indent_level: usize,
replacer_whitelist: Option<&Vec<String>>, 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<u16>, 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<u16>, u32, Option<u32>)> = 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<H: VmHost>(
#[allow(clippy::too_many_arguments)]
fn stringify_array<H: VmHost>(
vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut String, replacer_fn: Option<JsValue>,
replacer_whitelist: Option<&HashSet<String>>, space: &str, indent_level: usize,
replacer_whitelist: Option<&Vec<String>>, space: &str, indent_level: usize,
) -> Result<(), JsValue> {
let has_space = !space.is_empty();
out.push('[');
@@ -606,29 +690,49 @@ fn stringify_array<H: VmHost>(
#[allow(clippy::too_many_arguments)]
fn stringify_typed_array<H: VmHost>(
vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut String, replacer_fn: Option<JsValue>,
replacer_whitelist: Option<&HashSet<String>>, space: &str, indent_level: usize,
replacer_whitelist: Option<&Vec<String>>, 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<u16>, JsValue)> = if let Some(whitelist) = replacer_whitelist {
let mut list: Vec<(Vec<u16>, 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<u16> = 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<u16>, 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() {
@@ -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<String, String> {
let (vm, result) = eval(source)?;
vm.lookup_str(result)