From f31d45dc68d91657418216bd0248aa801ad553b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=89=B9=E6=AF=8F=E7=BE=8A=E9=98=9F?= Date: Sat, 27 Jun 2026 15:12:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20JsObject=20=E6=96=B0=E5=A2=9E=20frozen/?= =?UTF-8?q?sealed=20=E6=A0=87=E5=BF=97=E4=BD=8D=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20freeze/seal/preventExtensions/isFrozen/isSealed/isExtensible?= =?UTF-8?q?=20=E5=85=AD=E4=B8=AA=E5=AE=8C=E6=95=B4=E6=80=A7=E6=96=B9?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/object.rs | 92 ++++++++++++++++--- .../tests/object_boundary_tests.rs | 44 +++++++++ .../tests/object_integrity_tests.rs | 59 ++++++++++++ crates/oxide_types/src/object.rs | 24 +++++ 4 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 crates/oxide_builtins/tests/object_integrity_tests.rs diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 005f07c..1dbd891 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -339,62 +339,132 @@ macro_rules! native_try { pub fn object_freeze(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { - return NativeResult::Ok(JsValue::undefined()); + return NativeResult::Err(crate::error::create_type_error(vm, "Object.freeze called on non-object")); } let val = vm.reg(args[1]); if !val.is_object() || val.as_js_object_ptr().is_null() { return NativeResult::Ok(val); } + let obj_ptr = val.as_js_object_ptr(); + unsafe { + (*obj_ptr).set_frozen(true); + (*obj_ptr).set_extensible(false); + } NativeResult::Ok(val) } pub fn object_seal(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { - return NativeResult::Ok(JsValue::undefined()); + return NativeResult::Err(crate::error::create_type_error(vm, "Object.seal called on non-object")); } let val = vm.reg(args[1]); if !val.is_object() || val.as_js_object_ptr().is_null() { return NativeResult::Ok(val); } + let obj_ptr = val.as_js_object_ptr(); + unsafe { + (*obj_ptr).set_sealed(true); + (*obj_ptr).set_extensible(false); + } NativeResult::Ok(val) } pub fn object_prevent_extensions(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { - return NativeResult::Ok(JsValue::undefined()); + return NativeResult::Err(crate::error::create_type_error(vm, "Object.preventExtensions called on non-object")); } let val = vm.reg(args[1]); if !val.is_object() || val.as_js_object_ptr().is_null() { return NativeResult::Ok(val); } + let obj_ptr = val.as_js_object_ptr(); + unsafe { + (*obj_ptr).set_extensible(false); + } NativeResult::Ok(val) } pub fn object_is_frozen(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { - return NativeResult::Ok(JsValue::bool(true)); + return NativeResult::Err(crate::error::create_type_error(vm, "Object.isFrozen called on non-object")); } let val = vm.reg(args[1]); - if !val.is_object() || val.as_js_object_ptr().is_null() { + if !val.is_object() { return NativeResult::Ok(JsValue::bool(true)); } - NativeResult::Ok(JsValue::bool(false)) + let obj_ptr = val.as_js_object_ptr(); + if obj_ptr.is_null() { + return NativeResult::Ok(JsValue::bool(true)); + } + let obj = unsafe { &*obj_ptr }; + if obj.is_frozen() { + return NativeResult::Ok(JsValue::bool(true)); + } + if obj.is_extensible() { + return NativeResult::Ok(JsValue::bool(false)); + } + let keys = walk_own_keys(vm, obj); + for (_si, offset) in keys { + if let Some(meta) = obj.prop_meta_at(offset) { + if meta.attributes.configurable() { + return NativeResult::Ok(JsValue::bool(false)); + } + if !meta.is_accessor && meta.attributes.writable() { + return NativeResult::Ok(JsValue::bool(false)); + } + } else { + return NativeResult::Ok(JsValue::bool(false)); + } + } + NativeResult::Ok(JsValue::bool(true)) } pub fn object_is_sealed(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 2 { - return NativeResult::Ok(JsValue::bool(true)); + return NativeResult::Err(crate::error::create_type_error(vm, "Object.isSealed called on non-object")); } let val = vm.reg(args[1]); - if !val.is_object() || val.as_js_object_ptr().is_null() { + if !val.is_object() { return NativeResult::Ok(JsValue::bool(true)); } - NativeResult::Ok(JsValue::bool(false)) + let obj_ptr = val.as_js_object_ptr(); + if obj_ptr.is_null() { + return NativeResult::Ok(JsValue::bool(true)); + } + let obj = unsafe { &*obj_ptr }; + if obj.is_sealed() { + return NativeResult::Ok(JsValue::bool(true)); + } + if obj.is_extensible() { + return NativeResult::Ok(JsValue::bool(false)); + } + let keys = walk_own_keys(vm, obj); + for (_si, offset) in keys { + if let Some(meta) = obj.prop_meta_at(offset) { + if meta.attributes.configurable() { + return NativeResult::Ok(JsValue::bool(false)); + } + } else { + return NativeResult::Ok(JsValue::bool(false)); + } + } + NativeResult::Ok(JsValue::bool(true)) } pub fn object_is_extensible(vm: &mut H, args: &[u8]) -> NativeResult { - native_try!(require_obj_arg(vm, args, "isExtensible")); - NativeResult::Ok(JsValue::bool(true)) + if args.len() < 2 { + return NativeResult::Err(crate::error::create_type_error(vm, "Object.isExtensible called on non-object")); + } + let val = vm.reg(args[1]); + if !val.is_object() { + return NativeResult::Ok(JsValue::bool(false)); + } + let obj_ptr = val.as_js_object_ptr(); + if obj_ptr.is_null() { + return NativeResult::Ok(JsValue::bool(false)); + } + let obj = unsafe { &*obj_ptr }; + NativeResult::Ok(JsValue::bool(obj.is_extensible())) } pub fn object_get_own_property_names(vm: &mut H, args: &[u8]) -> NativeResult { diff --git a/crates/oxide_builtins/tests/object_boundary_tests.rs b/crates/oxide_builtins/tests/object_boundary_tests.rs index 4bd14df..06dc718 100644 --- a/crates/oxide_builtins/tests/object_boundary_tests.rs +++ b/crates/oxide_builtins/tests/object_boundary_tests.rs @@ -99,3 +99,47 @@ fn test_assign_merges_two_sources() { let (_vm, result) = eval("var t = {a:1}; Object.assign(t, {b:2}); t.b").unwrap(); assert!(result.is_int() || result.is_double(), "Object.assign should copy from source"); } + +#[test] +fn test_integrity_freeze_returns_object() { + let (_vm, result) = eval("var o = {x:1}; Object.freeze(o) === o").unwrap(); + assert!(result.is_bool() && result.as_bool(), "freeze should return the object"); +} + +#[test] +fn test_integrity_freeze_sets_non_extensible() { + let (_vm, result) = eval("var o = {x:1}; Object.freeze(o); Object.isExtensible(o)").unwrap(); + assert!(result.is_bool() && !result.as_bool(), "freeze should set non-extensible"); +} + +#[test] +fn test_integrity_seal_non_extensible() { + let (_vm, result) = eval("var o = {x:1}; Object.seal(o); Object.isExtensible(o)").unwrap(); + assert!(result.is_bool() && !result.as_bool(), "seal should set non-extensible"); +} + +#[test] +fn test_integrity_prevent_extensions() { + let (_vm, result) = eval("var o = {x:1}; Object.preventExtensions(o); Object.isExtensible(o)").unwrap(); + assert!(result.is_bool() && !result.as_bool(), "preventExtensions should set non-extensible"); +} + +#[test] +fn test_is_frozen_non_object_returns_true() { + let (_vm, result) = eval("Object.isFrozen(42)").unwrap(); + assert!(result.is_bool() && result.as_bool(), "non-object should be frozen per ES spec"); +} + +#[test] +fn test_is_sealed_non_object_returns_true() { + let (_vm, result) = eval("Object.isSealed(42)").unwrap(); + assert!(result.is_bool() && result.as_bool(), "non-object should be sealed per ES spec"); +} + +#[test] +fn test_is_extensible_non_object_returns_false() { + let (_vm, result) = eval("Object.isExtensible(42)").unwrap(); + assert!(result.is_bool() && !result.as_bool(), "non-object should not be extensible per ES spec"); +} + + diff --git a/crates/oxide_builtins/tests/object_integrity_tests.rs b/crates/oxide_builtins/tests/object_integrity_tests.rs new file mode 100644 index 0000000..ce1b08b --- /dev/null +++ b/crates/oxide_builtins/tests/object_integrity_tests.rs @@ -0,0 +1,59 @@ +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +fn new_obj() -> JsObject { + JsObject::new_empty(0, JsValue::null()) +} + +#[test] +fn test_set_frozen() { + let mut obj = new_obj(); + assert!(!obj.is_frozen()); + obj.set_frozen(true); + assert!(obj.is_frozen()); +} + +#[test] +fn test_set_sealed() { + let mut obj = new_obj(); + assert!(!obj.is_sealed()); + obj.set_sealed(true); + assert!(obj.is_sealed()); +} + +#[test] +fn test_frozen_and_sealed_independent() { + let mut obj = new_obj(); + obj.set_frozen(true); + assert!(obj.is_frozen()); + assert!(!obj.is_sealed()); + obj.set_sealed(true); + assert!(obj.is_frozen()); + assert!(obj.is_sealed()); + obj.set_frozen(false); + assert!(!obj.is_frozen()); + assert!(obj.is_sealed()); +} + +#[test] +fn test_defaults_are_false() { + let obj = new_obj(); + assert!(!obj.is_frozen()); + assert!(!obj.is_sealed()); +} + +#[test] +fn test_clear_frozen() { + let mut obj = new_obj(); + obj.set_frozen(true); + obj.set_frozen(false); + assert!(!obj.is_frozen()); +} + +#[test] +fn test_clear_sealed() { + let mut obj = new_obj(); + obj.set_sealed(true); + obj.set_sealed(false); + assert!(!obj.is_sealed()); +} diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 7220869..e842ce8 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -574,6 +574,30 @@ impl JsObject { } } + pub fn is_frozen(&self) -> bool { + (self._pad) & 1 != 0 + } + + pub fn set_frozen(&mut self, frozen: bool) { + if frozen { + self._pad |= 1 << 0; + } else { + self._pad &= !(1 << 0); + } + } + + pub fn is_sealed(&self) -> bool { + (self._pad >> 1) & 1 != 0 + } + + pub fn set_sealed(&mut self, sealed: bool) { + if sealed { + self._pad |= 1 << 1; + } else { + self._pad &= !(1 << 1); + } + } + pub fn is_set(&self) -> bool { (self.header >> 24) & 1 != 0 }