reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
// 基础 env 模板:提供网页端最小对象,并按描述符、原型链和 native-like 规则固化。
|
||||
// 复制到 case/result 或 case/env 后,必须根据 fixtures 和 RuyiTrace 证据调整具体字段。
|
||||
'use strict';
|
||||
|
||||
let protect = null;
|
||||
try { protect = require('./native-protect'); } catch { protect = null; }
|
||||
|
||||
function defineValue(obj, key, value, options = {}) {
|
||||
if (protect && protect.defineValue) return protect.defineValue(obj, key, value, options);
|
||||
Object.defineProperty(obj, key, {
|
||||
value,
|
||||
writable: options.writable ?? false,
|
||||
enumerable: options.enumerable ?? true,
|
||||
configurable: options.configurable ?? true,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
function defineGetter(obj, key, getter, options = {}) {
|
||||
Object.defineProperty(obj, key, {
|
||||
get: getter,
|
||||
enumerable: options.enumerable ?? true,
|
||||
configurable: options.configurable ?? true,
|
||||
});
|
||||
return getter;
|
||||
}
|
||||
|
||||
function markNativeFunction(fn, name) {
|
||||
if (protect && protect.markNativeFunction) return protect.markNativeFunction(fn, name);
|
||||
return fn;
|
||||
}
|
||||
|
||||
function markObjectToString(obj, tag) {
|
||||
if (protect && protect.markObjectToString) return protect.markObjectToString(obj, tag);
|
||||
try { Object.defineProperty(obj, Symbol.toStringTag, { value: tag, enumerable: false, configurable: true }); } catch {}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function defineNativeGetter(obj, key, impl, options = {}, addon) {
|
||||
if (protect && protect.defineNativeGetter) return protect.defineNativeGetter(obj, key, impl, options, addon);
|
||||
return defineGetter(obj, key, impl, options);
|
||||
}
|
||||
|
||||
function defineNativeValue(obj, key, impl, options = {}, addon) {
|
||||
if (protect && protect.defineNativeValue) return protect.defineNativeValue(obj, key, impl, options, addon);
|
||||
return defineValue(obj, key, impl, { writable: true, ...options });
|
||||
}
|
||||
|
||||
function createNativeConstructor(name, length, impl, addon) {
|
||||
if (protect && protect.createNativeConstructor) return protect.createNativeConstructor(name, length, impl, addon);
|
||||
function Ctor(...args) { return impl.call(this, new.target ? true : false, ...args); }
|
||||
try { Object.defineProperty(Ctor, 'name', { value: name, configurable: true }); } catch {}
|
||||
return markNativeFunction(Ctor, name);
|
||||
}
|
||||
|
||||
function throwIllegalConstructor(name, addon) {
|
||||
if (protect && protect.throwIllegalConstructor) return protect.throwIllegalConstructor(name, addon);
|
||||
throw new TypeError(`Failed to construct '${name}': Illegal constructor`);
|
||||
}
|
||||
|
||||
function installBaseEnv(globalObject, fixture = {}, options = {}) {
|
||||
const addon = options.addon || null;
|
||||
const browser = fixture.browser || {};
|
||||
const pageUrl = fixture.pageUrl || fixture.apiUrl || 'https://example.com/';
|
||||
const url = new URL(pageUrl);
|
||||
|
||||
const Window = createNativeConstructor('Window', 0, function Window(isNew) {
|
||||
return throwIllegalConstructor('Window', addon);
|
||||
}, addon);
|
||||
const Location = createNativeConstructor('Location', 0, function Location(isNew) {
|
||||
return throwIllegalConstructor('Location', addon);
|
||||
}, addon);
|
||||
const Navigator = createNativeConstructor('Navigator', 0, function Navigator(isNew) {
|
||||
return throwIllegalConstructor('Navigator', addon);
|
||||
}, addon);
|
||||
|
||||
defineValue(Window.prototype, 'constructor', Window, { writable: true, enumerable: false, configurable: true });
|
||||
defineValue(Location.prototype, 'constructor', Location, { writable: true, enumerable: false, configurable: true });
|
||||
defineValue(Navigator.prototype, 'constructor', Navigator, { writable: true, enumerable: false, configurable: true });
|
||||
markObjectToString(Window.prototype, 'Window');
|
||||
markObjectToString(Location.prototype, 'Location');
|
||||
markObjectToString(Navigator.prototype, 'Navigator');
|
||||
|
||||
const location = Object.create(Location.prototype);
|
||||
const locationState = {
|
||||
href: url.href,
|
||||
origin: url.origin,
|
||||
protocol: url.protocol,
|
||||
host: url.host,
|
||||
hostname: url.hostname,
|
||||
port: url.port,
|
||||
pathname: url.pathname,
|
||||
search: url.search,
|
||||
hash: url.hash,
|
||||
};
|
||||
for (const key of ['href', 'origin', 'protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash']) {
|
||||
defineNativeGetter(Location.prototype, key, function () { return locationState[key]; }, { enumerable: true, configurable: true, name: key }, addon);
|
||||
}
|
||||
defineNativeValue(Location.prototype, 'toString', function toString() { return this.href; }, { enumerable: false, configurable: true, writable: true, name: 'toString', length: 0 }, addon);
|
||||
markObjectToString(location, 'Location');
|
||||
|
||||
const navigator = Object.create(Navigator.prototype);
|
||||
const navState = {
|
||||
userAgent: browser.userAgent || '',
|
||||
language: browser.language || 'zh-CN',
|
||||
languages: browser.languages || ['zh-CN', 'zh'],
|
||||
platform: browser.platform || 'Win32',
|
||||
hardwareConcurrency: browser.hardwareConcurrency ?? 8,
|
||||
deviceMemory: browser.deviceMemory ?? 8,
|
||||
webdriver: browser.webdriver ?? false,
|
||||
cookieEnabled: browser.cookieEnabled ?? true,
|
||||
};
|
||||
for (const key of Object.keys(navState)) {
|
||||
defineNativeGetter(Navigator.prototype, key, function () { return navState[key]; }, { enumerable: true, configurable: true, name: key }, addon);
|
||||
}
|
||||
markObjectToString(navigator, 'Navigator');
|
||||
|
||||
defineValue(globalObject, 'Window', Window, { writable: true, enumerable: false, configurable: true });
|
||||
defineValue(globalObject, 'Location', Location, { writable: true, enumerable: false, configurable: true });
|
||||
defineValue(globalObject, 'Navigator', Navigator, { writable: true, enumerable: false, configurable: true });
|
||||
|
||||
defineValue(globalObject, 'window', globalObject, { writable: false, enumerable: true, configurable: true });
|
||||
defineValue(globalObject, 'self', globalObject, { writable: false, enumerable: true, configurable: true });
|
||||
defineValue(globalObject, 'top', globalObject, { writable: false, enumerable: true, configurable: true });
|
||||
defineValue(globalObject, 'parent', globalObject, { writable: false, enumerable: true, configurable: true });
|
||||
defineValue(globalObject, 'location', location, { writable: false, enumerable: true, configurable: true });
|
||||
defineValue(globalObject, 'navigator', navigator, { writable: false, enumerable: true, configurable: true });
|
||||
markObjectToString(globalObject, 'Window');
|
||||
|
||||
return { Window, Location, Navigator, location, navigator };
|
||||
}
|
||||
|
||||
module.exports = { installBaseEnv };
|
||||
@@ -0,0 +1,177 @@
|
||||
// Document 模块模板:提供最小 document / DOM 原型链,并优先用 addon.createUndetectable 处理 document.all。
|
||||
'use strict';
|
||||
|
||||
let protect = null;
|
||||
try { protect = require('./native-protect'); } catch { protect = null; }
|
||||
|
||||
function defineValue(obj, key, value, options = {}) {
|
||||
if (protect && protect.defineValue) return protect.defineValue(obj, key, value, options);
|
||||
Object.defineProperty(obj, key, {
|
||||
value,
|
||||
writable: options.writable ?? false,
|
||||
enumerable: options.enumerable ?? true,
|
||||
configurable: options.configurable ?? true,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
function markNativeFunction(fn, name) {
|
||||
if (protect && protect.markNativeFunction) return protect.markNativeFunction(fn, name);
|
||||
return fn;
|
||||
}
|
||||
|
||||
function markObjectToString(obj, tag) {
|
||||
if (protect && protect.markObjectToString) return protect.markObjectToString(obj, tag);
|
||||
try { Object.defineProperty(obj, Symbol.toStringTag, { value: tag, enumerable: false, configurable: true }); } catch {}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function defineNativeGetter(obj, key, impl, options = {}, addon) {
|
||||
if (protect && protect.defineNativeGetter) return protect.defineNativeGetter(obj, key, impl, options, addon);
|
||||
Object.defineProperty(obj, key, { get: impl, enumerable: options.enumerable ?? true, configurable: options.configurable ?? true });
|
||||
return impl;
|
||||
}
|
||||
|
||||
function defineNativeSetter(obj, key, impl, options = {}, addon) {
|
||||
if (protect && protect.defineNativeSetter) return protect.defineNativeSetter(obj, key, impl, options, addon);
|
||||
Object.defineProperty(obj, key, { set: impl, enumerable: options.enumerable ?? true, configurable: options.configurable ?? true });
|
||||
return impl;
|
||||
}
|
||||
|
||||
function defineNativeValue(obj, key, impl, options = {}, addon) {
|
||||
if (protect && protect.defineNativeValue) return protect.defineNativeValue(obj, key, impl, options, addon);
|
||||
return defineValue(obj, key, impl, { writable: true, ...options });
|
||||
}
|
||||
|
||||
function createNativeConstructor(name, length, impl, addon) {
|
||||
if (protect && protect.createNativeConstructor) return protect.createNativeConstructor(name, length, impl, addon);
|
||||
function Ctor(...args) { return impl.call(this, new.target ? true : false, ...args); }
|
||||
try { Object.defineProperty(Ctor, 'name', { value: name, configurable: true }); } catch {}
|
||||
return markNativeFunction(Ctor, name);
|
||||
}
|
||||
|
||||
function throwIllegalConstructor(name, addon) {
|
||||
if (protect && protect.throwIllegalConstructor) return protect.throwIllegalConstructor(name, addon);
|
||||
throw new TypeError(`Failed to construct '${name}': Illegal constructor`);
|
||||
}
|
||||
|
||||
function throwConstructorRequiresNew(name, addon) {
|
||||
if (protect && protect.throwConstructorRequiresNew) return protect.throwConstructorRequiresNew(name, addon);
|
||||
throw new TypeError(`Failed to construct '${name}': Please use the 'new' operator, this DOM object constructor cannot be called as a function.`);
|
||||
}
|
||||
|
||||
function installDocumentAll(document, addon) {
|
||||
let value;
|
||||
let exact = false;
|
||||
if (protect && typeof protect.createUndetectable === 'function') {
|
||||
value = protect.createUndetectable(function documentAll() { return undefined; }, addon);
|
||||
exact = value !== undefined;
|
||||
} else if (addon && typeof addon.createUndetectable === 'function') {
|
||||
try {
|
||||
value = addon.createUndetectable(function documentAll() { return undefined; });
|
||||
exact = true;
|
||||
} catch {}
|
||||
}
|
||||
if (!exact) value = undefined;
|
||||
Object.defineProperty(document, 'all', {
|
||||
value,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
});
|
||||
return { exact, value };
|
||||
}
|
||||
|
||||
function installDocumentEnv(globalObject, fixture = {}, options = {}) {
|
||||
const addon = options.addon || null;
|
||||
const pageUrl = fixture.pageUrl || fixture.apiUrl || (globalObject.location && globalObject.location.href) || 'https://example.com/';
|
||||
const url = new URL(pageUrl);
|
||||
const cookieJar = new Map();
|
||||
const initialCookie = (fixture.request && fixture.request.cookie) || fixture.cookie || '';
|
||||
if (initialCookie) {
|
||||
for (const pair of String(initialCookie).split(';')) {
|
||||
const idx = pair.indexOf('=');
|
||||
if (idx > 0) cookieJar.set(pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
||||
}
|
||||
}
|
||||
|
||||
const EventTarget = createNativeConstructor('EventTarget', 0, function EventTarget(isNew) {
|
||||
if (!isNew) return throwConstructorRequiresNew('EventTarget', addon);
|
||||
}, addon);
|
||||
const Node = createNativeConstructor('Node', 0, function Node(isNew) {
|
||||
return throwIllegalConstructor('Node', addon);
|
||||
}, addon);
|
||||
const Document = createNativeConstructor('Document', 0, function Document(isNew) {
|
||||
return throwIllegalConstructor('Document', addon);
|
||||
}, addon);
|
||||
const HTMLDocument = createNativeConstructor('HTMLDocument', 0, function HTMLDocument(isNew) {
|
||||
return throwIllegalConstructor('HTMLDocument', addon);
|
||||
}, addon);
|
||||
const Element = createNativeConstructor('Element', 0, function Element(isNew) {
|
||||
return throwIllegalConstructor('Element', addon);
|
||||
}, addon);
|
||||
const HTMLElement = createNativeConstructor('HTMLElement', 0, function HTMLElement(isNew) {
|
||||
return throwIllegalConstructor('HTMLElement', addon);
|
||||
}, addon);
|
||||
const HTMLHtmlElement = createNativeConstructor('HTMLHtmlElement', 0, function HTMLHtmlElement(isNew) {
|
||||
return throwIllegalConstructor('HTMLHtmlElement', addon);
|
||||
}, addon);
|
||||
|
||||
for (const ctor of [EventTarget, Node, Document, HTMLDocument, Element, HTMLElement, HTMLHtmlElement]) {
|
||||
defineValue(ctor.prototype, 'constructor', ctor, { writable: true, enumerable: false, configurable: true });
|
||||
markObjectToString(ctor.prototype, ctor.name);
|
||||
}
|
||||
|
||||
Object.setPrototypeOf(Node.prototype, EventTarget.prototype);
|
||||
Object.setPrototypeOf(Document.prototype, Node.prototype);
|
||||
Object.setPrototypeOf(HTMLDocument.prototype, Document.prototype);
|
||||
Object.setPrototypeOf(Element.prototype, Node.prototype);
|
||||
Object.setPrototypeOf(HTMLElement.prototype, Element.prototype);
|
||||
Object.setPrototypeOf(HTMLHtmlElement.prototype, HTMLElement.prototype);
|
||||
|
||||
const document = Object.create(HTMLDocument.prototype);
|
||||
const documentElement = Object.create(HTMLHtmlElement.prototype);
|
||||
markObjectToString(document, 'HTMLDocument');
|
||||
markObjectToString(documentElement, 'HTMLHtmlElement');
|
||||
|
||||
defineNativeGetter(Document.prototype, 'URL', function () { return url.href; }, { enumerable: true, configurable: true, name: 'URL' }, addon);
|
||||
defineNativeGetter(Document.prototype, 'documentURI', function () { return url.href; }, { enumerable: true, configurable: true, name: 'documentURI' }, addon);
|
||||
defineNativeGetter(Document.prototype, 'referrer', function () { return fixture.referrer || ''; }, { enumerable: true, configurable: true, name: 'referrer' }, addon);
|
||||
defineNativeGetter(Document.prototype, 'hidden', function () { return false; }, { enumerable: true, configurable: true, name: 'hidden' }, addon);
|
||||
defineNativeGetter(Document.prototype, 'visibilityState', function () { return 'visible'; }, { enumerable: true, configurable: true, name: 'visibilityState' }, addon);
|
||||
defineNativeGetter(Document.prototype, 'documentElement', function () { return documentElement; }, { enumerable: true, configurable: true, name: 'documentElement' }, addon);
|
||||
defineNativeGetter(Document.prototype, 'defaultView', function () { return globalObject.window || globalObject; }, { enumerable: true, configurable: true, name: 'defaultView' }, addon);
|
||||
|
||||
const getCookie = function () {
|
||||
return Array.from(cookieJar.entries()).map(([k, v]) => `${k}=${v}`).join('; ');
|
||||
};
|
||||
const setCookie = function (value) {
|
||||
const first = String(value || '').split(';')[0];
|
||||
const idx = first.indexOf('=');
|
||||
if (idx > 0) cookieJar.set(first.slice(0, idx).trim(), first.slice(idx + 1).trim());
|
||||
};
|
||||
const cookieGetter = protect && protect.createNativeGetter ? protect.createNativeGetter('cookie', getCookie, addon) : getCookie;
|
||||
const cookieSetter = protect && protect.createNativeSetter ? protect.createNativeSetter('cookie', setCookie, addon) : setCookie;
|
||||
Object.defineProperty(Document.prototype, 'cookie', { get: cookieGetter, set: cookieSetter, enumerable: true, configurable: true });
|
||||
|
||||
defineNativeValue(Document.prototype, 'querySelector', function querySelector() { return null; }, { enumerable: true, configurable: true, writable: true, name: 'querySelector', length: 1 }, addon);
|
||||
defineNativeValue(Document.prototype, 'getElementById', function getElementById() { return null; }, { enumerable: true, configurable: true, writable: true, name: 'getElementById', length: 1 }, addon);
|
||||
defineNativeValue(Document.prototype, 'createElement', function createElement(tagName) {
|
||||
const el = Object.create(HTMLElement.prototype);
|
||||
defineValue(el, 'tagName', String(tagName || '').toUpperCase(), { writable: false, enumerable: true, configurable: true });
|
||||
defineValue(el, 'nodeName', String(tagName || '').toUpperCase(), { writable: false, enumerable: true, configurable: true });
|
||||
markObjectToString(el, 'HTMLElement');
|
||||
return el;
|
||||
}, { enumerable: true, configurable: true, writable: true, name: 'createElement', length: 1 }, addon);
|
||||
|
||||
const allStatus = installDocumentAll(document, addon);
|
||||
|
||||
for (const ctor of [EventTarget, Node, Document, HTMLDocument, Element, HTMLElement, HTMLHtmlElement]) {
|
||||
defineValue(globalObject, ctor.name, ctor, { writable: true, enumerable: false, configurable: true });
|
||||
}
|
||||
defineValue(globalObject, 'document', document, { writable: false, enumerable: true, configurable: true });
|
||||
|
||||
return { document, documentElement, allStatus, EventTarget, Node, Document, HTMLDocument, Element, HTMLElement, HTMLHtmlElement };
|
||||
}
|
||||
|
||||
module.exports = { installDocumentEnv, installDocumentAll };
|
||||
@@ -0,0 +1,349 @@
|
||||
// 指纹值回放模板:用于 Canvas / WebGL / DOM 几何等终端 API 的真实浏览器采样值回放。
|
||||
// 使用前复制到 case/result/src/env/ 中,并接入 case/fixtures/fingerprint.fixture.json。
|
||||
// 目标:回放真实浏览器最终返回值,而不是在 Node.js 中真实模拟渲染。
|
||||
'use strict';
|
||||
|
||||
let native = {};
|
||||
try { native = require('./native-protect'); } catch {}
|
||||
|
||||
const defineValue = native.defineValue || function defineValue(obj, key, value, options = {}) {
|
||||
Object.defineProperty(obj, key, {
|
||||
value,
|
||||
writable: options.writable ?? true,
|
||||
enumerable: options.enumerable ?? true,
|
||||
configurable: options.configurable ?? true,
|
||||
});
|
||||
return value;
|
||||
};
|
||||
|
||||
const defineNativeValue = native.defineNativeValue || function defineNativeValue(obj, key, impl, options = {}) {
|
||||
return defineValue(obj, key, impl, options);
|
||||
};
|
||||
|
||||
const defineNativeGetter = native.defineNativeGetter || function defineNativeGetter(obj, key, impl, options = {}) {
|
||||
Object.defineProperty(obj, key, {
|
||||
get: impl,
|
||||
enumerable: options.enumerable ?? true,
|
||||
configurable: options.configurable ?? true,
|
||||
});
|
||||
return impl;
|
||||
};
|
||||
|
||||
const markObjectToString = native.markObjectToString || function markObjectToString(obj, tag) {
|
||||
try {
|
||||
Object.defineProperty(obj, Symbol.toStringTag, {
|
||||
value: tag,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
});
|
||||
} catch {}
|
||||
return obj;
|
||||
};
|
||||
|
||||
function stackText() {
|
||||
try { return String(new Error().stack || ''); } catch { return ''; }
|
||||
}
|
||||
|
||||
function b64ToBytes(value) {
|
||||
if (!value) return new Uint8ClampedArray();
|
||||
const buf = Buffer.from(String(value), 'base64');
|
||||
return new Uint8ClampedArray(buf.buffer, buf.byteOffset, buf.byteLength);
|
||||
}
|
||||
|
||||
function includesStack(match, stack) {
|
||||
if (!match || !match.stackIncludes) return true;
|
||||
const needles = Array.isArray(match.stackIncludes) ? match.stackIncludes : [match.stackIncludes];
|
||||
return needles.some(s => stack.includes(String(s)));
|
||||
}
|
||||
|
||||
function matchScalar(expected, actual) {
|
||||
if (expected === undefined) return true;
|
||||
return String(expected) === String(actual);
|
||||
}
|
||||
|
||||
function findReplay(list, ctx, options = {}) {
|
||||
const items = Array.isArray(list) ? list : [];
|
||||
const stack = ctx.stack || stackText();
|
||||
for (const item of items) {
|
||||
const match = item.match || {};
|
||||
let ok = includesStack(match, stack);
|
||||
for (const [key, expected] of Object.entries(match)) {
|
||||
if (key === 'stackIncludes') continue;
|
||||
ok = ok && matchScalar(expected, ctx[key]);
|
||||
}
|
||||
if (ok) return item.result;
|
||||
}
|
||||
if (options.strict !== false) {
|
||||
const brief = Object.fromEntries(Object.entries(ctx).filter(([k]) => k !== 'stack'));
|
||||
throw new Error(`缺少浏览器指纹回放样本:${ctx.api || 'unknown'} ${JSON.stringify(brief)}`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function createImageData(result) {
|
||||
const width = Number(result && result.width) || 0;
|
||||
const height = Number(result && result.height) || 0;
|
||||
const data = result && result.dataBase64 ? b64ToBytes(result.dataBase64) : new Uint8ClampedArray(width * height * 4);
|
||||
return markObjectToString({ width, height, data }, 'ImageData');
|
||||
}
|
||||
|
||||
function ensureConstructors(g, options = {}) {
|
||||
const addon = options.addon;
|
||||
if (!g.HTMLCanvasElement) {
|
||||
const HTMLCanvasElement = native.createNativeConstructor ? native.createNativeConstructor('HTMLCanvasElement', 0, function HTMLCanvasElement() {}, addon) : function HTMLCanvasElement() {};
|
||||
defineValue(g, 'HTMLCanvasElement', HTMLCanvasElement, { enumerable: false, configurable: true });
|
||||
}
|
||||
if (!g.CanvasRenderingContext2D) {
|
||||
const CanvasRenderingContext2D = native.createNativeConstructor ? native.createNativeConstructor('CanvasRenderingContext2D', 0, function CanvasRenderingContext2D() {}, addon) : function CanvasRenderingContext2D() {};
|
||||
defineValue(g, 'CanvasRenderingContext2D', CanvasRenderingContext2D, { enumerable: false, configurable: true });
|
||||
}
|
||||
if (!g.WebGLRenderingContext) {
|
||||
const WebGLRenderingContext = native.createNativeConstructor ? native.createNativeConstructor('WebGLRenderingContext', 0, function WebGLRenderingContext() {}, addon) : function WebGLRenderingContext() {};
|
||||
defineValue(g, 'WebGLRenderingContext', WebGLRenderingContext, { enumerable: false, configurable: true });
|
||||
}
|
||||
if (!g.DOMRect) {
|
||||
const DOMRect = native.createNativeConstructor ? native.createNativeConstructor('DOMRect', 4, function DOMRect(isNew, x = 0, y = 0, width = 0, height = 0) {
|
||||
if (!isNew && !(this instanceof DOMRect)) return new g.DOMRect(x, y, width, height);
|
||||
defineValue(this, 'x', Number(x), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'y', Number(y), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'width', Number(width), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'height', Number(height), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'top', Number(y), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'left', Number(x), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'right', Number(x) + Number(width), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'bottom', Number(y) + Number(height), { enumerable: true, writable: false, configurable: true });
|
||||
markObjectToString(this, 'DOMRect');
|
||||
}, addon) : function DOMRect(x = 0, y = 0, width = 0, height = 0) {
|
||||
defineValue(this, 'x', Number(x), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'y', Number(y), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'width', Number(width), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'height', Number(height), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'top', Number(y), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'left', Number(x), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'right', Number(x) + Number(width), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'bottom', Number(y) + Number(height), { enumerable: true, writable: false, configurable: true });
|
||||
markObjectToString(this, 'DOMRect');
|
||||
};
|
||||
defineValue(g, 'DOMRect', DOMRect, { enumerable: false, configurable: true });
|
||||
}
|
||||
|
||||
for (const Ctor of [g.HTMLCanvasElement, g.CanvasRenderingContext2D, g.WebGLRenderingContext, g.DOMRect]) {
|
||||
try {
|
||||
defineValue(Ctor.prototype, 'constructor', Ctor, { enumerable: false, writable: true, configurable: true });
|
||||
} catch {}
|
||||
}
|
||||
try { markObjectToString(g.HTMLCanvasElement.prototype, 'HTMLCanvasElement'); } catch {}
|
||||
try { markObjectToString(g.CanvasRenderingContext2D.prototype, 'CanvasRenderingContext2D'); } catch {}
|
||||
try { markObjectToString(g.WebGLRenderingContext.prototype, 'WebGLRenderingContext'); } catch {}
|
||||
}
|
||||
|
||||
function create2DContext(g, canvas, state) {
|
||||
const ctx = Object.create(g.CanvasRenderingContext2D.prototype);
|
||||
ctx.__canvas = canvas;
|
||||
ctx.__state = state || { font: '10px sans-serif' };
|
||||
markObjectToString(ctx, 'CanvasRenderingContext2D');
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function createWebGLContext(g, canvas, state) {
|
||||
const gl = Object.create(g.WebGLRenderingContext.prototype);
|
||||
gl.__canvas = canvas;
|
||||
gl.__state = state || {};
|
||||
markObjectToString(gl, 'WebGLRenderingContext');
|
||||
return gl;
|
||||
}
|
||||
|
||||
function installCanvasReplay(g, fixture, options) {
|
||||
const canvasFx = fixture.canvas || {};
|
||||
const proto = g.HTMLCanvasElement.prototype;
|
||||
const ctx2dProto = g.CanvasRenderingContext2D.prototype;
|
||||
|
||||
defineNativeValue(proto, 'getContext', function getContext(type) {
|
||||
const kind = String(type || '').toLowerCase();
|
||||
if (kind === '2d') return this.__ctx2d || (this.__ctx2d = create2DContext(g, this, { font: '10px sans-serif' }));
|
||||
if (kind === 'webgl' || kind === 'experimental-webgl' || kind === 'webgl2') return this.__webgl || (this.__webgl = createWebGLContext(g, this, {}));
|
||||
return null;
|
||||
}, { name: 'getContext', length: 1, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'toDataURL', function toDataURL(type = 'image/png', quality) {
|
||||
const result = findReplay(canvasFx.toDataURL, {
|
||||
api: 'canvas.toDataURL',
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
type,
|
||||
quality,
|
||||
stack: stackText(),
|
||||
}, options);
|
||||
return result === undefined ? '' : String(result);
|
||||
}, { name: 'toDataURL', length: 0, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'toBlob', function toBlob(callback, type = 'image/png', quality) {
|
||||
const result = findReplay(canvasFx.toBlob, {
|
||||
api: 'canvas.toBlob',
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
type,
|
||||
quality,
|
||||
stack: stackText(),
|
||||
}, { ...options, strict: options.strictToBlob ?? options.strict });
|
||||
if (typeof callback === 'function') {
|
||||
const blob = markObjectToString({ type, size: result && result.size ? Number(result.size) : 0, __replay: result }, 'Blob');
|
||||
callback(blob);
|
||||
}
|
||||
}, { name: 'toBlob', length: 1, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeGetter(ctx2dProto, 'font', function getFont() {
|
||||
return this.__state && this.__state.font || '10px sans-serif';
|
||||
}, { name: 'font', enumerable: true, configurable: true }, options.addon);
|
||||
Object.defineProperty(ctx2dProto, 'font', {
|
||||
get: Object.getOwnPropertyDescriptor(ctx2dProto, 'font').get,
|
||||
set: function setFont(value) { this.__state.font = String(value); },
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
defineNativeValue(ctx2dProto, 'measureText', function measureText(text) {
|
||||
const result = findReplay(canvasFx.measureText, {
|
||||
api: 'canvas.measureText',
|
||||
text: String(text),
|
||||
font: this.font,
|
||||
stack: stackText(),
|
||||
}, options) || { width: 0 };
|
||||
return markObjectToString({ ...result }, 'TextMetrics');
|
||||
}, { name: 'measureText', length: 1, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(ctx2dProto, 'getImageData', function getImageData(sx, sy, sw, sh) {
|
||||
const result = findReplay(canvasFx.getImageData, {
|
||||
api: 'canvas.getImageData',
|
||||
sx, sy, sw, sh,
|
||||
stack: stackText(),
|
||||
}, options);
|
||||
return createImageData(result || { width: sw, height: sh });
|
||||
}, { name: 'getImageData', length: 4, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
for (const name of ['fillText', 'strokeText', 'drawImage', 'fillRect', 'clearRect', 'rect', 'arc', 'beginPath', 'closePath', 'stroke', 'fill', 'putImageData']) {
|
||||
if (!ctx2dProto[name]) {
|
||||
defineNativeValue(ctx2dProto, name, function noop() {}, { name, length: 0, enumerable: true, configurable: true }, options.addon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function installWebGLReplay(g, fixture, options) {
|
||||
const webglFx = fixture.webgl || {};
|
||||
const proto = g.WebGLRenderingContext.prototype;
|
||||
|
||||
defineNativeValue(proto, 'getParameter', function getParameter(pname) {
|
||||
return findReplay(webglFx.getParameter, { api: 'webgl.getParameter', pname, stack: stackText() }, options);
|
||||
}, { name: 'getParameter', length: 1, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'getSupportedExtensions', function getSupportedExtensions() {
|
||||
const configured = webglFx.getSupportedExtensions;
|
||||
if (configured && Array.isArray(configured.result)) return configured.result.slice();
|
||||
return findReplay(webglFx.getSupportedExtensions, { api: 'webgl.getSupportedExtensions', stack: stackText() }, options) || [];
|
||||
}, { name: 'getSupportedExtensions', length: 0, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'getExtension', function getExtension(name) {
|
||||
const configured = webglFx.getExtension;
|
||||
const result = findReplay(configured, { api: 'webgl.getExtension', name: String(name), stack: stackText() }, { ...options, strict: false });
|
||||
return result === undefined ? null : markObjectToString({ name: String(name), ...result }, String(name));
|
||||
}, { name: 'getExtension', length: 1, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'getShaderPrecisionFormat', function getShaderPrecisionFormat(shaderType, precisionType) {
|
||||
const result = findReplay(webglFx.getShaderPrecisionFormat, {
|
||||
api: 'webgl.getShaderPrecisionFormat',
|
||||
shaderType,
|
||||
precisionType,
|
||||
stack: stackText(),
|
||||
}, options) || { rangeMin: 0, rangeMax: 0, precision: 0 };
|
||||
return markObjectToString({ ...result }, 'WebGLShaderPrecisionFormat');
|
||||
}, { name: 'getShaderPrecisionFormat', length: 2, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'readPixels', function readPixels(x, y, width, height, format, type, pixels) {
|
||||
const result = findReplay(webglFx.readPixels, {
|
||||
api: 'webgl.readPixels',
|
||||
x, y, width, height, format, type,
|
||||
stack: stackText(),
|
||||
}, options);
|
||||
if (pixels && result && result.dataBase64) {
|
||||
const bytes = b64ToBytes(result.dataBase64);
|
||||
for (let i = 0; i < Math.min(pixels.length, bytes.length); i++) pixels[i] = bytes[i];
|
||||
}
|
||||
}, { name: 'readPixels', length: 7, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'getContextAttributes', function getContextAttributes() {
|
||||
return { alpha: true, antialias: true, depth: true, desynchronized: false, failIfMajorPerformanceCaveat: false, powerPreference: 'default', premultipliedAlpha: true, preserveDrawingBuffer: false, stencil: false, xrCompatible: false };
|
||||
}, { name: 'getContextAttributes', length: 0, enumerable: true, configurable: true }, options.addon);
|
||||
}
|
||||
|
||||
function installDomGeometryReplay(g, fixture, options) {
|
||||
const domFx = fixture.domGeometry || {};
|
||||
if (!g.Element) return;
|
||||
const proto = g.Element.prototype;
|
||||
|
||||
function selectorOf(el) {
|
||||
return el.__fingerprintSelector || el.id && `#${el.id}` || el.className && `.${String(el.className).split(/\s+/)[0]}` || String(el.tagName || '').toLowerCase();
|
||||
}
|
||||
|
||||
defineNativeValue(proto, 'getBoundingClientRect', function getBoundingClientRect() {
|
||||
const result = findReplay(domFx.getBoundingClientRect, {
|
||||
api: 'dom.getBoundingClientRect',
|
||||
selector: selectorOf(this),
|
||||
stack: stackText(),
|
||||
}, options) || {};
|
||||
return new g.DOMRect(result.x || result.left || 0, result.y || result.top || 0, result.width || 0, result.height || 0);
|
||||
}, { name: 'getBoundingClientRect', length: 0, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'getClientRects', function getClientRects() {
|
||||
const rect = this.getBoundingClientRect();
|
||||
return markObjectToString({ 0: rect, length: 1, item(i) { return i === 0 ? rect : null; } }, 'DOMRectList');
|
||||
}, { name: 'getClientRects', length: 0, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
for (const key of ['offsetWidth', 'offsetHeight', 'scrollWidth', 'scrollHeight', 'clientWidth', 'clientHeight']) {
|
||||
defineNativeGetter(proto, key, function geometryGetter() {
|
||||
const result = findReplay(domFx.offset, {
|
||||
api: `dom.${key}`,
|
||||
selector: selectorOf(this),
|
||||
stack: stackText(),
|
||||
}, { ...options, strict: false });
|
||||
if (result && result[key] !== undefined) return Number(result[key]);
|
||||
const rect = this.getBoundingClientRect();
|
||||
return /height/i.test(key) ? Math.round(rect.height || 0) : Math.round(rect.width || 0);
|
||||
}, { name: key, enumerable: true, configurable: true }, options.addon);
|
||||
}
|
||||
}
|
||||
|
||||
function createCanvasElement(globalObj, width = 300, height = 150) {
|
||||
const g = globalObj || globalThis;
|
||||
ensureConstructors(g, options);
|
||||
const canvas = Object.create(g.HTMLCanvasElement.prototype);
|
||||
defineValue(canvas, 'width', Number(width), { enumerable: true, writable: true, configurable: true });
|
||||
defineValue(canvas, 'height', Number(height), { enumerable: true, writable: true, configurable: true });
|
||||
markObjectToString(canvas, 'HTMLCanvasElement');
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function installFingerprintValueReplay(globalObj, fixture = {}, options = {}) {
|
||||
const g = globalObj || globalThis;
|
||||
const replayOptions = { strict: true, ...options };
|
||||
ensureConstructors(g, replayOptions);
|
||||
installCanvasReplay(g, fixture, replayOptions);
|
||||
installWebGLReplay(g, fixture, replayOptions);
|
||||
installDomGeometryReplay(g, fixture, replayOptions);
|
||||
if (!g.document) return { createCanvasElement: (w, h) => createCanvasElement(g, w, h) };
|
||||
if (g.document && typeof g.document.createElement === 'function' && !g.document.__fingerprintCreateElementPatched) {
|
||||
const rawCreateElement = g.document.createElement;
|
||||
defineNativeValue(g.document, 'createElement', function createElement(tagName, optionsArg) {
|
||||
const tag = String(tagName || '').toLowerCase();
|
||||
if (tag === 'canvas') return createCanvasElement(g);
|
||||
return rawCreateElement.call(this, tagName, optionsArg);
|
||||
}, { name: 'createElement', length: 1, enumerable: true, configurable: true }, replayOptions.addon);
|
||||
defineValue(g.document, '__fingerprintCreateElementPatched', true, { enumerable: false, configurable: true });
|
||||
}
|
||||
return { createCanvasElement: (w, h) => createCanvasElement(g, w, h) };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
installFingerprintValueReplay,
|
||||
createCanvasElement,
|
||||
findReplay,
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
||||
// Storage 模块模板:按 fixture 注入 localStorage / sessionStorage,并固化原型链、描述符和 native-like 方法。
|
||||
'use strict';
|
||||
|
||||
let protect = null;
|
||||
try { protect = require('./native-protect'); } catch { protect = null; }
|
||||
|
||||
function defineValue(obj, key, value, options = {}) {
|
||||
if (protect && protect.defineValue) return protect.defineValue(obj, key, value, options);
|
||||
Object.defineProperty(obj, key, {
|
||||
value,
|
||||
writable: options.writable ?? false,
|
||||
enumerable: options.enumerable ?? true,
|
||||
configurable: options.configurable ?? true,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
function markNativeFunction(fn, name) {
|
||||
if (protect && protect.markNativeFunction) return protect.markNativeFunction(fn, name);
|
||||
return fn;
|
||||
}
|
||||
|
||||
function markObjectToString(obj, tag) {
|
||||
if (protect && protect.markObjectToString) return protect.markObjectToString(obj, tag);
|
||||
try { Object.defineProperty(obj, Symbol.toStringTag, { value: tag, enumerable: false, configurable: true }); } catch {}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function defineNativeGetter(obj, key, impl, options = {}, addon) {
|
||||
if (protect && protect.defineNativeGetter) return protect.defineNativeGetter(obj, key, impl, options, addon);
|
||||
Object.defineProperty(obj, key, { get: impl, enumerable: options.enumerable ?? true, configurable: options.configurable ?? true });
|
||||
return impl;
|
||||
}
|
||||
|
||||
function defineNativeValue(obj, key, impl, options = {}, addon) {
|
||||
if (protect && protect.defineNativeValue) return protect.defineNativeValue(obj, key, impl, options, addon);
|
||||
return defineValue(obj, key, impl, { writable: true, ...options });
|
||||
}
|
||||
|
||||
function createNativeConstructor(name, length, impl, addon) {
|
||||
if (protect && protect.createNativeConstructor) return protect.createNativeConstructor(name, length, impl, addon);
|
||||
function Ctor(...args) { return impl.call(this, new.target ? true : false, ...args); }
|
||||
try { Object.defineProperty(Ctor, 'name', { value: name, configurable: true }); } catch {}
|
||||
return markNativeFunction(Ctor, name);
|
||||
}
|
||||
|
||||
function throwIllegalConstructor(name, addon) {
|
||||
if (protect && protect.throwIllegalConstructor) return protect.throwIllegalConstructor(name, addon);
|
||||
throw new TypeError(`Failed to construct '${name}': Illegal constructor`);
|
||||
}
|
||||
|
||||
function installStorage(globalObject, fixture = {}, options = {}) {
|
||||
const addon = options.addon || null;
|
||||
const request = fixture.request || {};
|
||||
|
||||
const Storage = createNativeConstructor('Storage', 0, function Storage(isNew) {
|
||||
return throwIllegalConstructor('Storage', addon);
|
||||
}, addon);
|
||||
defineValue(Storage.prototype, 'constructor', Storage, { writable: true, enumerable: false, configurable: true });
|
||||
markObjectToString(Storage.prototype, 'Storage');
|
||||
|
||||
defineNativeGetter(Storage.prototype, 'length', function () { return this.__data.size; }, { enumerable: true, configurable: true, name: 'length' }, addon);
|
||||
defineNativeValue(Storage.prototype, 'key', function key(index) { return Array.from(this.__data.keys())[Number(index)] ?? null; }, { enumerable: true, configurable: true, writable: true, name: 'key', length: 1 }, addon);
|
||||
defineNativeValue(Storage.prototype, 'getItem', function getItem(key) { key = String(key); return this.__data.has(key) ? this.__data.get(key) : null; }, { enumerable: true, configurable: true, writable: true, name: 'getItem', length: 1 }, addon);
|
||||
defineNativeValue(Storage.prototype, 'setItem', function setItem(key, value) { this.__data.set(String(key), String(value)); }, { enumerable: true, configurable: true, writable: true, name: 'setItem', length: 2 }, addon);
|
||||
defineNativeValue(Storage.prototype, 'removeItem', function removeItem(key) { this.__data.delete(String(key)); }, { enumerable: true, configurable: true, writable: true, name: 'removeItem', length: 1 }, addon);
|
||||
defineNativeValue(Storage.prototype, 'clear', function clear() { this.__data.clear(); }, { enumerable: true, configurable: true, writable: true, name: 'clear', length: 0 }, addon);
|
||||
|
||||
function createStorage(initial = {}) {
|
||||
const data = new Map(Object.entries(initial).map(([k, v]) => [String(k), String(v)]));
|
||||
const storage = Object.create(Storage.prototype);
|
||||
defineValue(storage, '__data', data, { enumerable: false, configurable: false, writable: false });
|
||||
markObjectToString(storage, 'Storage');
|
||||
return storage;
|
||||
}
|
||||
|
||||
const localStorage = createStorage(request.localStorage || fixture.localStorage || {});
|
||||
const sessionStorage = createStorage(request.sessionStorage || fixture.sessionStorage || {});
|
||||
|
||||
defineValue(globalObject, 'Storage', Storage, { writable: true, enumerable: false, configurable: true });
|
||||
defineValue(globalObject, 'localStorage', localStorage, { writable: false, enumerable: true, configurable: true });
|
||||
defineValue(globalObject, 'sessionStorage', sessionStorage, { writable: false, enumerable: true, configurable: true });
|
||||
|
||||
return { Storage, localStorage, sessionStorage, createStorage };
|
||||
}
|
||||
|
||||
function createStorage(initial = {}, options = {}) {
|
||||
const context = {};
|
||||
return installStorage(context, { localStorage: initial }, options).localStorage;
|
||||
}
|
||||
|
||||
module.exports = { createStorage, installStorage };
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,435 @@
|
||||
// xbs isolated-vm runtime 模板:仅在用户明确选择 isolated-vm 时复制到最终项目。
|
||||
// 默认加载随 Skill 携带的魔改 isolated-vm 二进制,并在隔离 Context 内使用 window.xbs / globalThis.xbs。
|
||||
// 不要在该模式下桥接旧 addon.node;xbs API 就是 isolated-vm Context 内的 native-first 能力。
|
||||
// 补环境源码应保存为真实 .js 文件,通过 runFile/runFiles 读取后注入 Context,避免大段 String.raw 聚合脚本。
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const EXPECTED_XBS_APIS = Object.freeze([
|
||||
'clearProtoChainRegistry',
|
||||
'createGetter',
|
||||
'createInterceptor',
|
||||
'createNativeCollection',
|
||||
'createNativeFunction',
|
||||
'createNativeObject',
|
||||
'createProtoChains',
|
||||
'createSetter',
|
||||
'createUndetectable',
|
||||
'deletePrivate',
|
||||
'deleteProtoChainRegistryEntry',
|
||||
'getMimeTypesAndPlugins',
|
||||
'getPrivate',
|
||||
'getProtoChainRegistry',
|
||||
'hasPrivate',
|
||||
'setPrivate',
|
||||
'throwTypeError',
|
||||
]);
|
||||
|
||||
function getPlatformKey() {
|
||||
return process.platform + '-' + process.arch;
|
||||
}
|
||||
|
||||
function toPosixPath(value) {
|
||||
return String(value || '').replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
function resolveSourceRoot(options = {}) {
|
||||
const explicit = options.sourceRoot || options.projectRoot || process.env.WEB_JS_ENV_PATCHER_SOURCE_ROOT;
|
||||
return path.resolve(explicit || process.cwd());
|
||||
}
|
||||
|
||||
function resolveSourceFilePath(sourceRoot, requestedPath, allowOutsideSourceRoot = false) {
|
||||
if (!requestedPath) throw new Error('runFile 需要传入文件路径');
|
||||
const resolved = path.resolve(sourceRoot, requestedPath);
|
||||
const relativePath = path.relative(sourceRoot, resolved);
|
||||
const escaped = relativePath.startsWith('..') || path.isAbsolute(relativePath);
|
||||
if (escaped && !allowOutsideSourceRoot) {
|
||||
throw new Error('runFile 路径逃逸 sourceRoot:' + requestedPath);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function readSourceFile(sourceRoot, requestedPath, options = {}) {
|
||||
const filePath = resolveSourceFilePath(sourceRoot, requestedPath, !!options.allowOutsideSourceRoot);
|
||||
const source = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
|
||||
const filename = options.filename || toPosixPath(path.relative(sourceRoot, filePath));
|
||||
return { filePath, filename, source };
|
||||
}
|
||||
|
||||
function getBundledXbsIsolatedVmPath() {
|
||||
return path.join(__dirname, 'xbs-isolated-vm', getPlatformKey(), 'isolated_vm.node');
|
||||
}
|
||||
|
||||
function resolveXbsIsolatedVmPath(options = {}) {
|
||||
const explicit = options.xbsIsolatedVmPath || process.env.WEB_JS_ENV_PATCHER_XBS_ISOLATED_VM;
|
||||
return path.resolve(explicit || getBundledXbsIsolatedVmPath());
|
||||
}
|
||||
|
||||
function isAbiMismatch(error) {
|
||||
const message = String(error && error.message ? error.message : error || '');
|
||||
return /NODE_MODULE_VERSION|Module version mismatch|was compiled against a different Node\.js version|ABI/i.test(message);
|
||||
}
|
||||
|
||||
function formatLoadFailure(error, binaryPath) {
|
||||
const message = String(error && error.message ? error.message : error || '未知错误');
|
||||
const lines = [
|
||||
'无法加载随包魔改版 xbs isolated-vm。',
|
||||
'二进制路径:' + binaryPath,
|
||||
'当前平台:' + getPlatformKey(),
|
||||
'当前 Node:' + process.version + ',ABI:' + (process.versions && process.versions.modules ? process.versions.modules : 'unknown'),
|
||||
'原始错误:' + message,
|
||||
];
|
||||
if (!fs.existsSync(binaryPath)) {
|
||||
lines.push('当前平台目录下未找到 isolated_vm.node;请提供当前平台 / 架构 / Node ABI 匹配的魔改构建产物,或改选不使用框架 / vm / jsEnv。');
|
||||
} else if (isAbiMismatch(error)) {
|
||||
lines.push('这是 Node ABI 不匹配问题;请提供与当前 Node ABI 匹配的魔改 xbs isolated-vm 构建产物,或切换到匹配 Node 版本,或改选不使用框架 / vm / jsEnv。');
|
||||
} else {
|
||||
lines.push('请确认该二进制来自魔改 xbs isolated-vm,且与当前平台、架构、Node ABI 兼容。不要自动退回 npm 原版 isolated-vm。');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function normalizeIvmExport(loadedModule) {
|
||||
if (loadedModule && typeof loadedModule.Isolate === 'function') return loadedModule;
|
||||
if (loadedModule && loadedModule.ivm && typeof loadedModule.ivm.Isolate === 'function') return loadedModule.ivm;
|
||||
if (loadedModule && loadedModule.default && typeof loadedModule.default.Isolate === 'function') return loadedModule.default;
|
||||
if (loadedModule && loadedModule.default && loadedModule.default.ivm && typeof loadedModule.default.ivm.Isolate === 'function') return loadedModule.default.ivm;
|
||||
throw new TypeError('魔改 isolated_vm.node 已加载,但未导出可用的 Isolate 构造函数。');
|
||||
}
|
||||
|
||||
function loadXbsIsolatedVm(options = {}) {
|
||||
const binaryPath = resolveXbsIsolatedVmPath(options);
|
||||
try {
|
||||
const loadedModule = require(binaryPath);
|
||||
const ivm = normalizeIvmExport(loadedModule);
|
||||
return {
|
||||
ivm,
|
||||
binaryPath,
|
||||
platformKey: getPlatformKey(),
|
||||
nodeVersion: process.version,
|
||||
nodeAbi: process.versions && process.versions.modules ? process.versions.modules : '',
|
||||
expectedApis: EXPECTED_XBS_APIS.slice(),
|
||||
};
|
||||
} catch (error) {
|
||||
const wrapped = new Error(formatLoadFailure(error, binaryPath));
|
||||
wrapped.cause = error;
|
||||
wrapped.binaryPath = binaryPath;
|
||||
wrapped.abiMismatch = isAbiMismatch(error);
|
||||
throw wrapped;
|
||||
}
|
||||
}
|
||||
|
||||
async function maybeAwait(value) {
|
||||
return value && typeof value.then === 'function' ? await value : value;
|
||||
}
|
||||
|
||||
async function createContext(isolate) {
|
||||
if (typeof isolate.createContextSync === 'function') return isolate.createContextSync();
|
||||
return maybeAwait(isolate.createContext());
|
||||
}
|
||||
|
||||
async function compileScript(isolate, source, options) {
|
||||
if (typeof isolate.compileScriptSync === 'function') return isolate.compileScriptSync(String(source), options);
|
||||
return maybeAwait(isolate.compileScript(String(source), options));
|
||||
}
|
||||
|
||||
async function runCompiledScript(script, context, options) {
|
||||
if (typeof script.runSync === 'function') return script.runSync(context, options);
|
||||
return maybeAwait(script.run(context, options));
|
||||
}
|
||||
|
||||
async function runSource(isolate, context, source, options = {}) {
|
||||
const script = await compileScript(isolate, source, { filename: options.filename || 'anonymous.js' });
|
||||
return runCompiledScript(script, context, {
|
||||
timeout: options.timeoutMs || 5000,
|
||||
copy: options.copy !== false,
|
||||
});
|
||||
}
|
||||
|
||||
function copyIntoIsolate(ivm, value) {
|
||||
if (ivm && typeof ivm.ExternalCopy === 'function') {
|
||||
return new ivm.ExternalCopy(value).copyInto();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function setGlobalValue(ivm, globalRef, name, value) {
|
||||
const copied = copyIntoIsolate(ivm, value);
|
||||
if (globalRef && typeof globalRef.setSync === 'function') return globalRef.setSync(name, copied);
|
||||
if (globalRef && typeof globalRef.set === 'function') return maybeAwait(globalRef.set(name, copied));
|
||||
throw new Error('isolated-vm global 对象不支持 set:' + name);
|
||||
}
|
||||
|
||||
async function getGlobalValue(globalRef, name) {
|
||||
if (globalRef && typeof globalRef.getSync === 'function') return globalRef.getSync(name);
|
||||
if (globalRef && typeof globalRef.get === 'function') return maybeAwait(globalRef.get(name));
|
||||
throw new Error('isolated-vm global 对象不支持 get:' + name);
|
||||
}
|
||||
|
||||
function buildXbsSelfCheckSource() {
|
||||
const expectedJson = JSON.stringify(EXPECTED_XBS_APIS.slice().sort());
|
||||
return '(() => {\n' +
|
||||
' const expected = ' + expectedJson + ';\n' +
|
||||
' const api = globalThis.xbs || (typeof window !== "undefined" && window.xbs);\n' +
|
||||
' const apiNames = api ? Object.keys(api).sort() : [];\n' +
|
||||
' const missing = expected.filter(name => !apiNames.includes(name));\n' +
|
||||
' const extra = apiNames.filter(name => !expected.includes(name));\n' +
|
||||
' const dom = { ok: false, hasDom: false, hasCreateDocument: false };\n' +
|
||||
' try {\n' +
|
||||
' dom.hasDom = !!(api && api.dom);\n' +
|
||||
' dom.hasCreateDocument = !!(api && api.dom && typeof api.dom.createDocument === "function");\n' +
|
||||
' dom.windowDocumentWasPreinstalled = typeof window !== "undefined" && "document" in window;\n' +
|
||||
' if (dom.hasCreateDocument) {\n' +
|
||||
' const document = api.dom.createDocument({\n' +
|
||||
' url: "https://example.com/path?q=1",\n' +
|
||||
' html: "<main id=\\"app\\"><span class=\\"hot\\">hello</span><iframe id=\\"f\\"></iframe></main>"\n' +
|
||||
' });\n' +
|
||||
' const withoutAll = api.dom.createDocument({ omitApis: ["document.all"] });\n' +
|
||||
' const iframe = document.getElementById("f");\n' +
|
||||
' dom.documentCtor = document && document.constructor ? document.constructor.name : "";\n' +
|
||||
' dom.url = document.URL;\n' +
|
||||
' dom.documentURI = document.documentURI;\n' +
|
||||
' dom.text = document.querySelector(".hot").textContent;\n' +
|
||||
' dom.hasAll = "all" in document;\n' +
|
||||
' dom.allType = typeof document.all;\n' +
|
||||
' const allValue = document.all;\n' +
|
||||
' dom.allLooseNull = document.all == null;\n' +
|
||||
' dom.allStrictUndefined = document.all === undefined;\n' +
|
||||
' dom.allBoolean = Boolean(document.all);\n' +
|
||||
' dom.allLengthType = typeof document.all.length;\n' +
|
||||
' dom.allItemType = typeof document.all.item;\n' +
|
||||
' dom.allNamedItemType = typeof document.all.namedItem;\n' +
|
||||
' dom.allObjectToString = Object.prototype.toString.call(document.all);\n' +
|
||||
' dom.allOwnLength = Object.hasOwn(document.all, \"length\");\n' +
|
||||
' dom.allOwnItem = Object.hasOwn(document.all, \"item\");\n' +
|
||||
' dom.allOwnNamedItem = Object.hasOwn(document.all, \"namedItem\");\n' +
|
||||
' dom.allSameReference = allValue === document.all;\n' +
|
||||
' dom.omitAllWorks = !("all" in withoutAll);\n' +
|
||||
' dom.iframeContentDocument = !!(iframe && iframe.contentDocument && iframe.contentWindow && iframe.contentWindow.document === iframe.contentDocument);\n' +
|
||||
' dom.ok = dom.documentCtor === "HTMLDocument" && dom.text === "hello" &&\n' +
|
||||
' dom.url === "https://example.com/path?q=1" && dom.documentURI === dom.url &&\n' +
|
||||
' dom.hasAll && dom.allType === "undefined" && dom.allLooseNull &&\n' +
|
||||
' !dom.allStrictUndefined && dom.allBoolean === false &&\n' +
|
||||
' dom.allLengthType === "number" && dom.allItemType === "function" &&\n' +
|
||||
' dom.allNamedItemType === "function" && dom.allObjectToString === "[object HTMLAllCollection]" &&\n' +
|
||||
' dom.allOwnLength === false && dom.allOwnItem === false && dom.allOwnNamedItem === false &&\n' +
|
||||
' dom.allSameReference && dom.omitAllWorks;\n' +
|
||||
' }\n' +
|
||||
' } catch (error) {\n' +
|
||||
' dom.error = String(error && error.message ? error.message : error);\n' +
|
||||
' }\n' +
|
||||
' return {\n' +
|
||||
' windowIsGlobal: typeof window !== "undefined" && window === globalThis,\n' +
|
||||
' selfIsWindow: typeof self !== "undefined" && typeof window !== "undefined" && self === window,\n' +
|
||||
' topIsWindow: typeof top !== "undefined" && typeof window !== "undefined" && top === window,\n' +
|
||||
' parentIsWindow: typeof parent !== "undefined" && typeof window !== "undefined" && parent === window,\n' +
|
||||
' hasWindowConstructor: typeof Window === "function",\n' +
|
||||
' windowInstanceOfWindow: typeof Window === "function" && typeof window !== "undefined" && window instanceof Window,\n' +
|
||||
' hasXbs: !!api, hasXbsDom: dom.hasDom, hasCreateDocument: dom.hasCreateDocument, apiNames, missing, extra, dom\n' +
|
||||
' };\n' +
|
||||
'})()';
|
||||
}
|
||||
|
||||
async function inspectXbsApi(isolate, context, timeoutMs = 5000) {
|
||||
const result = await runSource(isolate, context, buildXbsSelfCheckSource(), {
|
||||
filename: 'xbs-self-check.js',
|
||||
timeoutMs,
|
||||
copy: true,
|
||||
});
|
||||
const ok = !!(
|
||||
result &&
|
||||
result.windowIsGlobal &&
|
||||
result.hasXbs &&
|
||||
Array.isArray(result.missing) &&
|
||||
result.missing.length === 0 &&
|
||||
result.hasXbsDom &&
|
||||
result.hasCreateDocument &&
|
||||
result.dom &&
|
||||
result.dom.ok
|
||||
);
|
||||
return Object.assign({ ok }, result || {});
|
||||
}
|
||||
|
||||
function buildXbsSelfCheckError(selfCheck) {
|
||||
const dom = selfCheck && selfCheck.dom ? selfCheck.dom : null;
|
||||
return [
|
||||
'xbs isolated-vm Context 自检失败。',
|
||||
'window === globalThis:' + (selfCheck && selfCheck.windowIsGlobal ? '是' : '否'),
|
||||
'是否存在 xbs:' + (selfCheck && selfCheck.hasXbs ? '是' : '否'),
|
||||
'缺失核心 API:' + (selfCheck && selfCheck.missing && selfCheck.missing.length ? selfCheck.missing.join(', ') : '无'),
|
||||
'额外 API:' + (selfCheck && selfCheck.extra && selfCheck.extra.length ? selfCheck.extra.join(', ') : '无'),
|
||||
'是否存在 xbs.dom:' + (selfCheck && selfCheck.hasXbsDom ? '是' : '否'),
|
||||
'是否存在 xbs.dom.createDocument:' + (selfCheck && selfCheck.hasCreateDocument ? '是' : '否'),
|
||||
'DOM smoke test:' + (dom && dom.ok ? '通过' : '未通过'),
|
||||
'DOM 错误:' + (dom && dom.error ? dom.error : '无'),
|
||||
'请确认加载的是魔改版 xbs isolated-vm,而不是 npm 原版 isolated-vm。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function buildInstallDomSource() {
|
||||
return '(() => {\n' +
|
||||
' const options = Object.assign({}, globalThis.__xbsDomOptions__ || {});\n' +
|
||||
' const attachToWindow = options.attachToWindow !== false;\n' +
|
||||
' delete options.enabled;\n' +
|
||||
' delete options.attachToWindow;\n' +
|
||||
' const document = xbs.dom.createDocument(options);\n' +
|
||||
' if (attachToWindow) {\n' +
|
||||
' Object.defineProperty(window, "document", {\n' +
|
||||
' value: document,\n' +
|
||||
' writable: true,\n' +
|
||||
' enumerable: true,\n' +
|
||||
' configurable: true\n' +
|
||||
' });\n' +
|
||||
' }\n' +
|
||||
' globalThis.__xbsDocument__ = document;\n' +
|
||||
' return {\n' +
|
||||
' enabled: true,\n' +
|
||||
' attachToWindow,\n' +
|
||||
' hasWindowDocument: typeof window !== "undefined" && "document" in window,\n' +
|
||||
' documentCtor: document && document.constructor ? document.constructor.name : "",\n' +
|
||||
' url: document.URL,\n' +
|
||||
' documentURI: document.documentURI,\n' +
|
||||
' hasAll: "all" in document,\n' +
|
||||
' allType: typeof document.all,\n' +
|
||||
' allLooseNull: document.all == null,\n' +
|
||||
' allStrictUndefined: document.all === undefined,\n' +
|
||||
' allBoolean: Boolean(document.all),\n' +
|
||||
' allLengthType: typeof document.all.length,\n' +
|
||||
' allItemType: typeof document.all.item,\n' +
|
||||
' allNamedItemType: typeof document.all.namedItem,\n' +
|
||||
' allObjectToString: Object.prototype.toString.call(document.all),\n' +
|
||||
' allOwnLength: Object.hasOwn(document.all, \"length\"),\n' +
|
||||
' allOwnItem: Object.hasOwn(document.all, \"item\"),\n' +
|
||||
' allOwnNamedItem: Object.hasOwn(document.all, \"namedItem\")\n' +
|
||||
' };\n' +
|
||||
'})()';
|
||||
}
|
||||
|
||||
function createIsolatedVmRuntime(options = {}) {
|
||||
const loaded = loadXbsIsolatedVm(options);
|
||||
const ivm = loaded.ivm;
|
||||
const installEnvSource = options.installEnvSource || '';
|
||||
const memoryLimit = options.memoryLimitMb || 128;
|
||||
const sourceRoot = resolveSourceRoot(options);
|
||||
const isolate = new ivm.Isolate({ memoryLimit });
|
||||
let context = null;
|
||||
let jail = null;
|
||||
let xbsSelfCheck = null;
|
||||
let domInstallSummary = null;
|
||||
|
||||
return {
|
||||
name: 'xbs-isolated-vm',
|
||||
mode: 'isolated-vm',
|
||||
isolate,
|
||||
binaryPath: loaded.binaryPath,
|
||||
nodeAbi: loaded.nodeAbi,
|
||||
platformKey: loaded.platformKey,
|
||||
|
||||
async initialize(fixture = {}) {
|
||||
context = await createContext(isolate);
|
||||
jail = context.global;
|
||||
await setGlobalValue(ivm, jail, '__fixture__', fixture);
|
||||
xbsSelfCheck = await inspectXbsApi(isolate, context, options.timeoutMs || 5000);
|
||||
if (options.requireXbs !== false && !xbsSelfCheck.ok) throw new Error(buildXbsSelfCheckError(xbsSelfCheck));
|
||||
if (options.dom && options.dom.enabled) {
|
||||
await setGlobalValue(ivm, jail, '__xbsDomOptions__', options.dom);
|
||||
domInstallSummary = await runSource(isolate, context, buildInstallDomSource(), {
|
||||
filename: 'xbs-dom-install.js',
|
||||
timeoutMs: options.timeoutMs || 5000,
|
||||
copy: true,
|
||||
});
|
||||
}
|
||||
if (installEnvSource) {
|
||||
await runSource(isolate, context, String(installEnvSource), {
|
||||
filename: 'install-env.js',
|
||||
timeoutMs: options.timeoutMs || 5000,
|
||||
copy: true,
|
||||
});
|
||||
}
|
||||
return { context, jail, xbs: xbsSelfCheck, dom: domInstallSummary, binaryPath: loaded.binaryPath, nodeAbi: loaded.nodeAbi, platformKey: loaded.platformKey };
|
||||
},
|
||||
|
||||
async load(sourceCode, meta = {}) {
|
||||
if (!context) throw new Error('xbs isolated-vm runtime 尚未初始化');
|
||||
return runSource(isolate, context, String(sourceCode), {
|
||||
filename: meta.filename || 'target.js',
|
||||
timeoutMs: options.timeoutMs || 5000,
|
||||
copy: meta.copy !== false,
|
||||
});
|
||||
},
|
||||
|
||||
async runFile(filePath, meta = {}) {
|
||||
if (!context) throw new Error('xbs isolated-vm runtime 尚未初始化');
|
||||
const loadedFile = readSourceFile(sourceRoot, filePath, {
|
||||
filename: meta.filename,
|
||||
allowOutsideSourceRoot: meta.allowOutsideSourceRoot || options.allowOutsideSourceRoot,
|
||||
});
|
||||
return runSource(isolate, context, loadedFile.source, {
|
||||
filename: loadedFile.filename,
|
||||
timeoutMs: meta.timeoutMs || options.timeoutMs || 5000,
|
||||
copy: meta.copy !== false,
|
||||
});
|
||||
},
|
||||
|
||||
async runFiles(files, meta = {}) {
|
||||
if (!Array.isArray(files)) throw new TypeError('runFiles 需要传入文件路径数组');
|
||||
const results = [];
|
||||
for (const item of files) {
|
||||
const fileItem = typeof item === 'string' ? { path: item } : Object(item || {});
|
||||
results.push(await this.runFile(fileItem.path || fileItem.file, Object.assign({}, meta, fileItem.options || {}, {
|
||||
filename: fileItem.filename || (fileItem.options && fileItem.options.filename),
|
||||
})));
|
||||
}
|
||||
return results;
|
||||
},
|
||||
|
||||
getSourceRoot() {
|
||||
return sourceRoot;
|
||||
},
|
||||
|
||||
async call(entry, args = []) {
|
||||
if (!context) throw new Error('xbs isolated-vm runtime 尚未初始化');
|
||||
const fn = await getGlobalValue(context.global, String(entry));
|
||||
if (!fn || typeof fn.apply !== 'function') throw new TypeError('入口函数不存在或不可调用:' + String(entry));
|
||||
const copiedArgs = copyIntoIsolate(ivm, args);
|
||||
return maybeAwait(fn.apply(undefined, copiedArgs, {
|
||||
arguments: { copy: true },
|
||||
result: { copy: true },
|
||||
timeout: options.timeoutMs || 5000,
|
||||
}));
|
||||
},
|
||||
|
||||
getXbsApiSummary() {
|
||||
return xbsSelfCheck ? Object.assign({}, xbsSelfCheck) : null;
|
||||
},
|
||||
|
||||
getDomInstallSummary() {
|
||||
return domInstallSummary ? Object.assign({}, domInstallSummary) : null;
|
||||
},
|
||||
|
||||
async dispose() {
|
||||
if (isolate && !isolate.isDisposed && typeof isolate.dispose === 'function') isolate.dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
EXPECTED_XBS_APIS,
|
||||
buildXbsSelfCheckError,
|
||||
createIsolatedVmRuntime,
|
||||
createXbsIsolatedVmRuntime: createIsolatedVmRuntime,
|
||||
getBundledXbsIsolatedVmPath,
|
||||
getPlatformKey,
|
||||
inspectXbsApi,
|
||||
buildInstallDomSource,
|
||||
isAbiMismatch,
|
||||
loadXbsIsolatedVm,
|
||||
normalizeIvmExport,
|
||||
readSourceFile,
|
||||
resolveSourceFilePath,
|
||||
resolveSourceRoot,
|
||||
resolveXbsIsolatedVmPath,
|
||||
toPosixPath,
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
// jsEnv runtime 适配模板:仅在用户明确选择 jsEnv 且提供项目路径、入口和文档后复制到最终项目。
|
||||
// 本模板不假设任何具体 jsEnv API;必须由 case 根据用户提供的 jsEnv 文档注入 adapter。
|
||||
'use strict';
|
||||
|
||||
function loadJsEnvAdapter(options = {}) {
|
||||
if (options.adapter && typeof options.adapter === 'object') return options.adapter;
|
||||
if (typeof options.adapterFactory === 'function') return options.adapterFactory(options);
|
||||
if (options.adapterPath) {
|
||||
// adapterPath 应为最终项目内的相对路径或用户运行时显式提供的路径,不要写入本机绝对路径。
|
||||
return require(options.adapterPath);
|
||||
}
|
||||
throw new Error('已选择 jsEnv,但未提供 adapter、adapterFactory 或 adapterPath;请先根据 jsEnv 文档完成适配。');
|
||||
}
|
||||
|
||||
function assertAdapter(adapter) {
|
||||
const missing = [];
|
||||
for (const name of ['initialize', 'load', 'call']) {
|
||||
if (typeof adapter[name] !== 'function') missing.push(name);
|
||||
}
|
||||
if (missing.length) {
|
||||
throw new Error(`jsEnv adapter 缺少必要方法:${missing.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
function createJsEnvRuntime(options = {}) {
|
||||
const adapter = loadJsEnvAdapter(options);
|
||||
assertAdapter(adapter);
|
||||
let runtimeState = null;
|
||||
|
||||
return {
|
||||
name: 'jsEnv',
|
||||
|
||||
async initialize(fixture = {}) {
|
||||
runtimeState = await adapter.initialize({
|
||||
fixture,
|
||||
options,
|
||||
});
|
||||
return runtimeState;
|
||||
},
|
||||
|
||||
async load(sourceCode, meta = {}) {
|
||||
if (!runtimeState) throw new Error('jsEnv runtime 尚未初始化');
|
||||
return adapter.load(runtimeState, String(sourceCode), meta);
|
||||
},
|
||||
|
||||
async call(entry, args = []) {
|
||||
if (!runtimeState) throw new Error('jsEnv runtime 尚未初始化');
|
||||
return adapter.call(runtimeState, entry, args);
|
||||
},
|
||||
|
||||
async dispose() {
|
||||
if (adapter.dispose && runtimeState) {
|
||||
await adapter.dispose(runtimeState);
|
||||
}
|
||||
runtimeState = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { createJsEnvRuntime };
|
||||
@@ -0,0 +1,38 @@
|
||||
// 默认 runtime 模板:不使用 isolated-vm、vm 或 jsEnv 框架。
|
||||
// 复制到最终项目前,应先完成 Node 泄露阻断,并由 installEnv 安装浏览器环境对象。
|
||||
'use strict';
|
||||
|
||||
function createNoFrameworkRuntime(options = {}) {
|
||||
const globalObject = options.globalObject || globalThis;
|
||||
const installEnv = options.installEnv || function installEnv() {};
|
||||
const loadTarget = options.loadTarget || function loadTarget() {};
|
||||
|
||||
return {
|
||||
name: 'none',
|
||||
|
||||
async initialize(fixture = {}) {
|
||||
// 在当前受控全局对象上安装补环境,字段值来自 fixture 和取证样本
|
||||
await installEnv(globalObject, fixture, options);
|
||||
return globalObject;
|
||||
},
|
||||
|
||||
async load(sourceCode, meta = {}) {
|
||||
// 不使用框架时由调用方提供安全加载逻辑,不在模板中暴露 require/process
|
||||
return loadTarget(globalObject, sourceCode, meta);
|
||||
},
|
||||
|
||||
async call(entry, args = []) {
|
||||
const fn = typeof entry === 'function' ? entry : globalObject[entry];
|
||||
if (typeof fn !== 'function') {
|
||||
throw new TypeError(`入口函数不存在:${String(entry)}`);
|
||||
}
|
||||
return fn.apply(globalObject, args);
|
||||
},
|
||||
|
||||
async dispose() {
|
||||
// 默认模式没有额外 isolate 资源需要释放
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { createNoFrameworkRuntime };
|
||||
@@ -0,0 +1,38 @@
|
||||
// runtime 工厂模板:根据用户确认的补环境框架选择加载对应 runtime。
|
||||
// 未选择框架时默认 none;不要因为 Trace 复杂度高而自动切换到 isolated-vm、vm 或 jsEnv。
|
||||
'use strict';
|
||||
|
||||
function normalizeRuntimeMode(mode) {
|
||||
const value = String(mode || 'none').trim().toLowerCase();
|
||||
if (value === 'isolated-vm' || value === 'isolated_vm' || value === 'ivm') return 'isolated-vm';
|
||||
if (value === 'vm' || value === 'node-vm' || value === 'nodejs-vm') return 'vm';
|
||||
if (value === 'jsenv' || value === 'js-env' || value === 'js_env') return 'jsEnv';
|
||||
return 'none';
|
||||
}
|
||||
|
||||
function createRuntime(options = {}) {
|
||||
const mode = normalizeRuntimeMode(options.mode);
|
||||
|
||||
if (mode === 'isolated-vm') {
|
||||
const { createIsolatedVmRuntime } = require('./isolated-vm-runtime');
|
||||
return createIsolatedVmRuntime(options);
|
||||
}
|
||||
|
||||
if (mode === 'vm') {
|
||||
const { createVmRuntime } = require('./vm-runtime');
|
||||
return createVmRuntime(options);
|
||||
}
|
||||
|
||||
if (mode === 'jsEnv') {
|
||||
const { createJsEnvRuntime } = require('./jsenv-runtime');
|
||||
return createJsEnvRuntime(options);
|
||||
}
|
||||
|
||||
const { createNoFrameworkRuntime } = require('./no-framework-runtime');
|
||||
return createNoFrameworkRuntime(options);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createRuntime,
|
||||
normalizeRuntimeMode,
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
// Node.js vm runtime 模板:仅在用户明确选择 vm 时复制到最终项目。
|
||||
// vm 是轻量隔离,不是强安全边界;仍需严格阻断 Node 泄露。
|
||||
'use strict';
|
||||
|
||||
const vm = require('vm');
|
||||
|
||||
function createVmRuntime(options = {}) {
|
||||
const installEnv = options.installEnv || function installEnv() {};
|
||||
const sandbox = Object.create(null);
|
||||
const context = vm.createContext(sandbox, {
|
||||
name: options.name || 'web-js-env-patcher-context',
|
||||
codeGeneration: options.codeGeneration || {
|
||||
strings: true,
|
||||
wasm: false,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
name: 'vm',
|
||||
context,
|
||||
sandbox,
|
||||
|
||||
async initialize(fixture = {}) {
|
||||
// 先安装浏览器式环境对象,不向目标上下文注入 process、Buffer、require、module
|
||||
await installEnv(sandbox, fixture, options);
|
||||
return sandbox;
|
||||
},
|
||||
|
||||
async load(sourceCode, meta = {}) {
|
||||
const script = new vm.Script(String(sourceCode), {
|
||||
filename: meta.filename || 'target.js',
|
||||
displayErrors: true,
|
||||
});
|
||||
return script.runInContext(context, {
|
||||
timeout: options.timeoutMs || 5000,
|
||||
displayErrors: true,
|
||||
});
|
||||
},
|
||||
|
||||
async call(entry, args = []) {
|
||||
const fn = typeof entry === 'function' ? entry : sandbox[entry];
|
||||
if (typeof fn !== 'function') {
|
||||
throw new TypeError(`入口函数不存在:${String(entry)}`);
|
||||
}
|
||||
return fn.apply(sandbox, args);
|
||||
},
|
||||
|
||||
async dispose() {
|
||||
// vm context 没有显式释放 API,敏感状态由外层清理 case 临时目录时处理
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { createVmRuntime };
|
||||
@@ -0,0 +1,640 @@
|
||||
# XBS API 使用说明
|
||||
|
||||
本文档说明当前魔改版 `isolated-vm` 中 `window.xbs` 的用法。
|
||||
`xbs` 只会注入到 isolated-vm 创建出的浏览器式 Context 内,即 `window.xbs` / `globalThis.xbs`,不会污染宿主侧 `require("isolated-vm")` 的原始 API。
|
||||
|
||||
## 1. 基本使用方式
|
||||
|
||||
```js
|
||||
const ivm = require("./isolated-vm");
|
||||
|
||||
const isolate = new ivm.Isolate();
|
||||
const context = isolate.createContextSync();
|
||||
|
||||
const result = isolate.compileScriptSync(`
|
||||
({
|
||||
windowIsGlobal: window === globalThis,
|
||||
hasXbs: !!window.xbs,
|
||||
apiNames: Object.keys(window.xbs).sort(),
|
||||
})
|
||||
`).runSync(context, { copy: true });
|
||||
```
|
||||
|
||||
在 Context 内可以直接使用:
|
||||
|
||||
```js
|
||||
const fn = xbs.createNativeFunction(false, "demo", 0, function () {
|
||||
return "ok";
|
||||
});
|
||||
|
||||
fn(); // "ok"
|
||||
```
|
||||
|
||||
当前 Context 默认具备以下基础关系:
|
||||
|
||||
```js
|
||||
window === globalThis; // true
|
||||
self === window; // true
|
||||
top === window; // true
|
||||
parent === window; // true
|
||||
window instanceof Window; // true
|
||||
Object.getPrototypeOf(window) === Window.prototype; // true
|
||||
Object.getPrototypeOf(Window.prototype) === WindowProperties.prototype; // true
|
||||
Object.getPrototypeOf(WindowProperties.prototype) === EventTarget.prototype; // true
|
||||
```
|
||||
|
||||
## 2. API 总览
|
||||
|
||||
当前 `window.xbs` 暴露 17 个 API:
|
||||
|
||||
| API | 说明 |
|
||||
| --- | --- |
|
||||
| `createNativeObject` | 创建带 native-like 构造函数、实例与原型链的对象。 |
|
||||
| `createNativeFunction` | 创建 native-like 函数或构造函数。 |
|
||||
| `createProtoChains` | 批量创建浏览器风格构造函数、原型链、别名、实例工厂与注册表。 |
|
||||
| `createGetter` | 创建 native-like getter。 |
|
||||
| `createSetter` | 创建 native-like setter。 |
|
||||
| `createInterceptor` | 创建带 V8 属性拦截器的对象。 |
|
||||
| `createNativeCollection` | 创建 `HTMLCollection` / `NodeList` 等浏览器集合风格对象。 |
|
||||
| `getMimeTypesAndPlugins` | 创建浏览器风格 `navigator.mimeTypes` 与 `navigator.plugins`。 |
|
||||
| `createUndetectable` | 创建 V8 undetectable 对象,常用于近似 `document.all`。 |
|
||||
| `getPrivate` | 读取 V8 Private slot。 |
|
||||
| `setPrivate` | 写入 V8 Private slot。 |
|
||||
| `hasPrivate` | 判断 V8 Private slot 是否存在。 |
|
||||
| `deletePrivate` | 删除 V8 Private slot。 |
|
||||
| `getProtoChainRegistry` | 查看当前 isolate 的原型链注册表。 |
|
||||
| `clearProtoChainRegistry` | 清空当前 isolate 的原型链注册表。 |
|
||||
| `deleteProtoChainRegistryEntry` | 删除当前 isolate 注册表中的指定构造函数或别名。 |
|
||||
| `throwTypeError` | 从 native 层抛出 `TypeError`。 |
|
||||
|
||||
## 3. `createNativeFunction(isConstructor, name, length, callback)`
|
||||
|
||||
创建 `Function.prototype.toString` 结果为 native-like 的函数。
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `isConstructor` | `boolean` | 是否允许 `new` 调用。 |
|
||||
| `name` | `string` | 函数名。 |
|
||||
| `length` | `number` | 函数的 `length`。 |
|
||||
| `callback` | `function` | 实际 JS 回调。 |
|
||||
|
||||
### 普通函数示例
|
||||
|
||||
```js
|
||||
const add = xbs.createNativeFunction(false, "add", 2, function (a, b) {
|
||||
return a + b;
|
||||
});
|
||||
|
||||
add(1, 2); // 3
|
||||
Function.prototype.toString.call(add);
|
||||
// "function add() { [native code] }"
|
||||
```
|
||||
|
||||
### 构造函数示例
|
||||
|
||||
构造调用时,`callback` 的第一个参数是 `isNew`,后面才是实际传入参数。构造调用最终返回 `this`。
|
||||
|
||||
```js
|
||||
const Storage = xbs.createNativeFunction(true, "Storage", 0, function (isNew) {
|
||||
this.createdByNew = isNew;
|
||||
});
|
||||
|
||||
const storage = new Storage();
|
||||
storage.createdByNew; // true
|
||||
```
|
||||
|
||||
## 4. `createGetter(name, length, callback)`
|
||||
|
||||
创建 native-like getter,常配合 `Object.defineProperty` 使用。
|
||||
|
||||
```js
|
||||
const navigator = {};
|
||||
|
||||
Object.defineProperty(navigator, "userAgent", {
|
||||
get: xbs.createGetter("userAgent", 0, function () {
|
||||
return "Mozilla/5.0";
|
||||
}),
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
navigator.userAgent; // "Mozilla/5.0"
|
||||
Function.prototype.toString.call(Object.getOwnPropertyDescriptor(navigator, "userAgent").get);
|
||||
// "function get userAgent() { [native code] }"
|
||||
```
|
||||
|
||||
## 5. `createSetter(name, length, callback)`
|
||||
|
||||
创建 native-like setter。
|
||||
|
||||
```js
|
||||
const location = {};
|
||||
let href = "https://example.com/";
|
||||
|
||||
Object.defineProperty(location, "href", {
|
||||
get: xbs.createGetter("href", 0, function () {
|
||||
return href;
|
||||
}),
|
||||
set: xbs.createSetter("href", 1, function (value) {
|
||||
href = String(value);
|
||||
}),
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
location.href = "https://example.org/";
|
||||
location.href; // "https://example.org/"
|
||||
```
|
||||
|
||||
## 6. `createProtoChains(descriptors)`
|
||||
|
||||
批量创建构造函数、原型对象、继承关系、实例工厂和别名。该 API 是补浏览器对象模型时最常用的基础能力。
|
||||
|
||||
### 常用 descriptor 字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `name` | `string` | 构造函数名称。必填。 |
|
||||
| `length` | `number` | 构造函数 `length`。 |
|
||||
| `constructor` | `function` | 构造函数回调。普通构造函数必填。 |
|
||||
| `prototypeParent` | `string` | 原型父级构造函数名称。 |
|
||||
| `constructorParent` | `string` / `null` | 构造函数对象父级。未设置时默认跟随 `prototypeParent`;设为 `null` 可禁用默认继承。 |
|
||||
| `readOnlyPrototypeProperty` | `boolean` | 是否让构造函数的 `.prototype` 属性只读。 |
|
||||
| `immutablePrototypeObject` | `boolean` | 是否禁止修改构造函数 `.prototype` 对象的原型。 |
|
||||
| `immutableInstancePrototype` | `boolean` | 是否禁止修改实例对象的原型。 |
|
||||
| `hasToStringTag` | `boolean` | 是否在 prototype 上设置 `Symbol.toStringTag`,默认 `true`。 |
|
||||
| `toStringTag` | `string` | 自定义 `Symbol.toStringTag` 值。 |
|
||||
| `aliases` | `string[]` | 当前构造函数的别名列表。 |
|
||||
| `aliasOf` | `string` | 创建指向已有构造函数的别名。 |
|
||||
| `internalClassName` | `string` | 设置内部类名,用于影响 `Object.prototype.toString`。 |
|
||||
| `constructorBehavior` | `"allow" \| "throw" \| "illegal"` | 控制 `new Constructor()` 行为。 |
|
||||
| `callBehavior` | `"allow" \| "throw" \| "illegal"` | 控制 `Constructor()` 直接调用行为。 |
|
||||
| `constructorErrorMessage` | `string` | 构造调用报错信息。 |
|
||||
| `callErrorMessage` | `string` | 直接调用报错信息。 |
|
||||
| `prototypeMethods` | `Array` | 安装到 prototype 的 native-like 方法。 |
|
||||
| `staticMethods` | `Array` | 安装到构造函数对象上的 native-like 静态方法。 |
|
||||
| `instanceFactoryName` | `string` | 额外导出一个内部实例工厂。 |
|
||||
| `instanceInitializer` | `function` | 实例工厂创建对象后的初始化函数。 |
|
||||
|
||||
### 创建继承链
|
||||
|
||||
```js
|
||||
const chain = xbs.createProtoChains([
|
||||
{
|
||||
name: "EventTarget",
|
||||
length: 0,
|
||||
constructor: function () {},
|
||||
readOnlyPrototypeProperty: true,
|
||||
immutablePrototypeObject: true,
|
||||
},
|
||||
{
|
||||
name: "Node",
|
||||
length: 0,
|
||||
constructor: function () {},
|
||||
prototypeParent: "EventTarget",
|
||||
readOnlyPrototypeProperty: true,
|
||||
immutablePrototypeObject: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const EventTarget = chain.EventTarget;
|
||||
const Node = chain.Node;
|
||||
|
||||
Object.getPrototypeOf(Node.prototype) === EventTarget.prototype; // true
|
||||
Object.getPrototypeOf(Node) === EventTarget; // true
|
||||
new Node() instanceof EventTarget; // true
|
||||
```
|
||||
|
||||
### 非法构造 + 内部实例工厂
|
||||
|
||||
适合模拟 `Document`、`Location` 等不能直接 `new`,但框架内部需要创建实例的对象。
|
||||
|
||||
```js
|
||||
const chain = xbs.createProtoChains([
|
||||
{
|
||||
name: "Document",
|
||||
length: 0,
|
||||
constructor() {},
|
||||
constructorBehavior: "throw",
|
||||
constructorErrorMessage: "Illegal constructor",
|
||||
callBehavior: "throw",
|
||||
callErrorMessage: "Illegal constructor",
|
||||
instanceFactoryName: "createDocument",
|
||||
instanceInitializer(url) {
|
||||
this.URL = url;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const Document = chain.Document;
|
||||
const document = chain.createDocument("https://example.com/");
|
||||
|
||||
document instanceof Document; // true
|
||||
new Document(); // TypeError: Illegal constructor
|
||||
```
|
||||
|
||||
## 7. 原型链注册表 API
|
||||
|
||||
`createProtoChains` 创建的构造函数会登记到当前 isolate 的注册表中。注册表按 isolate 隔离,不同 `new ivm.Isolate()` 之间不会互相污染。
|
||||
|
||||
```js
|
||||
const registry = xbs.getProtoChainRegistry();
|
||||
registry.constructors; // 构造函数名称数组
|
||||
registry.aliases; // 别名映射对象
|
||||
|
||||
xbs.deleteProtoChainRegistryEntry("URL"); // 删除指定构造函数或别名
|
||||
xbs.clearProtoChainRegistry(); // 清空当前 isolate 的注册表
|
||||
```
|
||||
|
||||
## 8. `createNativeObject(options)`
|
||||
|
||||
创建带 native-like 构造函数、实例对象与可选父级原型链的对象。新代码更推荐使用 `createProtoChains`,但该 API 仍可用于兼容旧逻辑。
|
||||
|
||||
```js
|
||||
const result = xbs.createNativeObject({
|
||||
name: "Navigator",
|
||||
length: 0,
|
||||
constructor: function () {},
|
||||
isReadOnlyPrototype: true,
|
||||
isImmutableProto: true,
|
||||
});
|
||||
|
||||
result.constructor; // Navigator 构造函数
|
||||
result.instance; // Navigator 实例
|
||||
```
|
||||
|
||||
## 9. `createInterceptor(options)`
|
||||
|
||||
创建带 V8 属性拦截器的对象,用于模拟需要自定义读取、写入、枚举、描述符等行为的宿主对象。
|
||||
|
||||
### handlers
|
||||
|
||||
| handler | 签名 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `getter` | `(target, property)` | 读取属性时调用。返回 `{ intercept: false }` 可放行默认读取。 |
|
||||
| `setter` | `(target, property, value)` | 写属性时调用。可返回 `{ intercept: true, value }` 改写保存值。 |
|
||||
| `query` | `(target, property)` | 属性存在性查询。 |
|
||||
| `deleter` | `(target, property)` | 删除属性时调用。 |
|
||||
| `enumerator` | `(target)` | 枚举属性名时调用。 |
|
||||
| `definer` | `(target, property, descriptor)` | `Object.defineProperty` 时调用。 |
|
||||
| `descriptor` | `(target, property)` | `Object.getOwnPropertyDescriptor` 时调用。 |
|
||||
|
||||
### 示例
|
||||
|
||||
```js
|
||||
const target = { existing: 1 };
|
||||
|
||||
const obj = xbs.createInterceptor({
|
||||
target,
|
||||
internalClassName: "MagicObject",
|
||||
handlers: {
|
||||
getter(targetObject, property) {
|
||||
if (property === "virtual") {
|
||||
return { value: 42 };
|
||||
}
|
||||
return { intercept: false };
|
||||
},
|
||||
setter(targetObject, property, value) {
|
||||
return { intercept: true, value: String(value) };
|
||||
},
|
||||
descriptor(targetObject, property) {
|
||||
if (property === "virtual") {
|
||||
return {
|
||||
value: 42,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
enumerator() {
|
||||
return ["virtual"];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
obj.virtual; // 42
|
||||
obj.existing; // 1
|
||||
obj.created = 123;
|
||||
target.created; // "123"
|
||||
Object.keys(obj); // ["virtual"]
|
||||
Object.prototype.toString.call(obj); // "[object MagicObject]"
|
||||
```
|
||||
|
||||
## 10. `createNativeCollection(options)`
|
||||
|
||||
创建浏览器集合对象,例如 `HTMLCollection`、`NodeList`、`PluginArray` 类似对象。
|
||||
|
||||
```js
|
||||
const first = { id: 1 };
|
||||
const second = { id: 2 };
|
||||
|
||||
const result = xbs.createNativeCollection({
|
||||
name: "HTMLCollection",
|
||||
items: [
|
||||
{ name: "first", value: first },
|
||||
{ name: "second", value: second },
|
||||
],
|
||||
hasToStringTag: false,
|
||||
internalClassName: "HTMLCollection",
|
||||
});
|
||||
|
||||
const collection = result.collection;
|
||||
|
||||
collection.length; // 2
|
||||
collection[0] === first; // true
|
||||
collection.first === first; // true
|
||||
collection.item(1) === second; // true
|
||||
collection.namedItem("second") === second; // true
|
||||
collection.namedItem("missing"); // null
|
||||
[...collection]; // [first, second]
|
||||
Object.prototype.toString.call(collection); // "[object HTMLCollection]"
|
||||
```
|
||||
|
||||
返回对象通常包含:
|
||||
|
||||
- `collection`:集合实例。
|
||||
- `constructor`:集合构造函数。
|
||||
- `[name]`:以集合名称命名的构造函数,例如 `result.HTMLCollection`。
|
||||
|
||||
## 11. `getMimeTypesAndPlugins([config])`
|
||||
|
||||
创建浏览器风格 `PluginArray`、`MimeTypeArray`、`Plugin`、`MimeType`,适合补 `navigator.plugins` 和 `navigator.mimeTypes`。
|
||||
|
||||
### 默认用法
|
||||
|
||||
```js
|
||||
const {
|
||||
mimeTypes,
|
||||
plugins,
|
||||
PluginArray,
|
||||
MimeTypeArray,
|
||||
MimeType,
|
||||
Plugin,
|
||||
} = xbs.getMimeTypesAndPlugins();
|
||||
|
||||
navigator.plugins = plugins;
|
||||
navigator.mimeTypes = mimeTypes;
|
||||
```
|
||||
|
||||
### 参数化配置
|
||||
|
||||
```js
|
||||
const result = xbs.getMimeTypesAndPlugins({
|
||||
plugins: [
|
||||
{
|
||||
name: "Custom PDF Viewer",
|
||||
filename: "custom-pdf-viewer",
|
||||
description: "Custom Portable Document Format",
|
||||
mimeTypes: [
|
||||
{
|
||||
type: "application/x-custom-pdf",
|
||||
suffixes: "cpdf",
|
||||
description: "Custom PDF",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
result.plugins.length; // 1
|
||||
result.mimeTypes.length; // 1
|
||||
result.plugins[0].name; // "Custom PDF Viewer"
|
||||
result.mimeTypes[0].type; // "application/x-custom-pdf"
|
||||
result.mimeTypes[0].enabledPlugin === result.plugins[0]; // true
|
||||
```
|
||||
|
||||
## 12. `createUndetectable(callback[, handlers])`
|
||||
|
||||
创建 V8 `MarkAsUndetectable()` 对象,常用于近似 `document.all` / HTMLDDA 行为。
|
||||
|
||||
```js
|
||||
const all = xbs.createUndetectable(function () {
|
||||
return undefined;
|
||||
});
|
||||
|
||||
typeof all; // "undefined"
|
||||
Boolean(all); // false
|
||||
all == null; // true
|
||||
all === undefined; // false
|
||||
all(); // undefined
|
||||
```
|
||||
|
||||
### handler 行为
|
||||
|
||||
`createUndetectable` 支持与 `createInterceptor` 类似的 `handlers`,常用字段如下:
|
||||
|
||||
| handler | 签名 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `getter` | `(target, property)` | 读取属性时调用。返回 `{ value }` 可直接返回虚拟值;返回 `{ intercept: false }` 会放行,让 V8 继续查找 backing target 或原型链。 |
|
||||
| `query` | `(target, property)` | 属性存在性查询。可返回属性 attribute 数值;也可配合 `descriptor` 生成属性描述。 |
|
||||
| `descriptor` | `(target, property)` | `Object.getOwnPropertyDescriptor` 时调用。可返回标准 descriptor;返回 `{ intercept: false }` 表示放行。 |
|
||||
| `enumerator` | `(target)` | 枚举属性名时调用。 |
|
||||
| `setter` / `definer` / `deleter` | 同 `createInterceptor` | 用于写入、`Object.defineProperty` 和删除。 |
|
||||
|
||||
注意:当前实现已经支持原型链 fallback。也就是说,如果 handler 不存在,或 handler 返回 `{ intercept: false }`,并且 backing target 没有该自有属性,读取会继续查找对象原型链。这对手动模拟 `document.all.length`、`document.all.item()`、`Symbol.toStringTag` 很重要。
|
||||
|
||||
模拟 `document.all` 时,建议 **all 创建都带 handlers**,并把 `length / item / namedItem / Symbol.toStringTag` 放到 `HTMLAllCollection.prototype` 上,避免 `length` 变成 `document.all` 的自有属性:
|
||||
|
||||
```js
|
||||
const document = { _elements: [] };
|
||||
|
||||
const chain = xbs.createProtoChains([{
|
||||
name: "HTMLAllCollection",
|
||||
length: 0,
|
||||
constructor() {},
|
||||
constructorBehavior: "throw",
|
||||
callBehavior: "throw",
|
||||
readOnlyPrototypeProperty: true,
|
||||
hasToStringTag: true,
|
||||
toStringTag: "HTMLAllCollection",
|
||||
}]);
|
||||
|
||||
const HTMLAllCollection = chain.HTMLAllCollection;
|
||||
|
||||
Object.defineProperty(HTMLAllCollection.prototype, "length", {
|
||||
get: xbs.createGetter("length", 0, function () {
|
||||
return document._elements.length;
|
||||
}),
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(HTMLAllCollection.prototype, "item", {
|
||||
value: xbs.createNativeFunction(false, "item", 1, function (index) {
|
||||
index = Number(index) >>> 0;
|
||||
return index < document._elements.length ? document._elements[index] : null;
|
||||
}),
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(HTMLAllCollection.prototype, "namedItem", {
|
||||
value: xbs.createNativeFunction(false, "namedItem", 1, function (name) {
|
||||
name = String(name);
|
||||
return document._elements.find(el => el.id === name || el.name === name) || null;
|
||||
}),
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
function isIndex(property) {
|
||||
return typeof property === "number" || /^(0|[1-9]\d*)$/.test(String(property));
|
||||
}
|
||||
|
||||
function resolveAll(property) {
|
||||
if (isIndex(property)) {
|
||||
const index = Number(property) >>> 0;
|
||||
return index < document._elements.length
|
||||
? { value: document._elements[index], enumerable: true }
|
||||
: null;
|
||||
}
|
||||
const name = String(property);
|
||||
if (name === "length" || name === "item" || name === "namedItem") {
|
||||
return null; // 放行给 HTMLAllCollection.prototype
|
||||
}
|
||||
const element = document._elements.find(el => el.id === name || el.name === name);
|
||||
return element ? { value: element, enumerable: false } : null;
|
||||
}
|
||||
|
||||
function defineAllProperty(target, property) {
|
||||
const hit = resolveAll(property);
|
||||
if (!hit) return false;
|
||||
Object.defineProperty(target, String(property), {
|
||||
value: hit.value,
|
||||
writable: false,
|
||||
enumerable: hit.enumerable,
|
||||
configurable: true,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const all = xbs.createUndetectable(function (value) {
|
||||
if (arguments.length === 0 || value == null) return null;
|
||||
const hit = resolveAll(value);
|
||||
return hit ? hit.value : null;
|
||||
}, {
|
||||
getter(target, property) {
|
||||
if (typeof property === "symbol" || !defineAllProperty(target, property)) {
|
||||
return { intercept: false };
|
||||
}
|
||||
},
|
||||
query(target, property) {
|
||||
if (typeof property === "symbol" || !defineAllProperty(target, property)) {
|
||||
return { intercept: false };
|
||||
}
|
||||
},
|
||||
descriptor(target, property) {
|
||||
if (typeof property === "symbol" || !defineAllProperty(target, property)) {
|
||||
return { intercept: false };
|
||||
}
|
||||
},
|
||||
enumerator(target) {
|
||||
for (let i = 0; i < document._elements.length; i++) {
|
||||
defineAllProperty(target, String(i));
|
||||
}
|
||||
return Object.getOwnPropertyNames(target);
|
||||
},
|
||||
});
|
||||
|
||||
Object.setPrototypeOf(all, HTMLAllCollection.prototype);
|
||||
|
||||
Object.defineProperty(document, "all", {
|
||||
value: all,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
Object.hasOwn(document.all, "length"); // false
|
||||
Object.getOwnPropertyDescriptor(document.all, "length"); // undefined
|
||||
document.all.length; // 来自 HTMLAllCollection.prototype.length
|
||||
```
|
||||
|
||||
如果要更贴近浏览器,`all` 最好安装在 `Document.prototype` 的 getter 上,而不是直接作为 `document` 自有属性:
|
||||
|
||||
```js
|
||||
Object.defineProperty(Document.prototype, "all", {
|
||||
get: xbs.createGetter("all", 0, function () {
|
||||
return all;
|
||||
}),
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
```
|
||||
|
||||
## 13. Private slot API
|
||||
|
||||
基于 V8 `Private::ForApi` 实现,适合把内部状态存放到 JS 对象上,同时避免普通属性枚举或直接访问。
|
||||
|
||||
```js
|
||||
const obj = {};
|
||||
|
||||
xbs.hasPrivate(obj, "slot"); // false
|
||||
xbs.setPrivate(obj, "slot", 123); // true
|
||||
xbs.hasPrivate(obj, "slot"); // true
|
||||
xbs.getPrivate(obj, "slot"); // 123
|
||||
xbs.deletePrivate(obj, "slot"); // true
|
||||
xbs.hasPrivate(obj, "slot"); // false
|
||||
```
|
||||
|
||||
## 14. `throwTypeError(message)`
|
||||
|
||||
从 native 层抛出 `TypeError`。
|
||||
|
||||
```js
|
||||
function assertIllegalInvocation(thisValue, Ctor) {
|
||||
if (!(thisValue instanceof Ctor)) {
|
||||
xbs.throwTypeError("Illegal invocation");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- 不传参数会抛出参数数量错误。
|
||||
- 第一个参数不是字符串会抛出类型错误。
|
||||
|
||||
## 15. 推荐实践
|
||||
|
||||
1. 所有非 WebAPI 的辅助能力都放在 `window.xbs` 下,不要直接污染 `window`。
|
||||
2. 对外可见的浏览器对象优先使用 `createProtoChains` 创建构造函数与原型链。
|
||||
3. 对 `document.all` 这类特殊对象优先使用 `createUndetectable`。
|
||||
4. 对 `navigator.plugins` / `navigator.mimeTypes` 优先使用 `getMimeTypesAndPlugins`。
|
||||
5. 对集合对象优先使用 `createNativeCollection`,不要直接用普通数组代替。
|
||||
6. 对内部状态优先使用 private slot,减少普通属性泄露。
|
||||
7. 多 isolate 场景下,原型链注册表会按 isolate 隔离;清理 isolate 时会同步清理对应 XBS 注册表。
|
||||
8. 宿主侧 `isolated-vm` API 保持原样,`xbs` 只存在于创建出的 Context 内。
|
||||
|
||||
## 16. 快速自检
|
||||
|
||||
```js
|
||||
const expected = [
|
||||
"clearProtoChainRegistry",
|
||||
"createGetter",
|
||||
"createInterceptor",
|
||||
"createNativeCollection",
|
||||
"createNativeFunction",
|
||||
"createNativeObject",
|
||||
"createProtoChains",
|
||||
"createSetter",
|
||||
"createUndetectable",
|
||||
"deletePrivate",
|
||||
"deleteProtoChainRegistryEntry",
|
||||
"getMimeTypesAndPlugins",
|
||||
"getPrivate",
|
||||
"getProtoChainRegistry",
|
||||
"hasPrivate",
|
||||
"setPrivate",
|
||||
"throwTypeError",
|
||||
];
|
||||
|
||||
Object.keys(window.xbs).sort().join("\n") === expected.join("\n");
|
||||
```
|
||||
|
||||
也可以直接运行项目内测试:
|
||||
|
||||
```powershell
|
||||
node --no-node-snapshot tests\xbs-basic.js
|
||||
```
|
||||
@@ -0,0 +1,522 @@
|
||||
# XBS DOM API 使用说明
|
||||
|
||||
本文说明当前 `xbsVm` 在 `isolated-vm` Context 内提供的 DOM 能力。入口统一为 `window.xbs.dom`,不会修改 host 侧 `isolated-vm` 的原始 API。
|
||||
|
||||
## 1. 当前公开 API
|
||||
|
||||
当前 `xbs.dom` 只保留一个公开方法:
|
||||
|
||||
```js
|
||||
xbs.dom.createDocument(options)
|
||||
```
|
||||
|
||||
以下 API 已删除,不再对外暴露:
|
||||
|
||||
- `attachDocument()` / `detachDocument()`
|
||||
- `installConstructors()` / `getConstructors()`
|
||||
- `createElement()`
|
||||
- `createIframeDocument()` / `attachIframeDocument()` / `detachIframeDocument()`
|
||||
- `patchApi()` / `removeApi()`
|
||||
- `createProfile()` / `getProfile()` / `listProfiles()`
|
||||
- `snapshotSurface()` / `restoreSurface()`
|
||||
- `getLastError()`
|
||||
|
||||
`createDocument()` 中也不再支持 `profile` 参数。
|
||||
|
||||
## 2. 设计原则
|
||||
|
||||
- 默认不会把 `document` 挂到 `window` 上;调用方自行决定是否执行 `window.document = document`。
|
||||
- `createDocument()` 会在返回的 document 对象上安装基础 DOM 属性、方法和原型链。
|
||||
- DOM 构造函数不会默认挂到 `window` 上;对象仍然具有内部构造函数和原型链,例如 `document.constructor.name === "HTMLDocument"`。
|
||||
- 删除或禁用某个 DOM API 应在创建 document 前通过配置声明,而不是运行时强删已发布的不可配置属性。
|
||||
- C++ 文件中不再通过 JS bootstrap 字符串实现 DOM;DOM 相关逻辑位于 `src/xbs/dom/`。
|
||||
|
||||
## 3. 快速示例
|
||||
|
||||
```js
|
||||
const ivm = require("isolated-vm");
|
||||
|
||||
const isolate = new ivm.Isolate();
|
||||
const context = isolate.createContextSync();
|
||||
|
||||
const result = isolate.compileScriptSync(`
|
||||
(() => {
|
||||
const document = xbs.dom.createDocument({
|
||||
url: "https://example.com/",
|
||||
html: '<main id="app"><span class="hot">hello</span></main>'
|
||||
});
|
||||
|
||||
return {
|
||||
hasWindowDocument: "document" in window,
|
||||
text: document.querySelector(".hot").textContent,
|
||||
url: document.URL
|
||||
};
|
||||
})()
|
||||
`).runSync(context, { copy: true });
|
||||
|
||||
console.log(result);
|
||||
```
|
||||
|
||||
## 4. createDocument(options)
|
||||
|
||||
### 参数
|
||||
|
||||
```js
|
||||
const document = xbs.dom.createDocument({
|
||||
url: "https://example.com/path",
|
||||
html: '<div id="app">ok</div>',
|
||||
skeleton: true,
|
||||
omitApis: ["document.all"]
|
||||
});
|
||||
```
|
||||
|
||||
支持字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `url` | `string` | 设置 `document.URL`,默认是 `about:blank`。 |
|
||||
| `html` | `string` | 创建后解析到 `document.body`,当前为简化 HTML 解析器。 |
|
||||
| `skeleton` | `boolean` | 是否创建 `html/head/body` 骨架,默认 `true`。 |
|
||||
| `omitApis` | `string[]` | 创建前不安装指定 API。 |
|
||||
| `disabledApis` | `string[]` | `omitApis` 的兼容别名。 |
|
||||
| `removedApis` | `string[]` | `omitApis` 的兼容别名。 |
|
||||
| `features.documentAll` | `boolean` | 为 `false` 时不安装 `document.all`。 |
|
||||
| `features.iframeContentDocument` | `boolean` | 为 `false` 时不安装 iframe 的 `contentDocument/contentWindow`。 |
|
||||
|
||||
## 5. 创建前禁用 API
|
||||
|
||||
示例:创建一个不带 `document.all` 的 document。
|
||||
|
||||
```js
|
||||
const document = xbs.dom.createDocument({
|
||||
omitApis: ["document.all"]
|
||||
});
|
||||
|
||||
console.log("all" in document); // false
|
||||
```
|
||||
|
||||
示例:创建一个不带 `Document.prototype.createComment` 的 document。
|
||||
|
||||
```js
|
||||
const document = xbs.dom.createDocument({
|
||||
omitApis: ["Document.prototype.createComment"]
|
||||
});
|
||||
|
||||
console.log(typeof document.createComment); // undefined
|
||||
```
|
||||
|
||||
示例:禁用 iframe 的 `contentDocument`。
|
||||
|
||||
```js
|
||||
const document = xbs.dom.createDocument({
|
||||
omitApis: ["HTMLIFrameElement.prototype.contentDocument"]
|
||||
});
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
console.log(typeof iframe.contentDocument); // undefined
|
||||
```
|
||||
|
||||
也支持通配前缀:
|
||||
|
||||
```js
|
||||
xbs.dom.createDocument({
|
||||
omitApis: ["Element.prototype.*"]
|
||||
});
|
||||
```
|
||||
|
||||
## 6. 常用 DOM 能力
|
||||
|
||||
当前基础实现包含以下对象和能力:
|
||||
|
||||
- `HTMLDocument` / `Document`
|
||||
- `Node` / `Element` / `HTMLElement` / `HTMLIFrameElement`
|
||||
- `Text` / `Comment` / `CDATASection` / `ProcessingInstruction` / `DocumentFragment`
|
||||
- `Attr`
|
||||
- `DOMTokenList` / `NodeList` / `HTMLCollection`
|
||||
- `HTMLAllCollection` / `HTMLFormControlsCollection` / `HTMLOptionsCollection` / `RadioNodeList`
|
||||
- `NamedNodeMap` / `DOMStringMap`
|
||||
|
||||
常用示例:
|
||||
|
||||
```js
|
||||
const document = xbs.dom.createDocument();
|
||||
const div = document.createElement("div");
|
||||
|
||||
div.id = "box";
|
||||
div.className = "a b";
|
||||
div.classList.add("c");
|
||||
div.innerHTML = '<span name="s1">text</span>';
|
||||
|
||||
document.body.appendChild(div);
|
||||
|
||||
console.log(document.querySelector("#box") === div); // true
|
||||
console.log(div.querySelector("span").textContent); // text
|
||||
console.log(div.children.namedItem("s1").tagName); // SPAN
|
||||
```
|
||||
|
||||
## 7. 本阶段新增 DOM API
|
||||
|
||||
说明:`xbs.dom` 对外仍然只暴露 `createDocument()`。以下新增 API 是通过 `createDocument()` 返回的 `document`、其节点对象和原型链对外可用。
|
||||
|
||||
### 7.1 Document 新增属性
|
||||
|
||||
```js
|
||||
const document = xbs.dom.createDocument();
|
||||
|
||||
document.documentURI; // 与 document.URL 保持一致
|
||||
document.embeds; // HTMLCollection
|
||||
document.plugins; // HTMLCollection
|
||||
```
|
||||
|
||||
当前 `Document` 可用的主要属性包括:
|
||||
|
||||
- `documentElement`
|
||||
- `head`
|
||||
- `body`
|
||||
- `title`
|
||||
- `readyState`
|
||||
- `URL`
|
||||
- `documentURI`
|
||||
- `referrer`
|
||||
- `characterSet`
|
||||
- `compatMode`
|
||||
- `contentType`
|
||||
- `defaultView`
|
||||
- `activeElement`
|
||||
- `doctype`
|
||||
- `implementation`
|
||||
- `currentScript`
|
||||
- `all`
|
||||
- `scripts`
|
||||
- `forms`
|
||||
- `images`
|
||||
- `links`
|
||||
- `anchors`
|
||||
- `embeds`
|
||||
- `plugins`
|
||||
- `children`
|
||||
|
||||
### 7.2 Document 新增创建方法
|
||||
|
||||
```js
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg:rect");
|
||||
console.log(svg.namespaceURI); // http://www.w3.org/2000/svg
|
||||
console.log(svg.prefix); // svg
|
||||
console.log(svg.localName); // rect
|
||||
|
||||
const attr = document.createAttribute("data-foo");
|
||||
attr.value = "bar";
|
||||
|
||||
const attrNS = document.createAttributeNS("urn:x", "x:key");
|
||||
|
||||
const cdata = document.createCDATASection("abc");
|
||||
console.log(cdata.nodeType); // 4
|
||||
|
||||
const pi = document.createProcessingInstruction("xml-stylesheet", "href=x");
|
||||
console.log(pi.nodeType); // 7
|
||||
```
|
||||
|
||||
当前 `Document` 可用的主要查询和创建方法包括:
|
||||
|
||||
- `getElementById()`
|
||||
- `querySelector()`
|
||||
- `querySelectorAll()`
|
||||
- `getElementsByTagName()`
|
||||
- `getElementsByClassName()`
|
||||
- `getElementsByName()`
|
||||
- `createElement()`
|
||||
- `createElementNS()`
|
||||
- `createTextNode()`
|
||||
- `createComment()`
|
||||
- `createDocumentFragment()`
|
||||
- `createAttribute()`
|
||||
- `createAttributeNS()`
|
||||
- `createCDATASection()`
|
||||
- `createProcessingInstruction()`
|
||||
|
||||
### 7.3 Node 新增方法
|
||||
|
||||
```js
|
||||
const node = document.createTextNode("hello");
|
||||
const clone = node.cloneNode(false);
|
||||
|
||||
console.log(node.isEqualNode(clone)); // true
|
||||
```
|
||||
|
||||
新增或补齐的方法:
|
||||
|
||||
- `isEqualNode(otherNode)`
|
||||
- `lookupNamespaceURI(prefix)`
|
||||
- `lookupPrefix(namespaceURI)`
|
||||
- `isDefaultNamespace(namespaceURI)`
|
||||
|
||||
当前 `Node` 可用的主要属性和方法包括:
|
||||
|
||||
- 属性:`nodeType`、`nodeName`、`nodeValue`、`textContent`、`parentNode`、`parentElement`、`childNodes`、`firstChild`、`lastChild`、`nextSibling`、`previousSibling`、`ownerDocument`、`isConnected`、`baseURI`
|
||||
- 方法:`appendChild()`、`removeChild()`、`insertBefore()`、`replaceChild()`、`cloneNode()`、`contains()`、`hasChildNodes()`、`getRootNode()`、`isSameNode()`、`isEqualNode()`、`compareDocumentPosition()`、`normalize()`、`lookupNamespaceURI()`、`lookupPrefix()`、`isDefaultNamespace()`
|
||||
|
||||
### 7.4 Element 新增属性和方法
|
||||
|
||||
```js
|
||||
const el = document.createElementNS("http://www.w3.org/2000/svg", "svg:rect");
|
||||
|
||||
console.log(el.namespaceURI); // http://www.w3.org/2000/svg
|
||||
console.log(el.prefix); // svg
|
||||
console.log(el.localName); // rect
|
||||
|
||||
el.setAttributeNS("urn:x", "x:key", "value");
|
||||
console.log(el.getAttributeNS("urn:x", "key")); // value
|
||||
console.log(el.hasAttributeNS("urn:x", "key")); // true
|
||||
|
||||
const attr = document.createAttribute("data-user-id");
|
||||
attr.value = "10001";
|
||||
el.setAttributeNode(attr);
|
||||
|
||||
console.log(el.getAttributeNode("data-user-id").ownerElement === el); // true
|
||||
console.log(el.attributes.getNamedItem("data-user-id").value); // 10001
|
||||
|
||||
console.log(el.dataset.userId); // 10001
|
||||
el.dataset.token = "abc";
|
||||
console.log(el.getAttribute("data-token")); // abc
|
||||
delete el.dataset.token;
|
||||
```
|
||||
|
||||
新增或补齐的属性:
|
||||
|
||||
- `namespaceURI`
|
||||
- `prefix`
|
||||
- `localName`
|
||||
- `slot`
|
||||
- `dir`
|
||||
- `shadowRoot`
|
||||
- `assignedSlot`
|
||||
- `attributes`
|
||||
- `dataset`
|
||||
|
||||
新增或补齐的方法:
|
||||
|
||||
- `getAttributeNS()`
|
||||
- `setAttributeNS()`
|
||||
- `hasAttributeNS()`
|
||||
- `getAttributeNode()`
|
||||
- `setAttributeNode()`
|
||||
- `removeAttributeNode()`
|
||||
- `webkitMatchesSelector()`
|
||||
|
||||
当前 `Element` 可用的主要属性和方法包括:
|
||||
|
||||
- 属性:`tagName`、`id`、`className`、`classList`、`attributes`、`innerHTML`、`outerHTML`、`innerText`、`textContent`、`children`、`firstElementChild`、`lastElementChild`、`nextElementSibling`、`previousElementSibling`、`childElementCount`、`namespaceURI`、`prefix`、`localName`、`style`、`dataset`、`slot`、`dir`、`shadowRoot`、`assignedSlot`
|
||||
- 方法:`getAttribute()`、`setAttribute()`、`hasAttribute()`、`removeAttribute()`、`toggleAttribute()`、`getAttributeNames()`、`getAttributeNS()`、`setAttributeNS()`、`hasAttributeNS()`、`getAttributeNode()`、`setAttributeNode()`、`removeAttributeNode()`、`matches()`、`webkitMatchesSelector()`、`closest()`、`querySelector()`、`querySelectorAll()`、`getElementsByTagName()`、`getElementsByClassName()`、`append()`、`prepend()`、`before()`、`after()`、`remove()`、`replaceWith()`、`replaceChildren()`、`insertAdjacentHTML()`、`insertAdjacentElement()`、`insertAdjacentText()`、`focus()`、`blur()`、`click()`、`getBoundingClientRect()`、`getClientRects()`、`scrollIntoView()`
|
||||
|
||||
### 7.5 Attr
|
||||
|
||||
`Attr` 表示属性节点,可由 `document.createAttribute()` 或 `document.createAttributeNS()` 创建,也可通过 `element.getAttributeNode()` 取得。
|
||||
|
||||
```js
|
||||
const attr = document.createAttribute("data-id");
|
||||
attr.value = "1";
|
||||
|
||||
console.log(attr.name); // data-id
|
||||
console.log(attr.value); // 1
|
||||
console.log(attr.nodeType); // 2
|
||||
console.log(attr.nodeName); // data-id
|
||||
console.log(attr.nodeValue); // 1
|
||||
console.log(attr.textContent); // 1
|
||||
```
|
||||
|
||||
主要属性:
|
||||
|
||||
- `name`
|
||||
- `value`
|
||||
- `namespaceURI`
|
||||
- `prefix`
|
||||
- `localName`
|
||||
- `specified`
|
||||
- `ownerElement`
|
||||
- `nodeType`
|
||||
- `nodeName`
|
||||
- `nodeValue`
|
||||
- `textContent`
|
||||
|
||||
### 7.6 NamedNodeMap
|
||||
|
||||
`element.attributes` 返回 `NamedNodeMap`,用于访问元素属性节点。
|
||||
|
||||
```js
|
||||
const el = document.createElement("div");
|
||||
el.setAttribute("id", "box");
|
||||
|
||||
console.log(el.attributes.length); // 1
|
||||
console.log(el.attributes.item(0).name); // id
|
||||
console.log(el.attributes.getNamedItem("id").value); // box
|
||||
```
|
||||
|
||||
主要 API:
|
||||
|
||||
- `length`
|
||||
- `item(index)`
|
||||
- `getNamedItem(name)`
|
||||
- `getNamedItemNS(namespaceURI, localName)`
|
||||
- `setNamedItem(attr)`
|
||||
- `setNamedItemNS(attr)`
|
||||
- `removeNamedItem(name)`
|
||||
- `removeNamedItemNS(namespaceURI, localName)`
|
||||
|
||||
### 7.7 DOMStringMap
|
||||
|
||||
`element.dataset` 返回 `DOMStringMap`,会映射到元素的 `data-*` 属性。
|
||||
|
||||
```js
|
||||
const el = document.createElement("div");
|
||||
|
||||
el.dataset.userId = "10001";
|
||||
console.log(el.getAttribute("data-user-id")); // 10001
|
||||
console.log(el.dataset.userId); // 10001
|
||||
|
||||
delete el.dataset.userId;
|
||||
console.log(el.hasAttribute("data-user-id")); // false
|
||||
```
|
||||
|
||||
### 7.8 集合对象
|
||||
|
||||
本阶段新增和补齐的集合对象:
|
||||
|
||||
- `HTMLAllCollection`
|
||||
- 来源:`document.all`
|
||||
- 特性:保持不可检测语义,`typeof document.all === "undefined"`,但仍可访问 `length`、`item()`、`namedItem()`
|
||||
- `HTMLFormControlsCollection`
|
||||
- 预留给表单控件集合能力
|
||||
- 支持 `length`、`item()`、`namedItem()`
|
||||
- `HTMLOptionsCollection`
|
||||
- 预留给 `select.options` 类场景
|
||||
- 支持 `length`、`selectedIndex`、`item()`、`namedItem()`、`add()`、`remove()`
|
||||
- `RadioNodeList`
|
||||
- 预留给同名 radio 控件集合
|
||||
- 支持 `length`、`value`、`item()`、`namedItem()`
|
||||
|
||||
示例:
|
||||
|
||||
```js
|
||||
const document = xbs.dom.createDocument({
|
||||
html: '<main id="app"><span name="s1">text</span></main>'
|
||||
});
|
||||
|
||||
console.log(document.all.length >= 1); // true
|
||||
console.log(document.all.item(0)); // html 元素
|
||||
console.log(document.body.children.namedItem("s1").tagName); // SPAN
|
||||
```
|
||||
|
||||
### 7.9 手动创建 document.all
|
||||
|
||||
如果用户不使用 `xbs.dom.createDocument()`,而是自己创建 `document`,再通过 `xbs.createUndetectable()` 创建 `all` 并挂载到 `document` 上,需要注意:
|
||||
|
||||
- `xbs.createUndetectable()` 只负责 HTMLDDA/不可检测语义,例如 `typeof all === "undefined"`、`Boolean(all) === false`、`all == null`。
|
||||
- `document.all.length`、`item()`、`namedItem()`、`[0]`、命名属性、枚举和描述符,需要由 `HTMLAllCollection.prototype` 与 handlers 配合实现。
|
||||
- all 创建建议都传入 handlers。handler 未命中时返回 `{ intercept: false }`,让 V8 继续查找 backing target 或原型链。
|
||||
- `length / item / namedItem / constructor` 等原型链已有属性不要定义成 `document.all` 自有属性,否则会和真实浏览器不一致。
|
||||
|
||||
最小结构示例:
|
||||
|
||||
```js
|
||||
const all = xbs.createUndetectable(function (value) {
|
||||
if (arguments.length === 0 || value == null) return null;
|
||||
// 这里根据自己的 document 元素表处理索引或 id/name 查询
|
||||
return null;
|
||||
}, {
|
||||
getter(target, property) {
|
||||
// 命中索引或 id/name 时,可 Object.defineProperty(target, property, descriptor)
|
||||
// 未命中或遇到 length/item/namedItem 等原型属性时必须放行
|
||||
return { intercept: false };
|
||||
},
|
||||
query(target, property) {
|
||||
return { intercept: false };
|
||||
},
|
||||
descriptor(target, property) {
|
||||
return { intercept: false };
|
||||
},
|
||||
enumerator(target) {
|
||||
return Object.getOwnPropertyNames(target);
|
||||
},
|
||||
});
|
||||
|
||||
Object.setPrototypeOf(all, HTMLAllCollection.prototype);
|
||||
```
|
||||
|
||||
如果直接挂到 `document`:
|
||||
|
||||
```js
|
||||
Object.defineProperty(document, "all", {
|
||||
value: all,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
```
|
||||
|
||||
则 `Object.hasOwn(document, "all")` 会是 `true`。如果要更接近真实浏览器,应挂到 `Document.prototype`:
|
||||
|
||||
```js
|
||||
Object.defineProperty(Document.prototype, "all", {
|
||||
get: xbs.createGetter("all", 0, function () {
|
||||
return all;
|
||||
}),
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
```
|
||||
|
||||
## 8. 新增 API 的 omitApis 路径
|
||||
|
||||
新增 API 仍然遵循创建前禁用规则。常用路径示例:
|
||||
|
||||
```js
|
||||
const document = xbs.dom.createDocument({
|
||||
omitApis: [
|
||||
"Document.prototype.createElementNS",
|
||||
"Document.prototype.createAttribute",
|
||||
"Document.prototype.createCDATASection",
|
||||
"Document.prototype.createProcessingInstruction",
|
||||
"Element.prototype.dataset",
|
||||
"Element.prototype.getAttributeNS",
|
||||
"Element.prototype.setAttributeNS",
|
||||
"Element.prototype.webkitMatchesSelector",
|
||||
"Node.prototype.isEqualNode",
|
||||
"Node.prototype.lookupNamespaceURI",
|
||||
"Attr.prototype.value",
|
||||
"NamedNodeMap.prototype.getNamedItem"
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
也可以继续使用通配前缀:
|
||||
|
||||
```js
|
||||
xbs.dom.createDocument({
|
||||
omitApis: [
|
||||
"Document.prototype.*",
|
||||
"Element.prototype.*",
|
||||
"Node.prototype.*"
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
## 9. iframe 行为
|
||||
|
||||
不再提供 `createIframeDocument()` 或 `attachIframeDocument()`。现在 iframe 的 `contentDocument` 会按需创建。
|
||||
|
||||
```js
|
||||
const document = xbs.dom.createDocument({
|
||||
html: '<iframe id="f"></iframe>'
|
||||
});
|
||||
|
||||
const iframe = document.getElementById("f");
|
||||
console.log(iframe.contentDocument.body.tagName); // BODY
|
||||
console.log(iframe.contentWindow.document === iframe.contentDocument); // true
|
||||
```
|
||||
|
||||
## 10. 当前边界
|
||||
|
||||
当前 DOM 是补环境用的基础实现,不是完整浏览器内核:
|
||||
|
||||
- HTML parser 为简化实现,适合基础标签、属性、文本和注释。
|
||||
- selector 目前支持标签、`#id`、`.class`、简单属性选择器和逗号分组。
|
||||
- 暂不支持完整 HTML5 解析、复杂 CSS selector、真实布局、CSSOM、事件系统、MutationObserver、Canvas、WebGL 等。
|
||||
Binary file not shown.
Reference in New Issue
Block a user