mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 02:31:13 +08:00
fix: 模块命名空间暴露符号标签自身键
This commit is contained in:
@@ -8,8 +8,12 @@
|
||||
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
|
||||
use oxide_runtime_api::{NativeResult, VmHost};
|
||||
use oxide_types::object::{JsObject, PropAttributes};
|
||||
use oxide_types::private_key::make_well_known_symbol_key;
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
/// `Symbol.toStringTag` 在 well-known symbol 表中的序号,命名空间标签键按其编码。
|
||||
const TO_STRING_TAG_SYMBOL_ID: u32 = 9;
|
||||
|
||||
/// 模块命名空间导出属性描述符:不可写、可枚举、不可配置(module namespace exotic)。
|
||||
fn ns_attrs() -> PropAttributes {
|
||||
PropAttributes::new(false, true, false)
|
||||
@@ -19,10 +23,10 @@ fn type_error<H: VmHost>(vm: &mut H, msg: &str) -> NativeResult {
|
||||
NativeResult::Err(crate::error::create_type_error(vm, msg))
|
||||
}
|
||||
|
||||
/// `__moduleObject()`:创建模块命名空间对象(null 原型,带 @@toStringTag)。
|
||||
/// `__moduleObject()`:创建模块命名空间对象(null 原型,带 @@toStringTag Symbol 键)。
|
||||
pub fn module_object<H: VmHost>(vm: &mut H, _args: &[u8]) -> NativeResult {
|
||||
let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()));
|
||||
let tag_si = vm.kernel_core().perm_interner().intern("@@toStringTag").0;
|
||||
let tag_si = make_well_known_symbol_key(TO_STRING_TAG_SYMBOL_ID);
|
||||
let tag_val = vm.new_string("Module");
|
||||
let obj_ref = unsafe { &mut *obj };
|
||||
if let Err(e) = vm.define_data_property(obj_ref, tag_si, tag_val, PropAttributes::new(false, false, false)) {
|
||||
@@ -49,6 +53,8 @@ pub fn module_set<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
if let Err(e) = vm.define_data_property(obj, name_si, value, ns_attrs()) {
|
||||
return NativeResult::Err(crate::error::create_error(vm, &e));
|
||||
}
|
||||
// 每次导出写入后重排:自导入在 body 内即可见有序的 [[OwnPropertyKeys]]。
|
||||
crate::object::sort_namespace_exports(vm, obj);
|
||||
NativeResult::Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
@@ -136,10 +142,12 @@ pub fn module_star<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
return NativeResult::Err(crate::error::create_error(vm, &e));
|
||||
}
|
||||
}
|
||||
// star 复制后重排:直接导出与 star 导出的合并键序保持规范顺序。
|
||||
crate::object::sort_namespace_exports(vm, dst);
|
||||
NativeResult::Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
/// `__moduleSeal(ns)`:封冻命名空间(不可扩展;toStringTag 留待后续)。
|
||||
/// `__moduleSeal(ns)`:封冻命名空间(不可扩展)。
|
||||
pub fn module_seal<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
if args.len() < 2 {
|
||||
return type_error(vm, "__moduleSeal: 1 argument required");
|
||||
|
||||
@@ -165,6 +165,80 @@ fn decode_symbol_key<H: VmHost>(vm: &H, key: u32) -> JsValue {
|
||||
JsValue::symbol(symbol_index_from_key(key))
|
||||
}
|
||||
|
||||
/// 收集对象自身全部 Symbol 键并按插入序物化为 Symbol 值。
|
||||
///
|
||||
/// 供 `Object.getOwnPropertySymbols` 与 `Reflect.ownKeys` 的 Symbol 段共用;
|
||||
/// 与 [`walk_own_keys`] 互补,后者只返回字符串键与整数键。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 无自身 Symbol 键时返回空向量;数组元素区不含 Symbol 键。
|
||||
///
|
||||
/// # 注意事项
|
||||
/// - 只读 shape 链,不分配对象;well-known symbol 值指向 session 内建 symbol 对象。
|
||||
pub fn own_symbol_key_values<H: VmHost>(vm: &H, obj: &JsObject) -> Vec<JsValue> {
|
||||
walk_own_symbol_keys(vm, obj)
|
||||
.iter()
|
||||
.map(|(key, _)| decode_symbol_key(vm, *key))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 按规范 [[OwnPropertyKeys]] 重排命名空间导出:字符串键按 UTF-16 单元序升序,
|
||||
/// Symbol 键(`@@toStringTag`)保持在其后。
|
||||
///
|
||||
/// 命名空间导出在模块 body 执行期按语句序写入,而规范要求导出键有序。重建 shape
|
||||
/// 链使插入序即枚举序,字符串枚举消费端(getOwnPropertyNames / Reflect.ownKeys /
|
||||
/// for-in / Object.keys / JSON)无需各自特判。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 仅对模块命名空间对象调用;无自身字符串键时只保留 Symbol 键。
|
||||
///
|
||||
/// # 副作用
|
||||
/// - 重写 obj 的 shape 链与属性表并 bump generation;对象 identity、属性值与
|
||||
/// 描述符均不变。
|
||||
pub fn sort_namespace_exports<H: VmHost>(vm: &mut H, obj: &mut JsObject) {
|
||||
// 收集字符串导出与其值/描述符;Symbol 键不参与字符串排序,收集后原序追加。
|
||||
let mut entries: Vec<(u32, JsValue, Option<PropMetaEntry>)> = walk_own_keys(vm, obj)
|
||||
.into_iter()
|
||||
.map(|(si, pos)| (si, obj.get_prop_at(pos), obj.prop_meta_at(pos)))
|
||||
.collect();
|
||||
let symbols: Vec<(u32, JsValue, Option<PropMetaEntry>)> = walk_own_symbol_keys(vm, obj)
|
||||
.into_iter()
|
||||
.map(|(si, pos)| (si, obj.get_prop_at(pos), obj.prop_meta_at(pos)))
|
||||
.collect();
|
||||
|
||||
// 排序口径 = UTF-16 单元序(BMP 外字符的单元序与码点序不同)。
|
||||
entries.sort_by_cached_key(|(si, _, _)| namespace_key_units(vm, *si));
|
||||
|
||||
// 重建 shape 链与属性表:插入序 = 枚举序。
|
||||
obj.set_shape_id(EMPTY_SHAPE_ID);
|
||||
obj.clear_props();
|
||||
for (si, value, meta) in entries.into_iter().chain(symbols) {
|
||||
let shape = vm.kernel_core().shape_forge().make_shape(obj.shape_id(), si);
|
||||
obj.set_shape_id(shape);
|
||||
let pos = obj.push_prop(value);
|
||||
if let Some(meta) = meta {
|
||||
if meta.is_accessor {
|
||||
obj.set_accessor_meta(pos, meta.get, meta.set, meta.attributes);
|
||||
} else {
|
||||
obj.set_data_meta(pos, meta.attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
obj.bump_generation();
|
||||
}
|
||||
|
||||
/// 命名空间导出键的排序单元序列:字符串键取 UTF-16 单元,整数键取十进制文本。
|
||||
fn namespace_key_units<H: VmHost>(vm: &H, si: u32) -> Vec<u16> {
|
||||
if is_int_key(si) {
|
||||
return int_key_value(si).to_string().encode_utf16().collect();
|
||||
}
|
||||
vm.kernel_core()
|
||||
.perm_interner()
|
||||
.lookup(si)
|
||||
.map(|key| key.encode_utf16().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// `Object.getOwnPropertySymbols(obj)`:返回全部自身 Symbol 键数组。
|
||||
pub fn object_get_own_property_symbols<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let obj_ptr = match require_obj_arg(vm, args, "getOwnPropertySymbols") {
|
||||
@@ -174,10 +248,7 @@ pub fn object_get_own_property_symbols<H: VmHost>(vm: &mut H, args: &[u8]) -> Na
|
||||
|
||||
let symbols: Vec<JsValue> = {
|
||||
let obj = unsafe { &*obj_ptr };
|
||||
walk_own_symbol_keys(vm, obj)
|
||||
.iter()
|
||||
.map(|(key, _)| decode_symbol_key(vm, *key))
|
||||
.collect()
|
||||
own_symbol_key_values(vm, obj)
|
||||
};
|
||||
|
||||
let n = symbols.len();
|
||||
|
||||
@@ -3,7 +3,7 @@ use oxide_types::mem::P;
|
||||
use oxide_types::object::{JsObject, PropAttributes};
|
||||
use oxide_types::value::JsValue;
|
||||
|
||||
use crate::object::{delete_own_property, key_si_to_js_value, walk_own_keys};
|
||||
use crate::object::{delete_own_property, key_si_to_js_value, own_symbol_key_values, walk_own_keys};
|
||||
|
||||
use oxide_runtime_api::{NativeResult, VmHost};
|
||||
|
||||
@@ -184,15 +184,27 @@ pub fn reflect_is_extensible<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
|
||||
NativeResult::Ok(JsValue::bool(unsafe { &*target_ptr }.is_extensible()))
|
||||
}
|
||||
|
||||
/// `Reflect.ownKeys(target)`:返回对象全部自身属性名(字符串数组)。
|
||||
/// `Reflect.ownKeys(target)`:返回对象全部自身属性键,字符串键在前、Symbol 键在后。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 收集字符串键与整数键(整数升序,其余插入序)。
|
||||
/// 2. 追加自身 Symbol 键(保持插入序),符合 OrdinaryOwnPropertyKeys 的排序。
|
||||
///
|
||||
/// # 副作用
|
||||
/// - 结果数组分配在当前 epoch。
|
||||
pub fn reflect_own_keys<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let target_val = arg(vm, args, 1);
|
||||
let Some(target_ptr) = object_ptr(target_val) else {
|
||||
return type_error(vm, "Reflect.ownKeys target is not an object");
|
||||
};
|
||||
let target = unsafe { &*target_ptr };
|
||||
|
||||
// Symbol 键分流在消费端追加:底层字符串枚举源(Object.keys / JSON / for-in)
|
||||
// 语义不变,不泄漏 Symbol 键。
|
||||
let keys = walk_own_keys(vm, target);
|
||||
let n = keys.len();
|
||||
let symbols = own_symbol_key_values(vm, target);
|
||||
let n = keys.len() + symbols.len();
|
||||
|
||||
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
|
||||
let arr = vm.alloc_object(JsObject::new_array(
|
||||
EMPTY_SHAPE_ID,
|
||||
@@ -206,6 +218,12 @@ pub fn reflect_own_keys<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
(*arr).set_prop_at(i, key_val);
|
||||
}
|
||||
}
|
||||
let base = keys.len();
|
||||
for (j, sym) in symbols.iter().enumerate() {
|
||||
unsafe {
|
||||
(*arr).set_prop_at(base + j, *sym);
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
(*arr).set_prop_count(n);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
//! 模块命名空间 @@toStringTag Symbol 键与自身键枚举口径:
|
||||
//! 命名空间标签以 well-known symbol 键写入,`Reflect.ownKeys` 的 Symbol 段跟在
|
||||
//! 字符串段之后,字符串枚举面(getOwnPropertyNames / Object.keys)不受污染。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
use oxide_emit::module::{ModuleKind, ModuleSourceLoader, ResolvedModule};
|
||||
use oxide_vm::vm::Vm;
|
||||
|
||||
/// 文件加载器:base_dir/specifier join 后读源(依赖模块经真实文件解析)。
|
||||
struct FileLoader;
|
||||
|
||||
impl ModuleSourceLoader for FileLoader {
|
||||
fn resolve(
|
||||
&mut self, base_dir: &str, specifier: &str, _attributes: &[(&str, &str)],
|
||||
) -> Result<ResolvedModule, String> {
|
||||
let full = std::path::Path::new(base_dir).join(specifier);
|
||||
let source = std::fs::read_to_string(&full).map_err(|e| format!("cannot read {specifier}: {e}"))?;
|
||||
Ok(ResolvedModule {
|
||||
source,
|
||||
path: full.to_string_lossy().into_owned(),
|
||||
kind: ModuleKind::Js,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试结束删除临时目录,避免在 crate 树残留文件。
|
||||
struct Cleanup(std::path::PathBuf);
|
||||
impl Drop for Cleanup {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// 同一 VM 续跑脚本并返回字符串完成值(读 global 探针用)。
|
||||
fn probe_str(vm: &mut Vm, source: &str) -> String {
|
||||
let allocator = oxide_parser::Allocator::default();
|
||||
let program = oxide_parser::parse(&allocator, source).expect("parse");
|
||||
let module = Compiler::new().compile(&program).expect("compile");
|
||||
let value = vm.run(&Arc::new(module)).expect("probe run");
|
||||
vm.lookup_str(value).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 编译并运行带依赖的模块,返回其探针字符串。
|
||||
fn run_namespace_module(dir: &std::path::Path) -> String {
|
||||
let allocator = oxide_parser::Allocator::default();
|
||||
let source = std::fs::read_to_string(dir.join("main.mjs")).expect("read main");
|
||||
let program = oxide_parser::parse_module(&allocator, &source).expect("parse module");
|
||||
let module = Compiler::new()
|
||||
.compile_module(&program, dir.join("main.mjs").to_string_lossy().as_ref(), &mut FileLoader)
|
||||
.expect("compile module");
|
||||
let mut vm = Vm::new();
|
||||
vm.run(&Arc::new(module)).expect("module run");
|
||||
probe_str(&mut vm, "globalThis.__ns")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_to_string_tag_symbol_key_and_own_keys() {
|
||||
// 命名空间标签按 Symbol.toStringTag Symbol 键暴露:描述符值/属性正确,
|
||||
// 字符串导出按 UTF-16 单元序排序,Symbol 段排在字符串段之后,
|
||||
// 字符串枚举面不含标签。
|
||||
let cwd = std::env::current_dir().expect("cwd");
|
||||
let dir = cwd.join("__module_namespace__");
|
||||
std::fs::create_dir_all(&dir).expect("mkdir");
|
||||
std::fs::write(dir.join("dep.mjs"), "export const b = 1;\nexport const a = 2;\nexport default 3;")
|
||||
.expect("write dep");
|
||||
std::fs::write(
|
||||
dir.join("main.mjs"),
|
||||
"import * as ns from './dep.mjs';\n \
|
||||
var desc = Object.getOwnPropertyDescriptor(ns, Symbol.toStringTag);\n \
|
||||
globalThis.__ns = [\n \
|
||||
ns[Symbol.toStringTag],\n \
|
||||
typeof ns[Symbol.toStringTag],\n \
|
||||
Object.prototype.toString.call(ns),\n \
|
||||
Object.getOwnPropertySymbols(ns).length,\n \
|
||||
Object.getOwnPropertySymbols(ns)[0] === Symbol.toStringTag,\n \
|
||||
Object.getOwnPropertyNames(ns).length,\n \
|
||||
Object.getOwnPropertyNames(ns).join(','),\n \
|
||||
Reflect.ownKeys(ns).length,\n \
|
||||
Reflect.ownKeys(ns).indexOf(Symbol.toStringTag),\n \
|
||||
Symbol.toStringTag in ns,\n \
|
||||
Reflect.has(ns, Symbol.toStringTag),\n \
|
||||
Object.prototype.hasOwnProperty.call(ns, Symbol.toStringTag),\n \
|
||||
desc.writable,\n \
|
||||
desc.enumerable,\n \
|
||||
desc.configurable,\n \
|
||||
Object.getOwnPropertyNames(ns).indexOf('@@toStringTag'),\n \
|
||||
Object.keys(ns).length,\n \
|
||||
].join('|');",
|
||||
)
|
||||
.expect("write main");
|
||||
let _cleanup = Cleanup(dir.clone());
|
||||
|
||||
let result = run_namespace_module(&dir);
|
||||
assert_eq!(
|
||||
result, "Module|string|[object Module]|1|true|3|a,b,default|4|3|true|true|true|false|false|false|-1|3",
|
||||
"命名空间标签 Symbol 键与自身键枚举面不符"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user