diff --git a/moli-renderer-v8/src/context_bootstrap/stream_adapter/construction.rs b/moli-renderer-v8/src/context_bootstrap/stream_adapter/construction.rs index d14e93e785..4ecef784bc 100644 --- a/moli-renderer-v8/src/context_bootstrap/stream_adapter/construction.rs +++ b/moli-renderer-v8/src/context_bootstrap/stream_adapter/construction.rs @@ -618,15 +618,15 @@ fn initialize_readable_stream_object_fields<'s>( let source_value = source .map(|value| value.into()) .unwrap_or_else(|| v8::null(scope).into()); - let algorithms = v8::Array::new(scope, 4); - let _ = algorithms.set_index(scope, READABLE_STREAM_ALGORITHM_SOURCE_INDEX, source_value); - let _ = algorithms.set_index(scope, READABLE_STREAM_ALGORITHM_PULL_INDEX, pull_algorithm); - let _ = algorithms.set_index( + let algorithms = v8::Array::new_with_elements( scope, - READABLE_STREAM_ALGORITHM_CANCEL_INDEX, - cancel_algorithm, + &[ + source_value, + pull_algorithm, + cancel_algorithm, + size_algorithm, + ], ); - let _ = algorithms.set_index(scope, READABLE_STREAM_ALGORITHM_SIZE_INDEX, size_algorithm); let controller = super::super::stream_objects::new_readable_stream_controller_object( scope, stream, diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 002c3d9878..50fc47b6b5 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2163,6 +2163,7 @@ mod observer_receivers; mod offline_audio_float; mod post_parse; mod queue_microtask; +mod readable_algorithm_arrays; mod remote_playback_interface; mod rendering_update; mod script_terminal_completion; diff --git a/moli-renderer-v8/src/script_vm/tests/readable_algorithm_arrays.js b/moli-renderer-v8/src/script_vm/tests/readable_algorithm_arrays.js new file mode 100644 index 0000000000..b6be64a243 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/readable_algorithm_arrays.js @@ -0,0 +1,57 @@ +(async () => { + const checks = []; + async function check(name, test) { + try { checks.push({name, passed: (await test()) === true}); } + catch (error) { checks.push({name, passed: false, error: String(error)}); } + } + let frame; + const owners = [['main', globalThis]]; + if (typeof document !== 'undefined') { + frame = document.createElement('iframe');document.body.append(frame); + owners.push(['child', frame.contentWindow]); + } + try { + for (const [label, owner] of owners) for (const kind of ['default', 'bytes', 'response']) { + for (const prototype of ['Array', 'Object']) for (const index of [0, 1, 2, 3]) { + for (const poison of ['setter', 'throwing-setter', 'getter-only', 'readonly-data']) { + await check(`${label}/${kind}/${prototype}/${index}/${poison}`, async () => { + const target = owner[prototype].prototype; + const previous = Object.getOwnPropertyDescriptor(target, String(index)); + const reason = {numericPoison: poison}; + let calls = 0, starts = 0, pulls = 0, stream, canceled, cancelReason; + const descriptor = poison === 'readonly-data' ? {configurable: true, value: 42, writable: false} + : poison === 'getter-only' ? {configurable: true, get() {calls++; throw reason;}} + : {configurable: true, set() {calls++; if (poison === 'throwing-setter') throw reason;}}; + Object.defineProperty(target, String(index), descriptor); + try { + if (kind === 'response') stream = new owner.Response(new owner.Uint8Array([65, 66])); + else { + const source = { + start(controller) {starts++; controller.enqueue(kind === 'bytes' ? new owner.Uint8Array([65]) : 'first');}, + pull(controller) {pulls++; controller.enqueue(kind === 'bytes' ? new owner.Uint8Array([66]) : 'second');controller.close();} + }; + if (kind === 'bytes') source.type = 'bytes'; + stream = new owner.ReadableStream(source); + canceled = new owner.ReadableStream({cancel(value) {cancelReason = value;}}); + } + } finally { + if (previous) Object.defineProperty(target, String(index), previous); + else delete target[String(index)]; + } + if (calls !== 0) return false; + if (kind === 'response') return Object.getPrototypeOf(stream) === owner.Response.prototype && await stream.text() === 'AB'; + if (Object.getPrototypeOf(stream) !== owner.ReadableStream.prototype) return false; + const reader = stream.getReader(); + const first = await reader.read(), second = await reader.read(), done = await reader.read(); + if (starts !== 1 || pulls !== 1 || first.done || second.done || !done.done) return false; + if (kind === 'bytes' ? first.value[0] !== 65 || second.value[0] !== 66 : first.value !== 'first' || second.value !== 'second') return false; + const cancelValue = {cancelValue: label};await canceled.cancel(cancelValue); + return cancelReason === cancelValue; + }); + } + } + } + } finally {if (frame) frame.remove();} + globalThis.__readableAlgorithmResults = {checks, total: checks.length, passed: checks.filter(row => row.passed).length, complete: true}; + return true; +})() diff --git a/moli-renderer-v8/src/script_vm/tests/readable_algorithm_arrays.rs b/moli-renderer-v8/src/script_vm/tests/readable_algorithm_arrays.rs new file mode 100644 index 0000000000..4ab32c9b7b --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/readable_algorithm_arrays.rs @@ -0,0 +1,26 @@ +use super::*; + +#[tokio::test(flavor = "current_thread")] +async fn readable_stream_construction_ignores_inherited_numeric_properties() { + let loader = static_http_loader(std::iter::empty::()); + let mut vm = new_storage_page_task_executor_test_vm_with_loader( + "https://readable-algorithm-arrays.test/", + &loader, + ); + vm.exec(include_str!("readable_algorithm_arrays.js"), None) + .unwrap(); + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(globalThis.__readableAlgorithmResults?.complete===true)", + "true", + "readable-algorithm-array-prototype-fixture", + ) + .await; + assert_eq!(vm.eval("__readableAlgorithmResults.total").unwrap(), "192"); + assert_eq!( + vm.eval("JSON.stringify(__readableAlgorithmResults.checks.filter(row=>!row.passed))") + .unwrap(), + "[]" + ); +}