fix: 良形 FFFD 属性键收口到编码键空间形态

This commit is contained in:
2026-09-14 10:55:53 +00:00
parent e6fb1196f5
commit 75b98e7f78
4 changed files with 55 additions and 15 deletions
+3 -3
View File
@@ -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<u16> {
let mut out: Vec<u16> = Vec::with_capacity(text.len());
let mut chars = text.chars().peekable();
+3 -1
View File
@@ -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<u32, String>;
/// 字符串→键规范化:规范数字串(`"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<JsValue>;
+22 -11
View File
@@ -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<u16> = 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
@@ -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());
}