diff --git a/crates/oxide_builtins/src/error.rs b/crates/oxide_builtins/src/error.rs index 9cdd5b14..ee146bde 100644 --- a/crates/oxide_builtins/src/error.rs +++ b/crates/oxide_builtins/src/error.rs @@ -107,6 +107,18 @@ pub fn create_range_error(host: &mut H, msg: &str) -> JsValue { create_kind_error(host, "RangeError", msg) } +/// 从 `defineProperty` 通道的错误文本恢复异常对象。 +/// +/// 数组 length 的 `ArraySetLength` 非法值以 `"RangeError: "` 前缀标记 kind, +/// 须保留为 RangeError;其余 define 失败(非可配置收窄、不可扩展、accessor +/// 冲突等)统一投影为 TypeError。 +pub fn create_define_failure(host: &mut H, msg: &str) -> JsValue { + match msg.strip_prefix("RangeError: ") { + Some(rest) => create_range_error(host, rest), + None => create_type_error(host, msg), + } +} + /// 创建一个带指定 message 的 SyntaxError 对象。 pub fn create_syntax_error(host: &mut H, msg: &str) -> JsValue { create_kind_error(host, "SyntaxError", msg) diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 12b0004f..00a97852 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -669,18 +669,32 @@ pub(crate) fn define_from_descriptor( let set_field = own_field(vm, desc_val, set_si); let writable_field = own_field(vm, desc_val, writable_si); - let existing_pos = { + // 数组 length 是无 shape 槽的虚拟数据属性:需与普通已有属性一样参与描述符 + // 缺省回填(writable 保持当前值、value 保持当前长度),其当前描述符由元素 + // 计数与独立可写位虚拟构造;enumerable/configurable 恒为 false。 + let length_si = vm.kernel_core().perm_interner().intern("length").0; + let is_array_length = unsafe { &*obj_ptr }.is_array() && key_si == length_si; + + let existing_pos = if is_array_length { + None + } else { let obj = unsafe { &*obj_ptr }; vm.get_own_property_slot(obj, key_si) }; - let existing_meta = existing_pos.map(|pos| { - let obj = unsafe { &*obj_ptr }; - // 已有属性无显式 meta(普通数据属性/数组元素)时按默认属性回填: - // 描述符缺省字段保持现有值(writable/enumerable/configurable 均 true), - // 而非按新属性处理为 false(Object.defineProperty 省略字段不改已有属性)。 - obj.prop_meta_at(pos) - .unwrap_or_else(|| oxide_types::object::PropMetaEntry::data(PropAttributes::DEFAULT_DATA)) - }); + let existing_meta = if is_array_length { + let writable = unsafe { &*obj_ptr }.is_length_writable(); + Some(oxide_types::object::PropMetaEntry::data(PropAttributes::new(writable, false, false))) + } else { + existing_pos.map(|pos| { + let obj = unsafe { &*obj_ptr }; + // 已有属性无显式 meta(普通数据属性/数组元素)时按默认属性回填: + // 描述符缺省字段保持现有值(writable/enumerable/configurable 均 true), + // 而非按新属性处理为 false(Object.defineProperty 省略字段不改已有属性)。 + obj.prop_meta_at(pos) + .unwrap_or_else(|| oxide_types::object::PropMetaEntry::data(PropAttributes::DEFAULT_DATA)) + }) + }; + let has_existing = existing_pos.is_some() || is_array_length; // 模块命名空间 exotic [[DefineOwnProperty]](规范 10.4.6.5):Symbol 键委托 // 普通语义;导出键仅接受无变更、值不变或 writable:true,值变与 writable:false @@ -715,10 +729,14 @@ pub(crate) fn define_from_descriptor( return Err("Invalid property descriptor: cannot mix data and accessor fields".to_string()); } - let existing_value = existing_pos.map_or(JsValue::undefined(), |pos| { - let obj = unsafe { &*obj_ptr }; - obj.get_prop_at(pos) - }); + let existing_value = if is_array_length { + unsafe { &*obj_ptr }.logical_len_value() + } else { + existing_pos.map_or(JsValue::undefined(), |pos| { + let obj = unsafe { &*obj_ptr }; + obj.get_prop_at(pos) + }) + }; let obj = unsafe { &mut *obj_ptr }; if has_accessor { @@ -739,12 +757,12 @@ pub(crate) fn define_from_descriptor( } vm.define_accessor_property(obj, key_si, get, set, PropAttributes::new(false, enumerable, configurable))?; } else if has_data { - let value = if existing_pos.is_some() { + let value = if has_existing { value_field.unwrap_or(existing_value) } else { value_field.unwrap_or(JsValue::undefined()) }; - let writable = if existing_pos.is_some() { + let writable = if has_existing { writable_field .map(oxide_runtime_api::to_boolean) .unwrap_or_else(|| existing_meta.map(|m| m.attributes.writable()).unwrap_or(false)) @@ -753,7 +771,7 @@ pub(crate) fn define_from_descriptor( }; vm.define_data_property(obj, key_si, value, PropAttributes::new(writable, enumerable, configurable))?; } else { - if existing_pos.is_none() { + if !has_existing { vm.define_data_property( obj, key_si, @@ -830,7 +848,7 @@ pub fn object_define_property(vm: &mut H, args: &[u8]) -> NativeResul let si = vm.property_key_si(vm.reg(args[2])); if let Err(msg) = define_from_descriptor(vm, obj_ptr, si, vm.reg(args[3])) { - return NativeResult::Err(crate::error::create_type_error(vm, &msg)); + return NativeResult::Err(crate::error::create_define_failure(vm, &msg)); } NativeResult::Ok(obj_val) } @@ -853,8 +871,8 @@ pub fn object_get_own_property_descriptor(vm: &mut H, args: &[u8]) -> let obj = unsafe { &*obj_ptr }; // 数组 length 是虚拟属性(无 shape 槽,ordinary_get 直接返回逻辑长度): - // 描述符 {value: len, writable: !frozen, enumerable: false, configurable: false}。 - // 冻结数组(含模板对象)writable=false;普通数组 writable=true。 + // 描述符 {value: len, writable: !frozen && 非显式收窄, enumerable: false, + // configurable: false}。冻结数组与经 defineProperty 收窄的数组 writable=false。 let length_si = vm.kernel_core().perm_interner().intern("length").0; if obj.is_array() && key == length_si { let desc_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject; @@ -865,7 +883,7 @@ pub fn object_get_own_property_descriptor(vm: &mut H, args: &[u8]) -> let sh = unsafe { &*sh }; let sf = unsafe { &*sf }; push_desc_prop(d, sh, sf.intern("value").0, obj.logical_len_value()); - push_desc_prop(d, sh, sf.intern("writable").0, JsValue::bool(!obj.is_frozen())); + push_desc_prop(d, sh, sf.intern("writable").0, JsValue::bool(obj.is_length_writable())); push_desc_prop(d, sh, sf.intern("enumerable").0, JsValue::bool(false)); push_desc_prop(d, sh, sf.intern("configurable").0, JsValue::bool(false)); return NativeResult::Ok(JsValue::from_js_object(desc)); diff --git a/crates/oxide_builtins/src/reflect.rs b/crates/oxide_builtins/src/reflect.rs index e370a7fa..5f006d1c 100644 --- a/crates/oxide_builtins/src/reflect.rs +++ b/crates/oxide_builtins/src/reflect.rs @@ -73,7 +73,9 @@ pub fn reflect_construct(vm: &mut H, args: &[u8]) -> NativeResult { /// /// # 注意事项 /// - 与 `Object.defineProperty` 共用 `define_from_descriptor`:缺失字段回填现有 -/// 属性、模块命名空间 exotic 收窄语义一处生效;错误投影为布尔 false。 +/// 属性、模块命名空间 exotic 收窄语义一处生效;普通失败投影为布尔 false。 +/// - 数组 length 的非法值(`"RangeError: "` 前缀)按规范抛 RangeError,不投影为 +/// false。 pub fn reflect_define_property(vm: &mut H, args: &[u8]) -> NativeResult { let target_val = arg(vm, args, 1); let Some(target_ptr) = object_ptr(target_val) else { @@ -85,8 +87,13 @@ pub fn reflect_define_property(vm: &mut H, args: &[u8]) -> NativeResu }; let key_si = vm.property_key_si(arg(vm, args, 2)); - let result = crate::object::define_from_descriptor(vm, target_ptr, key_si, desc_val); - NativeResult::Ok(JsValue::bool(result.is_ok())) + match crate::object::define_from_descriptor(vm, target_ptr, key_si, desc_val) { + Ok(()) => NativeResult::Ok(JsValue::bool(true)), + Err(msg) => match msg.strip_prefix("RangeError: ") { + Some(rest) => NativeResult::Err(crate::error::create_range_error(vm, rest)), + None => NativeResult::Ok(JsValue::bool(false)), + }, + } } /// `Reflect.deleteProperty(target, key)`:删除自身属性; diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 2ed146bd..890ac412 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -65,6 +65,7 @@ pub const MAX_DENSE_PROPS: usize = 1_000_000; /// \[0\] is_frozen /// \[1\] is_sealed /// \[2\] is_module_namespace +/// \[3\] is_length_non_writable(数组 length 虚拟属性的数据属性 writable=false) /// array_elements: *mut u8 (8 字节,数组对象元素区 Box\\>) /// array_elements_meta: *mut u8 (8 字节,数组元素元数据 Box\\>\>) /// hash_props: *mut u8 (8 字节,命名属性 Box\\>) @@ -722,6 +723,27 @@ impl JsObject { } } + /// 数组 length 虚拟属性是否可写。 + /// + /// # 边界与前提 + /// - 仅对数组对象有语义;冻结数组(`Object.freeze`)恒返回 `false`,不依赖独立位。 + /// - 独立位只记录 `defineProperty` 显式设置 `writable:false` 的收窄。 + pub fn is_length_writable(&self) -> bool { + !self.is_frozen() && (self._pad >> 3) & 1 == 0 + } + + /// 设置数组 length 虚拟属性的不可写标志。 + /// + /// # 副作用 + /// - 写 `_pad` bit3;`Clone`/晋升复制 `_pad`,标志随之保留。 + pub fn set_length_non_writable(&mut self, non_writable: bool) { + if non_writable { + self._pad |= 1 << 3; + } else { + self._pad &= !(1 << 3); + } + } + /// 是否 Set 实例(header bit 24)。 pub fn is_set(&self) -> bool { (self.header >> 24) & 1 != 0 diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index e5c1791f..8d2add8b 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -364,6 +364,13 @@ impl Vm { if obj.is_frozen() { return self.raise_type_error("Cannot assign to read only property 'length'"); } + // defineProperty 显式收窄的不可写 length:严格模式抛 TypeError,sloppy 静默。 + if !obj.is_length_writable() { + if strict { + return self.raise_type_error("Cannot assign to read only property 'length'"); + } + return Ok(()); + } let pc_before = self.pc; let number_len = self.coerce_number_bounded(val)?; // ToPrimitive 抛错(valueOf/toString throw)已被 unwind 定向到外围 catch 时 @@ -844,6 +851,11 @@ impl Vm { ); } } + // 数组 length 是无 shape 槽的虚拟数据属性:按 ArraySetLength 语义应用, + // 不得走通用命名属性路径建影子槽(会造成读写分叉)。 + if obj.is_array() && prop_name_si == self.length_si { + return self.define_array_length(obj, val, attributes); + } // 新命名属性(shape 链 lookup miss)且对象不可扩展 → 拒绝定义 // (Object.defineProperty 抛 TypeError;Reflect.defineProperty 自动转 false)。 if self @@ -924,6 +936,10 @@ impl Vm { return self.define_array_index_element(obj, index, JsValue::undefined(), attributes, true, get, set); } } + // 数组 length 当前为不可配置数据属性,禁止转为访问器属性。 + if obj.is_array() && prop_name_si == self.length_si { + return Err("cannot redefine non-configurable property".to_string()); + } // 新命名属性(shape 链 lookup miss)且对象不可扩展 → 拒绝定义。 if self .kernel_core @@ -977,6 +993,11 @@ impl Vm { if pos > oxide_types::object::MAX_DENSE_PROPS { return Err("array index out of dense range".to_string()); } + // 数组 exotic [[DefineOwnProperty]]:索引达到/超过当前 length 时须增长 length, + // length 不可写则拒绝;索引小于 length 的元素重定义不受此限。 + if pos as u32 >= obj.logical_len() && !obj.is_length_writable() { + return Err("cannot define property beyond non-writable length".to_string()); + } // 新元素(越界或 hole 空洞)要求对象可扩展;已有元素重定义不受限(走下方 // non-configurable 校验)。 let is_new = pos >= obj.array_prop_count as usize || obj.prop_meta_at(pos).is_some_and(|m| m.is_hole()); @@ -1015,6 +1036,75 @@ impl Vm { obj.bump_generation(); Ok(()) } + + /// 数组 length 虚拟属性的 define 路径:按 `ArraySetLength` 语义把描述符值 + /// 强转、校验并应用到元素区与逻辑长度。 + /// + /// # 步骤 + /// 1. `ToUint32` 与 `ToNumber` 两次强转(均可触发用户代码),结果不等抛 + /// RangeError;强转严格早于任何描述符校验。 + /// 2. 校验当前非可配置数据属性的收窄:configurable/enumerable 不得置真; + /// 当前不可写时不得再请求 writable 或改动值。 + /// 3. 收缩时先检查 `[newLen, oldLen)` 内是否存在不可配置元素,存在则整体失败 + /// 且不改动元素区。 + /// 4. 应用新逻辑长度(截断/扩 hole、dense 上限覆盖)并按描述符写可写位。 + /// + /// # 边界与前提 + /// - 仅由 `define_data_property` 对 `is_array()` 对象且键为 `length_si` 时调用。 + /// - 描述符缺 `value` 时调用方以当前逻辑长度为哨兵值,数值强转为恒等无副作用 + /// 转换。 + /// + /// # 副作用 + /// - 截断/扩展元素区与元素元数据、可能置/清 `array_len_override`、写 length + /// 可写位、bump 世代。 + /// + /// # 注意事项 + /// - 非法长度以 `"RangeError: "` 前缀标记 kind,由 Object/Reflect 入口各自投影; + /// 其余失败投影为 TypeError。 + fn define_array_length( + &mut self, obj: &mut JsObject, val: JsValue, attributes: PropAttributes, + ) -> Result<(), String> { + // [[Value]] 两次强转都要实际执行(可触发用户代码),且须早于描述符校验; + // 两次强转之间用户代码可能把 length 收窄为不可写。 + let new_len = self.coerce_uint32_bounded(val)?; + let number_len = self.coerce_number_bounded(val)?; + if new_len as f64 != number_len { + return Err("RangeError: Invalid array length".to_string()); + } + + let old_logical = obj.logical_len(); + // 当前 length 是不可配置、不可枚举的数据属性;writable 由冻结标志与独立位决定。 + if attributes.configurable() || attributes.enumerable() { + return Err("cannot redefine non-configurable property".to_string()); + } + if !obj.is_length_writable() && (attributes.writable() || new_len != old_logical) { + return Err("cannot redefine non-configurable property".to_string()); + } + // 收缩先验证不可配置元素:存在则整体失败,元素区保持原状(错误先于变更)。 + if new_len < old_logical { + let old_count = obj.array_prop_count as usize; + for idx in new_len as usize..old_count { + if obj.prop_meta_at(idx).is_some_and(|m| !m.attributes.configurable()) { + return Err("cannot redefine non-configurable property".to_string()); + } + } + } + // dense 物理槽以 MAX_DENSE_PROPS 封顶,超限长度另记逻辑长度覆盖。 + let old_count = obj.array_prop_count as usize; + let new_phys = (new_len as usize).min(oxide_types::object::MAX_DENSE_PROPS); + obj.set_prop_count(new_phys); + for idx in old_count..new_phys { + obj.mark_hole_at(idx); + } + if new_len as usize > oxide_types::object::MAX_DENSE_PROPS { + obj.set_array_len_override(new_len); + } else { + obj.clear_array_len_override(); + } + obj.set_length_non_writable(!attributes.writable()); + obj.bump_generation(); + Ok(()) + } } #[cfg(test)] diff --git a/crates/oxide_vm/tests/array_length_define_tests.rs b/crates/oxide_vm/tests/array_length_define_tests.rs new file mode 100644 index 00000000..06b99d0d --- /dev/null +++ b/crates/oxide_vm/tests/array_length_define_tests.rs @@ -0,0 +1,172 @@ +//! 数组 length 虚拟属性的 define 侧回归:`Object.defineProperty` / `Reflect.defineProperty` +//! 的截断、增长、可写位收窄、非法长度 RangeError 与访问器拒绝。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_vm::vm::Vm; + +/// 求值并取字符串结果内容(字符串值走 lookup_str,非字符串走 Display)。 +fn eval_str(source: &str) -> String { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse ok"); + let module = Compiler::new().compile(&program).expect("compile ok"); + let mut vm = Vm::new(); + let val = vm.run(&Arc::new(module)).expect("run ok"); + if val.is_string() { + vm.lookup_str(val).unwrap_or_default() + } else { + format!("{val}") + } +} + +#[test] +fn define_length_value_truncates_elements() { + // value 收缩:元素区被截断,越界元素不可见。 + assert_eq!( + eval_str("var a=[1,2,3]; Object.defineProperty(a,'length',{value:1}); ''+a.length+':'+a[2]+':'+(2 in a)"), + "1:undefined:false" + ); + // value 增长:新增槽为稀疏空洞。 + assert_eq!( + eval_str("var a=[1]; Object.defineProperty(a,'length',{value:3}); ''+a.length+':'+(1 in a)+':'+(2 in a)"), + "3:false:false" + ); +} + +#[test] +fn define_length_value_writable_defaults_to_current() { + // 只给 value:writable 缺省保持当前值 true。 + assert_eq!( + eval_str("var a=[1,2]; Object.defineProperty(a,'length',{value:1}); ''+Object.getOwnPropertyDescriptor(a,'length').writable"), + "true" + ); +} + +#[test] +fn define_length_non_writable_blocks_sloppy_assignment() { + assert_eq!( + eval_str("var a=[1,2,3]; Object.defineProperty(a,'length',{writable:false}); (function(){a.length=1;})(); ''+a.length"), + "3" + ); +} + +#[test] +fn define_length_non_writable_throws_strict_assignment() { + assert_eq!( + eval_str("var a=[1,2,3]; Object.defineProperty(a,'length',{writable:false}); (function(){'use strict'; try{a.length=1;return 'no-throw';}catch(e){return e.name+':'+a.length;}})()"), + "TypeError:3" + ); +} + +#[test] +fn define_length_non_writable_same_value_succeeds() { + assert_eq!( + eval_str("var a=[1,2]; Object.defineProperty(a,'length',{writable:false}); Object.defineProperty(a,'length',{value:2}); ''+a.length"), + "2" + ); +} + +#[test] +fn define_length_invalid_values_throw_range_error() { + let src = "var r=[]; [-1,NaN,4294967296,1.5].forEach(function(v){try{Object.defineProperty([],'length',{value:v});r.push('no-throw');}catch(e){r.push(e.name);}}); r.join(',')"; + assert_eq!(eval_str(src), "RangeError,RangeError,RangeError,RangeError"); + // 非法值检查早于 configurable 收窄校验。 + assert_eq!( + eval_str("try{Object.defineProperty([],'length',{value:-1,configurable:true});'no-throw';}catch(e){e.name}"), + "RangeError" + ); +} + +#[test] +fn define_length_descriptor_validation_throws_type_error() { + // configurable/enumerable 不得放宽。 + assert_eq!( + eval_str("try{Object.defineProperty([],'length',{configurable:true});'no-throw';}catch(e){e.name}"), + "TypeError" + ); + // 非可写后不得再请求 writable。 + assert_eq!( + eval_str("var a=[1]; Object.defineProperty(a,'length',{writable:false}); try{Object.defineProperty(a,'length',{writable:true});'no-throw';}catch(e){e.name}"), + "TypeError" + ); +} + +#[test] +fn define_length_accessor_descriptor_rejected() { + assert_eq!( + eval_str("var r=[]; try{Object.defineProperty([],'length',{get:function(){return 1;}});r.push('no-throw');}catch(e){r.push(e.name);} r.push(Reflect.defineProperty([],'length',{set:function(v){}})); r.join(',')"), + "TypeError,false" + ); +} + +#[test] +fn define_length_shrink_blocked_by_non_configurable_element() { + assert_eq!( + eval_str("var a=[1,2,3]; Object.defineProperty(a,'2',{configurable:false}); var r; try{Object.defineProperty(a,'length',{value:1});r='no-throw';}catch(e){r=e.name;} r+':'+a.length"), + "TypeError:3" + ); +} + +#[test] +fn define_length_configurable_true_error_leaves_elements() { + // TypeError 后元素区不截断。 + assert_eq!( + eval_str("var a=[1,2,3]; try{Object.defineProperty(a,'length',{value:1,configurable:true});}catch(e){} ''+a.length+':'+a[2]"), + "3:3" + ); +} + +#[test] +fn define_length_coerces_value_twice_before_validation() { + // ToUint32 + ToNumber 各触发一次 valueOf;第二次强转的副作用收窄可写位后 + // 由描述符校验拒绝。 + assert_eq!( + eval_str("var a=[1,2], n=0; var v={valueOf:function(){n++;return a.length;}}; Object.defineProperty(a,'length',{value:v}); ''+a.length+':'+n"), + "2:2" + ); + assert_eq!( + eval_str("var a=[1,2,3], n=0; var v={valueOf:function(){n++;Object.defineProperty(a,'length',{writable:false});return a.length;}}; var r; try{Object.defineProperty(a,'length',{value:v,writable:true});r='no-throw';}catch(e){r=e.name;} r+':'+n"), + "TypeError:2" + ); +} + +#[test] +fn reflect_define_length_error_channel() { + // 非法长度保留 RangeError kind(抛而非 false);普通失败投影 false。 + assert_eq!( + eval_str("var r=[]; try{Reflect.defineProperty([],'length',{value:-1});r.push('no-throw');}catch(e){r.push(e.name);} r.push(Reflect.defineProperty([],'length',{configurable:true})); r.join(',')"), + "RangeError,false" + ); +} + +#[test] +fn non_array_length_stays_ordinary_property() { + assert_eq!( + eval_str("var o={}; Object.defineProperty(o,'length',{value:1}); ''+o.length+':'+Object.getOwnPropertyDescriptor(o,'length').writable"), + "1:false" + ); +} + +#[test] +fn define_length_non_writable_reported_by_descriptor() { + assert_eq!( + eval_str("var a=[1]; Object.defineProperty(a,'length',{writable:false}); ''+Object.getOwnPropertyDescriptor(a,'length').writable+':'+Object.getOwnPropertyDescriptor(a,'length').value"), + "false:1" + ); +} + +#[test] +fn define_index_at_length_requires_writable_length() { + // 索引达到当前 length 需增长 length,length 不可写则拒绝。 + assert_eq!( + eval_str("var a=[1,2,3]; Object.defineProperty(a,'length',{writable:false}); var r; try{Object.defineProperty(a,3,{value:'x'});r='no-throw';}catch(e){r=e.name;} r+':'+a.length"), + "TypeError:3" + ); + // 索引小于 length 的已有元素重定义不受 length 可写性限制。 + assert_eq!( + eval_str("var a=[1,2,3]; Object.defineProperty(a,'length',{writable:false}); Object.defineProperty(a,1,{value:9}); ''+a[1]"), + "9" + ); +}