From 43c859f35d15764435c4c2c9d2db96ed86f9aab3 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 19 Sep 2026 18:03:27 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20Array.from/of=20=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E6=80=A7=E7=9B=AE=E6=A0=87=E6=94=B6=E5=8F=A3=E6=8C=89=E8=A7=84?= =?UTF-8?q?=E8=8C=83=E6=8A=9B=20TypeError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/array/from.rs | 39 ++-- .../tests/array_from_of_length_guard_tests.rs | 215 ++++++++++++++++++ 2 files changed, 238 insertions(+), 16 deletions(-) create mode 100644 crates/oxide_builtins/tests/array_from_of_length_guard_tests.rs diff --git a/crates/oxide_builtins/src/array/from.rs b/crates/oxide_builtins/src/array/from.rs index 5a6f1920..7d4067da 100644 --- a/crates/oxide_builtins/src/array/from.rs +++ b/crates/oxide_builtins/src/array/from.rs @@ -86,7 +86,7 @@ pub fn array_of(vm: &mut H, args: &[u8]) -> NativeResult { return NativeResult::Err(err); } } - if let Err(err) = array_from_set_length(vm, a_ptr, is_array, n_elems) { + if let Err(err) = array_from_set_length(vm, a_ptr, n_elems) { return NativeResult::Err(err); } NativeResult::Ok(JsValue::from_js_object(a_ptr)) @@ -200,7 +200,7 @@ pub fn array_from(vm: &mut H, args: &[u8]) -> NativeResult { close_iterator(vm, iterator); return NativeResult::Err(err); } - if let Err(err) = array_from_set_length(vm, a_ptr, is_array, k) { + if let Err(err) = array_from_set_length(vm, a_ptr, k) { return NativeResult::Err(err); } } else { @@ -257,7 +257,7 @@ pub fn array_from(vm: &mut H, args: &[u8]) -> NativeResult { return NativeResult::Err(err); } } - if let Err(err) = array_from_set_length(vm, a_ptr, is_array, len) { + if let Err(err) = array_from_set_length(vm, a_ptr, len) { return NativeResult::Err(err); } } @@ -376,31 +376,38 @@ pub(crate) fn create_data_property_or_throw( } } -/// 把元素写入 Array.from 的结果对象:真数组走密集元素区,普通对象走 -/// CreateDataProperty 语义(属性名按十进制索引字符串)。 +/// 把元素写入 Array.from 的结果对象(CreateDataPropertyOrThrow 语义):真数组 +/// 走密集元素区,普通对象属性名按十进制索引字符串。 +/// +/// # 边界与前提 +/// - 真数组快路径(可扩展 + length 可写 + 槽位无元数据)与规范 define 同形, +/// 裸写零漂移;完整性受限目标(frozen/sealed/preventExtensions)与携带元 +/// 数据的槽位(洞/只读/访问器)一律落慢路径,按规范抛 TypeError。 fn array_from_set_prop( vm: &mut H, a: *mut JsObject, is_array: bool, i: usize, val: JsValue, ) -> Result<(), JsValue> { if is_array { unsafe { - (*a).set_prop_at(i, val); + let a_obj = &mut *a; + let count = a_obj.array_prop_count as usize; + // 越界增长不读元数据(越界读取会落命名属性区,语义错位)。 + let fast = + a_obj.is_extensible() && a_obj.is_length_writable() && (i >= count || a_obj.prop_meta_at(i).is_none()); + if fast { + a_obj.set_prop_at(i, val); + return Ok(()); + } } - return Ok(()); } let key = vm.new_string(&i.to_string()); let key_si = vm.property_key_si(key); create_data_property_or_throw(vm, unsafe { &mut *a }, key_si, val) } -/// 收尾设置 Array.from 结果对象的 length:真数组直接改元素计数,普通对象走 -/// 普通 Set(可触发继承的 length setter 并透传其异常)。 -fn array_from_set_length(vm: &mut H, a: *mut JsObject, is_array: bool, len: usize) -> Result<(), JsValue> { - if is_array { - unsafe { - (*a).set_prop_count_fast(len); - } - return Ok(()); - } +/// 收尾设置 Array.from 结果对象的 length:统一走普通 Set——真数组 length 写 +/// 按 ArraySetLength 可写性判定(frozen / length 收窄不可写目标抛 TypeError), +/// 普通对象可触发继承的 length setter,其异常透传。 +fn array_from_set_length(vm: &mut H, a: *mut JsObject, len: usize) -> Result<(), JsValue> { let a_obj = unsafe { &mut *a }; let length_key = vm.new_string("length"); let length_si = vm.property_key_si(length_key); diff --git a/crates/oxide_builtins/tests/array_from_of_length_guard_tests.rs b/crates/oxide_builtins/tests/array_from_of_length_guard_tests.rs new file mode 100644 index 00000000..42950f40 --- /dev/null +++ b/crates/oxide_builtins/tests/array_from_of_length_guard_tests.rs @@ -0,0 +1,215 @@ +//! Array.from / Array.of 结果对象完整性收口引擎钉:frozen / sealed / +//! preventExtensions / 非可写自有索引目标按规范 CreateDataPropertyOrThrow 与 +//! Set(length) 语义抛 TypeError,新数组热路径与回退臂零漂移。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval_value(source: &str) -> Result<(Vm, JsValue), String> { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; + let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?; + let mut vm = Vm::new(); + let result = vm.run(&Arc::new(module))?; + Ok((vm, result)) +} + +fn eval_str(source: &str) -> Result { + let (vm, result) = eval_value(source)?; + vm.lookup_str(result) + .ok_or_else(|| "completion value is not a string".to_string()) +} + +// frozen 目标(iterable 源):首元素写即抛 TypeError,目标零改动。 +#[test] +fn test_from_frozen_target_iterable_throws_untouched() { + let out = eval_str( + "(() => { const A = Object.freeze([]); function Cf() { return A; } \ + let k = 'no-throw'; try { Array.from.call(Cf, [1, 2, 3]); } catch (e) { k = e.constructor.name; } \ + return k + '|' + A.length + '|' + (0 in A); })()", + ) + .unwrap(); + assert_eq!(out, "TypeError|0|false"); +} + +// frozen 目标零元素源:元素写不触发,length 写抛 TypeError。 +#[test] +fn test_from_frozen_target_empty_throws_length() { + let out = eval_str( + "(() => { function Cf() { return Object.freeze([]); } \ + let k = 'no-throw'; try { Array.from.call(Cf, []); } catch (e) { k = e.constructor.name; } \ + return k; })()", + ) + .unwrap(); + assert_eq!(out, "TypeError"); +} + +// frozen 目标 array-like 源:首元素写抛 TypeError。 +#[test] +fn test_from_frozen_target_arraylike_throws() { + let out = eval_str( + "(() => { function Cf() { return Object.freeze([]); } \ + let k = 'no-throw'; \ + try { Array.from.call(Cf, { length: 2, 0: 'x', 1: 'y' }); } catch (e) { k = e.constructor.name; } \ + return k; })()", + ) + .unwrap(); + assert_eq!(out, "TypeError"); +} + +// sealed 目标两形:空目标新槽不可扩展抛 TypeError;预置目标已有槽非可配置 +// 重定义抛 TypeError 且原值不动。 +#[test] +fn test_from_sealed_target_throws_existing_slot_preserved() { + let out = eval_str( + "(() => { const S0 = Object.seal([]); function Cf0() { return S0; } \ + let k1 = 'no-throw'; try { Array.from.call(Cf0, [0, 1]); } catch (e) { k1 = e.constructor.name; } \ + const S9 = Object.seal([9]); function Cf9() { return S9; } \ + let k2 = 'no-throw'; try { Array.from.call(Cf9, [0, 1]); } catch (e) { k2 = e.constructor.name; } \ + return k1 + '|' + k2 + '|' + S9[0]; })()", + ) + .unwrap(); + assert_eq!(out, "TypeError|TypeError|9"); +} + +// preventExtensions 目标空数组:新槽不可扩展抛 TypeError。 +#[test] +fn test_from_prevent_extensions_target_throws() { + let out = eval_str( + "(() => { function Cf() { return Object.preventExtensions([]); } \ + let k = 'no-throw'; try { Array.from.call(Cf, [0, 1]); } catch (e) { k = e.constructor.name; } \ + return k; })()", + ) + .unwrap(); + assert_eq!(out, "TypeError"); +} + +// preventExtensions 目标预置 [9] 且源仅重写已有槽:元素写成功、length 同长 +// 不抛,结果即原对象。 +#[test] +fn test_from_prevent_extensions_existing_slot_rewritten() { + let out = eval_str( + "(() => { const A = Object.preventExtensions([9]); function Cf() { return A; } \ + const r = Array.from.call(Cf, [7]); \ + return String(r === A) + '|' + A.length + '|' + A[0]; })()", + ) + .unwrap(); + assert_eq!(out, "true|1|7"); +} + +// 非可写可配置自有索引 "0"(iterable 源):CreateDataPropertyOrThrow 覆盖为 +// present,描述符恢复可写可枚举。 +#[test] +fn test_from_nonwritable_own_index_overwritten() { + let out = eval_str( + "(() => { function Cf() { const a = []; \ + Object.defineProperty(a, '0', { value: 1, writable: false, configurable: true }); return a; } \ + const r = Array.from.call(Cf, [2]); \ + const d = Object.getOwnPropertyDescriptor(r, '0'); \ + return r[0] + '|' + d.writable + '|' + d.enumerable; })()", + ) + .unwrap(); + assert_eq!(out, "2|true|true"); +} + +// Array.of frozen 目标:首元素写抛 TypeError。 +#[test] +fn test_of_frozen_target_throws() { + let out = eval_str( + "(() => { function Cf() { return Object.freeze([]); } \ + let k = 'no-throw'; try { Array.of.call(Cf, 1, 2); } catch (e) { k = e.constructor.name; } \ + return k; })()", + ) + .unwrap(); + assert_eq!(out, "TypeError"); +} + +// Array.of frozen 目标零参:元素写不触发,length 写抛 TypeError。 +#[test] +fn test_of_frozen_target_zero_args_throws_length() { + let out = eval_str( + "(() => { function Cf() { return Object.freeze([]); } \ + let k = 'no-throw'; try { Array.of.call(Cf); } catch (e) { k = e.constructor.name; } \ + return k; })()", + ) + .unwrap(); + assert_eq!(out, "TypeError"); +} + +// 冻结数组作 this(非构造器):回退臂新建普通数组,原 this 不动。 +#[test] +fn test_from_frozen_this_fallback_new_array() { + let out = eval_str( + "(() => { const r = Array.from.call(Object.freeze([1, 2]), 'xy'); \ + return r.join(',') + '|' + String(Object.isFrozen(r)); })()", + ) + .unwrap(); + assert_eq!(out, "x,y|false"); +} + +// Array.of 冻结 this 零参构造器回退:新建普通数组。 +#[test] +fn test_of_frozen_this_fallback_new_array() { + let out = eval_str( + "(() => { const r = Array.of.call(Object.freeze([1, 2]), 3, 4); \ + return r.join(',') + '|' + String(Object.isFrozen(r)); })()", + ) + .unwrap(); + assert_eq!(out, "3,4|false"); +} + +// mapFn 抛错时序:映射先于元素写执行,RangeError 原样透传(frozen 目标不干扰)。 +#[test] +fn test_from_mapfn_error_propagates_before_element_write() { + let out = eval_str( + "(() => { function Cf() { return Object.freeze([]); } \ + let k = 'no-throw'; \ + try { Array.from.call(Cf, [1], () => { throw new RangeError('m'); }); } \ + catch (e) { k = e.constructor.name; } \ + return k; })()", + ) + .unwrap(); + assert_eq!(out, "RangeError"); +} + +// 零漂移四连:常规 from/of 结果值与节点位不变。 +#[test] +fn test_from_of_plain_paths_unchanged() { + let out = eval_str( + "(() => { return Array.from([1, 2, 3]).join(',') + '|' + Array.from('ab').join(',') + '|' + \ + Array.from({ length: 2, 0: 'x' }).join(',') + '|' + Array.of(7, 8).join(','); })()", + ) + .unwrap(); + assert_eq!(out, "1,2,3|a,b|x,|7,8"); +} + +// 继承 length setter 抛错(非数组目标):setter 异常原样透传。 +#[test] +fn test_from_inherited_length_setter_error_propagates() { + let out = eval_str( + "(() => { function Cf() { const o = {}; \ + Object.defineProperty(o, 'length', { set: function () { throw new RangeError('ls'); } }); \ + return o; } \ + let k = 'no-throw'; try { Array.from.call(Cf, [1, 2]); } catch (e) { k = e.constructor.name; } \ + return k; })()", + ) + .unwrap(); + assert_eq!(out, "RangeError"); +} + +// species 回归:结果 instanceof C、构造器恰调用一次、构造 this 即结果对象。 +#[test] +fn test_from_species_ctor_contract() { + let out = eval_str( + "(() => { let callCount = 0, thisVal = null; \ + function C() { callCount++; thisVal = this; } \ + C.prototype = []; \ + const r = Array.from.call(C, [1, 2]); \ + return String(r instanceof C) + '|' + callCount + '|' + String(thisVal === r) + '|' + r.join(','); })()", + ) + .unwrap(); + assert_eq!(out, "true|1|true|1,2"); +}