mirror of
https://github.com/lexmount/moli.git
synced 2026-10-09 08:01:05 +00:00
784 lines
31 KiB
Rust
784 lines
31 KiB
Rust
use super::*;
|
|
|
|
#[test]
|
|
fn structured_clone_preserves_webassembly_module() {
|
|
let mut vm = new_storage_test_vm("https://example.com/wasm-clone");
|
|
|
|
let result = vm
|
|
.eval(
|
|
r#"
|
|
(() => {
|
|
const module = new WebAssembly.Module(
|
|
new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0])
|
|
);
|
|
const clone = structuredClone(module);
|
|
const instance = new WebAssembly.Instance(clone, {});
|
|
return [
|
|
clone instanceof WebAssembly.Module,
|
|
clone === module,
|
|
Object.keys(instance.exports).length
|
|
].join("|");
|
|
})()
|
|
"#,
|
|
)
|
|
.expect("WebAssembly.Module structuredClone should evaluate");
|
|
|
|
assert_eq!(result, "true|false|0");
|
|
}
|
|
|
|
#[test]
|
|
fn structured_clone_rejects_native_dom_nodes() {
|
|
let mut vm = new_storage_test_vm("https://example.com/dom-node-clone");
|
|
|
|
let result = vm
|
|
.eval(
|
|
r#"
|
|
(() => {
|
|
const probe = value => {
|
|
try {
|
|
structuredClone(value);
|
|
return "ok";
|
|
} catch (error) {
|
|
return error && error.name;
|
|
}
|
|
};
|
|
const liveElement = document.createElement("div");
|
|
const detached = document.implementation.createHTMLDocument("detached");
|
|
return [
|
|
probe(document),
|
|
probe(liveElement),
|
|
probe(detached),
|
|
probe(detached.body)
|
|
].join("|");
|
|
})()
|
|
"#,
|
|
)
|
|
.expect("DOM node structuredClone probe should evaluate");
|
|
|
|
assert_eq!(
|
|
result,
|
|
"DataCloneError|DataCloneError|DataCloneError|DataCloneError"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn structured_clone_rejects_non_serializable_platform_objects_without_prototype_spoofing() {
|
|
let mut vm = new_storage_test_vm("https://platform-object-structured-clone.test/");
|
|
|
|
let result = vm
|
|
.eval(
|
|
r#"
|
|
(() => {
|
|
const probe = value => {
|
|
try {
|
|
structuredClone(value);
|
|
return "ok";
|
|
} catch (error) {
|
|
return error && error.name;
|
|
}
|
|
};
|
|
|
|
const fakeResponse = Object.create(Response.prototype);
|
|
fakeResponse.value = 7;
|
|
const fakeResponseClone = structuredClone(fakeResponse);
|
|
const fakeEvent = Object.create(Event.prototype);
|
|
fakeEvent.value = 9;
|
|
const fakeEventClone = structuredClone(fakeEvent);
|
|
|
|
return JSON.stringify({
|
|
response: probe(new Response()),
|
|
event: probe(new Event("event")),
|
|
messageChannel: probe(new MessageChannel()),
|
|
fakeResponse: [
|
|
fakeResponseClone.value,
|
|
fakeResponseClone instanceof Response
|
|
],
|
|
fakeEvent: [fakeEventClone.value, fakeEventClone instanceof Event]
|
|
});
|
|
})()
|
|
"#,
|
|
)
|
|
.expect("non-serializable platform object structuredClone probe should evaluate");
|
|
|
|
assert_eq!(
|
|
result,
|
|
r#"{"response":"DataCloneError","event":"DataCloneError","messageChannel":"DataCloneError","fakeResponse":[7,false],"fakeEvent":[9,false]}"#
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn structured_clone_rejects_real_performance_entries_without_prototype_spoofing() {
|
|
let document_url = Url::parse("https://performance-entry-clone.test/").expect("document URL");
|
|
let mut vm = new_storage_test_vm(document_url.as_str());
|
|
vm.record_script_subresource_network_result(
|
|
document_url.clone(),
|
|
document_url.join("resource.js").expect("resource URL"),
|
|
&Err("synthetic resource failure".to_owned()),
|
|
);
|
|
|
|
let result = vm
|
|
.eval(
|
|
r#"
|
|
(() => {
|
|
const probe = callback => {
|
|
try {
|
|
callback();
|
|
return "ok";
|
|
} catch (error) {
|
|
return error && error.name;
|
|
}
|
|
};
|
|
const navigation = performance.getEntriesByType("navigation")[0];
|
|
const resource = performance.getEntriesByType("resource")[0];
|
|
performance.mark("clone-mark");
|
|
const mark = performance.getEntriesByName("clone-mark")[0];
|
|
performance.measure("clone-measure");
|
|
const measure = performance.getEntriesByName("clone-measure")[0];
|
|
navigation.expando = "still not clonable";
|
|
|
|
const fake = Object.create(PerformanceEntry.prototype);
|
|
fake.value = 7;
|
|
const fakeClone = structuredClone(fake);
|
|
const plainClone = structuredClone({
|
|
name: navigation.name,
|
|
entryType: navigation.entryType,
|
|
startTime: navigation.startTime,
|
|
duration: navigation.duration
|
|
});
|
|
|
|
return JSON.stringify({
|
|
navigation: probe(() => structuredClone(navigation)),
|
|
resource: probe(() => structuredClone(resource)),
|
|
mark: probe(() => structuredClone(mark)),
|
|
measure: probe(() => structuredClone(measure)),
|
|
postMessage: probe(() => window.postMessage(navigation, "*")),
|
|
fake: [fakeClone.value, fakeClone instanceof PerformanceEntry],
|
|
plain: [plainClone.entryType, typeof plainClone.startTime]
|
|
});
|
|
})()
|
|
"#,
|
|
)
|
|
.expect("PerformanceEntry structured clone probe should evaluate");
|
|
|
|
assert_eq!(
|
|
result,
|
|
r#"{"navigation":"DataCloneError","resource":"DataCloneError","mark":"DataCloneError","measure":"DataCloneError","postMessage":"DataCloneError","fake":[7,false],"plain":["navigation","number"]}"#
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn structured_clone_performance_entry_brand_is_private_and_survives_prototype_changes() {
|
|
let mut vm = new_storage_test_vm("https://performance-entry-brand.test/");
|
|
|
|
let result = vm
|
|
.eval(
|
|
r#"
|
|
(() => {
|
|
const probe = callback => {
|
|
try {
|
|
callback();
|
|
return "ok";
|
|
} catch (error) {
|
|
return error && error.name;
|
|
}
|
|
};
|
|
const mark = performance.mark("factory-mark");
|
|
const constructed = new PerformanceMark("constructed-mark");
|
|
class DerivedMark extends PerformanceMark {}
|
|
const derived = new DerivedMark("derived-mark");
|
|
const inherited = Object.create(mark);
|
|
inherited.value = 7;
|
|
const inheritedClone = structuredClone(inherited);
|
|
const forgedClone = structuredClone({ __moliWebApiType: true });
|
|
const hiddenBrand = !Reflect.ownKeys(mark).includes("__moliWebApiType");
|
|
mark.__moliWebApiType = false;
|
|
Object.setPrototypeOf(mark, null);
|
|
Object.setPrototypeOf(constructed, Object.prototype);
|
|
|
|
return JSON.stringify({
|
|
hiddenBrand,
|
|
alteredFactory: probe(() => structuredClone(mark)),
|
|
alteredConstructor: probe(() => structuredClone(constructed)),
|
|
derived: probe(() => structuredClone(derived)),
|
|
nested: probe(() => structuredClone({ entry: mark })),
|
|
detail: probe(() => performance.mark("entry-as-detail", { detail: mark })),
|
|
inherited: [inheritedClone.value, inheritedClone instanceof PerformanceEntry],
|
|
forged: [forgedClone.__moliWebApiType, forgedClone instanceof PerformanceEntry]
|
|
});
|
|
})()
|
|
"#,
|
|
)
|
|
.expect("PerformanceEntry private brand probe should evaluate");
|
|
|
|
assert_eq!(
|
|
result,
|
|
r#"{"hiddenBrand":true,"alteredFactory":"DataCloneError","alteredConstructor":"DataCloneError","derived":"DataCloneError","nested":"DataCloneError","detail":"DataCloneError","inherited":[7,false],"forged":[true,false]}"#
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn structured_clone_preserves_dom_exception_fields_and_brand() {
|
|
let mut vm = new_storage_test_vm("https://dom-exception-clone.test/");
|
|
|
|
let result = vm
|
|
.eval(
|
|
r#"
|
|
(() => {
|
|
const source = new DOMException("chunk could not be cloned", "DataCloneError");
|
|
source.expando = "not serialized";
|
|
const clone = structuredClone(source);
|
|
return [
|
|
clone instanceof DOMException,
|
|
Object.getPrototypeOf(clone) === DOMException.prototype,
|
|
clone.name,
|
|
clone.message,
|
|
clone.code,
|
|
clone.expando === undefined
|
|
].join("|");
|
|
})()
|
|
"#,
|
|
)
|
|
.expect("DOMException structured clone should evaluate");
|
|
|
|
assert_eq!(
|
|
result,
|
|
"true|true|DataCloneError|chunk could not be cloned|25|true"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn structured_clone_preserves_geometry_interfaces_and_internal_values() {
|
|
let mut vm = new_storage_test_vm("https://geometry-structured-clone.test/");
|
|
|
|
let result = vm
|
|
.eval(
|
|
r#"
|
|
(() => {
|
|
const same = (actual, expected) => Object.is(actual, expected);
|
|
const check = (name, source, attrs, expected, nested = false) => {
|
|
const originals = nested ? attrs.map(attr => source[attr]) : [];
|
|
source.expando = "not serialized";
|
|
for (const attr of attrs) {
|
|
Object.defineProperty(source, attr, {
|
|
get() { throw new Error(`unexpected ${name}.${attr} getter`); }
|
|
});
|
|
}
|
|
const clone = structuredClone(source);
|
|
const valuesMatch = attrs.every((attr, index) => {
|
|
if (!nested) return same(clone[attr], expected[index]);
|
|
return clone[attr] !== originals[index]
|
|
&& expected[index].every((value, component) =>
|
|
same(clone[attr][["x", "y", "z", "w"][component]], value));
|
|
});
|
|
return Object.prototype.toString.call(clone) === `[object ${name}]`
|
|
&& Object.getPrototypeOf(clone) === self[name].prototype
|
|
&& !("expando" in clone)
|
|
&& valuesMatch;
|
|
};
|
|
|
|
const pointAttrs = ["x", "y", "z", "w"];
|
|
const rectAttrs = ["x", "y", "width", "height"];
|
|
const quadAttrs = ["p1", "p2", "p3", "p4"];
|
|
const pointValues = [1, -0, Infinity, NaN];
|
|
const quadValues = [
|
|
[1, 2, 3, 4],
|
|
[-0, -0, -0, -0],
|
|
[Infinity, Infinity, Infinity, Infinity],
|
|
[NaN, NaN, NaN, NaN]
|
|
];
|
|
const matrixAttrs = [
|
|
"a", "b", "c", "d", "e", "f",
|
|
"m11", "m12", "m13", "m14",
|
|
"m21", "m22", "m23", "m24",
|
|
"m31", "m32", "m33", "m34",
|
|
"m41", "m42", "m43", "m44", "is2D"
|
|
];
|
|
const normalized2DAttrs = new Set([
|
|
"m13", "m14", "m23", "m24",
|
|
"m31", "m32", "m34", "m43"
|
|
]);
|
|
const matrixCheck = (name, values) => {
|
|
const source = new self[name](values);
|
|
if (values.length === 6) {
|
|
for (const attr of normalized2DAttrs) source[attr] = -0;
|
|
}
|
|
const expected = matrixAttrs.map(attr =>
|
|
normalized2DAttrs.has(attr) && values.length === 6 ? 0 : source[attr]);
|
|
return check(name, source, matrixAttrs, expected);
|
|
};
|
|
|
|
return [
|
|
check("DOMPointReadOnly", new DOMPointReadOnly(...pointValues),
|
|
pointAttrs, pointValues),
|
|
check("DOMPoint", new DOMPoint(...pointValues), pointAttrs, pointValues),
|
|
check("DOMRectReadOnly", new DOMRectReadOnly(...pointValues),
|
|
rectAttrs, pointValues),
|
|
check("DOMRect", new DOMRect(...pointValues), rectAttrs, pointValues),
|
|
check("DOMQuad", new DOMQuad(...quadValues.map(
|
|
([x, y, z, w]) => ({x, y, z, w}))), quadAttrs, quadValues, true),
|
|
matrixCheck("DOMMatrixReadOnly", [1, -0, Infinity, NaN, 5, 6]),
|
|
matrixCheck("DOMMatrix", [
|
|
11, -0, Infinity, NaN, 21, 22, 23, 24,
|
|
31, 32, 33, 34, 41, 42, 43, 44
|
|
])
|
|
].join("|");
|
|
})()
|
|
"#,
|
|
)
|
|
.expect("Geometry structuredClone probe should evaluate");
|
|
|
|
assert_eq!(result, "true|true|true|true|true|true|true");
|
|
}
|
|
|
|
#[test]
|
|
fn structured_clone_transfers_array_buffer_and_preserves_view_aliases() {
|
|
let mut vm = new_storage_test_vm("https://array-buffer-transfer.test/");
|
|
|
|
let result = vm
|
|
.eval(
|
|
r#"
|
|
(() => {
|
|
const buffer = new ArrayBuffer(10);
|
|
const data = new DataView(buffer);
|
|
data.setUint32(0, 0x01020304, false);
|
|
const words = new Uint16Array(buffer, 4, 3);
|
|
words.set([7, 500, 65535]);
|
|
const clone = structuredClone(
|
|
{ buffer, data, words, nested: new Map([["words", words]]) },
|
|
{ transfer: [buffer] }
|
|
);
|
|
let duplicate;
|
|
const duplicateBuffer = new ArrayBuffer(1);
|
|
try {
|
|
structuredClone(duplicateBuffer, {
|
|
transfer: [duplicateBuffer, duplicateBuffer]
|
|
});
|
|
duplicate = "missing";
|
|
} catch (error) {
|
|
duplicate = error.name;
|
|
}
|
|
return JSON.stringify({
|
|
sourceLength: buffer.byteLength,
|
|
cloneLength: clone.buffer.byteLength,
|
|
bytes: Array.from(new Uint8Array(clone.buffer)),
|
|
aliases: [
|
|
clone.data.buffer === clone.buffer,
|
|
clone.words.buffer === clone.buffer,
|
|
clone.nested.get("words").buffer === clone.buffer
|
|
],
|
|
brands: [
|
|
clone.data instanceof DataView,
|
|
clone.words instanceof Uint16Array,
|
|
clone.nested instanceof Map
|
|
],
|
|
duplicate
|
|
});
|
|
})()
|
|
"#,
|
|
)
|
|
.expect("ArrayBuffer structuredClone transfer should evaluate");
|
|
|
|
assert_eq!(
|
|
result,
|
|
r#"{"sourceLength":0,"cloneLength":10,"bytes":[1,2,3,4,7,0,244,1,255,255],"aliases":[true,true,true],"brands":[true,true,true],"duplicate":"DataCloneError"}"#
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn structured_clone_preserves_opfs_locator_and_source_handle() {
|
|
let mut vm = new_storage_page_task_executor_test_vm("https://opfs-structured-clone.test/");
|
|
|
|
vm.exec(
|
|
r#"
|
|
globalThis.__opfsStructuredCloneProbe = "pending";
|
|
(async () => {
|
|
const root = await navigator.storage.getDirectory();
|
|
const directory = await root.getDirectoryHandle("clone-dir", { create: true });
|
|
const file = await directory.getFileHandle("clone.txt", { create: true });
|
|
const writer = await file.createWritable();
|
|
await writer.write("clone bytes");
|
|
await writer.close();
|
|
|
|
const cloned = structuredClone({ root, handles: [file, file] });
|
|
const clonedFile = cloned.handles[0];
|
|
globalThis.__opfsStructuredCloneProbe = JSON.stringify({
|
|
rootBrand: cloned.root instanceof FileSystemDirectoryHandle,
|
|
fileBrand: clonedFile instanceof FileSystemFileHandle,
|
|
sharedReference: cloned.handles[0] === cloned.handles[1],
|
|
distinctFromSource: clonedFile !== file,
|
|
sameEntry: await clonedFile.isSameEntry(file),
|
|
resolved: await cloned.root.resolve(clonedFile),
|
|
text: await (await clonedFile.getFile()).text()
|
|
});
|
|
})().catch(error => {
|
|
globalThis.__opfsStructuredCloneProbe =
|
|
`error:${error && error.name}:${error && error.message}`;
|
|
});
|
|
"#,
|
|
None,
|
|
)
|
|
.expect("OPFS structuredClone probe should schedule");
|
|
|
|
let result = vm
|
|
.eval_after_selected_page_tasks("String(globalThis.__opfsStructuredCloneProbe)")
|
|
.expect("OPFS structuredClone probe should settle");
|
|
assert_eq!(
|
|
result,
|
|
r#"{"rootBrand":true,"fileBrand":true,"sharedReference":true,"distinctFromSource":true,"sameEntry":true,"resolved":["clone-dir","clone.txt"],"text":"clone bytes"}"#
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn blob_iframe_inherits_secure_origin_for_opfs_handle_messages() {
|
|
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
|
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
|
|
"https://opfs-blob-frame-clone.test/page.html",
|
|
&loader,
|
|
);
|
|
|
|
let setup = vm
|
|
.eval(
|
|
r#"
|
|
(() => {
|
|
globalThis.__opfsBlobFrameCloneProbe = "pending";
|
|
let frame;
|
|
addEventListener("message", async event => {
|
|
if (!event.data || event.source !== frame.contentWindow) {
|
|
return;
|
|
}
|
|
if (event.data.kind === "ready") {
|
|
const root = await navigator.storage.getDirectory();
|
|
frame.contentWindow.postMessage({ kind: "handle", root }, "*");
|
|
return;
|
|
}
|
|
if (event.data.kind === "result" || event.data.kind === "messageerror") {
|
|
globalThis.__opfsBlobFrameCloneProbe = JSON.stringify(event.data);
|
|
}
|
|
});
|
|
|
|
const childMarkup = `<!doctype html><script>
|
|
addEventListener("message", event => {
|
|
const root = event.data && event.data.root;
|
|
parent.postMessage({
|
|
kind: "result",
|
|
secure: isSecureContext,
|
|
interfaceExposed: "FileSystemHandle" in globalThis,
|
|
directoryInterfaceExposed: "FileSystemDirectoryHandle" in globalThis,
|
|
directoryBrand: root instanceof FileSystemDirectoryHandle,
|
|
rootName: root.name
|
|
}, "*");
|
|
});
|
|
addEventListener("messageerror", () => {
|
|
parent.postMessage({ kind: "messageerror", secure: isSecureContext }, "*");
|
|
});
|
|
parent.postMessage({ kind: "ready" }, "*");
|
|
<\/script>`;
|
|
frame = document.createElement("iframe");
|
|
frame.src = URL.createObjectURL(new Blob([childMarkup], { type: "text/html" }));
|
|
(document.body || document.documentElement || document).appendChild(frame);
|
|
return "queued";
|
|
})()
|
|
"#,
|
|
)
|
|
.expect("blob iframe OPFS clone setup should evaluate");
|
|
assert_eq!(setup, "queued");
|
|
|
|
advance_page_task_executor_until_eval_equals(
|
|
&mut vm,
|
|
&loader,
|
|
"String(globalThis.__opfsBlobFrameCloneProbe)",
|
|
r#"{"kind":"result","secure":true,"interfaceExposed":true,"directoryInterfaceExposed":true,"directoryBrand":true,"rootName":""}"#,
|
|
"blob iframe OPFS clone",
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn sandboxed_blob_iframe_keeps_opaque_storage_context_for_opfs_messages() {
|
|
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
|
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
|
|
"https://sandboxed-blob-frame.test/page.html",
|
|
&loader,
|
|
);
|
|
|
|
let setup = vm
|
|
.eval(
|
|
r#"
|
|
(() => {
|
|
globalThis.__sandboxedBlobFrameProbe = "pending";
|
|
let frame;
|
|
let sourceFile;
|
|
let sourceDirectory;
|
|
let rejectionCount = 0;
|
|
addEventListener("message", async event => {
|
|
if (!event.data || event.source !== frame.contentWindow) {
|
|
return;
|
|
}
|
|
if (event.data.kind === "ready") {
|
|
const root = await navigator.storage.getDirectory();
|
|
sourceFile = await root.getFileHandle("sequential-file", { create: true });
|
|
sourceDirectory =
|
|
await root.getDirectoryHandle("sequential-directory", { create: true });
|
|
frame.contentWindow.postMessage({ kind: "handle", handle: sourceFile }, "*");
|
|
return;
|
|
}
|
|
if (event.data.kind === "message") {
|
|
globalThis.__sandboxedBlobFrameProbe = "unexpected-message";
|
|
return;
|
|
}
|
|
if (event.data.kind === "messageerror") {
|
|
rejectionCount += 1;
|
|
if (rejectionCount === 1) {
|
|
frame.contentWindow.postMessage(
|
|
{ kind: "handle", handle: sourceDirectory },
|
|
"*"
|
|
);
|
|
return;
|
|
}
|
|
globalThis.__sandboxedBlobFrameProbe = JSON.stringify({
|
|
rejectionCount,
|
|
origin: event.data.origin,
|
|
secure: event.data.secure,
|
|
interfaceExposed: event.data.interfaceExposed,
|
|
storageExposed: event.data.storageExposed,
|
|
getDirectory: event.data.getDirectory,
|
|
sourceStillUsable:
|
|
sourceFile.name === "sequential-file" &&
|
|
sourceDirectory.name === "sequential-directory"
|
|
});
|
|
}
|
|
});
|
|
const childMarkup = `<!doctype html><script>
|
|
addEventListener("message", () => {
|
|
parent.postMessage({ kind: "message" }, "*");
|
|
});
|
|
addEventListener("messageerror", async () => {
|
|
let getDirectory;
|
|
try {
|
|
await navigator.storage.getDirectory();
|
|
getDirectory = "resolved";
|
|
} catch (error) {
|
|
getDirectory = error && error.name;
|
|
}
|
|
parent.postMessage({
|
|
kind: "messageerror",
|
|
origin: location.origin,
|
|
secure: isSecureContext,
|
|
interfaceExposed: "FileSystemHandle" in globalThis,
|
|
storageExposed: "storage" in navigator,
|
|
getDirectory
|
|
}, "*");
|
|
});
|
|
parent.postMessage({ kind: "ready" }, "*");
|
|
<\/script>`;
|
|
frame = document.createElement("iframe");
|
|
frame.setAttribute("sandbox", "allow-scripts");
|
|
frame.src = URL.createObjectURL(new Blob([childMarkup], { type: "text/html" }));
|
|
(document.body || document.documentElement || document).appendChild(frame);
|
|
return "queued";
|
|
})()
|
|
"#,
|
|
)
|
|
.expect("sandboxed blob iframe setup should evaluate");
|
|
assert_eq!(setup, "queued");
|
|
|
|
advance_page_task_executor_until_eval_equals(
|
|
&mut vm,
|
|
&loader,
|
|
"String(globalThis.__sandboxedBlobFrameProbe)",
|
|
r#"{"rejectionCount":2,"origin":"null","secure":true,"interfaceExposed":true,"storageExposed":true,"getDirectory":"SecurityError","sourceStillUsable":true}"#,
|
|
"sandboxed blob iframe OPFS rejection",
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[test]
|
|
fn structured_clone_preserves_quota_exceeded_error_fields_and_brand() {
|
|
let mut vm = new_storage_test_vm("https://quota-exceeded-error-clone.test/");
|
|
|
|
let result = vm
|
|
.eval(
|
|
r#"
|
|
(() => {
|
|
const source = new QuotaExceededError("full", {
|
|
quota: 0.1,
|
|
requested: 1
|
|
});
|
|
source.expando = "not serialized";
|
|
const clone = structuredClone(source);
|
|
const empty = structuredClone(new QuotaExceededError("empty"));
|
|
return [
|
|
clone instanceof QuotaExceededError,
|
|
clone instanceof DOMException,
|
|
Object.getPrototypeOf(clone) === QuotaExceededError.prototype,
|
|
clone.name,
|
|
clone.message,
|
|
clone.code,
|
|
clone.quota,
|
|
clone.requested,
|
|
clone.expando === undefined,
|
|
empty.quota === null,
|
|
empty.requested === null
|
|
].join("|");
|
|
})()
|
|
"#,
|
|
)
|
|
.expect("QuotaExceededError structured clone should evaluate");
|
|
|
|
assert_eq!(
|
|
result,
|
|
"true|true|true|QuotaExceededError|full|22|0.1|1|true|true|true"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn structured_clone_rejects_platform_objects_in_nested_graphs_without_running_their_getters() {
|
|
let mut vm = new_storage_test_vm("https://platform-identity-clone.test/");
|
|
let result = vm.eval(r#"
|
|
(() => {
|
|
const objects = [
|
|
new URL('https://example.test/'), new URLSearchParams('a=b'), new Headers(),
|
|
new Request('https://example.test/'), new Response('body'), new Response().headers, new FormData(),
|
|
new Event('event'), new EventTarget(), new AbortController(), new AbortController().signal,
|
|
navigator, performance, history, localStorage, new Highlight(), CSS.highlights, customElements, new CustomElementRegistry(),
|
|
document, document.implementation, document.childNodes, document.children, document.querySelectorAll('*'), document.createElement('div').attributes, new XMLHttpRequest().upload, document.createElement('select'), document.createTextNode('text'),
|
|
document.implementation.createHTMLDocument('detached').body,
|
|
new TextEncoder(), new TextDecoder(), new TextEncoderStream(), new TextDecoderStream(),
|
|
new CompressionStream('gzip'), new DecompressionStream('gzip'),
|
|
new URLSearchParams().entries(), new Headers().entries(), new Highlight().values(),
|
|
];
|
|
const failures = [];
|
|
for (const [index, object] of objects.entries()) {
|
|
let getterCalls = 0;
|
|
Object.defineProperty(object, 'cloneProbe', {enumerable: true, get() { getterCalls++; return 1; }});
|
|
try { structuredClone({nested: new Map([['entry', object]])}); failures.push(`${index}:accepted`); }
|
|
catch (error) { if (error.name !== 'DataCloneError') failures.push(`${index}:${error.name}`); }
|
|
if (getterCalls) failures.push(`${index}:getter`);
|
|
}
|
|
const buffer = new ArrayBuffer(4);
|
|
try { structuredClone({buffer, rejected: objects[0]}, {transfer: [buffer]}); }
|
|
catch (error) { if (error.name !== 'DataCloneError') failures.push('transfer-error'); }
|
|
if (buffer.byteLength !== 4) failures.push('premature-detach');
|
|
return failures.join('|');
|
|
})()
|
|
"#).expect("platform identity clone matrix should evaluate");
|
|
assert_eq!(result, "");
|
|
}
|
|
|
|
#[test]
|
|
fn structured_clone_does_not_read_fake_private_slots_or_inherit_native_identity() {
|
|
let mut vm = new_storage_test_vm("https://platform-identity-forgery.test/");
|
|
let result = vm.eval(r#"
|
|
(() => {
|
|
let reads = 0;
|
|
const prototype = Object.create(new URL('https://example.test/'));
|
|
for (const key of ['__moliWebApiType', '__lmUrlHref', '__lmUrlSearchParamsPairs', '__lmFormDataEntries']) {
|
|
Object.defineProperty(prototype, key, {get() { reads++; throw Error('must not run'); }});
|
|
}
|
|
const object = Object.create(prototype);
|
|
object.value = 7;
|
|
const clone = structuredClone(object);
|
|
const forged = structuredClone({__moliWebApiType: 'URL', __lmUrlHref: 'public'});
|
|
return [reads, clone.value, Object.getPrototypeOf(clone) === Object.prototype,
|
|
forged.__moliWebApiType, forged.__lmUrlHref].join('|');
|
|
})()
|
|
"#).expect("ordinary records must not trigger native identity checks through author properties");
|
|
assert_eq!(result, "0|7|true|URL|public");
|
|
}
|
|
|
|
#[test]
|
|
fn encoding_stream_wrappers_do_not_inherit_transform_transferability() {
|
|
let mut vm = new_storage_test_vm("https://platform-transfer-types.test/");
|
|
let result = vm.eval(r#"
|
|
[new TextEncoderStream(), new TextDecoderStream(), new CompressionStream('gzip'), new DecompressionStream('gzip')]
|
|
.map(stream => { try { structuredClone(stream, {transfer: [stream]}); return 'accepted'; }
|
|
catch (error) { return error.name; } }).join('|')
|
|
"#).expect("only actual transferable stream interfaces should be accepted");
|
|
assert_eq!(
|
|
result,
|
|
"DataCloneError|DataCloneError|DataCloneError|DataCloneError"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn shared_array_buffers_fail_consistently_at_root_and_inside_message_graphs() {
|
|
let mut vm = new_storage_test_vm("https://shared-buffer-clone.test/");
|
|
let result = vm.eval(r#"
|
|
(() => {
|
|
const shared = new SharedArrayBuffer(8);
|
|
const channel = new MessageChannel();
|
|
const probe = fn => { try { fn(); return 'accepted'; } catch (error) { return error.name; } };
|
|
return [
|
|
probe(() => structuredClone(shared)),
|
|
probe(() => structuredClone({nested: new Map([['shared', shared]])})),
|
|
probe(() => channel.port1.postMessage({shared})),
|
|
probe(() => window.postMessage({shared}, '*')),
|
|
shared.byteLength
|
|
].join('|');
|
|
})()
|
|
"#).expect("unsupported shared buffers must report DataCloneError at every depth");
|
|
assert_eq!(
|
|
result,
|
|
"DataCloneError|DataCloneError|DataCloneError|DataCloneError|8"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn supported_platform_codecs_preserve_payloads_after_prototype_changes() {
|
|
let mut vm = new_storage_test_vm("https://supported-platform-codecs.test/");
|
|
let result = vm.eval(r#"
|
|
(() => {
|
|
const blob = new (class extends Blob {})(['abc'], {type: 'text/plain'});
|
|
const file = new File(['def'], 'note.txt', {type: 'text/plain', lastModified: 17});
|
|
const pixels = new ImageData(new Uint8ClampedArray([1, 2, 3, 4]), 1, 1);
|
|
const exception = new DOMException('message', 'AbortError');
|
|
let getterCalls = 0;
|
|
for (const object of [blob, file, pixels, exception]) {
|
|
Object.setPrototypeOf(object, null);
|
|
Object.defineProperty(object, 'expando', {enumerable: true, get() { getterCalls++; return 1; }});
|
|
}
|
|
const clone = structuredClone({blob, file, pixels, exception, again: blob});
|
|
return [clone.blob instanceof Blob, clone.blob.size, clone.blob.type, clone.blob === clone.again,
|
|
clone.file instanceof File, clone.file.name, clone.file.lastModified,
|
|
clone.pixels instanceof ImageData, Array.from(clone.pixels.data).join(','),
|
|
clone.exception instanceof DOMException, clone.exception.name, clone.exception.message,
|
|
getterCalls, clone.blob.expando === undefined].join('|');
|
|
})()
|
|
"#).expect("native codecs should ignore mutable prototypes and author expandos");
|
|
assert_eq!(
|
|
result,
|
|
"true|3|text/plain|true|true|note.txt|17|true|1,2,3,4|true|AbortError|message|0|true"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn observer_and_webgl_factory_results_have_native_identity() {
|
|
let mut vm = new_storage_test_vm("https://factory-result-identity.test/");
|
|
let result = vm
|
|
.eval(
|
|
r#"
|
|
(() => {
|
|
const target = document.createElement('div');
|
|
target.style.cssText = 'width: 41px; height: 23px';
|
|
const html = document.documentElement || document.appendChild(document.createElement('html'));
|
|
const body = document.body || html.appendChild(document.createElement('body'));
|
|
body.appendChild(target);
|
|
const observer = new ResizeObserver(() => {});
|
|
observer.observe(target);
|
|
const entry = observer.takeRecords()[0];
|
|
const gl = document.createElement('canvas').getContext('webgl');
|
|
const precision = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT);
|
|
return [entry, entry.contentBoxSize[0], entry.borderBoxSize[0], precision].map(value => {
|
|
try { structuredClone({value}); return 'accepted'; }
|
|
catch (error) { return error.name; }
|
|
}).join('|');
|
|
})()
|
|
"#,
|
|
)
|
|
.expect("factory results must be identified before structured clone");
|
|
assert_eq!(
|
|
result,
|
|
"DataCloneError|DataCloneError|DataCloneError|DataCloneError"
|
|
);
|
|
}
|