mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 08:07:38 +00:00
fix(encoding): snapshot decoder input after options conversion
This commit is contained in:
@@ -124,9 +124,9 @@ struct TextDecoderOptions {
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "TextDecoder.decode")]
|
||||
struct TextDecoderDecodeArgs {
|
||||
#[webidl(converter = "buffer_source")]
|
||||
input: Option<webidl::BufferSource>,
|
||||
struct TextDecoderDecodeArgs<'s> {
|
||||
#[webidl(converter = "raw")]
|
||||
input: Option<webidl::AllowSharedBufferSource<'s>>,
|
||||
#[webidl(index = 1, dictionary)]
|
||||
options: TextDecodeOptions,
|
||||
}
|
||||
@@ -327,7 +327,7 @@ fn text_decoder_decode_callback<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s, v8::Value>,
|
||||
) {
|
||||
let Some(parsed) = webidl::parse_args::<TextDecoderDecodeArgs>(scope, &args) else {
|
||||
let Some(parsed) = webidl::parse_args::<TextDecoderDecodeArgs<'s>>(scope, &args) else {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
@@ -339,7 +339,7 @@ fn text_decoder_decode_callback<'s>(
|
||||
let stream = parsed.options.stream;
|
||||
let bytes = parsed
|
||||
.input
|
||||
.map(webidl::BufferSource::into_bytes)
|
||||
.map(|input| input.to_vec(scope))
|
||||
.unwrap_or_default();
|
||||
|
||||
let Some(decode_result) = decode_text_decoder_state(scope, decoder_id, &bytes, stream) else {
|
||||
|
||||
@@ -1634,3 +1634,26 @@ fn transform_stream_caught_enqueue_strategy_error_errors_stream_but_resolves_wri
|
||||
r#"["closed:RangeError","enqueue:RangeError","readable:RangeError","ready:RangeError","write:resolved"]"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_decoder_snapshots_input_after_options_conversion() {
|
||||
let mut vm = new_storage_test_vm("https://text-decoder-input-snapshot.test/");
|
||||
vm.eval(include_str!(
|
||||
"../../../../tests/fixtures/text-decoder-input-snapshot.js"
|
||||
))
|
||||
.expect("text decoder input snapshot contract should load");
|
||||
vm.eval("runTextDecoderInputSnapshotContract().then(result => globalThis.__textDecoderInputSnapshotResults = result)")
|
||||
.expect("text decoder input snapshot contract should execute");
|
||||
let facts: serde_json::Value = serde_json::from_str(
|
||||
&vm.eval("JSON.stringify(__textDecoderInputSnapshotResults)")
|
||||
.expect("text decoder input snapshot results should serialize"),
|
||||
)
|
||||
.expect("text decoder input snapshot results should be valid JSON");
|
||||
let checks = facts["checks"].as_array().expect("input snapshot checks");
|
||||
let failures = checks
|
||||
.iter()
|
||||
.filter(|check| check["passed"] != true)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(failures.is_empty(), "{failures:#?}");
|
||||
assert_eq!(checks.len(), 33);
|
||||
}
|
||||
|
||||
+34
@@ -1784,3 +1784,37 @@ async fn worker_xmlhttprequest_loadstart_handles_upload_abort_and_reopen() {
|
||||
r#"{"aborted":[["xhr-start:0:0:false","upload-abort:4:0:0:false","upload-loadend:4:0:0:false","xhr-abort:4","xhr-loadend:4","after-send:0:0"],["xhr-start:0:0:false","upload-start:0:2:true","upload-abort:4:0:0:false","upload-loadend:4:0:0:false","xhr-abort:4","xhr-loadend:4","after-send:0:0"],["xhr-start:0:0:false","upload-start:0:0:false","upload-abort:4:0:0:false","upload-loadend:4:0:0:false","xhr-abort:4","xhr-loadend:4","after-send:0:0"]],"reopened":["xhr-start:1","upload-start:0:2","xhr-start:1","after-reopen:1","load:replacement","loadend:4"]}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_text_decoder_snapshots_input_after_options_conversion() {
|
||||
ensure_v8();
|
||||
let source = [
|
||||
include_str!("../../../../../tests/fixtures/text-decoder-input-snapshot.js"),
|
||||
r#"
|
||||
runTextDecoderInputSnapshotContract().then(
|
||||
result => { postMessage(result); close(); },
|
||||
error => { postMessage({ failure: String(error) }); close(); }
|
||||
);
|
||||
"#,
|
||||
]
|
||||
.join("\n");
|
||||
let mut handle = spawn_worker(
|
||||
source,
|
||||
"https://text-decoder-input-snapshot.test/worker.js".into(),
|
||||
);
|
||||
let message = timeout(TIMEOUT, handle.recv())
|
||||
.await
|
||||
.expect("text decoder input snapshot contract should complete");
|
||||
handle.terminate_and_join();
|
||||
let facts: serde_json::Value = serde_json::from_str(&expect_post_json(
|
||||
message.expect("worker result channel should stay open"),
|
||||
))
|
||||
.expect("text decoder input snapshot results should be valid JSON");
|
||||
let checks = facts["checks"].as_array().expect("input snapshot checks");
|
||||
let failures = checks
|
||||
.iter()
|
||||
.filter(|check| check["passed"] != true)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(failures.is_empty(), "{failures:#?}");
|
||||
assert_eq!(checks.len(), 33);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
globalThis.runTextDecoderInputSnapshotContract = async function () {
|
||||
const checks = [];
|
||||
function check(name, operation) {
|
||||
try { checks.push({ name, passed: operation() === true }); }
|
||||
catch (error) { checks.push({ name, passed: false, error: String(error) }); }
|
||||
}
|
||||
for (const [name, make] of [
|
||||
["buffer", () => {
|
||||
const bytes = Uint8Array.of(65);
|
||||
return { input: bytes.buffer, mutate: () => bytes[0] = 66 };
|
||||
}],
|
||||
["view offset", () => {
|
||||
const bytes = Uint8Array.of(0, 65, 0);
|
||||
return { input: bytes.subarray(1, 2), mutate: () => bytes[1] = 66 };
|
||||
}],
|
||||
["DataView offset", () => {
|
||||
const bytes = Uint8Array.of(0, 65, 0);
|
||||
return { input: new DataView(bytes.buffer, 1, 1), mutate: () => bytes[1] = 66 };
|
||||
}],
|
||||
["shared buffer", () => {
|
||||
const bytes = new Uint8Array(new SharedArrayBuffer(1));
|
||||
bytes[0] = 65;
|
||||
return { input: bytes.buffer, mutate: () => Atomics.store(bytes, 0, 66) };
|
||||
}],
|
||||
["shared view offset", () => {
|
||||
const bytes = new Uint8Array(new SharedArrayBuffer(3));
|
||||
bytes[1] = 65;
|
||||
return { input: bytes.subarray(1, 2), mutate: () => Atomics.store(bytes, 1, 66) };
|
||||
}],
|
||||
]) {
|
||||
check(`snapshot after options: ${name}`, () => {
|
||||
const { input, mutate } = make();
|
||||
let reads = 0;
|
||||
const output = new TextDecoder().decode(input, {
|
||||
get stream() { reads++; mutate(); return false; },
|
||||
});
|
||||
return reads === 1 && output === "B";
|
||||
});
|
||||
}
|
||||
for (const length of [1, 10000]) {
|
||||
for (const kind of ["buffer", "view", "DataView"]) {
|
||||
check(`detach during options: ${kind} length=${length}`, () => {
|
||||
const bytes = new Uint8Array(length).fill(42);
|
||||
const input = kind === "buffer" ? bytes.buffer : kind === "view" ? bytes : new DataView(bytes.buffer);
|
||||
let reads = 0;
|
||||
const output = new TextDecoder().decode(input, {
|
||||
get stream() { reads++; bytes.buffer.transfer(0); return false; },
|
||||
});
|
||||
return reads === 1 && output === "";
|
||||
});
|
||||
}
|
||||
}
|
||||
const invalidSources = [
|
||||
["null", () => null],
|
||||
["object", () => ({})],
|
||||
["array", () => []],
|
||||
["forged view", () => Object.create(Uint8Array.prototype)],
|
||||
["Proxy", () => new Proxy(Uint8Array.of(65), {})],
|
||||
["revoked Proxy", () => {
|
||||
const proxy = Proxy.revocable(Uint8Array.of(65), {});
|
||||
proxy.revoke();
|
||||
return proxy.proxy;
|
||||
}],
|
||||
];
|
||||
for (const shared of [false, true]) {
|
||||
for (const kind of ["buffer", "view", "DataView"]) {
|
||||
invalidSources.push([`${shared ? "growable shared" : "resizable"} ${kind}`, () => {
|
||||
const buffer = shared ? new SharedArrayBuffer(1, { maxByteLength: 2 }) :
|
||||
new ArrayBuffer(1, { maxByteLength: 2 });
|
||||
return kind === "buffer" ? buffer : kind === "view" ? new Uint8Array(buffer) : new DataView(buffer);
|
||||
}]);
|
||||
}
|
||||
}
|
||||
for (const kind of ["buffer", "view"]) {
|
||||
invalidSources.push([`detached resizable ${kind}`, () => {
|
||||
const buffer = new ArrayBuffer(1, { maxByteLength: 2 });
|
||||
const view = new Uint8Array(buffer);
|
||||
buffer.transfer(0);
|
||||
return kind === "buffer" ? buffer : view;
|
||||
}]);
|
||||
}
|
||||
for (const [name, make] of invalidSources) {
|
||||
check(`reject before options: ${name}`, () => {
|
||||
const input = make();
|
||||
let reads = 0;
|
||||
try {
|
||||
new TextDecoder().decode(input, { get stream() { reads++; return false; } });
|
||||
return false;
|
||||
} catch (error) { return error instanceof TypeError && reads === 0; }
|
||||
});
|
||||
}
|
||||
check("options exception identity", () => {
|
||||
const sentinel = {};
|
||||
try {
|
||||
new TextDecoder().decode(Uint8Array.of(65), { get stream() { throw sentinel; } });
|
||||
return false;
|
||||
} catch (error) { return error === sentinel; }
|
||||
});
|
||||
check("undefined converts options and flushes", () => {
|
||||
const decoder = new TextDecoder();
|
||||
decoder.decode(Uint8Array.of(0xc9), { stream: true });
|
||||
let reads = 0;
|
||||
const output = decoder.decode(undefined, { get stream() { reads++; return false; } });
|
||||
return reads === 1 && output === "\ufffd";
|
||||
});
|
||||
for (const kind of ["buffer", "view", "DataView", "shared buffer"]) {
|
||||
check(`ignore author buffer getters: ${kind}`, () => {
|
||||
const buffer = kind === "shared buffer" ? new SharedArrayBuffer(1) : new ArrayBuffer(1);
|
||||
new Uint8Array(buffer)[0] = 65;
|
||||
const input = kind === "view" ? new Uint8Array(buffer) : kind === "DataView" ? new DataView(buffer) : buffer;
|
||||
let reads = 0;
|
||||
for (const name of ["byteLength", "buffer", "byteOffset"]) {
|
||||
Object.defineProperty(input, name, { get() { reads++; throw Error(name); } });
|
||||
}
|
||||
return new TextDecoder().decode(input) === "A" && reads === 0;
|
||||
});
|
||||
}
|
||||
check("receiver check before options", () => {
|
||||
let reads = 0;
|
||||
try {
|
||||
TextDecoder.prototype.decode.call({}, Uint8Array.of(65), { get stream() { reads++; return false; } });
|
||||
return false;
|
||||
} catch (error) { return error instanceof TypeError && reads === 0; }
|
||||
});
|
||||
check("reentrant options decode", () => {
|
||||
const decoder = new TextDecoder();
|
||||
let reads = 0;
|
||||
const output = decoder.decode(Uint8Array.of(0xac), {
|
||||
get stream() {
|
||||
reads++;
|
||||
decoder.decode(Uint8Array.of(0xe2, 0x82), { stream: true });
|
||||
return false;
|
||||
},
|
||||
});
|
||||
return reads === 1 && output === "\u20ac";
|
||||
});
|
||||
return { checks };
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
use crate::{Context, WebIdlConverter, WebIdlError};
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
/// `[AllowShared] BufferSource`, retaining the native buffer identity instead
|
||||
/// Web IDL `AllowSharedBufferSource`, retaining the native buffer identity instead
|
||||
/// of copying during conversion. Resizable and growable buffers require the
|
||||
/// separate `[AllowResizable]` annotation and are rejected here.
|
||||
pub enum AllowSharedBufferSource<'s> {
|
||||
|
||||
Reference in New Issue
Block a user