diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index bd66ddfe..d24b208f 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -731,6 +731,8 @@ pub(crate) fn define_from_descriptor( let get_field = own_field(vm, desc_val, get_si); let set_field = own_field(vm, desc_val, set_si); let writable_field = own_field(vm, desc_val, writable_si); + let enumerable_field = own_field(vm, desc_val, enumerable_si); + let configurable_field = own_field(vm, desc_val, configurable_si); // 数组 length 是无 shape 槽的虚拟数据属性:需与普通已有属性一样参与描述符 // 缺省回填(writable 保持当前值、value 保持当前长度),其当前描述符由元素 @@ -779,10 +781,10 @@ pub(crate) fn define_from_descriptor( } // 修改已有属性时缺省字段回填现有值,仅定义新属性时缺省才为 false。 - let enumerable = own_field(vm, desc_val, enumerable_si) + let enumerable = enumerable_field .map(oxide_runtime_api::to_boolean) .unwrap_or_else(|| existing_meta.map(|m| m.attributes.enumerable()).unwrap_or(false)); - let configurable = own_field(vm, desc_val, configurable_si) + let configurable = configurable_field .map(oxide_runtime_api::to_boolean) .unwrap_or_else(|| existing_meta.map(|m| m.attributes.configurable()).unwrap_or(false)); @@ -801,8 +803,9 @@ pub(crate) fn define_from_descriptor( }) }; - let obj = unsafe { &mut *obj_ptr }; - if has_accessor { + // 访问器描述符:解析 get/set(缺失字段回填现有属性)并做可调用性校验—— + // 规范步序先于索引/约束检查。 + let (get, set) = if has_accessor { let get = get_field.unwrap_or_else(|| { existing_meta .filter(|m| m.is_accessor) @@ -818,6 +821,43 @@ pub(crate) fn define_from_descriptor( if (!get.is_undefined() && !is_callable(get)) || (!set.is_undefined() && !is_callable(set)) { return Err("accessor descriptor get/set must be callable or undefined".to_string()); } + (get, set) + } else { + (JsValue::undefined(), JsValue::undefined()) + }; + + // TA 数值索引臂:置于混入/可调用检查之后、define 路由之前。字段在场四检查 + // 取原始描述符字段(非缺省回填后的属性位);有效索引值臂缺省读当前元素 + // (通用描述符);数字无效索引零副作用直接失败;非规范数字串 / symbol 键 + // 落真实自有属性路径。 + if unsafe { &*obj_ptr }.is_typed_array_obj() { + match crate::typed_array::ta_index_gate(vm, unsafe { &*obj_ptr }, key_si) { + crate::typed_array::TaIndexGate::NumericValid(index) => { + if has_accessor + || writable_field.is_some_and(|w| !oxide_runtime_api::to_boolean(w)) + || enumerable_field.is_some_and(|e| !oxide_runtime_api::to_boolean(e)) + || configurable_field.is_some_and(|c| !oxide_runtime_api::to_boolean(c)) + { + return Err( + "cannot define property: TypedArray index only accepts a writable, enumerable, configurable data descriptor" + .to_string(), + ); + } + let value = value_field.unwrap_or_else(|| { + crate::typed_array::typed_array_element_get(vm, unsafe { &*obj_ptr }, index) + .unwrap_or(JsValue::undefined()) + }); + return crate::typed_array::typed_array_element_define(vm, unsafe { &mut *obj_ptr }, index, value); + } + crate::typed_array::TaIndexGate::NumericInvalid => { + return Err("cannot define property: TypedArray index out of range".to_string()); + } + crate::typed_array::TaIndexGate::Ordinary => {} + } + } + + let obj = unsafe { &mut *obj_ptr }; + if has_accessor { vm.define_accessor_property(obj, key_si, get, set, PropAttributes::new(false, enumerable, configurable))?; } else if has_data { let value = if has_existing { diff --git a/crates/oxide_builtins/src/typed_array.rs b/crates/oxide_builtins/src/typed_array.rs index 9fa840f4..a1a3cbaa 100644 --- a/crates/oxide_builtins/src/typed_array.rs +++ b/crates/oxide_builtins/src/typed_array.rs @@ -543,11 +543,18 @@ pub fn typed_array_receiver_set( /// 定义 TypedArray 整数索引元素(defineProperty 语义)。 /// +/// # 步骤 +/// 1. 取视图(内部状态非法返回防御背板 Err)。 +/// 2. 界判:越界(含 detach:live 长 0)拒绝定义。 +/// 3. 清 uncaught 槽后强转写入;强转失败把原值转存 define 专用槽。 +/// /// # 边界与前提 /// - 索引越界:拒绝定义(Err),与整数索引 exotic 对象的 [[DefineOwnProperty]] 一致 /// /// # 副作用 /// - 把值 ToNumber 后写入底层 buffer +/// - 强转期用户回调抛错时原值入 define 专用槽,Object/Reflect 入口据此 +/// 原值重抛 // ponytail: 不校验 writable 描述符——调用方把"省略 writable"折叠为 false, // 无法与显式 false 区分;TA 元素天然可写,直接写入。 pub fn typed_array_element_define( @@ -558,7 +565,14 @@ pub fn typed_array_element_define( if index as usize >= ta_live_length(view) { return Err("cannot define property: TypedArray index out of range".to_string()); } - write_typed_array_element(vm, view, index, value) + // 强转前清 uncaught 槽(length 路径同纪律):失败后槽内值必为本次强转 + // 产生的原值。 + vm.clear_uncaught_value(); + let r = write_typed_array_element(vm, view, index, value); + if r.is_err() { + vm.move_uncaught_to_pending_length(); + } + r } fn write_typed_array_element( @@ -577,7 +591,12 @@ fn write_typed_array_element( Ok(v) => v, // 转换失败恢复为可捕获的 JS 异常:深度 0 原值入通道就地展开到外围 // catch(kind 保真),深度 >0 以 kind 前缀文本由原生调用边界恢复。 - Err(err) => return vm.raise_captured(err), + Err(err) => { + // 原值放回 uncaught 槽:define 通道专用槽转存依赖槽内原值 + // (set 通道经文本消费,槽不改变其可观测行为)。 + vm.restore_uncaught_value(Some(err)); + return vm.raise_captured(err); + } }; // live 边界:越界(含 detach)静默不写(规范 IntegerIndexedElementSet 的 // IsValidIndexedAccess 臂),转换副作用已先发生。 diff --git a/crates/oxide_runtime_api/src/host.rs b/crates/oxide_runtime_api/src/host.rs index 6103eeed..4396f42a 100644 --- a/crates/oxide_runtime_api/src/host.rs +++ b/crates/oxide_runtime_api/src/host.rs @@ -192,6 +192,14 @@ pub trait VmHost { /// 写入的值,本槽仅由 length 强转失败写入,入口据此区分「强转异常」与 /// 「描述符收敛失败」。 fn take_pending_length_exception(&mut self) -> Option; + /// 清空 uncaught 槽。define 通道强转入口先清后转:强转失败后槽内值必为 + /// 本次强转产生的原值,先前操作的残留不得被误转存为本次原值(与 length + /// 路径同纪律)。 + fn clear_uncaught_value(&mut self); + /// 把 uncaught 槽移入 define 专用槽(强转期用户回调抛出的原值): + /// Object/Reflect define 入口失败时先取专用槽原值重抛,不投影为 false + /// 或改写成引擎错误。 + fn move_uncaught_to_pending_length(&mut self); // 错误处理 fn checked_object_ptr(&mut self, val: JsValue, error_msg: &str) -> Result, String>; diff --git a/crates/oxide_vm/src/vm/props.rs b/crates/oxide_vm/src/vm/props.rs index e0c425f6..f061b82d 100644 --- a/crates/oxide_vm/src/vm/props.rs +++ b/crates/oxide_vm/src/vm/props.rs @@ -216,10 +216,10 @@ impl Vm { /// 属性即存在。数组 length 虚拟属性恒存在;数组元素区 hole 视同缺失; /// 其余按 shape 槽判定。 /// - /// 顶层 TypedArray 经统一数值键门(exotic [[HasProperty]]):界内整数索引 + /// TypedArray 经统一数值键门(exotic [[HasProperty]]):界内整数索引 /// 判存在;数字无效键(负/分数/±Infinity/NaN/越界,含 "-0" 特例)立即 - /// false,不查自身命名属性也不走原型链;非规范数字串落普通路径。原型链上 - /// 的 TA 按普通对象查 shape 槽。 + /// false,不查自身命名属性也不走原型链;非规范数字串落普通路径。原型链 + /// 上的 TA 按同口径经门判定。 /// /// 与 `resolve_property` 的差异:原型链上每层都检查数组元素区 /// (`resolve_property` 只在顶层检查元素区,链上仅走 shape 槽), @@ -233,8 +233,8 @@ impl Vm { return true; } // 统一数值键门(exotic [[HasProperty]]):同 `ordinary_get_inner` - // 的口径,门只落在顶层对象。 - if obj.is_typed_array_obj() && depth == 0 { + // 的口径,链上每层同口径判定。 + if obj.is_typed_array_obj() { match oxide_builtins::typed_array::ta_index_gate(self, obj, prop_name_si) { oxide_builtins::typed_array::TaIndexGate::NumericValid(_) => return true, oxide_builtins::typed_array::TaIndexGate::NumericInvalid => return false, diff --git a/crates/oxide_vm/src/vm/vmhost.rs b/crates/oxide_vm/src/vm/vmhost.rs index bb40367c..1167e50b 100644 --- a/crates/oxide_vm/src/vm/vmhost.rs +++ b/crates/oxide_vm/src/vm/vmhost.rs @@ -61,6 +61,12 @@ impl oxide_runtime_api::VmHost for Vm { fn take_pending_length_exception(&mut self) -> Option { self.pending_length_exception.take() } + fn clear_uncaught_value(&mut self) { + self.last_uncaught_value = None; + } + fn move_uncaught_to_pending_length(&mut self) { + self.pending_length_exception = self.last_uncaught_value.take(); + } fn property_key_si(&mut self, val: JsValue) -> u32 { // 对象键转换(to_string_full)失败时降级为空键:本 trait 路径供 // Reflect/Object 内置使用,这些内置按规范不传播键转换异常;计算属性 diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index 0484f8ed..26a3c5b2 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -103,8 +103,8 @@ impl Vm { // 统一数值键门(exotic [[Get]]):界内整数读底层 buffer;数字无效 // 键(负/分数/±Infinity/NaN/越界,含 "-0" 特例)立即 undefined 不 // 查自身命名属性也不走原型链;非规范数字串落下方普通属性路径。 - // 门只落在顶层对象:原型链上的 TA 维持普通 shape 槽查找。 - if obj.is_typed_array_obj() && depth == 0 { + // 链上每层同口径:原型链上的 TA 亦按 exotic 语义判定。 + if obj.is_typed_array_obj() { match oxide_builtins::typed_array::ta_index_gate(self, obj, prop_name_si) { oxide_builtins::typed_array::TaIndexGate::NumericValid(index) => { return oxide_builtins::typed_array::typed_array_element_get(self, obj, index); @@ -1105,6 +1105,17 @@ impl Vm { let target_ptr = obj as *mut JsObject; let get = self.promote_if_needed_for_write_ptr(target_ptr, get); let set = self.promote_if_needed_for_write_ptr(target_ptr, set); + // TA 数值索引臂:界内 / 数字无效索引不接受 accessor(false);非规范数字 + // 串 / symbol 键落真实 accessor 属性。 + if obj.is_typed_array_obj() { + match oxide_builtins::typed_array::ta_index_gate(self, obj, prop_name_si) { + oxide_builtins::typed_array::TaIndexGate::NumericValid(_) + | oxide_builtins::typed_array::TaIndexGate::NumericInvalid => { + return Err("cannot define property: TypedArray index only accepts a data descriptor".to_string()); + } + oxide_builtins::typed_array::TaIndexGate::Ordinary => {} + } + } if obj.is_array() { if let Some(index) = self.array_index_from_property_key(prop_name_si) { return self.define_array_index_element(obj, index, JsValue::undefined(), attributes, true, get, set); diff --git a/crates/oxide_vm/tests/builtins_typed_array_tests.rs b/crates/oxide_vm/tests/builtins_typed_array_tests.rs index 4c31db9b..fcc25813 100644 --- a/crates/oxide_vm/tests/builtins_typed_array_tests.rs +++ b/crates/oxide_vm/tests/builtins_typed_array_tests.rs @@ -1050,3 +1050,249 @@ fn ta_set_resize_convert_first() { .unwrap(); assert!(result.as_bool()); } + +// ── DefineOwnProperty 数值索引臂 ─────────────────────────────────────────── + +#[test] +fn ta_dop_valid_write() { + // 界内规范键 + 全 true 数据描述符:定义成功、元素写入、length 不变 + // (Reflect 与 Object.defineProperty 同臂)。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "(function () { var ta = new Uint8Array(4); \ + var r1 = Reflect.defineProperty(ta, '1', { value: 200, writable: true, enumerable: true, configurable: true }); \ + Object.defineProperty(ta, 3, { value: 7, writable: true, enumerable: true, configurable: true }); \ + return r1 === true && ta[1] === 200 && ta[3] === 7 && ta.length === 4 && ta[0] === 0 && ta[2] === 0; })()", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn ta_dop_partial_and_generic() { + // 部分描述符(缺省字段非显式 false)与通用描述符(无 [[Value]] 读当前 + // 元素回写)均定义成功;元素值除显式 value 外不变(NaN 样本)。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "(function () { var ta = new Float64Array(2); \ + ta[0] = NaN; ta[1] = NaN; \ + var r1 = Reflect.defineProperty(ta, '0', { value: 5 }); \ + var r2 = Reflect.defineProperty(ta, '1', { writable: true, enumerable: true, configurable: true }); \ + return r1 === true && ta[0] === 5 && r2 === true && Number.isNaN(ta[1]); })()", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn ta_dop_constraint_false() { + // 字段在场四检查(writable/enumerable/configurable 显式 false、accessor + // 描述符):Reflect 投影 false、元素不动、不建自有属性。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "(function () { var ta = new Int16Array(2); ta[0] = 9; \ + var shapes = [ \ + { value: 3, writable: false, enumerable: true, configurable: true }, \ + { value: 3, writable: true, enumerable: false, configurable: true }, \ + { value: 3, writable: true, enumerable: true, configurable: false }, \ + { get: function () { return 1; }, set: function () {} } ]; \ + for (var i = 0; i < shapes.length; i++) { \ + if (Reflect.defineProperty(ta, '0', shapes[i]) !== false) return false; \ + if (ta[0] !== 9) return false; } \ + return Object.getOwnPropertyNames(ta).length === 0; })()", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn ta_dop_constraint_throws() { + // 同四约束形经 Object.defineProperty:抛 TypeError、元素不动、无属性。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "(function () { var ta = new Int16Array(2); ta[0] = 9; \ + var t1 = 0; \ + try { Object.defineProperty(ta, '0', { value: 3, writable: false, enumerable: true, configurable: true }); } \ + catch (e) { if (!(e instanceof TypeError)) return false; t1 = 1; } \ + var t2 = 0; \ + try { Object.defineProperty(ta, '0', { get: function () { return 1; } }); } \ + catch (e) { if (!(e instanceof TypeError)) return false; t2 = 1; } \ + return t1 === 1 && t2 === 1 && ta[0] === 9 && Object.getOwnPropertyNames(ta).length === 0; })()", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn ta_dop_numeric_invalid_false() { + // 数字无效键("-0"/负/越界/分数):false、不抛、元素不动、不建属性。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "(function () { var ta = new Int8Array(2); ta[0] = 5; \ + var keys = ['-0', '-1', '2', '0.1', '0.000001', '3.5']; \ + for (var i = 0; i < keys.length; i++) { \ + var d = { value: 1, writable: true, enumerable: true, configurable: true }; \ + if (Reflect.defineProperty(ta, keys[i], d) !== false) return false; \ + if (ta[0] !== 5) return false; } \ + return Object.getOwnPropertyNames(ta).length === 0; })()", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn ta_dop_ordinary_key_property() { + // 非规范数字串键 round-trip 不成:定义真实自有属性(数据与访问器), + // 键 "1.0" 与 "1" 独立互不干扰。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "(function () { var ta = new Int8Array(1); \ + var keys = ['1.0', '+1', '1000000000000000000000', '0.0000001']; \ + for (var i = 0; i < keys.length; i++) { \ + var r = Reflect.defineProperty(ta, keys[i], { value: i + 1, writable: true, enumerable: true, configurable: true }); \ + if (r !== true) return false; \ + if (!Object.prototype.hasOwnProperty.call(ta, keys[i])) return false; \ + if (ta[keys[i]] !== i + 1) return false; } \ + var r2 = Reflect.defineProperty(ta, '1.0', { get: function () { return 'baz'; }, configurable: true }); \ + return r2 === true && ta['1.0'] === 'baz' && ta['1'] === undefined; })()", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn ta_dop_detach_false_no_throw() { + // detach 后 live 长 0:全数值键归数字无效,false 不抛(投错值不触发 + // 强转)、不建属性。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "(function () { var ab = new ArrayBuffer(8); var ta = new Int8Array(ab); ta[0] = 7; \ + ab.transfer(); \ + var keys = ['0', '-1', '1.1', '-0', '2']; \ + for (var i = 0; i < keys.length; i++) { \ + var r = Reflect.defineProperty(ta, keys[i], { \ + value: { valueOf: function () { throw new Error('no'); } }, \ + writable: true, enumerable: true, configurable: true }); \ + if (r !== false) return false; } \ + return Object.getOwnPropertyNames(ta).length === 0; })()", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn ta_dop_detach_in_valueof_true() { + // valueOf 期 detach:强转成功、写期 live 复判 detach 静默——定义 true、 + // 元素保持 undefined。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "(function () { var ab = new ArrayBuffer(8); var ta = new Int8Array(ab); \ + var v = { valueOf: function () { ab.transfer(); return 9; } }; \ + var r = Reflect.defineProperty(ta, '0', { value: v, writable: true, enumerable: true, configurable: true }); \ + return r === true && ta[0] === undefined; })()", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn ta_dop_value_throws_original() { + // 强转期用户回调抛出原值经专用槽重抛:catch 收到原对象身份(构造器 + // 身份不可丢)、不建属性。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "(function () { var ta = new Int8Array(2); \ + function Marker() { this.tag = 'marker'; } \ + var marker = new Marker(); \ + var v = { valueOf: function () { throw marker; } }; \ + var caught = null; \ + try { Object.defineProperty(ta, '0', { value: v, writable: true, enumerable: true, configurable: true }); \ + return false; } \ + catch (e) { caught = e; } \ + return caught === marker && caught.tag === 'marker' && Object.getOwnPropertyNames(ta).length === 0; })()", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn ta_dop_bigint_content_conversion() { + // BigInt 元素内容强转:Number 值 TypeError、非整数字符串 SyntaxError、 + // BigInt 值成功写入。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "(function () { var ta = new BigInt64Array(2); \ + var t1 = 0; \ + try { Object.defineProperty(ta, '0', { value: 42, writable: true, enumerable: true, configurable: true }); } \ + catch (e) { if (!(e instanceof TypeError)) return false; t1 = 1; } \ + var t2 = 0; \ + try { Object.defineProperty(ta, '0', { value: '1.5', writable: true, enumerable: true, configurable: true }); } \ + catch (e) { if (!(e instanceof SyntaxError)) return false; t2 = 1; } \ + var r3 = Reflect.defineProperty(ta, '1', { value: 7n, writable: true, enumerable: true, configurable: true }); \ + return t1 === 1 && t2 === 1 && r3 === true && ta[1] === 7n; })()", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn ta_dop_non_extensible() { + // 不可扩展 TA:界内索引定义不受限;数字无效/新命名/新 symbol 键 false; + // 既有真实命名属性重定义不受限。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "(function () { var ta = new Int8Array(2); ta[0] = 1; ta.newProp = 5; \ + Object.preventExtensions(ta); \ + var r1 = Reflect.defineProperty(ta, '1', { value: 9, writable: true, enumerable: true, configurable: true }); \ + var r2 = Reflect.defineProperty(ta, '2', { value: 9, writable: true, enumerable: true, configurable: true }); \ + var r3 = Reflect.defineProperty(ta, 'brandnew', { value: 1, writable: true, enumerable: true, configurable: true }); \ + var r4 = Reflect.defineProperty(ta, 'newProp', { value: 6, writable: true, enumerable: true, configurable: true }); \ + var r5 = Reflect.defineProperty(ta, Symbol('s'), { value: 1, writable: true, enumerable: true, configurable: true }); \ + return r1 === true && ta[1] === 9 && r2 === false && r3 === false && r4 === true && ta.newProp === 6 && r5 === false; })()", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn ta_dop_int_key_same_arm() { + // 整数键(非字符串)与字符串键同臂:界内定义成功、detach 后归数字无效。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "(function () { var ab = new ArrayBuffer(8); var ta = new Int8Array(ab); \ + var r1 = Reflect.defineProperty(ta, 1, { value: 4, writable: true, enumerable: true, configurable: true }); \ + if (r1 !== true || ta[1] !== 4) return false; \ + ab.transfer(); \ + var r2 = Reflect.defineProperty(ta, 0, { value: 7, writable: true, enumerable: true, configurable: true }); \ + return r2 === false; })()", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn ta_dop_get_dir_arm4() { + // 非规范数字串键 + accessor 描述符:定义真实 accessor 属性、getter 经 + // 普通读路径触发(Get 目录 key-is-not-canonical-index 联绿臂)。 + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "(function () { var ta = new Int8Array(2); \ + var r = Reflect.defineProperty(ta, '+1', { get: function () { return 'baz'; }, set: function () {}, configurable: true }); \ + return r === true && ta['+1'] === 'baz'; })()", + ) + .unwrap(); + assert!(result.as_bool()); +}