diff --git a/crates/oxide_kernel/src/string_forge.rs b/crates/oxide_kernel/src/string_forge.rs index e5df2a1a..ac747348 100644 --- a/crates/oxide_kernel/src/string_forge.rs +++ b/crates/oxide_kernel/src/string_forge.rs @@ -59,9 +59,9 @@ pub fn encode_key(units: &[u16]) -> String { /// 文本是防御性兜底(`encode_key` 不产生该形态):输出 FFFD 单元并把后续 /// 已消费字符原样补回。 /// -/// 已知边界:用户来源的键文本若含裸 FFFD 且其后恰为 `fffd` 或 surrogate -/// 段 4 位十六进制,物化时该 4 字符会被当作转义吞掉。物化面仅限 builtin -/// 永久键(恒为良形恒等文本),故该歧义不实际发生。 +/// 已知边界:键空间只收 `encode_key` 形态(无 FFFD 的良形文本即恒等形态), +/// 含 FFFD 的用户键同样经编码形态入键空间——裸 FFFD 文本不是键空间的合法 +/// 输入,"FFFD 后 4 字符被吞"在物化面不实际发生;防御性兜底仅覆盖越约文本。 pub fn decode_key(text: &str) -> Vec { let mut out: Vec = Vec::with_capacity(text.len()); let mut chars = text.chars().peekable(); diff --git a/crates/oxide_runtime_api/src/lib.rs b/crates/oxide_runtime_api/src/lib.rs index 3b747c33..3d0e58c9 100644 --- a/crates/oxide_runtime_api/src/lib.rs +++ b/crates/oxide_runtime_api/src/lib.rs @@ -153,7 +153,9 @@ pub trait VmHost { /// hint),转换异常以 `Err` 返回(调用方须取 `take_uncaught_value` 传播原异常)。 /// 与 `property_key_si` 的区别:后者对转换失败退化为空键,不传播异常。 fn to_property_key_si(&mut self, val: JsValue) -> Result; - /// 字符串→键规范化:规范数字串(`"5"`)映射整数键,其余 intern 字符串键。 + /// 字符串→键规范化:规范数字串(`"5"`)映射整数键,其余文本以 `encode_key` + /// 形态入键空间(无 FFFD 的良形文本恒等,含 FFFD 与运行时单元路径同形态, + /// 见 `string_key_units`)。 /// 供建键入口(fromEntries/json/rest excluded)与 `property_key_si` 的字符串分支统一口径。 fn string_key_si(&mut self, s: &str) -> u32; fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option; diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 93e70d3a..3df06421 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -1394,12 +1394,8 @@ impl Vm { // SAFETY: val 是字符串值,按载荷形态桥接为永久 key id。 let s = unsafe { &*val.as_string_ptr() }; if s.is_flat() { - // Flat:良形 UTF-8 文本,键推导与旧路径逐位一致。 - let text = s.as_str(); - if let Some(i) = canonical_index_of(text) { - return Ok(make_int_key(i)); - } - return Ok(self.kernel_core.perm_interner().intern(text).0); + // Flat:良形 UTF-8 文本,走与单元路径同口径的编码入键空间。 + return Ok(self.string_key_text(s.as_str())); } // 单元载荷:键推导同规范(见 string_key_units)。 return Ok(self.string_key_units(&s.units())); @@ -1436,6 +1432,23 @@ impl Vm { Ok(si) } + /// 从良形文本串推导属性键 si(`property_key_si` Flat 分支与 `string_key_si` + /// 条目的共享入口):规范数组下标 → 整数键,其余以 `encode_key` 形态入键空间 + /// ——无 FFFD 的良形文本编码恒等,含 FFFD 的键与单元路径(Cons 载荷、 + /// `string_key_si`)收敛到同一编码形态,同一逻辑键不因构造路径分裂。 + pub(crate) fn string_key_text(&self, text: &str) -> u32 { + if let Some(i) = canonical_index_of(text) { + return make_int_key(i); + } + if text.contains('\u{FFFD}') { + let units: Vec = text.encode_utf16().collect(); + let key = oxide_kernel::string_forge::encode_key(&units); + self.kernel_core.perm_interner().intern(&key).0 + } else { + self.kernel_core.perm_interner().intern(text).0 + } + } + /// 从单元序列推导属性键 si(`property_key_si` 字符串分支的口径抽取): /// 规范数组下标 → 整数键,其余以 `encode_key` 形态入键空间(孤立 surrogate /// 以转义形态区分,不与良形键碰撞)。 @@ -2584,11 +2597,9 @@ impl oxide_runtime_api::VmHost for Vm { self.property_key_si(val) } fn string_key_si(&mut self, s: &str) -> u32 { - if let Some(i) = canonical_index_of(s) { - make_int_key(i) - } else { - self.kernel_core.perm_interner().intern(s).0 - } + // 与运行时字符串键同口径:含 FFFD 的键文本按 encode_key 形态入键空间 + // (FFFD 键的 JSON.parse 键与字面量/拼接构造的运行时键身份一致)。 + self.string_key_text(s) } fn string_units(&self, val: JsValue) -> std::borrow::Cow<'_, [u16]> { // SAFETY: 调用方保证 val 为字符串值。perm 串由内核持有永不释放;session diff --git a/crates/oxide_vm/tests/builtins_string_tests.rs b/crates/oxide_vm/tests/builtins_string_tests.rs index fdd9fd71..5d539700 100644 --- a/crates/oxide_vm/tests/builtins_string_tests.rs +++ b/crates/oxide_vm/tests/builtins_string_tests.rs @@ -1347,3 +1347,30 @@ fn utf16_normalize_astral_identity() { .unwrap(); assert!(r.as_bool()); } + +#[test] +fn utf16_fffd_key_identity_flat_vs_concat() { + // FFFD 键收口:单字面量(Flat 载荷)与拼接(Cons 载荷,>128 单元)同一逻辑 + // 键在键空间同编码形态——仅一个属性;物化键值与原始串值相等。 + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "(function(){ const k2 = \"\\uFFFDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"; if (k2.length !== 131) { return false; } const o = {}; o[\"\\uFFFD\" + \"x\".repeat(130)] = 1; o[k2] = 2; const ks = Object.keys(o); return ks.length === 1 && ks[0] === k2 && o[k2] === 2; })()", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn utf16_fffd_hex4_key_materialization() { + // FFFD+hex4 键物化恒等:键 "\uFFFDd800"(5 单元)经编码形态入键空间, + // Object.keys 出 5 单元真值("d800"/"fffd" 4 字符不被当转义吞掉), + // JSON.stringify/parse 键面复原同值。 + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "(function(){ const o = {}; o[\"\\uFFFDd800\"] = 1; o[\"\\uFFFDfffd\"] = 2; const ks = Object.keys(o); const rks = Object.keys(JSON.parse(JSON.stringify(o))); return ks[0].length === 5 && ks[1].length === 5 && rks[0] === ks[0] && rks[1] === ks[1]; })()", + ) + .unwrap(); + assert!(r.as_bool()); +}