From 4de6ec2d4a06d23deb2a8f834ba871f7ab31ebb9 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Fri, 2 Oct 2026 00:13:03 +0200 Subject: [PATCH 01/11] Call Wizer-initialized instances wizened Rename the pre-initialization terms after Wizer's own: a snapshot's instance is wizened. --- crates/core-runtime/tests/snapshot_idle.rs | 2 +- crates/platform/src/clock.rs | 6 +++--- crates/serve-test-support/tests/serve_wasm_e2e.rs | 10 +++++----- scripts/test-wizer.sh | 2 +- tests/wpt-harness/run-wpt.mjs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/core-runtime/tests/snapshot_idle.rs b/crates/core-runtime/tests/snapshot_idle.rs index 5b0fafd..dffdd9c 100644 --- a/crates/core-runtime/tests/snapshot_idle.rs +++ b/crates/core-runtime/tests/snapshot_idle.rs @@ -31,7 +31,7 @@ fn pending_work_is_reported_with_its_origin() { let scope = rt.default_global(); install_timer_globals(&scope, scope.global()); - // A loop that holds nothing can be snapshotted. + // A loop that holds nothing can be wizened. assert!(EventLoop::new().ensure_idle_for_snapshot().is_ok()); // A timer is reported with the stack that created it, and clearing it leaves the loop idle. diff --git a/crates/platform/src/clock.rs b/crates/platform/src/clock.rs index dc9cf01..c1ddb4b 100644 --- a/crates/platform/src/clock.rs +++ b/crates/platform/src/clock.rs @@ -19,7 +19,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; static OFFSET_NS: AtomicU64 = AtomicU64::new(0); /// The reading [`record_snapshot_reading`] took, carried into the snapshot for -/// [`resume_from_snapshot`] to apply. Zero for an instance that was never snapshotted. +/// [`resume_from_snapshot`] to apply. Zero for an instance that was never wizened. static SNAPSHOT_NS: AtomicU64 = AtomicU64::new(0); #[cfg(target_arch = "wasm32")] @@ -86,7 +86,7 @@ pub fn coarse_monotonic_ns() -> u64 { /// Record where the clock stood as a snapshot was made. /// -/// Must be the last thing an instance being snapshotted does, since a reading taken afterwards +/// Must be the last thing an instance being wizened does, since a reading taken afterwards /// would sit past the recorded one and so still be in a resumed instance's future. pub fn record_snapshot_reading() { SNAPSHOT_NS.store(host_monotonic_ns(), Ordering::Relaxed); @@ -113,7 +113,7 @@ pub async fn sleep(duration: std::time::Duration) { /// A monotonic clock reading, for deadlines that outlive a Wizer snapshot. /// /// `std::time::Instant` reads the host clock directly, so a deadline taken before a snapshot sits -/// in the resumed instance's future by however long the snapshotted instance ran. This reads +/// in the resumed instance's future by however long the wizened instance ran. This reads /// [`monotonic_ns`] instead, whose offset makes a resumed instance continue the timeline the /// snapshot ended on, so such a deadline is already past when the instance resumes. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] diff --git a/crates/serve-test-support/tests/serve_wasm_e2e.rs b/crates/serve-test-support/tests/serve_wasm_e2e.rs index 696fe04..a7d998f 100644 --- a/crates/serve-test-support/tests/serve_wasm_e2e.rs +++ b/crates/serve-test-support/tests/serve_wasm_e2e.rs @@ -1076,14 +1076,14 @@ fn a_client_that_leaves_mid_startup_does_not_strand_the_instance() { ); } -/// A snapshotted instance serves many requests, so the repairs a resumed instance needs — the -/// monotonic clock's origin above all, anchored to the process the snapshot was taken in — have to -/// happen exactly once, on the first of them. Running them again on a later request would rebase +/// A wizened instance serves many requests, so the repairs a resumed instance needs have to happen +/// exactly once, on the first of them. The main one rebases the monotonic clock's origin, which is +/// anchored to the process the snapshot was taken in. Running them again on a later request would rebase /// the clock under a handler that had already read it, so `performance.now()` is checked for going /// forwards across requests rather than merely being plausible on the first. The shared global /// carries across requests here exactly as it does without a snapshot. #[test] -fn a_snapshotted_instance_keeps_its_clock_and_global_across_requests() { +fn a_wizened_instance_keeps_its_clock_and_global_across_requests() { const HANDLER: &str = r#"const atStartup = performance.now(); globalThis.served = 0; addEventListener('fetch', (event) => { @@ -2751,7 +2751,7 @@ fn nothing_but_wait_until_extends_the_event_lifetime() { /// mid-evaluation and the resumed instance's first request drives the rest (`test-wizer.sh`'s /// `awaited.mjs` case, extended here with the ordering the marker pins). #[test] -fn a_snapshotted_instance_waits_for_the_scripts_top_level_await() { +fn a_wizened_instance_waits_for_the_scripts_top_level_await() { // The ordering is recorded in a global and reported in the response rather than through the // usual stderr markers: writing to stdio before the snapshot is taken leaves the resumed // instance with a stale stream handle, and every later write traps (see the wizer item in diff --git a/scripts/test-wizer.sh b/scripts/test-wizer.sh index 9f84b9d..35e4881 100755 --- a/scripts/test-wizer.sh +++ b/scripts/test-wizer.sh @@ -123,7 +123,7 @@ await new Promise((resolve) => setTimeout(resolve, 50)); addEventListener('fetch', (event) => event.respondWith(new Response('after-await'))); JS snapshot snap-awaited.wasm "awaited.mjs" >/dev/null -check "a top-level await is snapshotted rather than refused" \ +check "a top-level await finishes before the snapshot rather than being refused" \ "after-await" \ "$(serve_and_get snap-awaited.wasm "awaited.mjs" 18352)" diff --git a/tests/wpt-harness/run-wpt.mjs b/tests/wpt-harness/run-wpt.mjs index 6ccb105..def09cc 100644 --- a/tests/wpt-harness/run-wpt.mjs +++ b/tests/wpt-harness/run-wpt.mjs @@ -746,7 +746,7 @@ function ensureWizenedRuntime() { writeFileSync(stamp, want); return output; } catch (e) { - console.warn(` Wizening failed, serving the un-snapshotted runtime: ${e.message}`); + console.warn(` Wizening failed, serving the un-wizened runtime: ${e.message}`); return null; } } From 2d51a57e458c12da15b14d140d3b84eef9b5f3e6 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Thu, 22 Oct 2026 15:07:02 +0200 Subject: [PATCH 02/11] Remove a core dump committed by accident `crates/core-runtime/core` is a 31 MB process core dump with no use in the build. --- crates/core-runtime/core | Bin 31772672 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 crates/core-runtime/core diff --git a/crates/core-runtime/core b/crates/core-runtime/core deleted file mode 100644 index 69d79bff52ed19ad43392130dec115afef3b1744..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31772672 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 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 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 ?wiW)>ax40?4Db8&nvrY%RGHiw2v4sdko_F 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 z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U_^&5$;PO>_Pn;-< z!*@&N*~L}m-|Z(Jz3`{xPzh)U+JSapJCFl%Kn}pzue4009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N 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%0Dmr1NQegt44^P-5;(xNO-$HTnoOd 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�t7x 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 _&Yk}ZeSGe$5B4joQH3mlJdAdIFsk& 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 z009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH z0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI z5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X z009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH z0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI z5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X z009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH z0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI z5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X z009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH0T2KI5C8!X009sH z0T2KI5C8!X009sH0T2KI5C8!X009sHfsQAT!2tpw00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAn^a#v76Ak$}|ALCvAn`CRY@f_6V*-Ax?{qQ8CMk&XB}no1ZQP3H%6I-N;j+_j9%&nDoIfi4Q-ApU^MEL>z&E{Nbp=t71vGjiwN z^S$q__tqv6QX1`ZA$<4yIq&y=&pA2gyN3V)0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N 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 z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+z-<(0Ep*@ScB^Xb(nIa0jQVnv4}3S-oL^ol{q{;_cD2MA?sr$5sD~Ol z-TtgqH4^j9t?WL}g`x3TUAqu@zB8C_FZm|^ROCMv>p2?b^}m$))(>Z-Quc;5$Kcsp zYw2@q%kF2=Gt10xyEkj*yn6(#1PJ`*1ddNkoOtxa@v5GBbLHB*pH3~7=hOBl4>!-@ zwRlceoukdt=c4}J{l}X2%Ois#?K9QIcmg)-^)!e{XaT?aY@+Uy1%%&p27%h@*;fXZ~2v zM11dj_EDLyTEEtO-`3wgSguDkRmwP@>*%~tJmmBC_5AK~ zSx+mHu2@gb|HFslPaOH` zrN`3kgUx^c>&Nvxf4j>1<=0+aSe&evS6+GR-277g_R9R?`N=x}+kW}9aeZ^(>CYc& zuH)utxt{5n`-=3-{z?J_2oNAZVE+Qc`>*|vyfb=s&eW^@{ch*)Wgj>{Jzf%fBlcfW zcFOCA=G{fV!)ENO+p*v4@4MUm2FHOGn8^F)Kr8IM_*3!wD1R62Jz}3d;&)EfsbfGq zp<4SKuN#41QQ%N7R{wAK$oDks@m^rKJ?mrnVfV7Vd$ZO%OkmIJmG{SR-aoV7+B06p z+2{R~-IWJS;Osr+ecATVd&$=9HoB3}Jm>3{unwwG~R<4;}P*yw#9JX_kc z|6tUcwbA&vbgJ~<&irn68Tgd*o5B8x1KNYI^&oAccXh~XTD7OO7zcq#>x6d995J%^T&E7 z;(O<_kIH;iyZe5Bz4f;bmg`YXl`_ufIyx^D&$;hQznN#seUSNXg~!>5fAQou&3b13 zR@O5W<&_vW`(lar>T@jv%l)RUiN`=)8i$vKgIql z%1-n4q3qoI9X4ZM-H!cUf8X8iH#iQoz(n3R2U=n8#h;4bNBO&G?-Bd#5x;Y)P8|c{ z3Dw%?c-;v6iULP^vHE|*N50NE?9cwjBGO74$iPVO<`@+f4rL(}%(N(sHq#<-6SOPDg^Lz~5@Ie|w782F zLxEhVz)j$yMT=^>=X~!wG*dGUQc?IEkneuy`+48*Ip4W+?vVfi0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk 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 zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly 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 zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?uxo!Ia$RKr!0)CtU8Ir~wW3g+NW`>3y6u*v zO_h#<7_s7t5mwB@wu!jXN`r1oplYVJO2wD)K}a8jfPyhkQ3@{DhpqOtFfDyp|HaX^ z5HOJleF#Kc=brO@Yj)Ue7mHHJZyx&Xx#!<^e)rrvcWz36009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk 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 zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72>hG9 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 Date: Thu, 22 Oct 2026 15:31:43 +0200 Subject: [PATCH 03/11] Harden and simplify the engine bindings, macros and core runtime Property names passed as `&'static CStr` are interned through `get_or_init_property_id`, with `*_by_name` variants for runtime names. A raw `Value` no longer implements `ToJSVal`, so a function taking `impl ToJSVal` can GC before it converts its argument, and `Object::from_raw_value` takes a raw value read from a rooted location right before the call. Typed array bytes are only borrowed inside closures, the `ArrayBuffer` and view brand checks accept resizable buffers and cross-compartment wrappers, and calling a non-callable value from native code throws "X is not a function". `#[jsclass]` accessors are non-enumerable, constructors called without `new` throw SpiderMonkey's class-constructor error, hidden classes register through `register_hidden_class`, and the generated module `register` is safe. Unused APIs are removed. Each global keeps its `setTimeout` and `setInterval` timers in a map of its own, through the new `js::class::global_state`, so `clearTimeout` and `clearInterval` clear the global's timers that another event loop created, and no other global's. Serve timeouts accept fractional seconds, `--enforce-fetch-restrictions` is renamed `--web-security-restrictions`, and `file:` URL imports resolve on wasm. The core-runtime tests that run on wasm share one test binary, and redundant tests are removed. Dependent crates get the call-site changes the new APIs require. --- Cargo.lock | 1 + .../builtins/fetch-event/src/fetch_event.rs | 2 +- crates/builtins/web-fetch/src/algorithms.rs | 12 +- .../builtins/web-fetch/src/outgoing_body.rs | 20 +- crates/builtins/web-fetch/src/request.rs | 14 +- crates/builtins/web-fetch/src/response.rs | 2 +- crates/builtins/web-fetch/src/transport.rs | 2 +- crates/builtins/web-fetch/tests/config.rs | 18 +- .../builtins/web-fetch/tests/fetch_network.rs | 2 +- .../web-globals/src/encoding/text_decoder.rs | 13 +- .../web-globals/src/encoding/text_encoder.rs | 21 +- .../web-globals/src/events/algorithms.rs | 4 +- .../web-globals/src/events/event_target.rs | 2 +- crates/builtins/web-streams/src/algorithms.rs | 2 +- crates/builtins/web-streams/src/lib.rs | 1 + .../web-streams/src/readable/algorithms.rs | 11 +- .../src/readable/read_all_bytes.rs | 8 +- .../src/readable/readable_stream.rs | 4 +- crates/core-runtime/Cargo.toml | 1 + crates/core-runtime/src/config.rs | 253 ++-- crates/core-runtime/src/event_loop.rs | 347 +++-- .../core-runtime/src/event_loop/interest.rs | 9 +- crates/core-runtime/src/event_loop/timer.rs | 73 +- crates/core-runtime/src/finalization.rs | 4 +- crates/core-runtime/src/invocation.rs | 10 - crates/core-runtime/src/lib.rs | 66 +- crates/core-runtime/src/module.rs | 353 ++--- crates/core-runtime/src/runtime.rs | 6 +- crates/core-runtime/src/test_util.rs | 20 + .../tests/accessor_return_styles.rs | 36 +- crates/core-runtime/tests/const_naming.rs | 211 +-- crates/core-runtime/tests/event_loop.rs | 884 ----------- crates/core-runtime/tests/heap_value.rs | 2 +- crates/core-runtime/tests/implicit_ctor.rs | 105 +- .../tests/instance_value_unforgeable.rs | 32 +- crates/core-runtime/tests/jsglobals_use.rs | 14 +- .../tests/multi_global_registry.rs | 2 +- .../core-runtime/tests/parameterized_types.rs | 574 ++------ .../core-runtime/tests/private_data_borrow.rs | 11 - crates/core-runtime/tests/promise_args.rs | 12 +- crates/core-runtime/tests/raw_ident_name.rs | 11 +- crates/core-runtime/tests/rest_args.rs | 441 ++---- .../core-runtime/tests/runtime/event_loop.rs | 557 +++++++ .../tests/{ => runtime}/js_api.rs | 128 +- crates/core-runtime/tests/runtime/main.rs | 8 + .../tests/{ => runtime}/snapshot_idle.rs | 5 +- crates/core-runtime/tests/scope.rs | 94 +- crates/core-runtime/tests/scope_gc_zeal.rs | 47 +- crates/core-runtime/tests/scope_root_enum.rs | 7 +- crates/core-runtime/tests/traceable_enum.rs | 12 +- crates/core-runtime/tests/typedef_macros.rs | 654 +++------ crates/js/src/class.rs | 245 ++-- crates/js/src/compartment.rs | 17 +- crates/js/src/compile.rs | 33 +- crates/js/src/conversion.rs | 42 +- crates/js/src/error.rs | 7 - crates/js/src/function.rs | 87 +- crates/js/src/gc/handle.rs | 2 +- crates/js/src/gc/pool.rs | 6 +- crates/js/src/gc/scope.rs | 1 - crates/js/src/id.rs | 2 +- crates/js/src/iteration.rs | 32 +- crates/js/src/jobs.rs | 12 +- crates/js/src/json.rs | 2 +- crates/js/src/lib.rs | 14 +- crates/js/src/object.rs | 128 +- crates/js/src/prelude.rs | 3 - crates/js/src/promise.rs | 8 +- crates/js/src/stack.rs | 8 +- crates/js/src/tls.rs | 6 +- crates/js/src/typedarray.rs | 357 +++-- crates/js/src/webidl_enum.rs | 3 +- crates/libstarling/tests/serve.rs | 30 +- crates/starling-macro/src/lib.rs | 1299 ++++++----------- crown/src/common.rs | 76 +- crown/src/trace_in_no_trace.rs | 21 +- crown/src/unrooted_must_root.rs | 134 +- docs/rooting.md | 6 + starling/examples/native_module.rs | 5 +- starling/examples/showcase.rs | 5 +- tests/wpt-harness/run-wpt.mjs | 2 +- 81 files changed, 2913 insertions(+), 4808 deletions(-) delete mode 100644 crates/core-runtime/tests/event_loop.rs create mode 100644 crates/core-runtime/tests/runtime/event_loop.rs rename crates/core-runtime/tests/{ => runtime}/js_api.rs (85%) create mode 100644 crates/core-runtime/tests/runtime/main.rs rename crates/core-runtime/tests/{ => runtime}/snapshot_idle.rs (96%) diff --git a/Cargo.lock b/Cargo.lock index 5d53900..dc055e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,6 +386,7 @@ dependencies = [ "shlex 2.0.1", "tempfile", "tokio", + "url", ] [[package]] diff --git a/crates/builtins/fetch-event/src/fetch_event.rs b/crates/builtins/fetch-event/src/fetch_event.rs index 32e65c0..8800373 100644 --- a/crates/builtins/fetch-event/src/fetch_event.rs +++ b/crates/builtins/fetch-event/src/fetch_event.rs @@ -393,7 +393,7 @@ impl<'s> FetchEvent<'s> { return; } let result = if responded { - handled.resolve(scope, value::undefined()) + handled.resolve(scope, HandleValue::undefined()) } else { let reason = if self.data().respond_with_error { "the promise passed to respondWith did not produce a Response" diff --git a/crates/builtins/web-fetch/src/algorithms.rs b/crates/builtins/web-fetch/src/algorithms.rs index 5b3d8dc..d672a0e 100644 --- a/crates/builtins/web-fetch/src/algorithms.rs +++ b/crates/builtins/web-fetch/src/algorithms.rs @@ -96,10 +96,10 @@ pub(crate) fn extract_body<'r>( } // Step 10 `BufferSource`: Set _source_ to a `copy of the bytes` held by _object_. BodyInitOrBytes::BodyInit(BodyInit::ArrayBuffer(buffer)) => { - source = BodySource::Bytes(bytes::Bytes::from(buffer.copy_bytes())); + source = BodySource::Bytes(bytes::Bytes::from(buffer.copy_bytes(scope)?)); } BodyInitOrBytes::BodyInit(BodyInit::ArrayBufferView(view)) => { - source = BodySource::Bytes(bytes::Bytes::from(view.copy_bytes())); + source = BodySource::Bytes(bytes::Bytes::from(view.copy_bytes(scope)?)); } // Step 10 `URLSearchParams`: Set _source_ to the result of running the // `application/x-www-form-urlencoded` serializer` with _object_’s `list`. Set _type_ @@ -801,7 +801,7 @@ pub(crate) fn append_to_headers( // return. // Steps 3.1–3.4 run inside `is_no_cors_safelisted_request_header_after_append`, which checks // the combined value without materializing it. - if core_runtime::config::enforce_fetch_restrictions() + if core_runtime::config::web_security_restrictions() && headers.data().guard == Guard::RequestNoCors { let safelisted = { @@ -847,7 +847,7 @@ pub(crate) fn validate_header_name( // request-header`, then return false. // Forbidden request-headers are a browser-security policy; skip when an embedder has disabled // request restrictions (server-side mode). - if core_runtime::config::enforce_fetch_restrictions() + if core_runtime::config::web_security_restrictions() && headers.data().guard == Guard::Request && forbidden_request_header(name, value) { @@ -855,7 +855,7 @@ pub(crate) fn validate_header_name( } // Step 4: If _headers_’s `guard` is "`response`" and _name_ is a `forbidden response-header // name`, then return false. - if core_runtime::config::enforce_fetch_restrictions() + if core_runtime::config::web_security_restrictions() && headers.data().guard == Guard::Response && is_forbidden_response_header_name(name) { @@ -923,7 +923,7 @@ pub(crate) fn fill_headers( pub(crate) fn refill_headers_from_own_list(headers: &Headers<'_>) { // With request restrictions off, both filters below are disabled, and re-appending an // existing header list's entries results in the same list. So we can just do nothing. - if !core_runtime::config::enforce_fetch_restrictions() { + if !core_runtime::config::web_security_restrictions() { return; } diff --git a/crates/builtins/web-fetch/src/outgoing_body.rs b/crates/builtins/web-fetch/src/outgoing_body.rs index f08664c..a7bc955 100644 --- a/crates/builtins/web-fetch/src/outgoing_body.rs +++ b/crates/builtins/web-fetch/src/outgoing_body.rs @@ -374,16 +374,16 @@ fn pump_chunk_step( match Uint8Array::from_jsval(scope, chunk, ()) { // The spec's "a copy of chunk": the chunk's bytes are copied once, into the batch. Ok(array) => { - // SAFETY: the view's bytes are only read by the copy, which cannot run JS or trigger a - // GC. - let bytes = unsafe { array.as_array_buffer_view().bytes() }; - // Appending to a batch reserves room up to the limit, so the batch is copied once - // rather than through every doubling of its capacity. - if !data.batch.is_empty() && data.batch.capacity() - data.batch.len() < bytes.len() { - let wanted = BATCH_LIMIT.max(data.batch.len() + bytes.len()) - data.batch.len(); - data.batch.reserve_exact(wanted); - } - data.batch.extend_from_slice(bytes); + let batch = &mut data.batch; + array.as_array_buffer_view().with_bytes(|bytes| { + // Appending to a batch reserves room up to the limit, so the batch is copied once + // rather than through every doubling of its capacity. + if !batch.is_empty() && batch.capacity() - batch.len() < bytes.len() { + let wanted = BATCH_LIMIT.max(batch.len() + bytes.len()) - batch.len(); + batch.reserve_exact(wanted); + } + batch.extend_from_slice(bytes); + }); } Err(_) => { data.end = Some(StreamEnd::Errored( diff --git a/crates/builtins/web-fetch/src/request.rs b/crates/builtins/web-fetch/src/request.rs index aa5df91..f558088 100644 --- a/crates/builtins/web-fetch/src/request.rs +++ b/crates/builtins/web-fetch/src/request.rs @@ -326,7 +326,7 @@ impl Request { throw_type_error(scope, c"Failed to parse URL from Request input") })?; // Step 5.3: If _parsedURL_ `includes credentials`, then `throw` a `TypeError`. - if config::enforce_fetch_restrictions() + if config::web_security_restrictions() && !input.is_empty() && (!parsed.username().is_empty() || parsed.password().is_some()) { @@ -374,7 +374,7 @@ impl Request { // Step 10: If _init_["`window`"] `exists` and is non-null, then `throw` a `TypeError`. // (There is no window in this runtime, so the rejection is a browser-model rule, gated by // the request-restrictions switch.) - if config::enforce_fetch_restrictions() { + if config::web_security_restrictions() { if let Some(window) = init.as_ref().and_then(|i| i.window.as_ref()) { if !window.is_null_or_undefined() { return Err(throw_type_error(scope, c"Request window must be null")); @@ -450,7 +450,7 @@ impl Request { // Step 16: Let _mode_ be _init_["`mode`"] if it `exists`, and _fallbackMode_ otherwise. let mode = init.as_ref().and_then(|i| i.mode).or(fallback_mode); // Step 17: If _mode_ is "`navigate`", then `throw` a `TypeError`. - if config::enforce_fetch_restrictions() && mode == Some(RequestMode::Navigate) { + if config::web_security_restrictions() && mode == Some(RequestMode::Navigate) { return Err(throw_type_error( scope, c"Request mode must not be navigate", @@ -473,7 +473,7 @@ impl Request { } // Step 21: If _request_’s `cache mode` is "`only-if-cached`" and _request_’s `mode` is // _not_ "`same-origin`", then `throw` a `TypeError`. - if config::enforce_fetch_restrictions() + if config::web_security_restrictions() && request.cache_mode == RequestCache::OnlyIfCached && request.mode != RequestMode::SameOrigin { @@ -504,7 +504,7 @@ impl Request { // Step 25.2: If _method_ is not a `method` or _method_ is a `forbidden method`, // then `throw` a `TypeError`. if !algorithms::is_method(&method) - || (config::enforce_fetch_restrictions() + || (config::web_security_restrictions() && algorithms::is_forbidden_method(&method)) { return Err(throw_type_error(scope, c"Invalid request method")); @@ -541,7 +541,7 @@ impl Request { let headers = Headers::from_list(scope, init_header_list, Guard::Request)?; // Step 32: If `this`’s `request`’s `mode` is "`no-cors`", then: // (A browser-security policy, gated by the request-restrictions switch.) - if config::enforce_fetch_restrictions() && self.data().request.mode == RequestMode::NoCors { + if config::web_security_restrictions() && self.data().request.mode == RequestMode::NoCors { // Step 32.1: If `this`’s `request`’s `method` is not a `CORS-safelisted method`, then // `throw` a `TypeError`. if !algorithms::is_cors_safelisted_method(&self.data().request.method) { @@ -650,7 +650,7 @@ impl Request { } // Step 39.2: If `this`’s `request`’s `mode` is neither "`same-origin`" nor // "`cors`", then throw a `TypeError`. - if config::enforce_fetch_restrictions() { + if config::web_security_restrictions() { let mode = self.data().request.mode; if mode != RequestMode::SameOrigin && mode != RequestMode::Cors { return Err(throw_type_error( diff --git a/crates/builtins/web-fetch/src/response.rs b/crates/builtins/web-fetch/src/response.rs index c4d3dfa..94b8ebd 100644 --- a/crates/builtins/web-fetch/src/response.rs +++ b/crates/builtins/web-fetch/src/response.rs @@ -579,7 +579,7 @@ pub(crate) fn response_from_platform<'r>( // (Hiding the redirect target is a browser-security policy, gated by the request-restrictions // switch: without it, `redirect: "manual"` hands the caller the real redirect response — // status, `Location` header, body and all.) - if core_runtime::config::enforce_fetch_restrictions() + if core_runtime::config::web_security_restrictions() && redirect_mode == RequestRedirect::Manual && is_redirect_status(response.status) { diff --git a/crates/builtins/web-fetch/src/transport.rs b/crates/builtins/web-fetch/src/transport.rs index f8df061..770e387 100644 --- a/crates/builtins/web-fetch/src/transport.rs +++ b/crates/builtins/web-fetch/src/transport.rs @@ -54,7 +54,7 @@ fn apply_main_fetch_switch( redirect_mode: RequestRedirect, tainting: &mut ResponseTainting, ) -> Result<(), Error> { - if !core_runtime::config::enforce_fetch_restrictions() { + if !core_runtime::config::web_security_restrictions() { return Ok(()); } let Some(request_origin) = request_origin else { diff --git a/crates/builtins/web-fetch/tests/config.rs b/crates/builtins/web-fetch/tests/config.rs index e9c40b2..13591c7 100644 --- a/crates/builtins/web-fetch/tests/config.rs +++ b/crates/builtins/web-fetch/tests/config.rs @@ -6,7 +6,7 @@ //! methods, no-CORS safelisting, origin/mode enforcement — are off by default: //! StarlingMonkey is primarily a server runtime, where a script must be able to //! set `Host` or use `CONNECT`. WPT mode turns them on by default, and an -//! explicit `--enforce-fetch-restrictions[=bool]` overrides either default. +//! explicit `--web-security-restrictions[=bool]` overrides either default. //! `Runtime::init` applies the configured value, which is what these tests //! exercise end to end: with restrictions off, a forbidden header (`Host`) and //! a forbidden method (`CONNECT`) are accepted; with them on, they are @@ -62,17 +62,17 @@ fn wpt_mode_enforces_restrictions() { assert_eq!(out, "host=null,connectThrew=true"); } -/// An explicit `--enforce-fetch-restrictions=false` overrides WPT mode's -/// default, and `--enforce-fetch-restrictions` (bare) enables them without +/// An explicit `--web-security-restrictions=false` overrides WPT mode's +/// default, and `--web-security-restrictions` (bare) enables them without /// WPT mode. #[test] fn explicit_flag_overrides_either_default() { let config = - RuntimeConfig::from_arg_string("--wpt-mode --enforce-fetch-restrictions=false").unwrap(); + RuntimeConfig::from_arg_string("--wpt-mode --web-security-restrictions=false").unwrap(); let out = run(&config, PERMISSIVE_PROBE); assert_eq!(out, "host=example.com,method=CONNECT"); - let config = RuntimeConfig::from_arg_string("--enforce-fetch-restrictions").unwrap(); + let config = RuntimeConfig::from_arg_string("--web-security-restrictions").unwrap(); let out = run(&config, ENFORCED_PROBE); assert_eq!(out, "host=null,connectThrew=true"); } @@ -85,21 +85,21 @@ fn setter_overrides_after_init() { libstarling::register_builtins(); let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); - core_runtime::config::set_enforce_fetch_restrictions(true); - assert!(core_runtime::config::enforce_fetch_restrictions()); + core_runtime::config::set_web_security_restrictions(true); + assert!(core_runtime::config::web_security_restrictions()); assert_eq!( eval_and_read_out(&scope, ENFORCED_PROBE), "host=null,connectThrew=true" ); // Restore for other tests on this thread. - core_runtime::config::set_enforce_fetch_restrictions(false); + core_runtime::config::set_web_security_restrictions(false); } /// `referrerPolicy` is stored and validated whether or not fetch restrictions /// are enforced. #[test] fn referrer_policy_is_kept_without_restrictions() { - let config = RuntimeConfig::from_arg_string("--enforce-fetch-restrictions=false").unwrap(); + let config = RuntimeConfig::from_arg_string("--web-security-restrictions=false").unwrap(); let out = run( &config, r#" diff --git a/crates/builtins/web-fetch/tests/fetch_network.rs b/crates/builtins/web-fetch/tests/fetch_network.rs index 8f5b869..3db7ab2 100644 --- a/crates/builtins/web-fetch/tests/fetch_network.rs +++ b/crates/builtins/web-fetch/tests/fetch_network.rs @@ -820,7 +820,7 @@ fn a_reused_abort_signal_does_not_accumulate_fetch_abort_algorithms() { let signal_value = js::compile::evaluate_with_filename(&scope, "globalThis.__controller.signal", "", 1) .unwrap(); - let signal = js::Object::from_value(&scope, signal_value.get()) + let signal = js::Object::from_raw_value(&scope, signal_value.get()) .unwrap() .cast::() .unwrap(); diff --git a/crates/builtins/web-globals/src/encoding/text_decoder.rs b/crates/builtins/web-globals/src/encoding/text_decoder.rs index 12d4010..c9a738a 100644 --- a/crates/builtins/web-globals/src/encoding/text_decoder.rs +++ b/crates/builtins/web-globals/src/encoding/text_decoder.rs @@ -106,15 +106,10 @@ impl TextDecoder<'_> { let decoder = data.decoder.as_mut().expect("set by step 1"); // Step 3: If _input_ is given, then `push` a `copy of` _input_ to `this`'s `I/O queue`. - // (We decode directly from _input_'s bytes. Nothing retains them after this call.) - // SAFETY: `input` is rooted via the Uint8Array handle and the slice - // is only borrowed for the duration of the decode call below, which - // is pure CPU work in `encoding_rs` and cannot trigger GC or detach - // the underlying buffer. - let bytes: &[u8] = match input { - Some(ArrayBufferViewOrArrayBuffer::Buffer(input)) => unsafe { input.bytes() }, - Some(ArrayBufferViewOrArrayBuffer::View(input)) => unsafe { input.bytes() }, - None => &[], + let bytes: &[u8] = &match input { + Some(ArrayBufferViewOrArrayBuffer::Buffer(input)) => input.copy_bytes(scope)?, + Some(ArrayBufferViewOrArrayBuffer::View(input)) => input.copy_bytes(scope)?, + None => Vec::new(), }; // windows-1252 decodes every byte outside 0x80..=0x9F to the code point of the same diff --git a/crates/builtins/web-globals/src/encoding/text_encoder.rs b/crates/builtins/web-globals/src/encoding/text_encoder.rs index 271e4fb..6ff1a02 100644 --- a/crates/builtins/web-globals/src/encoding/text_encoder.rs +++ b/crates/builtins/web-globals/src/encoding/text_encoder.rs @@ -77,18 +77,17 @@ impl TextEncoder<'_> { // encode whole scalar values while they fit, replace unpaired surrogates with U+FFFD as // step 5's conversion does, and return _read_ in code units and _written_ in bytes.) // - // SAFETY: the destination slice is only borrowed for the conversion, which cannot run JS - // or trigger a GC. - let dest_buf = unsafe { destination.as_mut_slice() }; - // SAFETY: same as above. + // SAFETY: the closure doesn't call into SpiderMonkey. let (read, written) = unsafe { - source.with_chars(scope, |chars| match chars { - StrChars::Latin1(chars) => { - encoding_rs::mem::convert_latin1_to_utf8_partial(chars, dest_buf) - } - StrChars::TwoByte(chars) => { - encoding_rs::mem::convert_utf16_to_utf8_partial(chars, dest_buf) - } + source.with_chars(scope, |chars| { + destination.with_slice_mut(|dest_buf| match chars { + StrChars::Latin1(chars) => { + encoding_rs::mem::convert_latin1_to_utf8_partial(chars, dest_buf) + } + StrChars::TwoByte(chars) => { + encoding_rs::mem::convert_utf16_to_utf8_partial(chars, dest_buf) + } + }) }) }?; diff --git a/crates/builtins/web-globals/src/events/algorithms.rs b/crates/builtins/web-globals/src/events/algorithms.rs index 8764321..0b84f2d 100644 --- a/crates/builtins/web-globals/src/events/algorithms.rs +++ b/crates/builtins/web-globals/src/events/algorithms.rs @@ -83,7 +83,7 @@ fn options_dictionary<'s>( match options { None => OptionsArgument::Dictionary(None), Some(val) if val.is_null_or_undefined() => OptionsArgument::Dictionary(None), - Some(val) => match Object::from_value(scope, *val) { + Some(val) => match Object::from_raw_value(scope, val.get()) { Ok(obj) => OptionsArgument::Dictionary(Some(obj)), Err(_) => OptionsArgument::Boolean( bool::from_jsval(scope, val, ()).expect("ToBoolean cannot fail"), @@ -96,7 +96,7 @@ fn options_dictionary<'s>( fn read_bool_option( scope: &Scope<'_>, dictionary: Option<&Object<'_>>, - name: &std::ffi::CStr, + name: &'static std::ffi::CStr, ) -> Result { let Some(dictionary) = dictionary else { return Ok(false); diff --git a/crates/builtins/web-globals/src/events/event_target.rs b/crates/builtins/web-globals/src/events/event_target.rs index f12197a..6c5ffd4 100644 --- a/crates/builtins/web-globals/src/events/event_target.rs +++ b/crates/builtins/web-globals/src/events/event_target.rs @@ -53,7 +53,7 @@ fn event_listener_argument<'s>( if callback.is_null_or_undefined() { return Ok(None); } - Object::from_value(scope, *callback) + Object::from_raw_value(scope, callback.get()) .map(Some) .map_err(|_| throw_type_error(scope, c"event listener must be an object")) } diff --git a/crates/builtins/web-streams/src/algorithms.rs b/crates/builtins/web-streams/src/algorithms.rs index 09e38af..ca28081 100644 --- a/crates/builtins/web-streams/src/algorithms.rs +++ b/crates/builtins/web-streams/src/algorithms.rs @@ -49,7 +49,7 @@ pub(crate) fn pair_parts<'r>( scope: &'r Scope<'_>, payload: HandleValue<'_>, ) -> (HandleValue<'r>, HandleValue<'r>) { - let arr = Object::from_value(scope, *payload).expect("payload is an array"); + let arr = Object::from_raw_value(scope, payload.get()).expect("payload is an array"); ( arr.get_element(scope, 0).expect("element 0"), arr.get_element(scope, 1).expect("element 1"), diff --git a/crates/builtins/web-streams/src/lib.rs b/crates/builtins/web-streams/src/lib.rs index 93cf275..9db6e55 100644 --- a/crates/builtins/web-streams/src/lib.rs +++ b/crates/builtins/web-streams/src/lib.rs @@ -43,6 +43,7 @@ pub fn add_to_global(scope: &Scope<'_>, global: Object<'_>) { readable::algorithms::PipeState::add_to_global(scope, global); readable::algorithms::FromIterableState::add_to_global(scope, global); readable::read_all_bytes::ReadAllBytesState::add_to_global(scope, global); + js::iteration::AsyncIteratorRecord::add_to_global(scope, global); // Chain the async iterator's prototype under `%AsyncIteratorPrototype%` so it // inherits `[Symbol.asyncIterator]` (returning `this`). diff --git a/crates/builtins/web-streams/src/readable/algorithms.rs b/crates/builtins/web-streams/src/readable/algorithms.rs index 93d68a9..b0b7466 100644 --- a/crates/builtins/web-streams/src/readable/algorithms.rs +++ b/crates/builtins/web-streams/src/readable/algorithms.rs @@ -2292,7 +2292,7 @@ fn byte_tee_forward_rejected( let r = args.get(0); // If _thisReader_ is not _reader_ (the current reader), return. let this_reader = - Object::from_value(scope, *this_reader_v).expect("payload reader is an object"); + Object::from_raw_value(scope, this_reader_v.get()).expect("payload reader is an object"); let current = byte_tee_reader(scope, state).as_object(); if this_reader.as_raw() != current.as_raw() { return Ok(value::undefined()); @@ -4315,14 +4315,7 @@ fn copy_data_block_bytes( if count == 0 { return; } - // SAFETY: `dest` and `src` are distinct live, non-detached `ArrayBuffer`s - // (the descriptor's buffer and a queue entry's buffer); no GC runs during - // the memcpy, and the ranges are bounds-checked by the slice indexing. - unsafe { - let d = dest.bytes_mut(); - let s = src.bytes(); - d[dest_start..dest_start + count].copy_from_slice(&s[src_start..src_start + count]); - } + dest.copy_region_from(dest_start, src, src_start, count); } /// The descriptor's `buffer` slot as a rooted `ArrayBuffer`. diff --git a/crates/builtins/web-streams/src/readable/read_all_bytes.rs b/crates/builtins/web-streams/src/readable/read_all_bytes.rs index 8f0f0c3..57c514d 100644 --- a/crates/builtins/web-streams/src/readable/read_all_bytes.rs +++ b/crates/builtins/web-streams/src/readable/read_all_bytes.rs @@ -157,10 +157,10 @@ pub(crate) fn read_all_bytes_chunk_steps( return promise.reject_with_pending(scope); }; // Step 2: Append the bytes represented by chunk to bytes. - // SAFETY: the slice is consumed immediately; appending to the Rust-side - // accumulator cannot trigger a GC or detach the chunk's buffer. - let chunk_bytes = unsafe { array.as_slice() }; - state.data_mut().bytes.extend_from_slice(chunk_bytes); + let mut data = state.data_mut(); + let bytes = &mut data.bytes; + array.with_slice(|chunk_bytes| bytes.extend_from_slice(chunk_bytes)); + drop(data); // Step 3: Read-loop — deferred one microtask (see above): queue the // state's deferred-read callback directly on the job queue. let microtask = state diff --git a/crates/builtins/web-streams/src/readable/readable_stream.rs b/crates/builtins/web-streams/src/readable/readable_stream.rs index 9b0dae1..afe2485 100644 --- a/crates/builtins/web-streams/src/readable/readable_stream.rs +++ b/crates/builtins/web-streams/src/readable/readable_stream.rs @@ -199,7 +199,7 @@ impl ReadableStream { 0.0, HandleValue::undefined(), )?; - if let Ok(source) = js::Object::from_value(scope, underlying_source.get()) { + if let Ok(source) = js::Object::from_value(scope, underlying_source) { stream.set_native_source(&source); } Ok(stream) @@ -220,7 +220,7 @@ impl ReadableStream { let underlying_source = underlying_source.to_jsval_throwing(scope)?; let stream = algorithms::create_readable_byte_stream(scope, AlgorithmArg::None, pull, cancel)?; - if let Ok(source) = Object::from_value(scope, underlying_source.get()) { + if let Ok(source) = Object::from_value(scope, underlying_source) { stream.set_native_source(&source); } Ok(stream) diff --git a/crates/core-runtime/Cargo.toml b/crates/core-runtime/Cargo.toml index 0de47f8..e39ab4a 100644 --- a/crates/core-runtime/Cargo.toml +++ b/crates/core-runtime/Cargo.toml @@ -20,6 +20,7 @@ oxc_resolver.workspace = true platform.workspace = true shlex = "2" tempfile = "3" +url.workspace = true [lints] workspace = true diff --git a/crates/core-runtime/src/config.rs b/crates/core-runtime/src/config.rs index 920ae6b..cd1bc43 100644 --- a/crates/core-runtime/src/config.rs +++ b/crates/core-runtime/src/config.rs @@ -29,11 +29,11 @@ pub struct RuntimeConfig { #[arg(short = 'i', long = "initializer-script")] pub initializer_script_path: Option, - /// Enable verbose logging. + /// No effect. #[arg(short = 'v', long = "verbose")] pub verbose: bool, - /// Enable script debugging via socket connection. + /// No effect. #[arg(short = 'd', long = "debug")] pub debugging: bool, @@ -54,21 +54,22 @@ pub struct RuntimeConfig { /// Enforce the browser-security Fetch constraints (forbidden /// request/response headers and methods, no-CORS safelisting, origin/mode - /// enforcement). Defaults to off except in `--wpt-mode`, which needs the + /// enforcement), and coarsen the HR-Time clocks, such as `performance.now()`, + /// to 100 microseconds. Defaults to off except in `--wpt-mode`, which needs the /// browser-observable behavior. /// Pass `=true`/`=false` to override either default. #[arg( - long = "enforce-fetch-restrictions", + long = "web-security-restrictions", num_args = 0..=1, default_missing_value = "true" )] - pub enforce_fetch_restrictions: Option, + pub web_security_restrictions: Option, /// Override the location URL for initialization. #[arg(long = "init-location")] pub init_location: Option, - /// Strip this prefix from script paths. + /// No effect. #[arg(long = "strip-path-prefix")] pub path_prefix: Option, @@ -94,9 +95,10 @@ pub struct RuntimeConfig { /// Give up on a served request whose `respondWith` has not settled after this many seconds and /// answer it with a 500. /// - /// No limit by default, except in `--wpt-mode`, where it's 120 seconds. `0` means no limit. - #[arg(long = "dispatch-timeout", value_name = "SECONDS")] - pub dispatch_timeout: Option, + /// No limit by default, except in `--wpt-mode`, where it's 120 seconds. Fractions such as + /// `0.25` are accepted. `0` means no limit. + #[arg(long = "dispatch-timeout", value_name = "SECONDS", value_parser = parse_seconds)] + pub dispatch_timeout: Option, /// Give up on a served request's response body after this many seconds, counted from its /// headers going out, and truncate it visibly (no terminating chunk natively, error trailers @@ -106,16 +108,18 @@ pub struct RuntimeConfig { /// body and a peer too slow to accept one alike; on wasm it covers handing the body to the /// host, which buffers it, so a slow peer is the host's to bound. /// - /// No limit by default, except in `--wpt-mode`, where it's 120 seconds. `0` means no limit. - #[arg(long = "response-body-timeout", value_name = "SECONDS")] - pub response_body_timeout: Option, + /// No limit by default, except in `--wpt-mode`, where it's 120 seconds. Fractions such as + /// `0.25` are accepted. `0` means no limit. + #[arg(long = "response-body-timeout", value_name = "SECONDS", value_parser = parse_seconds)] + pub response_body_timeout: Option, /// Stop waiting for the promises a served request passed to `FetchEvent#waitUntil` after this /// many seconds, counted from the response being fully sent. /// - /// No limit by default, except in `--wpt-mode`, where it's 120 seconds. `0` means no limit. - #[arg(long = "waituntil-timeout", value_name = "SECONDS")] - pub waituntil_timeout: Option, + /// No limit by default, except in `--wpt-mode`, where it's 120 seconds. Fractions such as + /// `0.25` are accepted. `0` means no limit. + #[arg(long = "waituntil-timeout", value_name = "SECONDS", value_parser = parse_seconds)] + pub waituntil_timeout: Option, /// Bound everything a served request runs, dispatch through `waitUntil` work, by this many /// seconds of wall clock, however the per-phase timeouts divide it. Each phase runs under the @@ -123,9 +127,9 @@ pub struct RuntimeConfig { /// is rejected. /// /// No limit by default (also in `--wpt-mode`, whose per-phase defaults already bound a run). - /// `0` means no limit. - #[arg(long = "end-to-end-timeout", value_name = "SECONDS")] - pub end_to_end_timeout: Option, + /// Fractions such as `0.25` are accepted. `0` means no limit. + #[arg(long = "end-to-end-timeout", value_name = "SECONDS", value_parser = parse_seconds)] + pub end_to_end_timeout: Option, /// Limits applied for native builds. On Wasm, `wasi:http` enforces equivalents. #[cfg(not(target_arch = "wasm32"))] @@ -146,10 +150,10 @@ impl RuntimeConfig { } /// The effective request-restrictions setting: an explicit - /// `--enforce-fetch-restrictions[=bool]` wins, otherwise restrictions are + /// `--web-security-restrictions[=bool]` wins, otherwise restrictions are /// on in WPT mode and off everywhere else. - pub fn enforce_fetch_restrictions(&self) -> bool { - self.enforce_fetch_restrictions.unwrap_or(self.wpt_mode) + pub fn web_security_restrictions(&self) -> bool { + self.web_security_restrictions.unwrap_or(self.wpt_mode) } /// The effective `--dispatch-timeout`, or `None` for no limit. @@ -178,19 +182,20 @@ impl RuntimeConfig { /// mistake under a shorter explicit deadline, and `0` on either side means "no bound of its /// own", which the deadline caps as a matter of course. pub fn validate_serve_timeouts(&self) -> Result<(), String> { - let Some(end_to_end) = self.end_to_end_timeout.filter(|&s| s != 0) else { + let Some(end_to_end) = self.end_to_end_timeout.filter(|t| !t.is_zero()) else { return Ok(()); }; - for (flag, seconds) in [ + for (flag, timeout) in [ ("--dispatch-timeout", self.dispatch_timeout), ("--response-body-timeout", self.response_body_timeout), ("--waituntil-timeout", self.waituntil_timeout), ] { - if seconds.is_some_and(|s| s != 0 && s > end_to_end) { + if let Some(timeout) = timeout.filter(|t| !t.is_zero() && *t > end_to_end) { return Err(format!( - "{flag} {} exceeds --end-to-end-timeout {end_to_end}: a phase cannot run \ - longer than the whole request", - seconds.unwrap(), + "{flag} {} exceeds --end-to-end-timeout {}: a phase cannot run longer than \ + the whole request", + timeout.as_secs_f64(), + end_to_end.as_secs_f64(), )); } } @@ -237,18 +242,6 @@ impl RuntimeConfig { Err(_) => Self::try_parse_from(["starling"]), } } - - /// Parse from stdin (for wizer pre-initialization). - /// Reads a single line of arguments from stdin. - pub fn from_stdin() -> Result { - let mut input = String::new(); - if std::io::Read::read_to_string(&mut std::io::stdin(), &mut input).is_err() { - return Self::try_parse_from(["starling"]); - } - let mut config = Self::from_arg_string(input.trim())?; - config.pre_initialize = true; - Ok(config) - } } /// A byte count on the command line, written with a unit (`512MiB`, `64KiB`, `1MB`) or as a plain @@ -389,39 +382,41 @@ pub struct ServeLimits { /// but waiting for a handler that never reads its body does — past it the rest of the body is /// drained rather than delivered. /// - /// `0` means no limit. + /// Fractions such as `0.25` are accepted. `0` means no limit. #[arg( long = "request-read-timeout", value_name = "SECONDS", - default_value_t = 30 + value_parser = parse_seconds, + default_value = "30" )] - pub request_read_timeout: u64, + pub request_read_timeout: Duration, /// Close a kept-alive connection that has sat idle for this many seconds without the next /// request's first byte. Expiring closes it quietly rather than answering a 408: between /// requests, silence is a client that is done with the connection. /// - /// `0` means no limit. + /// Fractions such as `0.25` are accepted. `0` means no limit. #[arg( long = "keepalive-timeout", value_name = "SECONDS", - default_value_t = 30 + value_parser = parse_seconds, + default_value = "30" )] - pub keepalive_timeout: u64, + pub keepalive_timeout: Duration, } #[cfg(not(target_arch = "wasm32"))] impl ServeLimits { /// The effective `--request-read-timeout`, or `None` for no limit. pub fn request_read_timeout(&self) -> Option { - let seconds = self.request_read_timeout; - (seconds != 0).then(|| Duration::from_secs(seconds)) + let timeout = self.request_read_timeout; + (!timeout.is_zero()).then_some(timeout) } /// The effective `--keepalive-timeout`, or `None` for no limit. pub fn keepalive_timeout(&self) -> Option { - let seconds = self.keepalive_timeout; - (seconds != 0).then(|| Duration::from_secs(seconds)) + let timeout = self.keepalive_timeout; + (!timeout.is_zero()).then_some(timeout) } /// Reject a size or count limit of zero for all limits except `--max-body-drain-bytes`, and a @@ -475,29 +470,30 @@ impl Default for RuntimeConfig { // contexts these policies are usually unwanted, so they're disabled by default. js::instance_local! { - static ENFORCE_REQUEST_RESTRICTIONS: Cell = const { Cell::new(false) }; + static WEB_SECURITY_RESTRICTIONS: Cell = const { Cell::new(false) }; } -/// Enable or disable enforcement of the browser-security Fetch constraints. -/// `Runtime::init` sets this from the [`RuntimeConfig`], embedders can override +/// Enable or disable enforcement of the browser-security Fetch constraints and the coarsening of +/// the HR-Time clocks. `Runtime::init` sets this from the [`RuntimeConfig`], embedders can override /// it at any point after. -pub fn set_enforce_fetch_restrictions(enabled: bool) { - ENFORCE_REQUEST_RESTRICTIONS.with(|cell| cell.set(enabled)); +pub fn set_web_security_restrictions(enabled: bool) { + WEB_SECURITY_RESTRICTIONS.with(|cell| cell.set(enabled)); } -/// Whether the browser-security Fetch constraints are currently enforced. +/// Whether the browser-security Fetch constraints are currently enforced. While they are, the +/// HR-Time clocks are coarsened too. #[inline] -pub fn enforce_fetch_restrictions() -> bool { - ENFORCE_REQUEST_RESTRICTIONS.with(|cell| cell.get()) +pub fn web_security_restrictions() -> bool { + WEB_SECURITY_RESTRICTIONS.with(|cell| cell.get()) } /// How long a serve-mode timeout given as `--…-timeout SECONDS` runs for: the value if it was /// given, otherwise [`WPT_SERVE_TIMEOUT`] in WPT mode and no limit everywhere else. An explicit /// `0` is how a WPT run asks for no limit. -fn serve_timeout(seconds: Option, wpt_mode: bool) -> Option { - match seconds { - Some(0) => None, - Some(seconds) => Some(Duration::from_secs(seconds)), +fn serve_timeout(timeout: Option, wpt_mode: bool) -> Option { + match timeout { + Some(timeout) if timeout.is_zero() => None, + Some(timeout) => Some(timeout), None if wpt_mode => Some(WPT_SERVE_TIMEOUT), None => None, } @@ -507,6 +503,17 @@ fn serve_timeout(seconds: Option, wpt_mode: bool) -> Option { /// is merely slow still reports a result and only a genuinely stuck one is cut off. const WPT_SERVE_TIMEOUT: Duration = Duration::from_secs(120); +/// Parse a serve-mode timeout given in seconds, whole (`30`) or fractional (`0.25`). Negative, +/// non-finite and out-of-range values are refused. +fn parse_seconds(value: &str) -> Result { + value + .trim() + .parse::() + .ok() + .and_then(|seconds| Duration::try_from_secs_f64(seconds).ok()) + .ok_or_else(|| format!("{value:?} is not a number of seconds (try 30 or 0.25)")) +} + /// Split an argument string into individual arguments, respecting quotes. fn split_args(s: &str) -> Result, clap::Error> { let mut args = vec!["starling".into()]; @@ -537,30 +544,23 @@ mod tests { fn serve_timeouts_default_to_no_limit_outside_wpt_mode() { let config = RuntimeConfig::default(); assert_eq!(config.dispatch_timeout(), None); + assert_eq!(config.response_body_timeout(), None); assert_eq!(config.waituntil_timeout(), None); // WPT runs want a stuck test cut off rather than stalling the run. let wpt = RuntimeConfig::from_arg_string("--wpt-mode").unwrap(); assert_eq!(wpt.dispatch_timeout(), Some(WPT_SERVE_TIMEOUT)); + assert_eq!(wpt.response_body_timeout(), Some(WPT_SERVE_TIMEOUT)); assert_eq!(wpt.waituntil_timeout(), Some(WPT_SERVE_TIMEOUT)); - // An explicit value wins over either default, and `0` is how to ask for no limit at all. - let explicit = - RuntimeConfig::from_arg_string("--wpt-mode --dispatch-timeout 5 --waituntil-timeout 0") - .unwrap(); + // An explicit value wins over either default. `0` means no limit at all. + let explicit = RuntimeConfig::from_arg_string( + "--wpt-mode --dispatch-timeout 5 --response-body-timeout 0 --waituntil-timeout 0", + ) + .unwrap(); assert_eq!(explicit.dispatch_timeout(), Some(Duration::from_secs(5))); - assert_eq!(explicit.waituntil_timeout(), None); - } - - #[test] - fn response_body_timeout_defaults_like_the_other_phase_timeouts() { - // No limit by default; WPT mode wants a stuck body cut off like a stuck dispatch. - assert_eq!(RuntimeConfig::default().response_body_timeout(), None); - let wpt = RuntimeConfig::from_arg_string("--wpt-mode").unwrap(); - assert_eq!(wpt.response_body_timeout(), Some(WPT_SERVE_TIMEOUT)); - let explicit = - RuntimeConfig::from_arg_string("--wpt-mode --response-body-timeout 0").unwrap(); assert_eq!(explicit.response_body_timeout(), None); + assert_eq!(explicit.waituntil_timeout(), None); } #[test] @@ -575,8 +575,8 @@ mod tests { } #[test] - fn a_phase_timeout_longer_than_end_to_end_is_a_config_error() { - // A phase that could never use its full window is a contradiction worth catching. + fn a_phase_timeout_longer_than_end_to_end_is_rejected() { + // A phase that could never use its full window is rejected. for phase in ["dispatch", "response-body", "waituntil"] { let config = RuntimeConfig::from_arg_string(&format!( "--end-to-end-timeout 10 --{phase}-timeout 11" @@ -588,6 +588,14 @@ mod tests { "error should name the offending flag and the limit, got: {error}" ); } + let config = + RuntimeConfig::from_arg_string("--end-to-end-timeout 0.25 --response-body-timeout 0.3") + .unwrap(); + let error = config.validate_serve_timeouts().unwrap_err(); + assert!( + error.contains("--response-body-timeout 0.3") && error.contains("0.25"), + "{error}" + ); } #[test] @@ -621,8 +629,8 @@ mod tests { assert_eq!(ByteSize::mib(512).to_string(), "512MiB"); assert_eq!(ByteSize::kib(64).to_string(), "64KiB"); assert_eq!(ByteSize::new(0).to_string(), "0B"); - // A size no unit divides stays in bytes rather than rounding: what `--help` prints has to - // parse back to the number it printed. + // A size not divisible by any unit stays in bytes rather than rounding: what `--help` + // prints has to parse back to the number it printed. assert_eq!(ByteSize::new(1000).to_string(), "1000B"); assert_eq!(ByteSize::new(1025).to_string(), "1025B"); } @@ -703,7 +711,41 @@ mod tests { } #[test] - fn a_zero_size_or_count_limit_is_a_config_error() { + fn serve_timeouts_take_fractional_seconds() { + let config = RuntimeConfig::from_arg_string( + "--dispatch-timeout 0.15 --response-body-timeout 0.25 --waituntil-timeout 1.5 \ + --end-to-end-timeout 2 --request-read-timeout 0.5 --keepalive-timeout 0.75", + ) + .unwrap(); + assert_eq!(config.dispatch_timeout(), Some(Duration::from_millis(150))); + assert_eq!( + config.response_body_timeout(), + Some(Duration::from_millis(250)) + ); + assert_eq!( + config.waituntil_timeout(), + Some(Duration::from_millis(1500)) + ); + assert_eq!(config.end_to_end_timeout(), Some(Duration::from_secs(2))); + assert_eq!( + config.serve_limits.request_read_timeout(), + Some(Duration::from_millis(500)) + ); + assert_eq!( + config.serve_limits.keepalive_timeout(), + Some(Duration::from_millis(750)) + ); + + for bad in ["-1", "abc", "nan", "inf", ""] { + assert!( + RuntimeConfig::from_arg_string(&format!("--dispatch-timeout={bad}")).is_err(), + "{bad:?} should not parse" + ); + } + } + + #[test] + fn a_zero_size_or_count_limit_is_rejected() { // Unlike a timeout, a cap of zero is not "no limit" but a server that can accept // nothing at all, which is never what was meant. for flag in [ @@ -731,7 +773,7 @@ mod tests { } #[test] - fn a_request_head_cap_below_the_read_buffer_minimum_is_a_config_error() { + fn a_request_head_cap_below_the_read_buffer_minimum_is_rejected() { let config = RuntimeConfig::from_arg_string("--max-connection-buffer-size 512").unwrap(); let error = config.serve_limits.validate().unwrap_err(); assert!( @@ -774,47 +816,32 @@ mod tests { } #[test] - fn enforce_fetch_restrictions_defaults_off_wpt_on_flag_wins() { - assert!(!RuntimeConfig::default().enforce_fetch_restrictions()); + fn web_security_restrictions_defaults_off_wpt_on_flag_wins() { + assert!(!RuntimeConfig::default().web_security_restrictions()); assert!(RuntimeConfig::from_arg_string("--wpt-mode") .unwrap() - .enforce_fetch_restrictions()); + .web_security_restrictions()); assert!( - !RuntimeConfig::from_arg_string("--wpt-mode --enforce-fetch-restrictions=false") + !RuntimeConfig::from_arg_string("--wpt-mode --web-security-restrictions=false") .unwrap() - .enforce_fetch_restrictions() + .web_security_restrictions() ); assert!( - RuntimeConfig::from_arg_string("--enforce-fetch-restrictions") + RuntimeConfig::from_arg_string("--web-security-restrictions") .unwrap() - .enforce_fetch_restrictions() + .web_security_restrictions() ); } + /// The base path is the current directory for `-e`, and otherwise the absolute directory of + /// the script. #[test] - fn base_path_for_eval_is_cwd() { - let config = RuntimeConfig::from_arg_string("-e 42").unwrap(); - assert_eq!(config.base_path(), env::current_dir().unwrap()); - } - - #[test] - fn base_path_for_bare_filename_is_cwd() { - let config = RuntimeConfig::from_arg_string("app.js").unwrap(); - assert_eq!(config.base_path(), env::current_dir().unwrap()); - } - - #[test] - fn base_path_for_absolute_script_is_its_directory() { - let config = RuntimeConfig::from_arg_string("/srv/js/app.js").unwrap(); - assert_eq!(config.base_path(), Path::new("/srv/js")); - } - - #[test] - fn base_path_for_relative_script_is_absolute() { - let config = RuntimeConfig::from_arg_string("sub/dir/app.js").unwrap(); - assert_eq!( - config.base_path(), - env::current_dir().unwrap().join("sub/dir") - ); + fn base_path_is_the_script_directory_or_the_current_directory() { + let cwd = env::current_dir().unwrap(); + let base_path = |args| RuntimeConfig::from_arg_string(args).unwrap().base_path(); + assert_eq!(base_path("-e 42"), cwd); + assert_eq!(base_path("app.js"), cwd); + assert_eq!(base_path("/srv/js/app.js"), Path::new("/srv/js")); + assert_eq!(base_path("sub/dir/app.js"), cwd.join("sub/dir")); } } diff --git a/crates/core-runtime/src/event_loop.rs b/crates/core-runtime/src/event_loop.rs index 1f982a6..93ae70d 100644 --- a/crates/core-runtime/src/event_loop.rs +++ b/crates/core-runtime/src/event_loop.rs @@ -55,7 +55,7 @@ pub mod timer; use platform::clock::Instant; use std::cell::{Cell, RefCell}; use std::collections::{BTreeSet, HashMap}; -use std::rc::Rc; +use std::rc::{Rc, Weak}; use std::time::Duration; use event_listener::{Event, EventListener}; @@ -78,7 +78,7 @@ js::instance_local! { /// [`with_active_event_loop`]. /// /// The pointee is only ever accessed through shared references, so re-entrant access from JS - /// callbacks aliases nothing. + /// callbacks doesn't alias a mutable reference. static CURRENT_EVENT_LOOP: Cell> = const { Cell::new(None) }; } @@ -112,33 +112,45 @@ pub fn with_event_loop(event_loop: &EventLoop, f: impl FnOnce(&EventLoop) -> f(event_loop) } +/// The closure passed to [`without_event_loop`] that returns why no event loop +/// is active. +type NoLoopReason = dyn Fn() -> String; + js::instance_local! { - /// Why no event loop is active, set by [`without_event_loop`] for the - /// duration of its closure. - static NO_LOOP_REASON: RefCell> = const { RefCell::new(None) }; + /// Pointer to the reason closure of the innermost [`without_event_loop`] + /// call, set for the duration of its `f`. + static NO_LOOP_REASON: Cell> = const { Cell::new(None) }; } -/// Run `f` with no active event loop, and `reason` as the explanation -/// [`throw_no_event_loop`] gives for work that needs one. The previous state is -/// restored afterwards. +/// Run `f` with no active event loop. `reason` returns the explanation that +/// [`throw_no_event_loop`] gives for work that needs one. It is called only +/// when that explanation is needed, and can be called more than once. The +/// previous state is restored afterwards. /// /// This is mainly useful for running code in sync exports, which mustn't block, and hence /// can't perform async operations. -pub fn without_event_loop(reason: String, f: impl FnOnce() -> R) -> R { +pub fn without_event_loop(reason: impl Fn() -> String, f: impl FnOnce() -> R) -> R { /// Restores the previous active loop and reason. struct Restore { prev_loop: Option<*const EventLoop>, - prev_reason: Option, + prev_reason: Option<*const NoLoopReason>, } impl Drop for Restore { fn drop(&mut self) { CURRENT_EVENT_LOOP.with(|el| el.set(self.prev_loop)); - NO_LOOP_REASON.with(|cell| *cell.borrow_mut() = self.prev_reason.take()); + NO_LOOP_REASON.with(|cell| cell.set(self.prev_reason)); } } + let reason: &(dyn Fn() -> String + '_) = &reason; + // SAFETY: only the lifetime bound changes. `Restore` removes the pointer + // before `reason` goes out of scope, so it is only dereferenced while + // `reason` is alive. + let reason = unsafe { + std::mem::transmute::<*const (dyn Fn() -> String + '_), *const NoLoopReason>(reason) + }; let prev_loop = CURRENT_EVENT_LOOP.with(|el| el.replace(None)); - let prev_reason = NO_LOOP_REASON.with(|cell| cell.borrow_mut().replace(reason)); + let prev_reason = NO_LOOP_REASON.with(|cell| cell.replace(Some(reason))); let _restore = Restore { prev_loop, prev_reason, @@ -146,14 +158,23 @@ pub fn without_event_loop(reason: String, f: impl FnOnce() -> R) -> R { f() } -/// Throw a `TypeError` whose message is that `what` needs an active event loop, -/// followed by the reason [`without_event_loop`] set, if any. -pub fn throw_no_event_loop(scope: &Scope<'_>, what: &str) -> ExnThrown { - let message = NO_LOOP_REASON.with(|cell| match &*cell.borrow() { - Some(reason) => format!("{what} needs an active event loop, and {reason}"), +/// The message that `what` needs an active event loop, followed by the reason +/// [`without_event_loop`] set, if any. +pub fn no_event_loop_message(what: &str) -> String { + match NO_LOOP_REASON.with(|cell| cell.get()) { + // SAFETY: the pointer is set only while the closure it points to is + // alive (see `without_event_loop`). + Some(reason) => { + let reason = unsafe { (*reason)() }; + format!("{what} needs an active event loop, and {reason}") + } None => format!("{what} needs an active event loop"), - }); - js::error::TypeError(message).throw(scope) + } +} + +/// Throw a `TypeError` with the [`no_event_loop_message`] for `what`. +pub fn throw_no_event_loop(scope: &Scope<'_>, what: &str) -> ExnThrown { + js::error::TypeError(no_event_loop_message(what)).throw(scope) } /// Reject `promise` with the `TypeError` [`throw_no_event_loop`] throws for @@ -164,7 +185,7 @@ pub fn reject_without_event_loop( promise: &js::Promise<'_>, what: &str, ) -> Result { - if with_active_event_loop(|_| ()).is_some() { + if has_active_event_loop() { return Ok(false); } throw_no_event_loop(scope, what); @@ -172,6 +193,11 @@ pub fn reject_without_event_loop( Ok(true) } +/// Whether an event loop is active. +pub fn has_active_event_loop() -> bool { + CURRENT_EVENT_LOOP.with(|el| el.get().is_some()) +} + /// Run a closure with a reference to the active event loop. /// /// Returns `None` if no event loop is active (i.e. we're not inside a @@ -182,7 +208,7 @@ pub fn with_active_event_loop(f: impl FnOnce(&EventLoop) -> R) -> Option { // SAFETY: the pointer was installed by `with_event_loop`, whose // shared borrow of the loop spans the closure currently on the // stack. The loop is alive, and all its state is behind cells, - // so shared access from here aliases nothing. + // so shared access from here doesn't alias a mutable reference. f(unsafe { &*ptr }) }) }) @@ -199,24 +225,6 @@ pub fn with_active_event_loop(f: impl FnOnce(&EventLoop) -> R) -> Option { #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct TaskId(u64); -impl TaskId { - /// Create a `TaskId` from a raw `u64` value. - /// - /// Test-only, since in production code it'd be a footgun. - #[cfg(test)] - pub fn from_raw(raw: u64) -> Self { - Self(raw) - } - - /// Returns the raw `u64` value. - /// - /// Test-only, since in production code it'd be a footgun. - #[cfg(test)] - pub fn as_raw(self) -> u64 { - self.0 - } -} - impl std::fmt::Display for TaskId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "TaskId({})", self.0) @@ -248,10 +256,30 @@ pub enum StepOutcome { Progressed, } -/// A `setTimeout`/`setInterval` timer, keyed by the id JS sees. -struct JsTimer { +/// A global's `setTimeout` and `setInterval` timers. +#[derive(Default)] +struct GlobalTimers { + /// The last timer id the global's `setTimeout` or `setInterval` returned. + last_id: Cell, + /// HTML's "map of setTimeout and setInterval IDs": the timer ids JS sees, mapped to the + /// loop and task each controls. + timers: RefCell>, +} + +/// An entry of [`GlobalTimers::timers`]. +struct GlobalTimer { + /// The tasks of the loop that holds the timer. + tasks: Weak, /// The task the timer controls. An interval re-queues itself under the same one. task: TaskId, +} + +/// A JS timer an [`EventLoop`] holds, keyed by the task it controls. +struct LoopTimer { + /// The timers of the global that created the timer. + global: Weak, + /// The id JS sees. + timer_id: u64, /// Where the timer was created, kept for each re-queue of an interval. origin: Option>, } @@ -420,11 +448,35 @@ impl TaskQueue { // EventLoop // --------------------------------------------------------------------------- -/// Namespace token for the per-global setTimeout/setInterval id counter. Its -/// address is a process-unique, stable `usize` key for -/// [`js::class::next_global_counter`], mirroring how shared functions key on -/// their native pointer. -static TIMER_ID_COUNTER_KEY: u8 = 0; +/// The tasks and JS timers of an [`EventLoop`], shared with [`GlobalTimers`] so that a timer can +/// be cleared while another loop is active. +struct LoopTasks { + /// All live tasks. + tasks: RefCell, + /// The JS timers this loop holds, by the task each controls. + js_timers: RefCell>, + /// The owning loop's driver notification. + notify: Rc, +} + +impl LoopTasks { + /// Remove the JS timer that controls `task` from this loop and from its global's map. + fn release_js_timer(&self, task: TaskId) { + let removed = self.js_timers.borrow_mut().remove(&task); + if let Some(timer) = removed { + timer.release(); + } + } +} + +impl LoopTimer { + /// Remove the timer from its global's map. + fn release(self) { + if let Some(global) = self.global.upgrade() { + global.timers.borrow_mut().remove(&self.timer_id); + } + } +} /// The event loop task registry. /// @@ -432,9 +484,10 @@ static TIMER_ID_COUNTER_KEY: u8 = 0; /// drivers (`native`, `wasi`) call into this struct to advance the /// loop. /// -/// The `EventLoop` is stored on the [`Runtime`](crate::runtime::Runtime) -/// and its [`trace`](EventLoop::trace) method is called during GC to keep -/// JS references inside tasks alive. +/// The `EventLoop` is held by an +/// [`InvocationState`](crate::invocation::InvocationState), and its +/// [`trace`](EventLoop::trace) method is called during GC to keep JS +/// references inside tasks alive. /// All mutable state lives in cells, so every method takes `&self`: JS /// callbacks re-entering through [`with_active_event_loop`] while the driver /// is mid-[`step`](Self::step) only ever alias shared references. @@ -448,11 +501,8 @@ pub struct EventLoop { loop_id: u64, /// Monotonically increasing counter for generating unique [`TaskId`]s. next_id: Cell, - /// All live tasks. - tasks: RefCell, - /// HTML's per-loop "map of setTimeout and setInterval IDs": the timer ids - /// JS sees, mapped to the internal task they control. - js_timers: RefCell>, + /// All live tasks and JS timers. + shared: Rc, /// Scratch buffer for [`step`](Self::step)'s per-batch dispatch list, /// reused across steps so the hot path stays allocation-free. batch_buf: RefCell>, @@ -468,15 +518,16 @@ pub struct EventLoop { /// Shared with this loop's [`InterestHandle`]s, whose release /// must wake this loop even when it happens during another loop's turn. notify: Rc, - /// Sends [`register_pending_send`](Self::register_pending_send) registered, - /// for the loop's driver to take, each with the JS call stack that - /// registered it, if origins recording was active during registration. + /// Sends registered by + /// [`register_pending_send`](Self::register_pending_send), for the loop's + /// driver to take, each with the JS call stack that registered it, if + /// origins recording was active during registration. pending_sends: RefCell>)>>, } -/// A send, such as a response body's, that the loop's work can feed. The loop's -/// driver awaits `done` while the loop has work, and calls `abandon` once no more -/// work is left, after which `done` completes as well. +/// A send that the loop's work can feed, such as sending a response body. The +/// loop's driver awaits `done` while the loop has work, and calls `abandon` once +/// no more work is left, after which `done` completes as well. pub struct PendingSend { pub done: std::pin::Pin>>, pub abandon: Box, @@ -490,8 +541,11 @@ impl EventLoop { Self { loop_id: NEXT_LOOP_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed), next_id: Cell::new(0), - tasks: RefCell::new(TaskQueue::default()), - js_timers: RefCell::new(HashMap::new()), + shared: Rc::new(LoopTasks { + tasks: RefCell::new(TaskQueue::default()), + js_timers: RefCell::new(HashMap::new()), + notify: Rc::clone(¬ify), + }), batch_buf: RefCell::new(Vec::new()), stop_requested: Cell::new(false), interest: InterestTracker::new(Rc::clone(¬ify)), @@ -507,17 +561,7 @@ impl EventLoop { /// /// Returns the [`TaskId`] assigned to this task. pub fn queue(&self, task: Box) -> TaskId { - let id = self.next_task_id(); - // Read before `tasks` is borrowed: recording the origin can GC, which traces the loop. - let origin = js::stack::current_origin(); - self.tasks.borrow_mut().insert(TaskEntry { - id, - task, - ready: false, - deadline: None, - origin, - }); - id + self.insert_task(task, false) } /// Queue a task that is immediately ready to run. @@ -525,17 +569,24 @@ impl EventLoop { /// This is a convenience for tasks that don't need to wait for an /// external event (e.g. resolved promises, `queueMicrotask` work). pub fn queue_ready(&self, task: Box) -> TaskId { + self.insert_task(task, true) + } + + /// Queue a task without a deadline, and wake the driver if it is `ready`. + fn insert_task(&self, task: Box, ready: bool) -> TaskId { let id = self.next_task_id(); // Read before `tasks` is borrowed: recording the origin can GC, which traces the loop. let origin = js::stack::current_origin(); - self.tasks.borrow_mut().insert(TaskEntry { + self.shared.tasks.borrow_mut().insert(TaskEntry { id, task, - ready: true, + ready, deadline: None, origin, }); - self.notify.notify(1); + if ready { + self.notify.notify(1); + } id } @@ -559,7 +610,7 @@ impl EventLoop { deadline: Instant, origin: Option>, ) { - self.tasks.borrow_mut().insert(TaskEntry { + self.shared.tasks.borrow_mut().insert(TaskEntry { id, task, ready: false, @@ -575,77 +626,93 @@ impl EventLoop { /// in the queue (already run, or currently being executed by the driver) /// is left alone. pub fn cancel_if_queued(&self, id: TaskId) -> bool { - self.tasks.borrow_mut().remove(id).is_some() + self.shared.tasks.borrow_mut().remove(id).is_some() } - /// Queue a `setTimeout`/`setInterval` task under a fresh JS timer id. + /// Queue a `setTimeout`/`setInterval` task of the scope's global under a fresh JS timer id, + /// greater than zero and unique among the global's timers. pub fn queue_js_timer( &self, scope: &Scope<'_>, deadline: Instant, make_task: impl FnOnce(u64) -> Box, ) -> u64 { - let timer_id = self.allocate_js_timer_id(scope); + let global = js::class::global_state::(scope); + let timer_id = global.last_id.get() + 1; + assert!( + timer_id < js::conversion::Number::MAX, + "JS timer ids wrapped around" + ); + global.last_id.set(timer_id); let task = self.next_task_id(); let origin = js::stack::current_origin(); self.insert_timer(task, make_task(timer_id), deadline, origin.clone()); - self.js_timers - .borrow_mut() - .insert(timer_id, JsTimer { task, origin }); + global.timers.borrow_mut().insert( + timer_id, + GlobalTimer { + tasks: Rc::downgrade(&self.shared), + task, + }, + ); + self.shared.js_timers.borrow_mut().insert( + task, + LoopTimer { + global: Rc::downgrade(&global), + timer_id, + origin, + }, + ); timer_id } - /// Re-queue an interval timer with the same JS timer id and [`TaskId`]. + /// Re-queue the interval timer that controls the task `id`, under the same JS timer id and + /// [`TaskId`]. /// /// If the timer was cleared while its callback ran (via `clearInterval` - /// from within the callback), the id no longer maps to this task and the + /// from within the callback), this loop no longer holds it, and the /// task is silently dropped instead of re-queued. - pub fn requeue_js_timer( - &self, - timer_id: u64, - id: TaskId, - task: Box, - deadline: Instant, - ) { - let origin = match self.js_timers.borrow().get(&timer_id) { - Some(timer) if timer.task == id => timer.origin.clone(), - _ => return, + pub fn requeue_js_timer(&self, id: TaskId, task: Box, deadline: Instant) { + let Some(origin) = self + .shared + .js_timers + .borrow() + .get(&id) + .map(|timer| timer.origin.clone()) + else { + return; }; self.insert_timer(id, task, deadline, origin); } - /// Cancel a JS timer by its `setTimeout`/`setInterval` id. + /// Cancel the JS timer of the scope's global with the `setTimeout`/`setInterval` id + /// `timer_id`, on the event loop that holds it. The driver of a loop other than this one is + /// woken afterwards. /// /// Invalid IDs are silently ignored per spec. - pub fn clear_js_timer(&self, timer_id: u64) { - let removed = self.js_timers.borrow_mut().remove(&timer_id); - if let Some(timer) = removed { - // If the task is still queued, remove it. If it is currently - // running (an interval clearing itself from its own callback), - // the now-missing map entry suppresses the re-queue instead. - self.cancel_if_queued(timer.task); + pub fn clear_js_timer(&self, scope: &Scope<'_>, timer_id: u64) { + let global = js::class::global_state::(scope); + let Some(timer) = global.timers.borrow_mut().remove(&timer_id) else { + return; + }; + let Some(tasks) = timer.tasks.upgrade() else { + return; + }; + tasks.js_timers.borrow_mut().remove(&timer.task); + // If the task is still queued, remove it. If it is currently + // running (an interval clearing itself from its own callback), + // the now-missing timer suppresses the re-queue instead. + tasks.tasks.borrow_mut().remove(timer.task); + if !Rc::ptr_eq(&tasks, &self.shared) { + tasks.notify.notify(1); } } - /// Release a one-shot JS timer's id after it fired. + /// Release the id of the one-shot JS timer that controls the task `id`, after it fired. /// /// Mirrors HTML's "remove global's map of `setTimeout` and `setInterval` /// `IDs[id]`" task substep for non-repeating timers. - pub fn js_timer_fired(&self, timer_id: u64) { - self.js_timers.borrow_mut().remove(&timer_id); - } - - /// Allocate the next JS timer id: greater than zero and not currently in - /// this loop's timer map. - fn allocate_js_timer_id(&self, scope: &Scope<'_>) -> u64 { - let key = &TIMER_ID_COUNTER_KEY as *const u8 as usize; - let js_timers = self.js_timers.borrow(); - loop { - let id = js::class::next_global_counter(scope, key); - if id != 0 && !js_timers.contains_key(&id) { - return id; - } - } + pub fn js_timer_fired(&self, id: TaskId) { + self.shared.release_js_timer(id); } /// Mark a queued task as ready to run. @@ -653,7 +720,7 @@ impl EventLoop { /// Has no effect if the task ID is not found (the task may have /// already been cancelled or run). pub fn signal_ready(&self, id: TaskId) { - if self.tasks.borrow_mut().mark_ready(id) { + if self.shared.tasks.borrow_mut().mark_ready(id) { self.notify.notify(1); } } @@ -665,7 +732,7 @@ impl EventLoop { /// "Next" follows [`step`](Self::step)'s dispatch order: ready non-timer /// tasks in allocation order, then expired timers by deadline. pub fn pop_ready(&self) -> Option<(TaskId, Box)> { - let mut tasks = self.tasks.borrow_mut(); + let mut tasks = self.shared.tasks.borrow_mut(); let &(_, id) = tasks.ready.first()?; let entry = tasks.remove(TaskId(id)).expect("ready tasks are live"); Some((entry.id, entry.task)) @@ -677,7 +744,7 @@ impl EventLoop { /// Returns the number of timers that became ready. // TODO: consider merging with `time_to_next_timer` and returning the tasks directly. pub fn advance_timers(&self) -> usize { - self.tasks.borrow_mut().expire_timers(Instant::now()) + self.shared.tasks.borrow_mut().expire_timers(Instant::now()) } /// Returns the duration until the next timer fires, or `None` if @@ -687,28 +754,28 @@ impl EventLoop { /// is already expired and [`advance_timers`](Self::advance_timers) /// should be called. pub fn time_to_next_timer(&self) -> Option { - let &(deadline, _) = self.tasks.borrow().timers.first()?; + let &(deadline, _) = self.shared.tasks.borrow().timers.first()?; Some(deadline.saturating_duration_since(Instant::now())) } /// Returns `true` if there are any tasks (ready or not) in the queue. pub fn has_pending(&self) -> bool { - !self.tasks.borrow().entries.is_empty() + !self.shared.tasks.borrow().entries.is_empty() } /// Returns `true` if at least one task is in the ready state. pub fn has_ready(&self) -> bool { - !self.tasks.borrow().ready.is_empty() + !self.shared.tasks.borrow().ready.is_empty() } /// Returns the number of tasks currently queued (ready or not). pub fn len(&self) -> usize { - self.tasks.borrow().entries.len() + self.shared.tasks.borrow().entries.len() } /// Returns `true` if the event loop has no tasks. pub fn is_empty(&self) -> bool { - self.tasks.borrow().entries.is_empty() + self.shared.tasks.borrow().entries.is_empty() } // ----------------------------------------------------------------------- @@ -803,7 +870,7 @@ impl EventLoop { /// /// Tasks come first, in the order they were queued, then interest, then futures, then sends. pub fn pending_work(&self) -> Vec { - let tasks = self.tasks.borrow(); + let tasks = self.shared.tasks.borrow(); let mut entries: Vec<&TaskEntry> = tasks.entries.values().collect(); entries.sort_unstable_by_key(|entry| entry.id.0); let mut work: Vec = entries @@ -960,7 +1027,7 @@ unsafe fn settle_completed_futures( // SAFETY: guaranteed by this function's caller. let scope = unsafe { js::gc::scope::RootScope::from_current_realm(raw_cx) }; if completed.is_empty() { - // A poll that completed nothing can still have run JS, such as the + // A poll that didn't complete a future can still have run JS, such as the // lowering of a stream nested in a stream element, which queues // microtasks. if jobs::has_pending_jobs(&scope) { @@ -1191,14 +1258,21 @@ impl EventLoop { // ties broken the same way, which is the order of the ready set. The batch buffer is // reused to keep the path allocation-free. let mut batch = self.batch_buf.take(); - batch.extend(self.tasks.borrow().ready.iter().map(|&(_, id)| TaskId(id))); + batch.extend( + self.shared + .tasks + .borrow() + .ready + .iter() + .map(|&(_, id)| TaskId(id)), + ); let mut ran_any = false; for &id in &batch { // Take the task out before running it, ending the borrow: the // task's JS can re-enter this loop (setTimeout, clearTimeout) // through `with_active_event_loop`. Re-locate by id — an earlier // task in the batch may have cancelled this one. - let Some(entry) = self.tasks.borrow_mut().remove(id) else { + let Some(entry) = self.shared.tasks.borrow_mut().remove(id) else { continue; }; if entry.task.run(scope, id).is_err() { @@ -1239,7 +1313,7 @@ impl EventLoop { /// allocation points, and no `tasks` borrow is held across a call into JS, so the borrow here /// cannot conflict. pub unsafe fn trace(&self, trc: *mut JSTracer) { - for entry in self.tasks.borrow().entries.values() { + for entry in self.shared.tasks.borrow().entries.values() { entry.task.trace(trc); } } @@ -1258,6 +1332,15 @@ impl Default for EventLoop { } } +impl Drop for EventLoop { + fn drop(&mut self) { + let timers = std::mem::take(&mut *self.shared.js_timers.borrow_mut()); + for timer in timers.into_values() { + timer.release(); + } + } +} + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- diff --git a/crates/core-runtime/src/event_loop/interest.rs b/crates/core-runtime/src/event_loop/interest.rs index ee98b86..8f4732b 100644 --- a/crates/core-runtime/src/event_loop/interest.rs +++ b/crates/core-runtime/src/event_loop/interest.rs @@ -145,16 +145,11 @@ mod tests { InterestTracker::new(Rc::new(Event::new())) } - #[test] - fn starts_at_zero() { - let tracker = tracker(); - assert!(!tracker.has_interest()); - assert_eq!(tracker.count(), 0); - } - #[test] fn acquire_and_release() { let tracker = tracker(); + assert_eq!(tracker.count(), 0); + assert!(!tracker.has_interest()); let first = tracker.acquire_handle(); assert!(tracker.has_interest()); assert_eq!(tracker.count(), 1); diff --git a/crates/core-runtime/src/event_loop/timer.rs b/crates/core-runtime/src/event_loop/timer.rs index 41b41d6..731c621 100644 --- a/crates/core-runtime/src/event_loop/timer.rs +++ b/crates/core-runtime/src/event_loop/timer.rs @@ -77,37 +77,6 @@ pub struct TimerTask { timer_id: u64, } -impl TimerTask { - /// Create a timer task with a function handler and its call arguments. - fn function( - callback: js::Callable<'_>, - args: Vec>>, - interval: Option, - nesting_level: u32, - timer_id: u64, - ) -> Self { - Self { - handler: TimerHandler::Function { - callback: RootedTraceableBox::new(Heap::from(callback)), - args, - }, - interval, - nesting_level, - timer_id, - } - } - - /// Create a timer task whose handler is a code string evaluated when it fires. - fn code(code: String, interval: Option, nesting_level: u32, timer_id: u64) -> Self { - Self { - handler: TimerHandler::Code(code), - interval, - nesting_level, - timer_id, - } - } -} - impl Task for TimerTask { fn kind(&self) -> &'static str { if self.interval.is_some() { @@ -146,7 +115,7 @@ impl Task for TimerTask { // — even when the handler threw. The spec's task substeps invoke the handler with "report" // (the exception is reported, not fatal) and still perform the repeat step, so a transient // error must not kill the interval. If the ID was cancelled during the run (via - // clearInterval), requeue_timer skips it. The repeat re-enters the timer initialization + // clearInterval), requeue_js_timer skips it. The repeat re-enters the timer initialization // steps, so the nested-timer clamp applies with this task's nesting level, and the // re-queued task is one level deeper. if let Some(interval) = interval { @@ -158,13 +127,13 @@ impl Task for TimerTask { timer_id, }; with_active_event_loop(|el| { - el.requeue_js_timer(timer_id, id, Box::new(new_task), Instant::now() + delay); + el.requeue_js_timer(id, Box::new(new_task), Instant::now() + delay); }); } else { // One-shot: release the timer id, mirroring HTML's "remove // global's map of setTimeout and setInterval IDs[id]" substep // after the handler ran. - with_active_event_loop(|el| el.js_timer_fired(timer_id)); + with_active_event_loop(|el| el.js_timer_fired(id)); } if failed { @@ -225,15 +194,15 @@ mod timer_globals { } /// - pub fn clear_timeout(id: Option) { + pub fn clear_timeout(scope: &Scope<'_>, id: Option) { // `remove` `this`'s `map of setTimeout and setInterval IDs`[_id_]. - super::with_active_event_loop(|el| el.clear_js_timer(id.unwrap_or(0))); + super::with_active_event_loop(|el| el.clear_js_timer(scope, id.unwrap_or(0))); } /// - pub fn clear_interval(id: Option) { + pub fn clear_interval(scope: &Scope<'_>, id: Option) { // `remove` `this`'s `map of setTimeout and setInterval IDs`[_id_]. - super::with_active_event_loop(|el| el.clear_js_timer(id.unwrap_or(0))); + super::with_active_event_loop(|el| el.clear_js_timer(scope, id.unwrap_or(0))); } } @@ -278,21 +247,25 @@ fn timer_initialization_steps( // Queue on the current event loop. let timer_id = with_active_event_loop(|el| { - el.queue_js_timer(scope, deadline, |timer_id| match callable { - Some(callback) => Box::new(TimerTask::function( - callback, - extra_args, - interval, - task_nesting, - timer_id, - )), - None => { + el.queue_js_timer(scope, deadline, |timer_id| { + let handler = match callable { + Some(callback) => TimerHandler::Function { + callback: RootedTraceableBox::new(Heap::from(callback)), + args: extra_args, + }, // Not callable: run the handler's string as code. The HTML timer initialization // steps create a new classic script from it each time the timer fires, so the // string is kept rather than a compiled script. - let code = code.expect("non-callable handler was converted to a string above"); - Box::new(TimerTask::code(code, interval, task_nesting, timer_id)) - } + None => TimerHandler::Code( + code.expect("non-callable handler was converted to a string above"), + ), + }; + Box::new(TimerTask { + handler, + interval, + nesting_level: task_nesting, + timer_id, + }) }) }); diff --git a/crates/core-runtime/src/finalization.rs b/crates/core-runtime/src/finalization.rs index 859394e..4e5502f 100644 --- a/crates/core-runtime/src/finalization.rs +++ b/crates/core-runtime/src/finalization.rs @@ -70,8 +70,8 @@ pub fn install(cx: &mut JSContext) { /// A callback that throws leaves the remaining ones to run: its exception is /// reported and cleared. pub fn run_pending(scope: &Scope<'_>) { - // Taken one at a time so the undrained functions stay in the traced queue - // through any GC a callback triggers. + // Take the functions one at a time, so the undrained functions stay in the + // traced queue through any GC a callback triggers. while let Some(fval) = with_pending(|cell| cell.borrow_mut().0.pop_front().map(|h| h.get(scope))) { diff --git a/crates/core-runtime/src/invocation.rs b/crates/core-runtime/src/invocation.rs index 2b59f6e..b8f141b 100644 --- a/crates/core-runtime/src/invocation.rs +++ b/crates/core-runtime/src/invocation.rs @@ -8,10 +8,6 @@ //! libstarling, componentize-js) decides how many invocations run //! concurrently and schedules them. //! -//! Note: right now there's no additional metadata, but in the future this -//! will include things like information about which incoming event started -//! an invocation. -//! //! [`InvocationRegistry`] tracks all live `InvocationState` instances so //! the GC can trace their event loops during garbage collection. @@ -216,12 +212,6 @@ impl Drop for OwnedInvocation { mod tests { use super::*; - #[test] - fn invocation_state_default() { - let state = InvocationState::new(); - assert!(state.event_loop().is_empty()); - } - #[test] fn registry_register_unregister() { let mut registry = InvocationRegistry::new(); diff --git a/crates/core-runtime/src/lib.rs b/crates/core-runtime/src/lib.rs index db82e15..94c64da 100644 --- a/crates/core-runtime/src/lib.rs +++ b/crates/core-runtime/src/lib.rs @@ -218,13 +218,14 @@ impl ScriptEvaluation { } } -/// Initialize the runtime and evaluate the script (the synchronous portion of an -/// invocation), draining initial microtasks. Returns the runtime and its -/// invocation, with the invocation unregistered from the GC tracer. It is -/// registered only for the eval phase here, then unregistered before being moved -/// out, since the registry holds a raw pointer that the move would invalidate. The -/// caller re-registers at the final location, drives the loop, then unregisters. -fn init_and_eval( +/// Like [`setup`], but always returns the runtime, for serve mode, where the runtime must +/// outlive the script (which registers `fetch` handlers) so it can handle incoming requests. +/// Initializes the runtime and evaluates the script, draining initial microtasks. The returned +/// invocation holds the script's initial event loop, and is unregistered from the GC tracer: the +/// registry holds a raw pointer that moving the invocation out would invalidate. The caller +/// re-registers it at its final location, drives it to completion (the script's top-level async) +/// before serving, then unregisters it and keeps the runtime alive for the accept loop. +pub fn setup_for_serve( config: config::RuntimeConfig, ) -> Result< ( @@ -299,7 +300,7 @@ pub fn setup( )>, String, > { - let (runtime, invocation, evaluation) = init_and_eval(config)?; + let (runtime, invocation, evaluation) = setup_for_serve(config)?; if !invocation.event_loop().is_alive() { let settled = evaluation.settled(&runtime.default_global(), "Script evaluation failed"); drop(evaluation); @@ -308,24 +309,6 @@ pub fn setup( Ok(Some((runtime, invocation, evaluation))) } -/// Like [`setup`], but always returns the runtime — for **serve mode**, where the -/// runtime must outlive the script (which registers `fetch` handlers) so it can -/// handle incoming requests. The returned invocation holds the script's initial -/// event loop; drive it to completion (the script's top-level async) before -/// serving, then keep the runtime alive for the accept loop. -pub fn setup_for_serve( - config: config::RuntimeConfig, -) -> Result< - ( - std::rc::Rc, - invocation::InvocationState, - ScriptEvaluation, - ), - String, -> { - init_and_eval(config) -} - #[cfg(test)] mod tests { use super::config::RuntimeConfig; @@ -339,7 +322,7 @@ mod tests { /// Event-loop driver for tests whose scripts complete synchronously. /// /// `run` only invokes the driver when the script left async work in the - /// event loop, so for these tests being called at all is a failure. + /// event loop, so the driver fails these tests if it is called at all. fn noop_driver( _runtime: std::rc::Rc, _invocation: invocation::InvocationState, @@ -366,18 +349,6 @@ mod tests { .expect("Run failed"); } - #[test] - fn run_file_module_mode() { - let dir = test_tempdir(); - let script = dir.path().join("test.mjs"); - std::fs::write(&script, "const x = 1 + 2;\n").unwrap(); - - let config = config_from(&["starling", &script.to_string_lossy()]); - run(config, noop_driver) - .map_err(|e| println!("{e}")) - .expect("Run failed"); - } - #[test] fn run_file_legacy_script() { let dir = test_tempdir(); @@ -390,23 +361,6 @@ mod tests { .expect("Run failed"); } - #[test] - fn run_file_with_imports() { - let dir = test_tempdir(); - std::fs::write(dir.path().join("helper.js"), "export const V = 10;\n").unwrap(); - let entry = dir.path().join("main.mjs"); - std::fs::write( - &entry, - r#"import { V } from "./helper.js"; globalThis._v = V;"#, - ) - .unwrap(); - - let config = config_from(&["starling", &entry.to_string_lossy()]); - run(config, noop_driver) - .map_err(|e| println!("{e}")) - .expect("Run failed"); - } - #[test] fn run_with_initializer_script() { let dir = test_tempdir(); diff --git a/crates/core-runtime/src/module.rs b/crates/core-runtime/src/module.rs index f130b9d..5313f00 100644 --- a/crates/core-runtime/src/module.rs +++ b/crates/core-runtime/src/module.rs @@ -18,7 +18,7 @@ //! `JS_DefineFunction` //! //! A module load hook maps specifier strings to compiled module objects -//! via a thread-local registry. +//! via the runtime's module registry. //! //! # Example //! @@ -32,10 +32,8 @@ //! // Register and use: //! let rt = Runtime::init(&config).expect("runtime init"); //! let scope = rt.default_global(); -//! unsafe { -//! register_module::(&scope); -//! // JS can now: import { PI, add } from "myMath"; -//! } +//! register_module::(&scope); +//! // JS can now: import { PI, add } from "myMath"; //! ``` //! //! The specifier is the `mod` name camelCased, unless overridden with @@ -109,7 +107,7 @@ pub trait NativeModule: 'static { } // ============================================================================ -// Module registry (thread-local) +// Module registry // ============================================================================ /// A cached compiled module object, stored in a `Heap` so SpiderMonkey's @@ -158,6 +156,27 @@ fn base_path(f: impl FnOnce(&RefCell>) -> R) -> R { with_state(|state| f(&state.base_path)) } +/// The module the registry holds under `key`. +fn cached_module<'s>(scope: &'s Scope<'_>, key: &str) -> Option> { + registry(|reg| { + reg.borrow() + .get(key) + .map(|entry| entry.module_obj.get(scope)) + }) +} + +/// Hold `module` in the registry under `key`. +fn cache_module(key: String, module: Object<'_>) { + registry(|reg| { + reg.borrow_mut().insert( + key, + ModuleEntry { + module_obj: Heap::from(module), + }, + ); + }); +} + /// Trace all cached module objects. /// /// # Safety @@ -227,7 +246,7 @@ pub fn settled_module_evaluation<'s>( eval_result: HandleValue<'s>, context: &str, ) -> Result>, String> { - let Some(promise) = js::Object::from_value(scope, eval_result.get()) + let Some(promise) = js::Object::from_value(scope, eval_result) .ok() .and_then(|object| object.cast::().ok()) else { @@ -319,11 +338,9 @@ unsafe extern "C" fn module_load_hook( // 1. Check the module registry for an exact match. The `json:`-prefixed keys // hold JSON modules, which a JavaScript import never gets. let cached = match kind { - ModuleKind::JavaScript if !specifier.starts_with("json:") => registry(|reg| { - reg.borrow() - .get(&specifier) - .map(|entry| entry.module_obj.get(&scope)) - }), + ModuleKind::JavaScript if !specifier.starts_with("json:") => { + cached_module(&scope, &specifier) + } ModuleKind::JavaScript | ModuleKind::Json => None, }; @@ -412,6 +429,17 @@ fn resolve_file_module<'r>( ) -> Result, ModuleLoadError> { let base_dir = base_dir .ok_or_else(|| format!("Module '{}' not found (no base path configured)", specifier))?; + // `oxc_resolver` converts `file:` URLs to paths only when it isn't compiled for wasm32. + let file_path; + let target = if specifier.starts_with("file:") { + file_path = url::Url::parse(specifier) + .ok() + .and_then(|url| url.to_file_path().ok()) + .ok_or_else(|| format!("Cannot resolve module '{specifier}': invalid file URL"))?; + file_path.to_str().unwrap_or(specifier) + } else { + specifier + }; // Resolve using oxc_resolver let resolved_path = resolver(|r| { @@ -420,7 +448,7 @@ fn resolve_file_module<'r>( .as_ref() .expect("resolver not initialized — call init_module_loader first"); resolver - .resolve(&base_dir, specifier) + .resolve(&base_dir, target) .map(|res| res.path().to_path_buf()) .map_err(|e| format!("Cannot resolve module '{}': {}", specifier, e)) })?; @@ -455,11 +483,7 @@ fn resolve_file_module<'r>( }; // Check if already compiled under the canonical path - let cached = registry(|reg| { - reg.borrow() - .get(&cache_key) - .map(|entry| entry.module_obj.get(scope)) - }); + let cached = cached_module(scope, &cache_key); if let Some(obj) = cached { return Ok(obj); } @@ -496,14 +520,7 @@ fn resolve_file_module<'r>( } }; - registry(|reg| { - reg.borrow_mut().insert( - cache_key, - ModuleEntry { - module_obj: Heap::from(module), - }, - ); - }); + cache_module(cache_key, module); Ok(module) } @@ -528,14 +545,7 @@ pub fn register_entry_module( let path_str = js::JSString::from_str(scope, &key).map_err(|_| ExnThrown)?; // SAFETY: `module` is a rooted module object, and `path_str` is rooted. unsafe { SetModulePrivate(module.as_raw(), &path_str.as_value()) }; - registry(|reg| { - reg.borrow_mut().insert( - key.clone(), - ModuleEntry { - module_obj: Heap::from(module), - }, - ); - }); + cache_module(key.clone(), module); with_state(|state| *state.entry.borrow_mut() = Some(key)); Ok(()) } @@ -545,14 +555,7 @@ pub fn register_entry_module( /// under canonical absolute paths, so only an import whose specifier is `name` /// verbatim gets `module`. fn record_entry_module(module: Object<'_>, name: &str) { - registry(|reg| { - reg.borrow_mut().insert( - name.to_string(), - ModuleEntry { - module_obj: Heap::from(module), - }, - ); - }); + cache_module(name.to_string(), module); with_state(|state| *state.entry.borrow_mut() = Some(name.to_string())); } @@ -561,11 +564,7 @@ fn record_entry_module(module: Object<'_>, name: &str) { /// namespace is only complete once the module has finished evaluating. pub fn entry_namespace<'s>(scope: &'s Scope<'_>) -> Option> { let key = with_state(|state| state.entry.borrow().clone())?; - let module = registry(|reg| { - reg.borrow() - .get(&key) - .map(|entry| entry.module_obj.get(scope)) - })?; + let module = cached_module(scope, &key)?; js::module::get_namespace(scope, module.handle()) .ok() .and_then(Object::from_handle) @@ -670,12 +669,12 @@ pub(crate) fn clear_module_state(state: &ModuleState) { /// 2. Compiles it as a module via `CompileModule` /// 3. Links and evaluates the module /// 4. Populates the module environment with native functions and values -/// 5. Stores the module in the thread-local registry for the load hook +/// 5. Stores the module in the runtime's module registry for the load hook /// -/// # Safety +/// # Panics /// -/// - [`init_module_loader`] must have been called first. -pub unsafe fn register_module(scope: &Scope<'_>) -> bool { +/// Panics if no runtime is current. +pub fn register_module(scope: &Scope<'_>) -> bool { let declarations = T::declarations(); // 1. Generate JS module source: `export var name1; export var name2; ...` @@ -703,14 +702,7 @@ pub unsafe fn register_module(scope: &Scope<'_>) -> bool { // `export var` source has no imports to resolve. // 3. Store in registry before loading (load hook must find it) - registry(|reg| { - reg.borrow_mut().insert( - T::NAME.to_string(), - ModuleEntry { - module_obj: Heap::from(module), - }, - ); - }); + cache_module(T::NAME.to_string(), module); // 4.-6. Link, evaluate, and populate the environment. On any failure, // remove the registry entry again so a later import of the module fails @@ -750,7 +742,7 @@ fn link_evaluate_and_populate( Ok(f) => f, Err(_) => return false, }; - if env.set_property(scope, &c_name, func).is_err() { + if env.set_property_by_name(scope, &c_name, func).is_err() { return false; } } @@ -804,12 +796,10 @@ fn link_evaluate_and_get_env<'s>(scope: &'s Scope<'_>, module: Object<'_>) -> Op /// [`register_module`]: a later import then fails to resolve rather than finding /// a module whose value exports are undefined. /// -/// # Safety +/// # Panics /// -/// - [`init_module_loader`] must have been called first. -/// - `exports`' value handles must be live and rooted for the duration of the -/// call. -pub unsafe fn register_synthetic_module( +/// Panics if no runtime is current. +pub fn register_synthetic_module( scope: &Scope<'_>, name: &str, exports: &[(&str, HandleValue)], @@ -834,14 +824,7 @@ pub unsafe fn register_synthetic_module( // 3. Store in the registry under the owned specifier before linking, so the // load hook finds it. - registry(|reg| { - reg.borrow_mut().insert( - name.to_string(), - ModuleEntry { - module_obj: Heap::from(module), - }, - ); - }); + cache_module(name.to_string(), module); // 4.-6. Link, evaluate, and populate the environment with the export values. // On any failure, remove the registry entry again. @@ -865,7 +848,7 @@ fn link_evaluate_and_populate_values( for (export_name, value) in exports { let c_name = CString::new(*export_name).map_err(|_| ExnThrown)?; - env.set_property(scope, &c_name, *value)?; + env.set_property_by_name(scope, &c_name, *value)?; } Ok(()) } @@ -931,7 +914,7 @@ pub unsafe fn set_module_export<'s, V: ToJSVal<'s> + ?Sized>( ) -> bool { let c_name = CString::new(name).unwrap(); let env_obj = js::Object::from_handle(env).expect("module environment object is null"); - env_obj.set_property(scope, &c_name, value).is_ok() + env_obj.set_property_by_name(scope, &c_name, value).is_ok() } #[cfg(test)] @@ -991,6 +974,11 @@ mod tests { } } + /// The `file:` URL of the absolute `path`. + fn file_url(path: &str) -> String { + url::Url::from_file_path(path).unwrap().to_string() + } + /// Helper: read back a globalThis property as String. fn read_global_string(scope: &js::gc::scope::Scope<'_>, expr: &str) -> String { let rval = evaluate_with_filename(scope, expr, "test_read.js", 1) @@ -999,17 +987,6 @@ mod tests { String::from_jsval(scope, rval, ()).expect("string conversion failed") } - #[test] - fn evaluate_module_inline() { - let rt = test_runtime(); - let scope = rt.default_global(); - unsafe { - let result = evaluate_module(&scope, "globalThis._moduleTest = 42;", "test_inline.mjs"); - assert!(result.is_ok()); - assert_eq!(read_global_f64(&scope, "globalThis._moduleTest"), 42.0); - } - } - #[test] fn evaluate_module_syntax_error_fails() { let rt = test_runtime(); @@ -1021,57 +998,38 @@ mod tests { } #[test] - fn resolve_file_import() { + fn file_urls_extensionless_paths_and_directories_resolve() { let dir = test_tempdir(); - let module_path = dir.path().join("helper.js"); - std::fs::write(&module_path, "export const VALUE = 99;\n").unwrap(); + let helper_path = dir.path().join("helper.js"); + std::fs::write(&helper_path, "export const VALUE = 7;\n").unwrap(); + std::fs::write(dir.path().join("utils.js"), "export const RATIO = 2.5;\n").unwrap(); + let lib = dir.path().join("mylib"); + std::fs::create_dir(&lib).unwrap(); + std::fs::write(lib.join("index.js"), "export const HELLO = 'world';\n").unwrap(); let rt = test_runtime(); rt.reset_module_loader(dir.path().to_path_buf()); let scope = rt.default_global(); + let url = file_url(helper_path.to_str().unwrap()); unsafe { let result = evaluate_module( &scope, - r#" - import { VALUE } from "./helper.js"; + &format!( + r#" + import {{ VALUE }} from {url:?}; + import {{ RATIO }} from "./utils"; + import {{ HELLO }} from "./mylib"; globalThis._imported = VALUE; - "#, + globalThis._ratio = RATIO; + globalThis._hello = HELLO; + "# + ), "entry.mjs", ); assert!(result.is_ok(), "module evaluation failed"); - assert_eq!(read_global_f64(&scope, "globalThis._imported"), 99.0); - } - } - - #[test] - fn resolve_nested_imports() { - let dir = test_tempdir(); - - // a.js imports from b.js, b.js imports from c.js - std::fs::write(dir.path().join("c.js"), "export const BASE = 10;\n").unwrap(); - std::fs::write( - dir.path().join("b.js"), - r#" - import { BASE } from "./c.js"; - export const DOUBLED = BASE * 2; - "#, - ) - .unwrap(); - - let rt = test_runtime(); - rt.reset_module_loader(dir.path().to_path_buf()); - let scope = rt.default_global(); - unsafe { - let result = evaluate_module( - &scope, - r#" - import { DOUBLED } from "./b.js"; - globalThis._nested = DOUBLED; - "#, - &dir.path().join("entry.mjs").to_string_lossy(), - ); - assert!(result.is_ok(), "nested module evaluation failed"); - assert_eq!(read_global_f64(&scope, "globalThis._nested"), 20.0); + assert_eq!(read_global_f64(&scope, "globalThis._imported"), 7.0); + assert_eq!(read_global_f64(&scope, "globalThis._ratio"), 2.5); + assert_eq!(read_global_string(&scope, "globalThis._hello"), "world"); } } @@ -1206,90 +1164,6 @@ mod tests { } } - #[test] - fn resolve_with_extension_inference() { - let dir = test_tempdir(); - std::fs::write(dir.path().join("utils.js"), "export const RATIO = 2.5;\n").unwrap(); - - let rt = test_runtime(); - rt.reset_module_loader(dir.path().to_path_buf()); - let scope = rt.default_global(); - unsafe { - // Import without .js extension — oxc_resolver should add it - let result = evaluate_module( - &scope, - r#" - import { RATIO } from "./utils"; - globalThis._ratio = RATIO; - "#, - "entry.mjs", - ); - assert!(result.is_ok(), "extension inference failed"); - assert_eq!(read_global_f64(&scope, "globalThis._ratio"), 2.5); - } - } - - #[test] - fn resolve_index_file() { - let dir = test_tempdir(); - let sub = dir.path().join("mylib"); - std::fs::create_dir(&sub).unwrap(); - std::fs::write(sub.join("index.js"), "export const HELLO = 'world';\n").unwrap(); - - let rt = test_runtime(); - rt.reset_module_loader(dir.path().to_path_buf()); - let scope = rt.default_global(); - unsafe { - let result = evaluate_module( - &scope, - r#" - import { HELLO } from "./mylib"; - globalThis._hello = HELLO; - "#, - "entry.mjs", - ); - assert!(result.is_ok(), "index.js resolution failed"); - assert_eq!(read_global_string(&scope, "globalThis._hello"), "world"); - } - } - - #[test] - fn duplicate_import_uses_cache() { - let dir = test_tempdir(); - - // A module with a side-effect counter - std::fs::write( - dir.path().join("counter.js"), - r#" - if (!globalThis._counter) globalThis._counter = 0; - globalThis._counter++; - export const dummy = 1; - "#, - ) - .unwrap(); - - let rt = test_runtime(); - rt.reset_module_loader(dir.path().to_path_buf()); - let scope = rt.default_global(); - unsafe { - // Two separate entry modules both import counter.js - let r1 = evaluate_module( - &scope, - r#"import { dummy } from "./counter.js";"#, - "entry1.mjs", - ); - assert!(r1.is_ok()); - // Second import of same module should be cached (not re-evaluated) - let r2 = evaluate_module( - &scope, - r#"import { dummy } from "./counter.js";"#, - "entry2.mjs", - ); - assert!(r2.is_ok()); - assert_eq!(read_global_f64(&scope, "globalThis._counter"), 1.0); - } - } - #[test] fn failed_native_module_is_not_importable() { // A native module whose `evaluate` fails must not stay registered: @@ -1368,30 +1242,6 @@ mod tests { } } - #[test] - fn json_import_evaluates_to_the_parsed_value() { - let dir = test_tempdir(); - std::fs::write(dir.path().join("data.json"), r#"{ "n": 7, "s": "seven" }"#).unwrap(); - - let rt = test_runtime(); - rt.reset_module_loader(dir.path().to_path_buf()); - let scope = rt.default_global(); - unsafe { - let result = evaluate_module( - &scope, - r#" - import data from "./data.json" with { type: "json" }; - globalThis._jsonN = data.n; - globalThis._jsonS = data.s; - "#, - "entry.mjs", - ); - assert!(result.is_ok(), "JSON module import failed"); - assert_eq!(read_global_f64(&scope, "globalThis._jsonN"), 7.0); - assert_eq!(read_global_string(&scope, "globalThis._jsonS"), "seven"); - } - } - #[test] fn invalid_json_module_fails_to_load() { let dir = test_tempdir(); @@ -1493,7 +1343,7 @@ mod tests { fn top_level_await_on_dynamic_import() { let dir = test_tempdir(); std::fs::write(dir.path().join("helper.js"), "export const VALUE = 5;\n").unwrap(); - std::fs::write(dir.path().join("data.json"), r#"{ "n": 3 }"#).unwrap(); + std::fs::write(dir.path().join("data.json"), r#"{ "n": 3, "s": "seven" }"#).unwrap(); let rt = test_runtime(); rt.reset_module_loader(dir.path().to_path_buf()); @@ -1507,6 +1357,7 @@ mod tests { const json = await import("./data.json", { with: { type: "json" } }); globalThis._sum = VALUE + json.default.n; globalThis._sameJson = json.default === data; + globalThis._jsonS = data.s; "#, "entry.mjs", ); @@ -1517,35 +1368,14 @@ mod tests { read_global_string(&scope, "String(globalThis._sameJson)"), "true" ); + assert_eq!(read_global_string(&scope, "globalThis._jsonS"), "seven"); } } #[test] - fn dynamic_import_of_a_missing_module_rejects() { - let dir = test_tempdir(); - - let rt = test_runtime(); - rt.reset_module_loader(dir.path().to_path_buf()); - let scope = rt.default_global(); - unsafe { - let result = evaluate_module( - &scope, - r#" - import("./missing.js").catch((e) => { globalThis._error = e.message; }); - "#, - "entry.mjs", - ); - assert!(result.is_ok(), "module evaluation failed"); - crate::event_loop::run_microtasks(&scope); - assert!(read_global_string(&scope, "globalThis._error") - .contains("Cannot resolve module './missing.js'")); - } - } - - #[test] - fn dynamic_import_of_a_module_with_a_missing_dependency_rejects() { + fn dynamic_imports_of_missing_modules_and_dependencies_reject() { let dir = test_tempdir(); - std::fs::write(dir.path().join("present.js"), "import './missing.js';\n").unwrap(); + std::fs::write(dir.path().join("present.js"), "import './absent.js';\n").unwrap(); let rt = test_runtime(); rt.reset_module_loader(dir.path().to_path_buf()); @@ -1554,14 +1384,17 @@ mod tests { let result = evaluate_module( &scope, r#" - import("./present.js").catch((e) => { globalThis._error = e.message; }); + import("./missing.js").catch((e) => { globalThis._missing = e.message; }); + import("./present.js").catch((e) => { globalThis._dependency = e.message; }); "#, "entry.mjs", ); assert!(result.is_ok(), "module evaluation failed"); crate::event_loop::run_microtasks(&scope); - assert!(read_global_string(&scope, "globalThis._error") + assert!(read_global_string(&scope, "globalThis._missing") .contains("Cannot resolve module './missing.js'")); + assert!(read_global_string(&scope, "globalThis._dependency") + .contains("Cannot resolve module './absent.js'")); } } } diff --git a/crates/core-runtime/src/runtime.rs b/crates/core-runtime/src/runtime.rs index 22651e6..3046f6e 100644 --- a/crates/core-runtime/src/runtime.rs +++ b/crates/core-runtime/src/runtime.rs @@ -244,7 +244,7 @@ impl Runtime { /// Returns `Err` with a description if the configured initializer script can't /// be read, throws, or leaves asynchronous work behind. pub fn init(config: &RuntimeConfig) -> Result, String> { - crate::config::set_enforce_fetch_restrictions(config.enforce_fetch_restrictions()); + crate::config::set_web_security_restrictions(config.web_security_restrictions()); // SAFETY: no parent runtime is given, which is the only condition. let mut mozjs_rt = unsafe { MozJSRuntime::create_with_internal_job_queues(engine_handle(), None) }; @@ -479,8 +479,8 @@ impl Drop for Runtime { // and their tracer is still registered. crate::finalization::clear(&self.finalization); - // Remove GC tracers (module registry is empty, so the tracer - // is a no-op even if called during barrier processing). + // Remove GC tracers (the module registry is empty, so the tracer + // does nothing even if called during barrier processing). // The class registry is owned by the global object and cleaned // up by its finalize hook — no explicit clearing needed. let self_ptr = self as *const Self as *mut c_void; diff --git a/crates/core-runtime/src/test_util.rs b/crates/core-runtime/src/test_util.rs index d25220a..55342ad 100644 --- a/crates/core-runtime/src/test_util.rs +++ b/crates/core-runtime/src/test_util.rs @@ -54,6 +54,11 @@ pub fn eval_and_read_out(scope: &Scope<'_>, code: &str) -> String { if js::compile::evaluate_with_filename(scope, code, "test.js", 1).is_err() { panic!("evaluation threw: {:?}", ExnThrown::capture(scope)); } + read_out(scope) +} + +/// Drain microtasks and return `String(globalThis.__out)`. +fn read_out(scope: &Scope<'_>) -> String { run_microtasks(scope); let out = js::compile::evaluate_with_filename(scope, "globalThis.__out", "out.js", 1) .expect("reading __out threw"); @@ -67,3 +72,18 @@ pub fn eval_out_with_setup(setup: impl FnOnce(), code: &str) -> String { let scope = rt.default_global(); eval_and_read_out(&scope, code) } + +/// Run setup, create a runtime, evaluate `source` as an ES module, drain microtasks, and return +/// `String(globalThis.__out)`. +/// +/// Panics if the module throws. +pub fn eval_module_out_with_setup(setup: impl FnOnce(), source: &str) -> String { + setup(); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + // SAFETY: `Runtime::init` initialized the module loader. + if unsafe { crate::module::evaluate_module(&scope, source, "test.mjs") }.is_err() { + panic!("module evaluation threw: {:?}", ExnThrown::capture(&scope)); + } + read_out(&scope) +} diff --git a/crates/core-runtime/tests/accessor_return_styles.rs b/crates/core-runtime/tests/accessor_return_styles.rs index beaa68c..d3f5967 100644 --- a/crates/core-runtime/tests/accessor_return_styles.rs +++ b/crates/core-runtime/tests/accessor_return_styles.rs @@ -1,19 +1,15 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception -//! Accessor return-style codegen regression test. -//! -//! A fallible setter whose body returns `Result` with a non-unit `T` -//! (classified `ResultValue`) used to fall into a catch-all arm that ran the -//! call and discarded the `Result`, silently swallowing the `Err`. The setter -//! must instead throw the error; its `Ok` value is ignored (a JS setter yields -//! undefined). +//! Accessor and method return styles: a fallible setter whose body returns `Result` with a +//! non-unit `T` (classified `ResultValue`) throws the `Err` and ignores the `Ok` value, and a +//! `Result` operation or attribute rejects with the `Err`. // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] use core_runtime::jsclass; use core_runtime::jsmethods; -use core_runtime::test_util::{eval_out_with_setup, eval_with_setup, throws_with_setup}; +use core_runtime::test_util::{eval_out_with_setup, eval_with_setup}; use js::error::TypeError; use js::gc::scope::Scope; use js::Promise; @@ -62,29 +58,15 @@ fn setup() { } #[test] -fn setter_ok_assigns() { - assert_eq!( - eval_with_setup(setup, "const c = new Cell(); c.value = 5; c.value"), - "5" - ); -} - -#[test] -fn setter_err_throws_not_swallowed() { - assert!(throws_with_setup( - setup, - "const c = new Cell(); c.value = -1;" - )); -} - -#[test] -fn setter_err_leaves_value_unchanged() { +fn fallible_setter_assigns_ok_and_throws_err() { assert_eq!( eval_with_setup( setup, - "const c = new Cell(); c.value = 9; try { c.value = -1; } catch (e) {} c.value" + "const c = new Cell(); c.value = 9; let threw = false; \ + try { c.value = -1; } catch (e) { threw = e instanceof TypeError; } \ + `${c.value} ${threw}`" ), - "9" + "9 true" ); } diff --git a/crates/core-runtime/tests/const_naming.rs b/crates/core-runtime/tests/const_naming.rs index b4f2fa9..5d63f69 100644 --- a/crates/core-runtime/tests/const_naming.rs +++ b/crates/core-runtime/tests/const_naming.rs @@ -1,25 +1,15 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception -//! Constants keep their declared Rust name in every macro that exposes them. -//! -//! Functions are camelCased (`safe_divide` → `safeDivide`), but constants are -//! not: `PI` stays `PI`. JS spells constants in SCREAMING_CASE too, so the Rust -//! name is already the right one, and camelCasing it produced names like `pi` -//! and `maxValue` that matched neither convention. -//! -//! `#[jsclass]`/`#[webidl_interface]` constants have always behaved this way -//! (see `typedef_macros::constant_tests`); these tests pin the same rule for -//! `#[jsmodule]`, `#[jsglobals]`, `#[jsnamespace]`, and `#[webidl_namespace]`. +//! Constants keep their declared Rust name in every macro that exposes them, while functions are +//! camelCased (`safe_divide` → `safeDivide`): `PI` stays `PI`, since JS spells constants in +//! SCREAMING_CASE too. `typedef_macros` covers the constants of `#[jsclass]` and +//! `#[webidl_interface]`. // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] -use core_runtime::config::RuntimeConfig; -use core_runtime::module::evaluate_module; -use core_runtime::runtime::Runtime; -use core_runtime::test_util::eval_with_setup; +use core_runtime::test_util::{eval_module_out_with_setup, eval_with_setup}; use core_runtime::{jsglobals, jsmodule, jsnamespace, webidl_namespace}; -use js::conversion::FromJSVal; #[jsmodule] mod const_module { @@ -59,127 +49,12 @@ mod const_webidl_ns { } } -fn setup() { - core_runtime::runtime::register_global_initializer(|scope, global| { - const_globals::add_to_global(scope, global); - const_ns::add_to_global(scope, global); - const_webidl_ns::add_to_global(scope, global); - // SAFETY: called during global initialization, before any JS runs. - unsafe { - const_module::register(scope); - } - }); -} - -fn eval(code: &str) -> String { - eval_with_setup(setup, code) -} - -/// Evaluate a module that imports from `const_module` (specifier `constModule`) -/// and assigns to `globalThis._result`, then read that value back as a string. -/// -/// `import()` can't be used from `eval_with_setup` — it returns a promise the -/// helper would stringify as `[object Promise]` — so this drives a static -/// import through `evaluate_module` the way `starling/examples/native_module.rs` -/// does. -fn eval_module(body: &str) -> String { - setup(); - let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); - let scope = rt.default_global(); - let source = format!("import * as m from \"constModule\";\nglobalThis._result = {body};"); - // SAFETY: `scope` outlives the evaluation, and the module registry was - // populated by `setup` before any JS ran. - unsafe { evaluate_module(&scope, &source, "const_test.mjs") }.expect("module eval failed"); - let val = js::compile::evaluate_with_filename(&scope, "globalThis._result", "read.js", 1) - .expect("readback failed"); - String::from_jsval(&scope, val, ()).expect("null string") -} - -// ============================================================================ -// #[jsmodule] -// ============================================================================ - -#[test] -fn module_const_keeps_declared_name() { - assert_eq!(eval_module("m.PI"), "3.5"); -} - -#[test] -fn module_multiword_const_keeps_declared_name() { - assert_eq!(eval_module("m.MAX_VALUE"), "1000"); -} - -#[test] -fn module_camel_cased_const_name_is_absent() { - assert_eq!( - eval_module("typeof m.pi + ',' + typeof m.maxValue"), - "undefined,undefined" - ); -} - -#[test] -fn module_function_is_still_camel_cased() { - assert_eq!(eval_module("m.safeDivide(10, 4)"), "2.5"); -} - -// ============================================================================ -// #[jsglobals] -// ============================================================================ - -#[test] -fn global_const_keeps_declared_name() { - assert_eq!(eval("APP_NAME"), "starling"); -} - -#[test] -fn global_multiword_const_keeps_declared_name() { - assert_eq!(eval("MAX_RETRIES"), "3"); -} - -#[test] -fn global_camel_cased_const_name_is_absent() { - assert_eq!( - eval("typeof globalThis.appName + ',' + typeof globalThis.maxRetries"), - "undefined,undefined" - ); -} - -#[test] -fn global_function_is_still_camel_cased() { - assert_eq!(eval("formatName('x')"), "x"); -} - -// ============================================================================ -// #[jsnamespace] / #[webidl_namespace] — already correct, pinned against regression -// ============================================================================ - -#[test] -fn namespace_const_keeps_declared_name() { - assert_eq!(eval("constNs.MEDIA_ERR"), "4"); -} - -#[test] -fn namespace_function_is_still_camel_cased() { - assert_eq!(eval("constNs.identityValue(7)"), "7"); -} - -#[test] -fn webidl_namespace_const_keeps_declared_name() { - assert_eq!(eval("ConstWebIDL.HIERARCHY_REQUEST_ERR"), "3"); -} - -#[test] -fn webidl_namespace_function_is_still_camel_cased() { - assert_eq!(eval("ConstWebIDL.escapeValue('y')"), "y"); -} - // ============================================================================ // Derived JS names for the containers themselves // -// A `mod` block's own name is camelCased to derive the JS-visible name it is -// reached by — the module's import specifier and the namespace's global -// property — matching how the functions inside it are renamed. An explicit -// `name = "..."` is always used verbatim. +// A `mod` block's own name is camelCased to derive the JS-visible name it is reached by: the +// module's import specifier and the namespace's global property. An explicit `name = "..."` is +// used verbatim. // ============================================================================ #[jsmodule] @@ -197,46 +72,64 @@ mod multi_word_ns { pub const ANSWER: i32 = 5; } -fn setup_derived_names() { +fn setup() { core_runtime::runtime::register_global_initializer(|scope, global| { + const_globals::add_to_global(scope, global); + const_ns::add_to_global(scope, global); + const_webidl_ns::add_to_global(scope, global); multi_word_ns::add_to_global(scope, global); - // SAFETY: called during global initialization, before any JS runs. - unsafe { - multi_word_module::register(scope); - ignored_module_name::register(scope); - } + const_module::register(scope); + multi_word_module::register(scope); + ignored_module_name::register(scope); }); } -/// Import from `specifier` and read `ANSWER` back as a string. -fn eval_import(specifier: &str) -> String { - setup_derived_names(); - let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); - let scope = rt.default_global(); - let source = format!("import {{ ANSWER }} from \"{specifier}\";\nglobalThis._result = ANSWER;"); - // SAFETY: `scope` outlives the evaluation, and the module registry was - // populated by `setup_derived_names` before any JS ran. - unsafe { evaluate_module(&scope, &source, "specifier_test.mjs") }.expect("module eval failed"); - let val = js::compile::evaluate_with_filename(&scope, "globalThis._result", "read.js", 1) - .expect("readback failed"); - String::from_jsval(&scope, val, ()).expect("null string") +#[test] +fn module_consts_keep_their_declared_name() { + assert_eq!( + eval_module_out_with_setup( + setup, + "import * as m from 'constModule'; + globalThis.__out = + [m.PI, m.MAX_VALUE, typeof m.pi, typeof m.maxValue, m.safeDivide(10, 4)].join();" + ), + "3.5,1000,undefined,undefined,2.5" + ); } #[test] -fn module_specifier_is_camel_cased() { - assert_eq!(eval_import("multiWordModule"), "42"); +fn global_consts_keep_their_declared_name() { + assert_eq!( + eval_with_setup( + setup, + "[APP_NAME, MAX_RETRIES, typeof globalThis.appName, typeof globalThis.maxRetries, + formatName('x')].join()" + ), + "starling,3,undefined,undefined,x" + ); } #[test] -fn explicit_module_name_is_used_verbatim() { - assert_eq!(eval_import("explicit_module_name"), "7"); +fn namespace_consts_keep_their_declared_name() { + assert_eq!( + eval_with_setup( + setup, + "[constNs.MEDIA_ERR, constNs.identityValue(7), ConstWebIDL.HIERARCHY_REQUEST_ERR, + ConstWebIDL.escapeValue('y')].join()" + ), + "4,7,3,y" + ); } #[test] -fn namespace_global_name_is_camel_cased() { - setup_derived_names(); +fn container_names_are_camel_cased_unless_explicit() { assert_eq!( - eval_with_setup(setup_derived_names, "multiWordNs.ANSWER"), - "5" + eval_module_out_with_setup( + setup, + "import { ANSWER as a } from 'multiWordModule'; + import { ANSWER as b } from 'explicit_module_name'; + globalThis.__out = [a, b, multiWordNs.ANSWER].join();" + ), + "42,7,5" ); } diff --git a/crates/core-runtime/tests/event_loop.rs b/crates/core-runtime/tests/event_loop.rs deleted file mode 100644 index 9aac357..0000000 --- a/crates/core-runtime/tests/event_loop.rs +++ /dev/null @@ -1,884 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception - -//! Integration tests for the event loop. -//! -//! Tests are grouped in a single test function because `JSEngine` can -//! only be initialized once per process, and must run with --test-threads=1. - -#[cfg(not(target_arch = "wasm32"))] -use core_runtime::event_loop::run_to_completion; -use core_runtime::event_loop::timer::install_timer_globals; -use core_runtime::event_loop::{run_microtasks, with_event_loop, EventLoop, StepOutcome, Task}; -use core_runtime::runtime::Runtime; -use js::gc::scope::Scope; -use js::native::JSTracer; - -use js::error::ExnThrown; -use platform::clock::Instant; -use std::cell::Cell; -use std::rc::Rc; -use std::time::Duration; - -/// Helper: create a tokio current-thread runtime with timer support and -/// block on `run_to_completion` using `tokio::time::sleep`. -#[cfg(not(target_arch = "wasm32"))] -fn block_on_event_loop(scope: &Scope<'_>, el: &mut EventLoop) { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_time() - .build() - .unwrap(); - rt.block_on(async { - // SAFETY: the scope (and its Runtime) must outlive this future. - // Since the caller owns both and `block_on` runs synchronously on - // the same thread, this is guaranteed. - let raw_cx = unsafe { scope.raw_cx_no_gc() }; - unsafe { run_to_completion(raw_cx, el, tokio::time::sleep).await } - }); -} - -// --------------------------------------------------------------------------- -// Custom task for testing -// --------------------------------------------------------------------------- - -/// A simple task that increments a shared counter when run. -struct CounterTask { - counter: Rc>, - #[allow(dead_code)] - label: &'static str, -} - -impl Task for CounterTask { - fn kind(&self) -> &'static str { - "counter" - } - - fn run( - self: Box, - _scope: &Scope<'_>, - _id: core_runtime::event_loop::TaskId, - ) -> Result<(), ExnThrown> { - self.counter.set(self.counter.get() + 1); - Ok(()) - } - - fn trace(&self, _trc: *mut JSTracer) {} -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -#[test] -fn test_event_loop() { - let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()).expect("runtime init"); - let scope = rt.default_global(); - - // Install timer globals for timer tests. - install_timer_globals(&scope, scope.global()); - - // ---- Test 1: Basic EventLoop operations ---- - { - let el = EventLoop::new(); - - // Empty loop. - assert!(el.is_empty()); - assert!(!el.has_pending()); - assert!(!el.has_ready()); - assert_eq!(el.len(), 0); - - // Queue a task. - let counter = Rc::new(Cell::new(0u32)); - let id = el.queue(Box::new(CounterTask { - counter: counter.clone(), - label: "task1", - })); - - assert!(el.has_pending()); - assert!(!el.has_ready()); - assert_eq!(el.len(), 1); - - // Not ready yet — pop_ready should return None. - assert!(el.pop_ready().is_none()); - - // Signal it ready. - el.signal_ready(id); - assert!(el.has_ready()); - - // Pop and run it. - let (popped_id, task) = el.pop_ready().unwrap(); - assert_eq!(popped_id, id); - assert_eq!(counter.get(), 0); - task.run(&scope, popped_id).unwrap(); - assert_eq!(counter.get(), 1); - - // Now the loop is empty. - assert!(el.is_empty()); - } - - // ---- Test 2: queue_ready (immediately ready) ---- - { - let el = EventLoop::new(); - let counter = Rc::new(Cell::new(0u32)); - - el.queue_ready(Box::new(CounterTask { - counter: counter.clone(), - label: "ready1", - })); - - assert!(el.has_ready()); - let (id, task) = el.pop_ready().unwrap(); - task.run(&scope, id).unwrap(); - assert_eq!(counter.get(), 1); - assert!(el.is_empty()); - } - - // ---- Test 3: Cancel ---- - { - let el = EventLoop::new(); - let counter = Rc::new(Cell::new(0u32)); - - let id = el.queue_ready(Box::new(CounterTask { - counter: counter.clone(), - label: "cancelled", - })); - - assert!(el.cancel_if_queued(id)); - assert!(el.is_empty()); - assert_eq!(counter.get(), 0); - - // Cancel of non-existent ID returns false. - assert!(!el.cancel_if_queued(id)); - } - - // ---- Test 4: Timer tasks ---- - { - let el = EventLoop::new(); - let counter = Rc::new(Cell::new(0u32)); - - // Queue a timer 5ms in the future. - let _id = el.queue_timer( - Box::new(CounterTask { - counter: counter.clone(), - label: "timer1", - }), - Instant::now() + Duration::from_millis(5), - ); - - assert!(el.has_pending()); - assert!(!el.has_ready()); - - // advance_timers should NOT mark it ready yet. - assert_eq!(el.advance_timers(), 0); - assert!(!el.has_ready()); - - // time_to_next_timer should be Some and > 0. - let wait = el.time_to_next_timer(); - assert!(wait.is_some()); - - // Sleep past the deadline. - std::thread::sleep(Duration::from_millis(10)); - - // Now advance_timers should mark it ready. - assert_eq!(el.advance_timers(), 1); - assert!(el.has_ready()); - - let (id, task) = el.pop_ready().unwrap(); - task.run(&scope, id).unwrap(); - assert_eq!(counter.get(), 1); - } - - // ---- Test 5: Multiple tasks, ordering ---- - { - let el = EventLoop::new(); - let counter = Rc::new(Cell::new(0u32)); - - let id1 = el.queue_ready(Box::new(CounterTask { - counter: counter.clone(), - label: "first", - })); - let id2 = el.queue_ready(Box::new(CounterTask { - counter: counter.clone(), - label: "second", - })); - let id3 = el.queue_ready(Box::new(CounterTask { - counter: counter.clone(), - label: "third", - })); - - // Pop and run all three. - let mut ids = Vec::new(); - while let Some((id, task)) = el.pop_ready() { - ids.push(id); - task.run(&scope, id).unwrap(); - } - - assert_eq!(counter.get(), 3); - assert!(el.is_empty()); - // All three IDs should be unique. - assert!(ids.contains(&id1)); - assert!(ids.contains(&id2)); - assert!(ids.contains(&id3)); - } - - // ---- Test 6: run_to_completion with immediate tasks ---- - #[cfg(not(target_arch = "wasm32"))] - { - let counter = Rc::new(Cell::new(0u32)); - let mut el = EventLoop::new(); - - el.queue_ready(Box::new(CounterTask { - counter: counter.clone(), - label: "native1", - })); - el.queue_ready(Box::new(CounterTask { - counter: counter.clone(), - label: "native2", - })); - - block_on_event_loop(&scope, &mut el); - assert_eq!(counter.get(), 2); - assert!(el.is_empty()); - } - - // ---- Test 7: run_to_completion with timer ---- - #[cfg(not(target_arch = "wasm32"))] - { - let counter = Rc::new(Cell::new(0u32)); - let mut el = EventLoop::new(); - - el.queue_timer( - Box::new(CounterTask { - counter: counter.clone(), - label: "delayed", - }), - Instant::now() + Duration::from_millis(5), - ); - - block_on_event_loop(&scope, &mut el); - assert_eq!(counter.get(), 1); - assert!(el.is_empty()); - } - - // ---- Test 8: setTimeout from JavaScript ---- - #[cfg(not(target_arch = "wasm32"))] - { - let mut el = EventLoop::new(); - - // Evaluate JS with the event loop thread-local set so setTimeout works. - { - with_event_loop(&el, |_| { - let ok = js::compile::evaluate_with_filename( - &scope, - "globalThis._timerFired = false; setTimeout(function() { globalThis._timerFired = true; }, 1);", - "", - 1, - ); - assert!(ok.is_ok(), "setTimeout JS evaluation failed"); - }); - } - - // The timer task should be queued. - assert!(el.has_pending()); - - // Run the event loop to fire the timer. - block_on_event_loop(&scope, &mut el); - - // Verify the timer callback ran. - let result = js::compile::evaluate_with_filename( - &scope, - "globalThis._timerFired", - "", - 1, - ); - assert!(result.is_ok()); - let val = result.unwrap(); - assert!( - val.is_boolean() && val.to_boolean(), - "setTimeout callback should have fired" - ); - } - - // ---- Test 8a: timer ids are unique per global across concurrent loops ---- - // - // Two event loops driving the same global (the WASIp3 concurrent-invocation - // shape) must not hand out colliding `setTimeout` ids: HTML's id namespace - // is per-global, so a `clearTimeout` from one invocation must never resolve - // to a different invocation's timer. - { - let el_a = EventLoop::new(); - let el_b = EventLoop::new(); - - with_event_loop(&el_a, |_| { - let ok = js::compile::evaluate_with_filename( - &scope, - "globalThis._idA = setTimeout(function() {}, 100000);", - "", - 1, - ); - assert!(ok.is_ok(), "setTimeout in loop A failed"); - }); - with_event_loop(&el_b, |_| { - let ok = js::compile::evaluate_with_filename( - &scope, - "globalThis._idB = setTimeout(function() {}, 100000);", - "", - 1, - ); - assert!(ok.is_ok(), "setTimeout in loop B failed"); - }); - - let read_id = |name: &str| -> i32 { - let r = js::compile::evaluate_with_filename(&scope, name, "", 1).unwrap(); - if r.is_int32() { - r.to_int32() - } else { - r.to_double() as i32 - } - }; - let id_a = read_id("globalThis._idA"); - let id_b = read_id("globalThis._idB"); - assert_ne!( - id_a, id_b, - "timer ids from concurrent event loops on the same global must be unique" - ); - } - - // ---- Test 8b: setTimeout forwards extra arguments to the callback ---- - #[cfg(not(target_arch = "wasm32"))] - { - let mut el = EventLoop::new(); - - { - with_event_loop(&el, |_| { - let ok = js::compile::evaluate_with_filename( - &scope, - "globalThis._timerArgs = null; \ - setTimeout(function(a, b, c) { globalThis._timerArgs = [a, b, c].join(','); }, 1, 'x', 42, true);", - "", - 1, - ); - assert!(ok.is_ok(), "setTimeout-with-args JS evaluation failed"); - }); - } - - block_on_event_loop(&scope, &mut el); - - let result = js::compile::evaluate_with_filename( - &scope, - "globalThis._timerArgs", - "", - 1, - ); - assert!(result.is_ok()); - let val = result.unwrap(); - assert!(val.is_string(), "timer args should have been recorded"); - let s = js::JSString::from_value(&scope, val) - .unwrap() - .to_utf8(&scope) - .unwrap(); - assert_eq!( - s, "x,42,true", - "extra setTimeout arguments must be forwarded" - ); - } - - // ---- Test 8c: handler is converted before the timeout (WebIDL order) ---- - { - let el = EventLoop::new(); - - { - with_event_loop(&el, |_| { - let ok = js::compile::evaluate_with_filename( - &scope, - "globalThis._convOrder = []; \ - globalThis._convTid = setTimeout(\ - { toString() { globalThis._convOrder.push('handler'); return ''; } }, \ - { valueOf() { globalThis._convOrder.push('timeout'); return 1e9; } });", - "", - 1, - ); - assert!(ok.is_ok(), "setTimeout with converting args failed"); - }); - } - - let result = js::compile::evaluate_with_filename( - &scope, - "globalThis._convOrder.join(',')", - "", - 1, - ); - let val = result.unwrap(); - let s = js::JSString::from_value(&scope, val) - .unwrap() - .to_utf8(&scope) - .unwrap(); - assert_eq!( - s, "handler,timeout", - "WebIDL requires left-to-right argument conversion" - ); - - // Drain the queued (far-future) timer so it can't leak into later tests. - let cleared = { - with_event_loop(&el, |_| { - js::compile::evaluate_with_filename( - &scope, - "clearTimeout(globalThis._convTid)", - "", - 1, - ) - .is_ok() - }) - }; - assert!(cleared); - assert!(el.is_empty(), "cleanup clearTimeout should empty the loop"); - } - - // ---- Test 8d: a throwing interval callback keeps repeating ---- - #[cfg(not(target_arch = "wasm32"))] - { - let mut el = EventLoop::new(); - - { - with_event_loop(&el, |_| { - let ok = js::compile::evaluate_with_filename( - &scope, - "globalThis._ivTicks = 0; \ - globalThis._iv = setInterval(function() { \ - globalThis._ivTicks++; \ - if (globalThis._ivTicks === 1) { throw new Error('boom'); } \ - if (globalThis._ivTicks >= 3) { clearInterval(globalThis._iv); } \ - }, 1);", - "", - 1, - ); - assert!(ok.is_ok(), "setInterval JS evaluation failed"); - }); - } - - block_on_event_loop(&scope, &mut el); - - let result = js::compile::evaluate_with_filename( - &scope, - "globalThis._ivTicks", - "", - 1, - ); - let val = result.unwrap(); - assert!(val.is_int32()); - assert_eq!( - val.to_int32(), - 3, - "a throwing callback must not kill the interval (spec: report + repeat)" - ); - } - - // ---- Test 8e: JS timer ids are spec-shaped; clearing is timer-only ---- - #[cfg(not(target_arch = "wasm32"))] - { - let mut el = EventLoop::new(); - let counter = Rc::new(Cell::new(0)); - - // An internal (non-JS) timer task on the same loop: clearTimeout must - // not be able to reach it, whatever ids JS throws at it. - el.queue_timer( - Box::new(CounterTask { - counter: Rc::clone(&counter), - label: "internal", - }), - Instant::now(), - ); - - { - with_event_loop(&el, |_| { - let ok = js::compile::evaluate_with_filename( - &scope, - "globalThis._t1Fired = false; \ - globalThis._tid1 = setTimeout(function() { globalThis._t1Fired = true; }, 1); \ - if (globalThis._tid1 <= 0) throw new Error('timer id must be > 0, got ' + globalThis._tid1); \ - clearTimeout(globalThis._tid1 + 1); \ - globalThis._ivTicks2 = 0; \ - globalThis._iv2 = setInterval(function() { \ - globalThis._ivTicks2++; \ - if (globalThis._ivTicks2 >= 2) clearInterval(globalThis._iv2); \ - }, 1); \ - clearTimeout(-0.5); clearTimeout(0); clearTimeout(999999); clearTimeout(-7); \ - globalThis._t3Fired = false; \ - globalThis._tid3 = setTimeout(function() { globalThis._t3Fired = true; }, 1); \ - clearTimeout(String(globalThis._tid3));", - "", - 1, - ); - assert!(ok.is_ok(), "timer-id JS evaluation failed"); - }); - } - - block_on_event_loop(&scope, &mut el); - - assert_eq!( - counter.get(), - 1, - "internal tasks must be unreachable from clearTimeout" - ); - let result = js::compile::evaluate_with_filename( - &scope, - "[globalThis._t1Fired, globalThis._ivTicks2, globalThis._t3Fired].join(',')", - "", - 1, - ); - let val = result.unwrap(); - let s = js::JSString::from_value(&scope, val) - .unwrap() - .to_utf8(&scope) - .unwrap(); - assert_eq!( - s, "true,2,false", - "junk clears must be no-ops, a pre-cleared id must not poison a \ - later interval, and a string id must coerce and clear" - ); - } - - // ---- Test 8f: expired timers fire in deadline order ---- - #[cfg(not(target_arch = "wasm32"))] - { - let mut el = EventLoop::new(); - - { - with_event_loop(&el, |_| { - let ok = js::compile::evaluate_with_filename( - &scope, - // `late` is registered last so that its deadline follows the - // others' however long each `setTimeout` call itself takes. - "globalThis._fireOrder = []; \ - setTimeout(function() { globalThis._fireOrder.push('early'); }, 10); \ - globalThis._goner = setTimeout(function() { globalThis._fireOrder.push('goner'); }, 10); \ - setTimeout(function() { globalThis._fireOrder.push('early2'); }, 10); \ - setTimeout(function() { globalThis._fireOrder.push('late'); }, 40); \ - clearTimeout(globalThis._goner);", - "", - 1, - ); - assert!(ok.is_ok(), "deadline-order JS evaluation failed"); - }); - } - - // Let every deadline pass before the loop runs, so one advance_timers - // call marks them all ready at once — the order must then come from - // the deadlines, not from queue positions (which the clearTimeout's - // swap_remove perturbed). - std::thread::sleep(Duration::from_millis(60)); - block_on_event_loop(&scope, &mut el); - - let result = js::compile::evaluate_with_filename( - &scope, - "globalThis._fireOrder.join(',')", - "", - 1, - ); - let val = result.unwrap(); - let s = js::JSString::from_value(&scope, val) - .unwrap() - .to_utf8(&scope) - .unwrap(); - assert_eq!( - s, "early,early2,late", - "expired timers must fire in deadline order, ties in scheduling \ - order, regardless of cancellations" - ); - } - - // ---- Test 8g: interval arguments survive moving GC across re-queues ---- - // While a task runs it is out of the loop's task list and untraced, so the - // re-queued interval must rebuild its argument Heaps from scope roots. - // Compacting on every allocation makes a stale Heap fail deterministically. - #[cfg(not(target_arch = "wasm32"))] - { - let mut el = EventLoop::new(); - - { - with_event_loop(&el, |_| { - let ok = js::compile::evaluate_with_filename( - &scope, - "globalThis._gcArgSeen = []; \ - globalThis._gcIv = setInterval(function(tag) { \ - globalThis._gcArgSeen.push(tag.name); \ - if (globalThis._gcArgSeen.length >= 3) clearInterval(globalThis._gcIv); \ - }, 1, { name: 'payload' });", - "", - 1, - ); - assert!(ok.is_ok(), "interval-with-args JS evaluation failed"); - }); - } - #[cfg(feature = "debugmozjs")] - unsafe { - js::gc::SetGCZeal(scope.raw_cx_no_gc(), 14, 1) - }; - block_on_event_loop(&scope, &mut el); - #[cfg(feature = "debugmozjs")] - unsafe { - js::gc::SetGCZeal(scope.raw_cx_no_gc(), 0, 0) - }; - - let result = js::compile::evaluate_with_filename( - &scope, - "globalThis._gcArgSeen.join(',')", - "", - 1, - ); - let val = result.unwrap(); - let s = js::JSString::from_value(&scope, val) - .unwrap() - .to_utf8(&scope) - .unwrap(); - assert_eq!( - s, "payload,payload,payload", - "interval arguments must survive compacting GC between re-queues" - ); - } - - // ---- Test 9: clearTimeout cancels a timer ---- - { - let el = EventLoop::new(); - - { - with_event_loop(&el, |_| { - let ok = js::compile::evaluate_with_filename( - &scope, - "globalThis._cleared = true; var tid = setTimeout(function() { globalThis._cleared = false; }, 1); clearTimeout(tid);", - "", - 1, - ); - assert!(ok.is_ok()); - }); - } - - // After clearTimeout, the event loop should be empty. - assert!(el.is_empty(), "clearTimeout should have removed the timer"); - - // Verify the callback did NOT run. - let result = js::compile::evaluate_with_filename( - &scope, - "globalThis._cleared", - "", - 1, - ); - assert!(result.is_ok()); - let val = result.unwrap(); - assert!( - val.is_boolean() && val.to_boolean(), - "clearTimeout should prevent callback" - ); - } - - // ---- Test 10: setInterval fires multiple times ---- - #[cfg(not(target_arch = "wasm32"))] - { - let mut el = EventLoop::new(); - - { - with_event_loop(&el, |_| { - let ok = js::compile::evaluate_with_filename( - &scope, - r#" - globalThis._intervalCount = 0; - globalThis._intervalId = setInterval(function() { - globalThis._intervalCount++; - if (globalThis._intervalCount >= 3) { - clearInterval(globalThis._intervalId); - } - }, 1); - "#, - "", - 1, - ); - assert!(ok.is_ok()); - }); - } - - assert!(el.has_pending()); - - block_on_event_loop(&scope, &mut el); - - // Verify the interval fired exactly 3 times and then stopped. - let result = js::compile::evaluate_with_filename( - &scope, - "globalThis._intervalCount", - "", - 1, - ); - assert!(result.is_ok()); - let val = result.unwrap(); - let count = if val.is_int32() { - val.to_int32() - } else { - val.to_double() as i32 - }; - assert_eq!(count, 3, "setInterval should have fired exactly 3 times"); - } - - // ---- Test 11: Event loop with microtasks (promise reactions) ---- - { - // Evaluate JS that creates a resolved promise — the .then callback - // should run during run_microtasks. - let ok = js::compile::evaluate_with_filename( - &scope, - "globalThis._promiseResult = 0; Promise.resolve(42).then(v => { globalThis._promiseResult = v; });", - "", - 1, - ); - assert!(ok.is_ok()); - - // Run microtasks to process the promise. - run_microtasks(&scope); - - let result = js::compile::evaluate_with_filename( - &scope, - "globalThis._promiseResult", - "", - 1, - ); - assert!(result.is_ok()); - let val = result.unwrap(); - let n = if val.is_int32() { - val.to_int32() - } else { - val.to_double() as i32 - }; - assert_eq!(n, 42, "Promise.resolve .then should have run"); - } - - // ---- Test 12: step() returns Done on empty event loop ---- - { - let el = EventLoop::new(); - let outcome = el.step(&scope); - assert_eq!(outcome, StepOutcome::Done); - } - - // ---- Test 13: step() returns Progressed when tasks run ---- - { - let el = EventLoop::new(); - let counter = Rc::new(Cell::new(0u32)); - - el.queue_ready(Box::new(CounterTask { - counter: counter.clone(), - label: "step-prog", - })); - - let outcome = el.step(&scope); - assert_eq!(outcome, StepOutcome::Progressed); - assert_eq!(counter.get(), 1); - } - - // ---- Test 14: step() returns Idle when tasks exist but none ready ---- - { - let el = EventLoop::new(); - let counter = Rc::new(Cell::new(0u32)); - - // Queue a task but don't signal it ready. - let _id = el.queue(Box::new(CounterTask { - counter: counter.clone(), - label: "step-idle", - })); - - let outcome = el.step(&scope); - assert_eq!(outcome, StepOutcome::Idle); - assert_eq!(counter.get(), 0); - } - - // ---- Test 15: step() returns Idle with interest but no tasks ---- - { - let el = EventLoop::new(); - - let interest = el.acquire_interest_handle(); - assert!(el.has_interest()); - assert!(el.is_alive()); - - let outcome = el.step(&scope); - assert_eq!(outcome, StepOutcome::Idle); - - interest.release(); - assert!(!el.has_interest()); - - let outcome = el.step(&scope); - assert_eq!(outcome, StepOutcome::Done); - } - - // ---- Test 16: is_alive combines interest and pending ---- - { - let el = EventLoop::new(); - - assert!(!el.is_alive()); - - // Interest alone keeps it alive. - let interest = el.acquire_interest_handle(); - assert!(el.is_alive()); - drop(interest); - assert!(!el.is_alive()); - - // Pending tasks alone keep it alive. - let counter = Rc::new(Cell::new(0u32)); - el.queue(Box::new(CounterTask { - counter: counter.clone(), - label: "alive-test", - })); - assert!(el.is_alive()); - } - - // ---- Test 17: step() runs timer tasks after advance ---- - { - let el = EventLoop::new(); - let counter = Rc::new(Cell::new(0u32)); - - // Queue a timer with deadline in the past. - el.queue_timer( - Box::new(CounterTask { - counter: counter.clone(), - label: "step-timer", - }), - Instant::now(), - ); - - // step() advances timers internally. - let outcome = el.step(&scope); - assert_eq!(outcome, StepOutcome::Progressed); - assert_eq!(counter.get(), 1); - assert!(el.is_empty()); - } - - // ---- Test 18: run_to_completion respects interest tracking ---- - #[cfg(not(target_arch = "wasm32"))] - { - let mut el = EventLoop::new(); - - // Acquire interest, queue a task that releases it (by dropping the handle). - let interest = el.acquire_interest_handle(); - - struct ReleaseInterestTask { - interest: Option, - } - impl Task for ReleaseInterestTask { - fn kind(&self) -> &'static str { - "release-interest" - } - fn run( - mut self: Box, - _scope: &Scope<'_>, - _id: core_runtime::event_loop::TaskId, - ) -> Result<(), ExnThrown> { - drop(self.interest.take()); - Ok(()) - } - fn trace(&self, _trc: *mut JSTracer) {} - } - - el.queue_ready(Box::new(ReleaseInterestTask { - interest: Some(interest), - })); - - // With interest, the native driver should run the task and then - // exit once interest drops to zero and no tasks remain. - block_on_event_loop(&scope, &mut el); - assert!(!el.has_interest()); - assert!(el.is_empty()); - } -} diff --git a/crates/core-runtime/tests/heap_value.rs b/crates/core-runtime/tests/heap_value.rs index 631a846..5318ee9 100644 --- a/crates/core-runtime/tests/heap_value.rs +++ b/crates/core-runtime/tests/heap_value.rs @@ -61,7 +61,7 @@ fn heap_value_survives_move_and_compacting_gc() { // Read back through a rooted get; the object must still be reachable and // carry its marker, proving the barrier tracked any relocation. let rooted = heaps[0].get(&scope); - let recovered = Object::from_value(&scope, *rooted).expect("still an object"); + let recovered = Object::from_raw_value(&scope, rooted.get()).expect("still an object"); let got = recovered.get_property(&scope, c"marker").unwrap(); assert_eq!(got.to_int32(), 4242); } diff --git a/crates/core-runtime/tests/implicit_ctor.rs b/crates/core-runtime/tests/implicit_ctor.rs index aa738f6..5f0b250 100644 --- a/crates/core-runtime/tests/implicit_ctor.rs +++ b/crates/core-runtime/tests/implicit_ctor.rs @@ -6,8 +6,8 @@ //! that is not constructible from JS: `new Foo()` throws a `TypeError` //! ("Illegal constructor"). An unannotated constructor-shaped fn — either //! `fn any_name() -> Self` or the setup-style `fn new(&self, ...)` — is still -//! treated as a Rust-side constructor, so `Foo::new(scope, ...)` keeps -//! working. +//! treated as a Rust-side constructor, so `Foo::new(scope, ...)` works. + // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] @@ -16,7 +16,7 @@ use core_runtime::runtime::Runtime; use core_runtime::test_util::eval_with_setup; use core_runtime::{jsclass, jsmethods}; -/// Old-style Rust constructor (`fn new() -> Self`), no `#[constructor]`. +/// A class with a Rust constructor (`fn new() -> Self`) and no `#[constructor]`. #[jsclass] struct RustOnly { n: i32, @@ -153,97 +153,36 @@ fn setup() { }); } -/// `new Foo()` must throw a `TypeError` when no `#[constructor]` is present. -#[test] -fn js_construction_throws() { - for class in [ - "RustOnly", - "RenamedCtor", - "ManyFactories", - "RustOnlySetup", - "NoCtorAtAll", - ] { - let result = eval_with_setup( - setup, - &format!( - "try {{ new {class}(); 'no-throw' }} \ - catch (e) {{ (e instanceof TypeError) + ':' + e.message }}" - ), - ); - assert_eq!(result, "true:Illegal constructor", "for class {class}"); - } -} - -/// The interface object is still installed and usable (statics, prototype). +/// `new Foo()` throws a `TypeError` when no `#[constructor]` is present. The interface object is +/// installed regardless. An extra factory next to a `#[constructor]` is not exposed to JS. #[test] -fn interface_object_still_installed() { - let result = eval_with_setup(setup, "NoCtorAtAll.answer()"); - assert_eq!(result, "42"); -} - -/// A class that does declare a `#[constructor]` stays constructible from JS. -#[test] -fn explicit_constructor_still_works() { - let result = eval_with_setup(setup, "new Constructible(5).n"); - assert_eq!(result, "5"); +fn only_explicit_constructors_are_js_constructible() { + let result = eval_with_setup( + setup, + "const outcomes = ['RustOnly', 'RenamedCtor', 'ManyFactories', 'RustOnlySetup', 'NoCtorAtAll'] + .map(name => { + try { new globalThis[name](); return `${name}:no-throw`; } + catch (e) { return `${e instanceof TypeError}:${e.message}`; } + }); + [...new Set(outcomes), NoCtorAtAll.answer(), new Constructible(5).n, + new CtorPlusFactory(5).n, typeof CtorPlusFactory.doubled].join()", + ); + assert_eq!(result, "true:Illegal constructor,42,5,5,undefined"); } -/// `Foo::new(scope)` still works from Rust for the old-style shape. +/// Every constructor-shaped fn gets a Rust-side factory under its own name. #[test] -fn rust_side_new_works() { +fn rust_side_factories_construct_instances() { setup(); let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); let obj = RustOnly::new(&scope).unwrap(); - assert_eq!(obj.data().n, 7); - assert_eq!(obj.n(), 7); -} - -/// The factory keeps the source fn's name: `fn from_parts` becomes -/// `Foo::from_parts(scope, …)`, and no `Foo::new` is conjured up. -#[test] -fn rust_side_renamed_ctor_works() { - setup(); - let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); - let scope = rt.default_global(); - let obj = RenamedCtor::from_parts(&scope, 3).unwrap(); - assert_eq!(obj.data().n, 3); -} - -/// Every constructor-shaped fn gets a factory under its own name. -#[test] -fn rust_side_multiple_factories() { - setup(); - let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); - let scope = rt.default_global(); + assert_eq!((obj.data().n, obj.n()), (7, 7)); + assert_eq!(RenamedCtor::from_parts(&scope, 3).unwrap().data().n, 3); assert_eq!(ManyFactories::new(&scope).unwrap().data().n, 0); assert_eq!(ManyFactories::from_n(&scope, 6).unwrap().data().n, 6); assert_eq!(ManyFactories::doubled(&scope, 6).unwrap().data().n, 12); -} - -/// An extra factory coexists with a JS constructor: `new` is the -/// `#[constructor]`'s, and the extra is reachable only from Rust. -#[test] -fn rust_side_factory_alongside_js_constructor() { - let js = eval_with_setup( - setup, - "new CtorPlusFactory(5).n + ':' + typeof CtorPlusFactory.doubled", - ); - assert_eq!(js, "5:undefined"); - - setup(); - let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); - let scope = rt.default_global(); assert_eq!(CtorPlusFactory::new(&scope, 5).unwrap().data().n, 5); assert_eq!(CtorPlusFactory::doubled(&scope, 5).unwrap().data().n, 10); -} - -/// `Foo::new(scope, args)` still works from Rust for the setup-style shape. -#[test] -fn rust_side_setup_new_works() { - setup(); - let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); - let scope = rt.default_global(); - let obj = RustOnlySetup::new(&scope, 11).unwrap(); - assert_eq!(obj.data().n, 11); + assert_eq!(RustOnlySetup::new(&scope, 11).unwrap().data().n, 11); } diff --git a/crates/core-runtime/tests/instance_value_unforgeable.rs b/crates/core-runtime/tests/instance_value_unforgeable.rs index 5010189..56f2d7f 100644 --- a/crates/core-runtime/tests/instance_value_unforgeable.rs +++ b/crates/core-runtime/tests/instance_value_unforgeable.rs @@ -1,10 +1,8 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception -//! Regression test: a method returning `Self` (the `InstanceValue` codegen -//! path) must mint instances that carry `[LegacyUnforgeable]` own accessors, -//! exactly like the constructor and Rust-side factory paths. The JS-native -//! trampoline used to skip `install_unforgeable`, so an instance returned from -//! a JS method call lacked the own accessor entirely. +//! Instances carry `[LegacyUnforgeable]` own accessors whichever path creates them: the +//! constructor, a method returning `Self` (the `InstanceValue` codegen path), or a JS or Rust +//! subclass. // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] @@ -72,21 +70,9 @@ fn dup_instance_has_unforgeable_own_accessor() { eval_with_setup( setup, "const d = new Widget().dup(); \ - typeof Object.getOwnPropertyDescriptor(d, 'kind') === 'object'" + [typeof Object.getOwnPropertyDescriptor(d, 'kind'), d.kind].join()" ), - "true" - ); - assert_eq!(eval_with_setup(setup, "new Widget().dup().kind"), "42"); -} - -#[test] -fn unforgeable_accessor_is_not_on_prototype() { - assert_eq!( - eval_with_setup( - setup, - "Object.getOwnPropertyDescriptor(Widget.prototype, 'kind') === undefined" - ), - "true" + "object,42" ); } @@ -103,15 +89,17 @@ fn instances_share_one_getter_function() { ); } +/// The accessor is an own property only, not one of the prototype. #[test] -fn unforgeable_accessor_is_enumerable_and_non_configurable() { +fn unforgeable_accessor_is_an_enumerable_non_configurable_own_property() { assert_eq!( eval_with_setup( setup, "const d = Object.getOwnPropertyDescriptor(new Widget(), 'kind'); \ - [d.enumerable, d.configurable, d.set].join()" + [d.enumerable, d.configurable, d.set, \ + Object.getOwnPropertyDescriptor(Widget.prototype, 'kind')].join()" ), - "true,false," + "true,false,," ); } diff --git a/crates/core-runtime/tests/jsglobals_use.rs b/crates/core-runtime/tests/jsglobals_use.rs index d738714..f6ec23d 100644 --- a/crates/core-runtime/tests/jsglobals_use.rs +++ b/crates/core-runtime/tests/jsglobals_use.rs @@ -1,10 +1,6 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception -//! Regression test for `#[jsglobals]` class registration via `pub use`. -//! -//! Only `pub use Foo;` (a plain name) was registered; a grouped -//! `pub use {A, B};` or a renamed `pub use Foo as Bar;` was silently skipped, -//! so the class was never installed on the global. All forms are now handled. +//! `#[jsglobals]` registers each class a grouped `pub use {A, B};` names. // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] @@ -57,8 +53,8 @@ fn setup() { #[test] fn grouped_use_registers_all_classes() { - assert_eq!(eval_with_setup(setup, "typeof Alpha"), "function"); - assert_eq!(eval_with_setup(setup, "typeof Beta"), "function"); - assert_eq!(eval_with_setup(setup, "new Alpha().tag"), "alpha"); - assert_eq!(eval_with_setup(setup, "new Beta().tag"), "beta"); + assert_eq!( + eval_with_setup(setup, "[new Alpha().tag, new Beta().tag].join()"), + "alpha,beta" + ); } diff --git a/crates/core-runtime/tests/multi_global_registry.rs b/crates/core-runtime/tests/multi_global_registry.rs index 8b71251..68bec7f 100644 --- a/crates/core-runtime/tests/multi_global_registry.rs +++ b/crates/core-runtime/tests/multi_global_registry.rs @@ -70,7 +70,7 @@ fn displaced_global_registry_survives_compacting_gc() { let proto_reg = js::class::get_prototype_object_for::(&scope_a) .expect("Probe prototype registered in realm A"); let ctor = scope_a.global().get_property(&scope_a, c"Probe").unwrap(); - let ctor = js::Object::from_value(&scope_a, *ctor).unwrap(); + let ctor = js::Object::from_raw_value(&scope_a, ctor.get()).unwrap(); let proto_js = ctor.get_property(&scope_a, c"prototype").unwrap(); assert!(proto_js.is_object()); assert_eq!( diff --git a/crates/core-runtime/tests/parameterized_types.rs b/crates/core-runtime/tests/parameterized_types.rs index cd17842..6b2feae 100644 --- a/crates/core-runtime/tests/parameterized_types.rs +++ b/crates/core-runtime/tests/parameterized_types.rs @@ -6,17 +6,25 @@ //! - `#[webidl_union]` (union) //! - `AsyncSequence` (async iterable) +// This file contains nothing platform-specific, so skip it on wasm32. +#![cfg(not(target_arch = "wasm32"))] + +/// Run [`eval_with_setup`](core_runtime::test_util::eval_with_setup) on `code` with `r(f)` +/// defined. `r(f)` returns `String(f())`, or `throws:` followed by the constructor name of the +/// exception `f` threw. +fn eval_outcomes(setup: impl FnOnce(), code: &str) -> String { + let prelude = + "const r = f => { try { return String(f()); } catch (e) { return `throws:${e.constructor.name}`; } };"; + core_runtime::test_util::eval_with_setup(setup, &format!("{prelude}\n{code}")) +} + // ============================================================================ // Vec (sequence) tests // ============================================================================ -// This file contains nothing platform-specific, so skip it on wasm32. -#![cfg(not(target_arch = "wasm32"))] - mod sequence_tests { use core_runtime::jsclass; use core_runtime::jsmethods; - use core_runtime::test_util::{eval_with_setup, throws_with_setup}; #[jsclass] struct SeqAcceptor {} @@ -57,18 +65,7 @@ mod sequence_tests { } fn eval(code: &str) -> String { - eval_with_setup( - || { - core_runtime::runtime::register_global_initializer(|scope, global| { - SeqAcceptor::add_to_global(scope, global); - }); - }, - code, - ) - } - - fn throws(code: &str) -> bool { - throws_with_setup( + super::eval_outcomes( || { core_runtime::runtime::register_global_initializer(|scope, global| { SeqAcceptor::add_to_global(scope, global); @@ -79,62 +76,31 @@ mod sequence_tests { } #[test] - fn join_string_array() { + fn sequences_convert_from_any_iterable() { assert_eq!( - eval("new SeqAcceptor().joinStrings(['a', 'b', 'c'])"), - "a,b,c" + eval( + "const s = new SeqAcceptor(); + [r(() => s.joinStrings(['a', 'b'])), r(() => s.joinStrings([])), + r(() => s.sumInts([1, 2, 3])), r(() => s.joinStrings(new Set(['x', 'y']))), + r(() => SeqAcceptor.count(['a', 'b'])), r(() => s.joinStrings(42))].join('|')" + ), + "a,b||6|x,y|2|throws:TypeError" ); } + /// `null` is a present value, which isn't iterable. Only `undefined` makes the argument + /// absent. #[test] - fn join_empty_array() { - assert_eq!(eval("new SeqAcceptor().joinStrings([])"), ""); - } - - #[test] - fn sum_int_array() { - assert_eq!(eval("new SeqAcceptor().sumInts([1, 2, 3])"), "6"); - } - - #[test] - fn sum_empty_array() { - assert_eq!(eval("new SeqAcceptor().sumInts([])"), "0"); - } - - #[test] - fn optional_seq_present() { - assert_eq!(eval("new SeqAcceptor().optionalSeq(['x', 'y'])"), "x,y"); - } - - #[test] - fn optional_seq_null_throws() { - // `null` is a present value (not absent); converting it to a sequence throws (not iterable). - assert!(throws("new SeqAcceptor().optionalSeq(null)")); - } - - #[test] - fn optional_seq_undefined() { - assert_eq!(eval("new SeqAcceptor().optionalSeq(undefined)"), "none"); - } - - #[test] - fn static_count() { - assert_eq!(eval("SeqAcceptor.count(['a', 'b'])"), "2"); - } - - #[test] - fn iterable_protocol() { - // Any iterable should work, not just arrays. + fn optional_sequences_are_absent_only_for_undefined() { assert_eq!( - eval("new SeqAcceptor().joinStrings(new Set(['x', 'y']))"), - "x,y" + eval( + "const s = new SeqAcceptor(); + [r(() => s.optionalSeq(['x', 'y'])), r(() => s.optionalSeq(undefined)), + r(() => s.optionalSeq(null))].join('|')" + ), + "x,y|none|throws:TypeError" ); } - - #[test] - fn non_iterable_throws() { - assert!(throws("new SeqAcceptor().joinStrings(42)")); - } } // ============================================================================ @@ -144,7 +110,6 @@ mod sequence_tests { mod record_tests { use core_runtime::jsclass; use core_runtime::jsmethods; - use core_runtime::test_util::{eval_with_setup, throws_with_setup}; use js::Record; #[jsclass] @@ -186,18 +151,7 @@ mod record_tests { } fn eval(code: &str) -> String { - eval_with_setup( - || { - core_runtime::runtime::register_global_initializer(|scope, global| { - RecordAcceptor::add_to_global(scope, global); - }); - }, - code, - ) - } - - fn throws(code: &str) -> bool { - throws_with_setup( + super::eval_outcomes( || { core_runtime::runtime::register_global_initializer(|scope, global| { RecordAcceptor::add_to_global(scope, global); @@ -207,102 +161,35 @@ mod record_tests { ) } + /// Every enumerable own key is converted to the key type, integer keys first in ascending + /// order, and a symbol key throws. Non-enumerable keys are skipped. `null` is a present value, + /// which isn't an object. #[test] - fn record_keys() { - assert_eq!(eval("new RecordAcceptor().keys({a: '1', b: '2'})"), "a,b"); - } - - #[test] - fn record_values() { - assert_eq!( - eval("new RecordAcceptor().values({x: 'hello', y: 'world'})"), - "hello,world" - ); - } - - #[test] - fn record_preserves_insertion_order() { - assert_eq!( - eval("new RecordAcceptor().keys({z: '1', a: '2', m: '3'})"), - "z,a,m" - ); - } - - #[test] - fn empty_record() { - assert_eq!(eval("new RecordAcceptor().keys({})"), ""); - } - - #[test] - fn optional_record_present() { - assert_eq!(eval("new RecordAcceptor().optionalRecord({k: 'v'})"), "k"); - } - - #[test] - fn optional_record_null_throws() { - // Per WebIDL, only `undefined`/a missing argument makes an optional argument absent; `null` - // is a present value, and converting it to a record throws (record conversion requires an - // object). - assert!(throws("new RecordAcceptor().optionalRecord(null)")); - } - - #[test] - fn record_roundtrip_non_ascii_keys() { - // Keys must survive the JS→Rust→JS round trip; a Latin-1 write of - // the UTF-8 key bytes would mojibake the property name. - assert_eq!( - eval( - "const r = new RecordAcceptor().echo({'é🦊': 'vé'}); \ - Object.keys(r).join(',') + ':' + r['é🦊']" - ), - "é🦊:vé" - ); - } - - #[test] - fn record_roundtrip_interior_nul_key() { - // A NUL is a legal property-name code unit; a C-string write would - // fail and surface as a phantom exception. + fn records_convert_enumerable_own_keys_in_order() { assert_eq!( eval( - "const r = new RecordAcceptor().echo({'a\\u0000b': 'v'}); \ - Object.keys(r).map(k => k.length).join(',') + ':' + r['a\\u0000b']" + "const a = new RecordAcceptor(); + const sym = {a: '1'}; sym[Symbol('x')] = '2'; + const hidden = {a: '1'}; + Object.defineProperty(hidden, 'b', {value: '2', enumerable: false}); + [r(() => a.keys({z: '1', a: '2', m: '3'})), r(() => a.values({x: 'hi', y: 'yo'})), + r(() => a.keys({})), r(() => a.keys({1: 'a', 0: 'b'})), r(() => a.keys(hidden)), + r(() => a.keys(sym)), r(() => a.keys(42)), r(() => a.optionalRecord({k: 'v'})), + r(() => a.optionalRecord(null))].join('|')" ), - "3:v" + "z,a,m|hi,yo||0,1|a|throws:TypeError|throws:TypeError|k|throws:TypeError" ); } + /// Non-ASCII and NUL code units in keys survive the round trip through Rust. #[test] - fn non_object_throws() { - assert!(throws("new RecordAcceptor().keys(42)")); - } - - #[test] - fn symbol_keys_throw() { - // Per WebIDL es-record, every enumerable own key — including symbols — is converted to the - // key type. Converting a Symbol to a string throws a TypeError, so an enumerable - // symbol-keyed property makes the whole conversion throw. - assert!(throws( - "let o = {a: '1'}; o[Symbol('x')] = '2'; new RecordAcceptor().keys(o)" - )); - } - - #[test] - fn integer_keys_kept() { - // Integer-index property keys are own enumerable keys; per WebIDL they are converted to - // their string form and kept (the [[OwnPropertyKeys]] order lists them first, in ascending - // numeric order), not skipped. - assert_eq!(eval("new RecordAcceptor().keys({1: 'a', 0: 'b'})"), "0,1"); - } - - #[test] - fn skips_non_enumerable_keys() { + fn record_keys_round_trip() { assert_eq!( eval( - "let o = {a: '1'}; Object.defineProperty(o, 'b', {value: '2', enumerable: false}); \ - new RecordAcceptor().keys(o)" + "const e = new RecordAcceptor().echo({'é🦊': 'vé', 'a\\u0000b': 'v'}); \ + Object.keys(e).map(k => k.length).join(',') + ':' + e['é🦊'] + e['a\\u0000b']" ), - "a" + "3,3:vév" ); } } @@ -314,7 +201,6 @@ mod record_tests { mod union_tests { use core_runtime::jsclass; use core_runtime::jsmethods; - use core_runtime::test_util::eval_with_setup; use core_runtime::webidl_union; #[webidl_union] @@ -396,7 +282,7 @@ mod union_tests { } fn eval(code: &str) -> String { - eval_with_setup( + super::eval_outcomes( || { core_runtime::runtime::register_global_initializer(|scope, global| { UnionAcceptor::add_to_global(scope, global); @@ -406,86 +292,21 @@ mod union_tests { ) } + /// A number picks the numeric member and a boolean the boolean member over a string member. + /// `null` is a present value, which the string member converts. Without a string member, the + /// numeric member is the fallback, and without either, the boolean member is. #[test] - fn string_or_long_with_number() { - assert_eq!(eval("new UnionAcceptor().stringOrLong(42)"), "long:42"); - } - - #[test] - fn string_or_long_with_string() { - assert_eq!( - eval("new UnionAcceptor().stringOrLong('hello')"), - "string:hello" - ); - } - - #[test] - fn string_or_bool_with_true() { - assert_eq!(eval("new UnionAcceptor().stringOrBool(true)"), "bool:true"); - } - - #[test] - fn string_or_bool_with_string() { - assert_eq!( - eval("new UnionAcceptor().stringOrBool('test')"), - "string:test" - ); - } - - #[test] - fn optional_union_null() { - // `null` is a present value (not absent); a union with a string member converts null via - // that member, yielding "null". - assert_eq!( - eval("new UnionAcceptor().optionalUnion(null)"), - "string:null" - ); - } - - #[test] - fn optional_union_undefined() { - assert_eq!(eval("new UnionAcceptor().optionalUnion(undefined)"), "none"); - } - - #[test] - fn optional_union_with_value() { - assert_eq!(eval("new UnionAcceptor().optionalUnion(99)"), "long:99"); - } - - #[test] - fn number_priority_over_string() { - // When a value is a number and union includes both numeric and string, - // numeric should take priority per spec. - assert_eq!(eval("new UnionAcceptor().stringOrLong(3.14)"), "long:3"); - } - - #[test] - fn boolean_priority_over_string() { - // Boolean should take priority over string per spec. - assert_eq!( - eval("new UnionAcceptor().stringOrBool(false)"), - "bool:false" - ); - } - - #[test] - fn numeric_fallback_without_string_member() { - // BoolOrLong has no string member, so a non-boolean value coerces to the - // numeric member via ToNumber (WebIDL §3.2.25 fallback). - assert_eq!(eval("new UnionAcceptor().boolOrLong('5')"), "long:5"); - assert_eq!(eval("new UnionAcceptor().boolOrLong(7)"), "long:7"); - assert_eq!(eval("new UnionAcceptor().boolOrLong(true)"), "bool:true"); - } - - #[test] - fn boolean_fallback_without_string_or_numeric_member() { - // SeqOrBool has neither a string nor a numeric member, so a non-iterable - // value coerces to the boolean member via ToBoolean. - assert_eq!(eval("new UnionAcceptor().seqOrBool(5)"), "flag:true"); - assert_eq!(eval("new UnionAcceptor().seqOrBool(0)"), "flag:false"); + fn unions_pick_the_member_the_value_converts_to() { assert_eq!( - eval("new UnionAcceptor().seqOrBool(['a', 'b', 'c'])"), - "seq:3" + eval( + "const u = new UnionAcceptor(); + [u.stringOrLong(3.14), u.stringOrLong('hello'), u.stringOrBool(false), + u.stringOrBool('test'), u.optionalUnion(null), u.optionalUnion(undefined), + u.optionalUnion(99), u.boolOrLong('5'), u.boolOrLong(true), u.seqOrBool(5), + u.seqOrBool(0), u.seqOrBool(['a', 'b'])].join('|')" + ), + "long:3|string:hello|bool:false|string:test|string:null|none|long:99|long:5|bool:true|\ + flag:true|flag:false|seq:2" ); } } @@ -493,7 +314,6 @@ mod union_tests { mod union_lifetime_tests { use core_runtime::jsclass; use core_runtime::jsmethods; - use core_runtime::test_util::{eval_with_setup, throws_with_setup}; use core_runtime::webidl_union; use js::ArrayBuffer; use js::Object; @@ -551,18 +371,7 @@ mod union_lifetime_tests { } fn eval(code: &str) -> String { - eval_with_setup( - || { - core_runtime::runtime::register_global_initializer(|scope, global| { - LifetimeUnionAcceptor::add_to_global(scope, global); - }); - }, - code, - ) - } - - fn throws(code: &str) -> bool { - throws_with_setup( + super::eval_outcomes( || { core_runtime::runtime::register_global_initializer(|scope, global| { LifetimeUnionAcceptor::add_to_global(scope, global); @@ -572,89 +381,24 @@ mod union_lifetime_tests { ) } + /// A value that doesn't match the `ArrayBuffer` member falls back to the string member. The + /// members are tried in declaration order. A union without a fallback member throws. #[test] - fn buffer_variant_with_array_buffer() { - assert_eq!( - eval("new LifetimeUnionAcceptor().bufferOrString(new ArrayBuffer(8))"), - "buffer:8" - ); - } - - #[test] - fn string_variant_with_string() { - assert_eq!( - eval("new LifetimeUnionAcceptor().bufferOrString('hello')"), - "string:hello" - ); - } - - #[test] - fn string_variant_with_number() { - // No numeric branch is declared, so the string fallback applies. - assert_eq!( - eval("new LifetimeUnionAcceptor().bufferOrString(42)"), - "string:42" - ); - } - - #[test] - fn string_variant_when_object_does_not_match_buffer() { - // Plain objects don't satisfy the ArrayBuffer branch and fall through - // to the string fallback (after `Object.prototype.toString`). - assert_eq!( - eval("new LifetimeUnionAcceptor().bufferOrString({})"), - "string:[object Object]" - ); - } - - #[test] - fn optional_buffer_or_string_null() { - // `null` is a present value (not absent); a union with a string member converts null via - // that member, yielding "null". - assert_eq!( - eval("new LifetimeUnionAcceptor().optionalBufferOrString(null)"), - "string:null" - ); - } - - #[test] - fn optional_buffer_or_string_undefined() { - assert_eq!( - eval("new LifetimeUnionAcceptor().optionalBufferOrString(undefined)"), - "none" - ); - } - - #[test] - fn optional_buffer_or_string_with_buffer() { - assert_eq!( - eval("new LifetimeUnionAcceptor().optionalBufferOrString(new ArrayBuffer(4))"), - "buffer:4" - ); - } - - #[test] - fn declared_order_picks_buffer_first() { - assert_eq!( - eval("new LifetimeUnionAcceptor().bufferOrObject(new ArrayBuffer(16))"), - "buffer:16" - ); - } - - #[test] - fn declared_order_falls_back_to_object() { + fn unions_with_rooted_members_convert() { assert_eq!( - eval("new LifetimeUnionAcceptor().bufferOrObject({a: 1})"), - "object" + eval( + "const u = new LifetimeUnionAcceptor(); + [r(() => u.bufferOrString(new ArrayBuffer(8))), r(() => u.bufferOrString(42)), + r(() => u.bufferOrString({})), r(() => u.optionalBufferOrString(null)), + r(() => u.optionalBufferOrString(undefined)), + r(() => u.optionalBufferOrString(new ArrayBuffer(4))), + r(() => u.bufferOrObject(new ArrayBuffer(16))), r(() => u.bufferOrObject({a: 1})), + r(() => u.bufferOrObject('not an object'))].join('|')" + ), + "buffer:8|string:42|string:[object Object]|string:null|none|buffer:4|buffer:16|object|\ + throws:TypeError" ); } - - #[test] - fn buffer_or_object_throws_on_non_object() { - assert!(throws( - "new LifetimeUnionAcceptor().bufferOrObject('not an object')" - )); - } } // ============================================================================ @@ -664,7 +408,6 @@ mod union_lifetime_tests { mod union_sequence_record_tests { use core_runtime::jsclass; use core_runtime::jsmethods; - use core_runtime::test_util::{eval_with_setup, throws_with_setup}; use core_runtime::webidl_union; use js::conversion::Record; @@ -712,7 +455,7 @@ mod union_sequence_record_tests { } fn eval(code: &str) -> String { - eval_with_setup( + super::eval_outcomes( || { core_runtime::runtime::register_global_initializer(|scope, global| { SeqRecordAcceptor::add_to_global(scope, global); @@ -722,72 +465,22 @@ mod union_sequence_record_tests { ) } - fn throws(code: &str) -> bool { - throws_with_setup( - || { - core_runtime::runtime::register_global_initializer(|scope, global| { - SeqRecordAcceptor::add_to_global(scope, global); - }); - }, - code, - ) - } - - #[test] - fn array_of_pairs_picks_sequence() { - assert_eq!( - eval("new SeqRecordAcceptor().classify([['a','1'], ['b','2']])"), - "pairs:a=1&b=2" - ); - } - - #[test] - fn plain_object_picks_record() { - assert_eq!( - eval("new SeqRecordAcceptor().classify({a:'1', b:'2'})"), - "record:a=1&b=2" - ); - } - - #[test] - fn string_picks_string() { - assert_eq!( - eval("new SeqRecordAcceptor().classify('hello')"), - "string:hello" - ); - } - + /// An iterable picks the sequence member, and then an element that isn't a sequence, or an + /// iterator that throws, makes the conversion throw rather than fall through to the record. #[test] - fn custom_iterable_picks_sequence() { - // A non-Array iterable with @@iterator must still be routed to the - // sequence branch. + fn sequence_or_record_unions_pick_by_iterability() { assert_eq!( eval( - "let it = { [Symbol.iterator]: function*() { yield ['a','1']; yield ['b','2']; } };\ - new SeqRecordAcceptor().classify(it)" + "const a = new SeqRecordAcceptor(); + const it = { [Symbol.iterator]: function*() { yield ['a','1']; yield ['b','2']; } }; + const bad = { [Symbol.iterator]() { return { next() { throw new Error('boom'); } }; } }; + [r(() => a.classify([['a','1'], ['b','2']])), r(() => a.classify({a:'1', b:'2'})), + r(() => a.classify('hello')), r(() => a.classify(it)), r(() => a.classify([1, 2])), + r(() => a.classify(bad))].join('|')" ), - "pairs:a=1&b=2" + "pairs:a=1&b=2|record:a=1&b=2|string:hello|pairs:a=1&b=2|throws:TypeError|throws:Error" ); } - - #[test] - fn iterable_with_non_iterable_element_throws() { - // WebIDL §3.2.25: once @@iterator is detected the sequence branch is - // selected; if an element can't be converted to the inner sequence - // type, the conversion *throws* — it does not silently fall through - // to the record branch. - assert!(throws("new SeqRecordAcceptor().classify([1, 2])")); - } - - #[test] - fn iterable_throwing_iterator_propagates() { - // If the iterator throws, the failure propagates as an exception - // rather than being silently swallowed. - assert!(throws( - "let it = { [Symbol.iterator]: function() { return { next() { throw new Error('boom'); } }; } };\ - new SeqRecordAcceptor().classify(it)" - )); - } } // ============================================================================ @@ -797,7 +490,6 @@ mod union_sequence_record_tests { mod async_sequence_tests { use core_runtime::jsclass; use core_runtime::jsmethods; - use core_runtime::test_util::{eval_with_setup, throws_with_setup}; use js::AsyncSequence; #[jsclass] @@ -827,7 +519,7 @@ mod async_sequence_tests { } fn eval(code: &str) -> String { - eval_with_setup( + super::eval_outcomes( || { core_runtime::runtime::register_global_initializer(|scope, global| { AsyncAcceptor::add_to_global(scope, global); @@ -837,82 +529,24 @@ mod async_sequence_tests { ) } - fn throws(code: &str) -> bool { - throws_with_setup( - || { - core_runtime::runtime::register_global_initializer(|scope, global| { - AsyncAcceptor::add_to_global(scope, global); - }); - }, - code, - ) - } - - #[test] - fn sync_iterable_detected() { - // Arrays have Symbol.iterator but not Symbol.asyncIterator. - assert_eq!(eval("new AsyncAcceptor().isAsync([1, 2, 3])"), "false"); - } - - #[test] - fn async_iterable_detected() { - assert_eq!( - eval( - "let obj = { [Symbol.asyncIterator]() { return { next() { return { done: true } } } } }; \ - new AsyncAcceptor().isAsync(obj)" - ), - "true" - ); - } - - #[test] - fn optional_null_throws() { - // Per WebIDL, only `undefined`/a missing argument makes an optional argument absent; `null` - // is a present value, and converting it to a sequence/async iterable throws (null is not - // iterable). - assert!(throws("new AsyncAcceptor().optionalAsync(null)")); - } - - #[test] - fn optional_with_iterable() { - assert_eq!(eval("new AsyncAcceptor().optionalAsync([1])"), "sync"); - } - - #[test] - fn non_iterable_throws() { - assert!(throws("new AsyncAcceptor().isAsync(42)")); - } - - #[test] - fn plain_object_throws() { - assert!(throws("new AsyncAcceptor().isAsync({})")); - } - - #[test] - fn throwing_getter_propagates() { - // GetMethod: a Get error propagates; it must not be swallowed and - // fall back to Symbol.iterator. - assert_eq!( - eval( - "let o = { get [Symbol.asyncIterator]() { throw new Error('trap') }, \ - [Symbol.iterator]() { return { next() { return { done: true } } } } }; \ - try { new AsyncAcceptor().isAsync(o); 'no throw' } catch (e) { e.message }" - ), - "trap" - ); - } - + /// A getter of `Symbol.asyncIterator` that throws, and a non-callable `Symbol.asyncIterator`, + /// throw rather than fall back to `Symbol.iterator`. #[test] - fn non_callable_method_throws() { - // GetMethod: a present, non-callable method is a TypeError, not a - // fallback to Symbol.iterator. + fn async_sequences_detect_async_iterables() { assert_eq!( eval( - "let o = { [Symbol.asyncIterator]: 42, \ - [Symbol.iterator]() { return { next() { return { done: true } } } } }; \ - try { new AsyncAcceptor().isAsync(o); 'no throw' } catch (e) { e.constructor.name }" + "const a = new AsyncAcceptor(); + const sync = { [Symbol.iterator]() { return { next() { return { done: true } } } } }; + const asyncIt = { [Symbol.asyncIterator]() { return { next() { return { done: true } } } } }; + const trap = { get [Symbol.asyncIterator]() { throw new RangeError('trap') }, \ + [Symbol.iterator]: sync[Symbol.iterator] }; + const notCallable = { [Symbol.asyncIterator]: 42, [Symbol.iterator]: sync[Symbol.iterator] }; + [r(() => a.isAsync([1, 2])), r(() => a.isAsync(asyncIt)), r(() => a.optionalAsync([1])), + r(() => a.optionalAsync(null)), r(() => a.isAsync(42)), r(() => a.isAsync({})), + r(() => a.isAsync(trap)), r(() => a.isAsync(notCallable))].join('|')" ), - "TypeError" + "false|true|sync|throws:TypeError|throws:TypeError|throws:TypeError|throws:RangeError|\ + throws:TypeError" ); } } diff --git a/crates/core-runtime/tests/private_data_borrow.rs b/crates/core-runtime/tests/private_data_borrow.rs index 1089b73..49db59d 100644 --- a/crates/core-runtime/tests/private_data_borrow.rs +++ b/crates/core-runtime/tests/private_data_borrow.rs @@ -50,17 +50,6 @@ fn setup() { }); } -#[test] -fn distinct_objects_do_not_conflict() { - // Each object has its own borrow flag, so borrowing two different objects at - // once is fine: `a.n` becomes 1, plus `b.n` (0). - let result = eval_with_setup( - setup, - "let a = new Borrowed(); let b = new Borrowed(); a.combine(b)", - ); - assert_eq!(result, "1"); -} - #[test] fn sequential_borrows_are_fine() { // Non-overlapping borrows (separate calls) never conflict; the second call diff --git a/crates/core-runtime/tests/promise_args.rs b/crates/core-runtime/tests/promise_args.rs index 01b59f3..781d914 100644 --- a/crates/core-runtime/tests/promise_args.rs +++ b/crates/core-runtime/tests/promise_args.rs @@ -102,11 +102,11 @@ impl PromiseArgs { ) -> Result, ExnThrown> { match members.unchecked { Some(p) => Ok(p), - None => Promise::new_resolved_with_value(scope, js::value::undefined()), + None => Promise::new_resolved_with_value(scope, js::prelude::HandleValue::undefined()), } } - /// The checked member, likewise — its `Payload` check runs the same way an argument's does. + /// The checked member, whose `Payload` check runs the same way as for an argument. #[method] fn checked_member<'r>( &self, @@ -115,7 +115,7 @@ impl PromiseArgs { ) -> Result, ExnThrown> { match members.checked { Some(p) => Ok(p.promise()), - None => Promise::new_resolved_with_value(scope, js::value::undefined()), + None => Promise::new_resolved_with_value(scope, js::prelude::HandleValue::undefined()), } } @@ -129,7 +129,7 @@ impl PromiseArgs { ) -> Result, ExnThrown> { match p { Some(p) => Ok(p.promise()), - None => Promise::new_resolved_with_value(scope, js::value::undefined()), + None => Promise::new_resolved_with_value(scope, js::prelude::HandleValue::undefined()), } } } @@ -156,8 +156,6 @@ fn eval_then_drain(code: &str, probe: &str) -> String { String::from_jsval(&scope, value, ()).expect("probe value is not a string") } -/// The whole surface in one process: `JSEngine` initializes once per test -/// binary, so the cases share a run and report through a single log. #[test] fn promise_arguments_wrap_any_value_and_check_the_type_later() { let log = eval_then_drain( @@ -236,7 +234,7 @@ fn promise_arguments_wrap_any_value_and_check_the_type_later() { ); } -/// A missing required argument is an error at the call, not a value to wrap: +/// A missing required argument fails at the call rather than being wrapped: /// WebIDL counts arguments before it converts any of them. Which *kind* of /// error is the ordinary rule for any operation — a promise-returning one /// rejects, everything else throws. diff --git a/crates/core-runtime/tests/raw_ident_name.rs b/crates/core-runtime/tests/raw_ident_name.rs index 9201d3e..9270bab 100644 --- a/crates/core-runtime/tests/raw_ident_name.rs +++ b/crates/core-runtime/tests/raw_ident_name.rs @@ -37,14 +37,9 @@ fn setup() { } #[test] -fn raw_identifier_getter_uses_keyword_name() { - assert_eq!(eval_with_setup(setup, "new Node().type"), "element"); -} - -#[test] -fn raw_identifier_method_uses_keyword_name() { +fn raw_identifiers_use_the_keyword_name() { assert_eq!( - eval_with_setup(setup, "String(new Node().match('element'))"), - "true" + eval_with_setup(setup, "`${new Node().type} ${new Node().match('element')}`"), + "element true" ); } diff --git a/crates/core-runtime/tests/rest_args.rs b/crates/core-runtime/tests/rest_args.rs index e8cb86d..f2e170f 100644 --- a/crates/core-runtime/tests/rest_args.rs +++ b/crates/core-runtime/tests/rest_args.rs @@ -77,8 +77,7 @@ impl Variadic { rest.iter().sum() } - /// A wide integer element type, confirming the fix spans `i8`..`u64` rather - /// than `i32` alone. Returned as `f64` so JS sees the exact small sums. + /// Typed `i64` variadic. The sum is returned as `f64` so JS sees the exact small sums. #[static_method] fn sum_longs(rest: RestArgs) -> f64 { rest.iter().sum::() as f64 @@ -99,179 +98,96 @@ fn throws(code: &str) -> bool { throws_with_setup(setup, code) } -// ============================================================================ -// Typed numeric collection -// ============================================================================ - -#[test] -fn sum_collects_numbers() { - assert_eq!(eval("Variadic.sum(1, 2, 3)"), "6"); -} - -#[test] -fn sum_handles_floats() { - assert_eq!(eval("Variadic.sum(1.5, 2.5)"), "4"); -} - -#[test] -fn sum_with_no_args_is_empty() { - assert_eq!(eval("Variadic.sum()"), "0"); -} - -#[test] -fn sum_with_single_arg() { - assert_eq!(eval("Variadic.sum(42)"), "42"); -} - -/// Each element is converted with the lenient `ToNumber`, so a numeric string -/// and a boolean coerce rather than throwing. -#[test] -fn sum_coerces_each_element() { - assert_eq!(eval("Variadic.sum(1, '2', true)"), "4"); -} - -// ============================================================================ -// Fixed parameter + variadic strings -// ============================================================================ - -#[test] -fn join_collects_after_fixed_param() { - assert_eq!(eval("new Variadic().join('-', 'a', 'b', 'c')"), "a-b-c"); -} - -#[test] -fn join_with_empty_rest_is_empty_string() { - assert_eq!(eval("new Variadic().join('-')"), ""); -} - -#[test] -fn join_with_single_rest_element() { - assert_eq!(eval("new Variadic().join('-', 'solo')"), "solo"); -} - -/// The fixed `sep` argument is not collected into the rest; only arguments at -/// or beyond the rest position are. -#[test] -fn join_does_not_swallow_the_separator() { - assert_eq!(eval("new Variadic().join(',', '1', '2')"), "1,2"); -} - -// ============================================================================ -// len() / is_empty() -// ============================================================================ - -#[test] -fn count_reports_rest_length() { - assert_eq!(eval("new Variadic().count('a', 'b', 'c', 'd')"), "4"); - assert_eq!(eval("new Variadic().count()"), "0"); -} - +/// Each element is converted with the lenient `ToNumber`, so a numeric string and a boolean +/// coerce rather than throwing. #[test] -fn is_empty_rest_reflects_argument_count() { - assert_eq!(eval("new Variadic().isEmptyRest()"), "true"); - assert_eq!(eval("new Variadic().isEmptyRest('x')"), "false"); +fn numeric_rest_args_collect_and_coerce() { + assert_eq!( + eval("[Variadic.sum(1.5, '2', true), Variadic.sum()].join()"), + "4.5,0" + ); } -// ============================================================================ -// Boolean variadic (IntoIterator by value) -// ============================================================================ - +/// The fixed `sep` argument is not collected into the rest. #[test] -fn any_true_finds_a_truthy_argument() { - assert_eq!(eval("Variadic.anyTrue(false, false, true)"), "true"); +fn rest_args_collect_after_fixed_params() { + assert_eq!( + eval("const v = new Variadic(); [v.join('-', 'a', 'b'), v.join('-')].join('|')"), + "a-b|" + ); } #[test] -fn any_true_is_false_when_all_falsy() { - assert_eq!(eval("Variadic.anyTrue(false, false)"), "false"); - assert_eq!(eval("Variadic.anyTrue()"), "false"); +fn rest_args_report_len_and_is_empty() { + assert_eq!( + eval( + "const v = new Variadic(); \ + [v.count('a', 'b'), v.count(), v.isEmptyRest(), v.isEmptyRest('x')].join()" + ), + "2,0,true,false" + ); } -/// `ToBoolean` coerces each argument; truthy values count. +/// `ToBoolean` coerces each argument. #[test] -fn any_true_coerces_arguments() { - assert_eq!(eval("Variadic.anyTrue(0, '', 'non-empty')"), "true"); +fn boolean_rest_args_coerce_each_element() { + assert_eq!( + eval( + "[Variadic.anyTrue(0, '', 'x'), Variadic.anyTrue(false, 0), Variadic.anyTrue()].join()" + ), + "true,false,false" + ); } -// ============================================================================ -// Variadic of class newtypes -// ============================================================================ - #[test] fn count_selves_collects_instances() { assert_eq!( - eval("Variadic.countSelves(new Variadic(), new Variadic())"), - "2" + eval( + "[Variadic.countSelves(new Variadic(), new Variadic()), Variadic.countSelves()].join()" + ), + "2,0" ); - assert_eq!(eval("Variadic.countSelves()"), "0"); -} - -// ============================================================================ -// Integer variadics (ConversionBehavior config) -// ============================================================================ - -#[test] -fn sum_ints_collects_integers() { - assert_eq!(eval("Variadic.sumInts(1, 2, 3)"), "6"); - assert_eq!(eval("Variadic.sumInts()"), "0"); -} - -/// Each element runs through `ToInt32`: a non-integer number truncates toward -/// zero and a numeric string coerces. -#[test] -fn sum_ints_applies_to_int32() { - assert_eq!(eval("Variadic.sumInts(1, 2.7, '3')"), "6"); - assert_eq!(eval("Variadic.sumInts(-1.9)"), "-1"); } -/// `ToInt32` wraps modulo 2^32, so 2^32 maps to 0. +/// Each integer element runs through `ToInt32`: a non-integer number truncates toward zero, a +/// numeric string coerces, and 2^32 + 1 wraps to 1. #[test] -fn sum_ints_wraps_modulo_2_32() { - assert_eq!(eval("Variadic.sumInts(4294967296)"), "0"); - assert_eq!(eval("Variadic.sumInts(4294967297)"), "1"); +fn integer_rest_args_convert_with_to_int32() { + assert_eq!( + eval( + "[Variadic.sumInts(1, 2.7, '3'), Variadic.sumInts(-1.9), Variadic.sumInts(4294967297), \ + Variadic.sumInts(), Variadic.sumLongs(1000, 2000, 3000)].join()" + ), + "6,-1,1,0,6000" + ); } -#[test] -fn sum_longs_handles_wide_integers() { - assert_eq!(eval("Variadic.sumLongs(1000, 2000, 3000)"), "6000"); - assert_eq!(eval("Variadic.sumLongs()"), "0"); -} - -// ============================================================================ -// .length excludes the rest parameter -// ============================================================================ - -/// A function's `.length` counts required (non-`Option`, non-`RestArgs`) -/// parameters, so the variadic tail never contributes. +/// A function's `.length` counts required (non-`Option`, non-`RestArgs`) parameters, so the +/// variadic tail never contributes. #[test] fn length_ignores_rest_parameter() { - // Only the rest param: length 0. - assert_eq!(eval("Variadic.sum.length"), "0"); - assert_eq!(eval("Variadic.anyTrue.length"), "0"); - assert_eq!(eval("Variadic.prototype.count.length"), "0"); - // One fixed param ahead of the rest: length 1. - assert_eq!(eval("Variadic.prototype.join.length"), "1"); + assert_eq!( + eval( + "[Variadic.sum.length, Variadic.anyTrue.length, Variadic.prototype.count.length, \ + Variadic.prototype.join.length].join()" + ), + "0,0,0,1" + ); } -// ============================================================================ -// Conversion-error propagation -// ============================================================================ - -/// `String::from_jsval` runs `ToString`, which throws on a `Symbol`. The -/// pending exception propagates out of the collection loop (the `ExnPending` -/// arm), so the call throws. +/// `String::from_jsval` runs `ToString`, which throws on a `Symbol`. The pending exception +/// propagates out of the collection loop (the `ExnPending` arm), so the call throws. #[test] fn string_rest_throws_on_symbol() { assert!(throws("new Variadic().count(Symbol('x'))")); } -/// A wrong-typed argument to a class-newtype rest fails the `cast`, producing -/// a `ConversionError::Failure` that is thrown as a `TypeError` (the -/// `throw_type_error` arm). +/// A wrong-typed argument to a class-newtype rest fails the `cast`, producing a +/// `ConversionError::Failure` that is thrown as a `TypeError`. #[test] fn class_rest_throws_on_wrong_type() { - assert!(throws("Variadic.countSelves({})")); assert!(throws("Variadic.countSelves(new Variadic(), 42)")); + assert!(throws("Variadic.countSelves({})")); } // ============================================================================ @@ -342,22 +258,18 @@ mod handle_value_tests { eval_with_setup(setup, code) } + /// `sumXAfterGc` runs a compacting GC while the handles are live, and reads the freshly + /// allocated objects back afterwards. #[test] - fn collects_raw_values() { - assert_eq!(eval("ValueVariadic.countValues(1, 'two', {}, null)"), "4"); - assert_eq!(eval("ValueVariadic.countValues()"), "0"); - } - - /// Calling `sumXAfterGc` runs a real compacting GC mid-method while the - /// handles are live; the objects are freshly allocated (movable) and read - /// back afterwards. - #[test] - fn handles_survive_compacting_gc() { + fn raw_values_are_collected_and_survive_compacting_gc() { assert_eq!( - eval("ValueVariadic.sumXAfterGc({x: 1}, {x: 2}, {x: 3})"), - "6" + eval( + "[ValueVariadic.countValues(1, 'two', {}, null), ValueVariadic.countValues(), \ + ValueVariadic.sumXAfterGc({x: 1}, {x: 2}, {x: 3}), ValueVariadic.sumXAfterGc()]\ + .join()" + ), + "4,0,6,0" ); - assert_eq!(eval("ValueVariadic.sumXAfterGc()"), "0"); } /// The same path under GC zeal mode 14 (compact-on-every-GC), which forces @@ -390,11 +302,6 @@ mod handle_value_tests { // ============================================================================ // `RestArgs` on constructors -// -// `parse_method_info` has always collected the rest parameter for every method -// kind, but `gen_constructor_body` dropped it on the floor, so a constructor -// taking `RestArgs` failed to compile. Constructors now go through the same -// collection as methods. // ============================================================================ mod constructor_tests { @@ -409,7 +316,6 @@ mod constructor_tests { #[jsclass] struct Tally { total: f64, - label: String, } #[jsmethods] @@ -419,7 +325,6 @@ mod constructor_tests { fn construct(rest: RestArgs) -> Self { Self { total: rest.iter().sum(), - label: String::new(), } } @@ -427,11 +332,6 @@ mod constructor_tests { fn total(&self) -> f64 { self.data().total } - - #[getter] - fn label(&self) -> String { - self.data().label.clone() - } } #[jsclass] @@ -475,24 +375,14 @@ mod constructor_tests { } #[test] - fn constructor_collects_variadic_args() { - assert_eq!(eval("new Tally(1, 2, 3).total"), "6"); - } - - #[test] - fn constructor_with_no_variadic_args() { - assert_eq!(eval("new Tally().total"), "0"); - } - - #[test] - fn constructor_collects_after_fixed_params() { - assert_eq!(eval("new Labelled('a', 'x', 'y', 'z').count"), "3"); - assert_eq!(eval("new Labelled('a', 'x', 'y', 'z').label"), "a"); - } - - #[test] - fn constructor_with_fixed_param_and_empty_tail() { - assert_eq!(eval("new Labelled('a').count"), "0"); + fn constructors_collect_variadic_args() { + assert_eq!( + eval( + "[new Tally(1, 2, 3).total, new Tally().total, new Labelled('a', 'x', 'y').count, \ + new Labelled('a', 'x').label, new Labelled('a').count].join()" + ), + "6,0,2,a,0" + ); } } @@ -500,19 +390,13 @@ mod constructor_tests { // `RestArgs` on free functions // // `#[jsmodule]`, `#[jsglobals]`, `#[jsnamespace]`, and `#[webidl_namespace]` -// share `parse_free_fn_export`/`gen_free_fn_native`, which previously ignored -// `RestArgs` entirely — the parameter was treated as an ordinary one and -// failed `FromJSVal`. All four now use the same collection as methods. +// share `parse_free_fn_export`/`gen_free_fn_native`. // ============================================================================ mod free_fn_tests { - use core_runtime::config::RuntimeConfig; - use core_runtime::module::evaluate_module; - use core_runtime::runtime::Runtime; - use core_runtime::test_util::eval_with_setup; + use core_runtime::test_util::{eval_module_out_with_setup, eval_with_setup}; use core_runtime::{jsglobals, jsmodule, jsnamespace, webidl_namespace}; use js::class::RestArgs; - use js::conversion::FromJSVal; #[jsmodule] mod rest_module { @@ -564,10 +448,7 @@ mod free_fn_tests { rest_globals::add_to_global(scope, global); rest_ns::add_to_global(scope, global); rest_webidl_ns::add_to_global(scope, global); - // SAFETY: called during global initialization, before any JS runs. - unsafe { - rest_module::register(scope); - } + rest_module::register(scope); }); } @@ -575,79 +456,44 @@ mod free_fn_tests { eval_with_setup(setup, code) } - fn eval_module(body: &str) -> String { - setup(); - let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); - let scope = rt.default_global(); - let source = format!("import * as m from \"restModule\";\nglobalThis._result = {body};"); - // SAFETY: `scope` outlives the evaluation, and the module registry was - // populated by `setup` before any JS ran. - unsafe { evaluate_module(&scope, &source, "rest_test.mjs") }.expect("module eval failed"); - let val = js::compile::evaluate_with_filename(&scope, "globalThis._result", "read.js", 1) - .expect("readback failed"); - String::from_jsval(&scope, val, ()).expect("null string") - } - - // --- #[jsmodule] --- - #[test] - fn module_fn_collects_variadic_args() { - assert_eq!(eval_module("m.sumAll(1, 2, 3)"), "6"); - assert_eq!(eval_module("m.sumAll()"), "0"); - } - - #[test] - fn module_fn_collects_after_fixed_param() { - assert_eq!(eval_module("m.joinAll('-', 'a', 'b', 'c')"), "a-b-c"); - } - - // --- #[jsglobals] --- - - #[test] - fn global_fn_collects_variadic_args() { - assert_eq!(eval("globalSum(1, 2, 3, 4)"), "10"); - assert_eq!(eval("globalSum()"), "0"); - } - - #[test] - fn global_fn_collects_after_fixed_param() { - assert_eq!(eval("globalJoin('+', 'x', 'y')"), "x+y"); - } - - // --- #[jsnamespace] / #[webidl_namespace] --- - - #[test] - fn namespace_fn_collects_variadic_args() { - assert_eq!(eval("restNs.nsSum(2, 4)"), "6"); - assert_eq!(eval("restNs.nsSum()"), "0"); - } - - #[test] - fn namespace_fn_with_scope_and_variadic() { - assert_eq!(eval("restNs.nsCount('a', 'b', 'c')"), "3"); + fn module_fns_collect_variadic_args() { + assert_eq!( + eval_module_out_with_setup( + setup, + "import * as m from 'restModule'; + globalThis.__out = [m.sumAll(1, 2, 3), m.sumAll(), m.joinAll('-', 'a', 'b')].join('|');" + ), + "6|0|a-b" + ); } #[test] - fn webidl_namespace_fn_collects_variadic_args() { - assert_eq!(eval("RestWebIDL.widlSum(1, 2, 3, 4, 5)"), "15"); + fn global_and_namespace_fns_collect_variadic_args() { + assert_eq!( + eval( + "[globalSum(1, 2), globalSum(), globalJoin('+', 'x', 'y'), restNs.nsSum(2, 4), \ + restNs.nsSum(), restNs.nsCount('a', 'b'), RestWebIDL.widlSum(1, 2, 3)].join('|')" + ), + "3|0|x+y|6|0|2|6" + ); } - /// A variadic tail is not a declared parameter, so it must not inflate - /// `fn.length` — the same rule `length_ignores_rest_parameter` pins for - /// methods, checked here because free functions compute `nargs` on a - /// different path. + /// Free functions compute `nargs` on a different path than methods. #[test] fn length_ignores_rest_parameter_on_free_fns() { - assert_eq!(eval("globalSum.length"), "0"); - assert_eq!(eval("globalJoin.length"), "1"); - assert_eq!(eval("restNs.nsSum.length"), "0"); - assert_eq!(eval("restNs.nsCount.length"), "0"); - assert_eq!(eval("RestWebIDL.widlSum.length"), "0"); + assert_eq!( + eval( + "[globalSum.length, globalJoin.length, restNs.nsSum.length, \ + restNs.nsCount.length, RestWebIDL.widlSum.length].join()" + ), + "0,1,0,0,0" + ); } - /// The unqualified `RestArgs` in each mod block above is rewritten to the - /// fully-qualified path by the macro, exactly as it is for methods — this - /// compiles only if that rewrite reaches the free-function paths. + /// The macro rewrites the unqualified `RestArgs` in each mod block above to the + /// fully-qualified path, so this compiles only if that rewrite reaches the free-function + /// paths. #[test] fn rest_args_type_is_rewritten_in_signatures() { assert_eq!(rest_module::sum_all(RestArgs::new(vec![1.0, 2.0])), 3.0); @@ -720,70 +566,23 @@ mod promise_return_tests { } #[test] - fn variadic_promise_op_resolves() { + fn bad_arguments_reject_rather_than_throw() { assert_eq!( run(r#" - globalThis.__out = "pending"; - new Waiter().joinAsync("a", "b").then(v => { globalThis.__out = v; }); - "#), - "a,b" - ); - } - - /// A `Symbol` can't convert to `String`, so collecting the tail fails. That - /// must surface as a rejection, not a synchronous throw — matching how a bad - /// *fixed* argument to the same operation behaves. - #[test] - fn bad_variadic_element_rejects_rather_than_throwing() { - assert_eq!( - run(r#" - globalThis.__out = "pending"; - let threw = false; - let p; - try { p = new Waiter().joinAsync(Symbol()); } catch (e) { threw = true; } - Promise.resolve(p).then( - () => { globalThis.__out = `threw=${threw},resolved`; }, - e => { globalThis.__out = `threw=${threw},rejected:${e.constructor.name}`; }, - ); - "#), - "threw=false,rejected:TypeError" - ); - } - - /// The same, with the tail behind a fixed parameter. - #[test] - fn bad_variadic_element_after_fixed_param_rejects() { - assert_eq!( - run(r#" - globalThis.__out = "pending"; - let threw = false; - let p; - try { p = new Waiter().joinAsyncAfter("-", Symbol()); } catch (e) { threw = true; } - Promise.resolve(p).then( - () => { globalThis.__out = `threw=${threw},resolved`; }, - e => { globalThis.__out = `threw=${threw},rejected:${e.constructor.name}`; }, - ); - "#), - "threw=false,rejected:TypeError" - ); - } - - /// The pre-existing behaviour for a bad *fixed* argument, as the baseline - /// the variadic tail has to match. - #[test] - fn bad_fixed_arg_rejects_rather_than_throwing() { - assert_eq!( - run(r#" - globalThis.__out = "pending"; - let threw = false; - let p; - try { p = new Waiter().joinAsyncAfter(Symbol()); } catch (e) { threw = true; } - Promise.resolve(p).then( - () => { globalThis.__out = `threw=${threw},resolved`; }, - e => { globalThis.__out = `threw=${threw},rejected:${e.constructor.name}`; }, - ); + const w = new Waiter(); + const outcome = (call) => { + let p; + try { p = call(); } catch (e) { return `threw:${e.constructor.name}`; } + return p.then(v => `resolved:${v}`, e => `rejected:${e.constructor.name}`); + }; + Promise.all([ + outcome(() => w.joinAsync("a", "b")), + outcome(() => w.joinAsync(Symbol())), + outcome(() => w.joinAsyncAfter("-", Symbol())), + outcome(() => w.joinAsyncAfter(Symbol())), + ]).then(v => { globalThis.__out = v.join("|"); }); "#), - "threw=false,rejected:TypeError" + "resolved:a,b|rejected:TypeError|rejected:TypeError|rejected:TypeError" ); } } diff --git a/crates/core-runtime/tests/runtime/event_loop.rs b/crates/core-runtime/tests/runtime/event_loop.rs new file mode 100644 index 0000000..4a8f2c8 --- /dev/null +++ b/crates/core-runtime/tests/runtime/event_loop.rs @@ -0,0 +1,557 @@ +// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception + +//! Integration tests for the event loop. + +#[cfg(not(target_arch = "wasm32"))] +use core_runtime::event_loop::run_to_completion; +use core_runtime::event_loop::timer::install_timer_globals; +use core_runtime::event_loop::{with_event_loop, EventLoop, StepOutcome, Task}; +use core_runtime::runtime::Runtime; +use js::gc::scope::Scope; + +use js::error::ExnThrown; +use platform::clock::Instant; +use std::cell::Cell; +use std::rc::Rc; +use std::time::Duration; + +/// Evaluate `source`, panicking if it throws. +fn eval<'s>(scope: &'s Scope<'_>, source: &str) -> js::prelude::HandleValue<'s> { + js::compile::evaluate(scope, source).expect("evaluation threw") +} + +/// [`eval`] `source`, and convert the result to a string. +fn eval_string(scope: &Scope<'_>, source: &str) -> String { + let value = eval(scope, source); + js::conversion::FromJSVal::from_jsval(scope, value, ()).expect("not a string") +} + +/// Helper: create a tokio current-thread runtime with timer support and +/// block on `run_to_completion` using `tokio::time::sleep`. +#[cfg(not(target_arch = "wasm32"))] +fn block_on_event_loop(scope: &Scope<'_>, el: &mut EventLoop) { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .unwrap(); + rt.block_on(async { + // SAFETY: the scope (and its Runtime) must outlive this future. + // Since the caller owns both and `block_on` runs synchronously on + // the same thread, this is guaranteed. + let raw_cx = unsafe { scope.raw_cx_no_gc() }; + unsafe { run_to_completion(raw_cx, el, tokio::time::sleep).await } + }); +} + +// --------------------------------------------------------------------------- +// Custom task for testing +// --------------------------------------------------------------------------- + +/// A simple task that increments a shared counter when run. +struct CounterTask { + counter: Rc>, +} + +impl Task for CounterTask { + fn kind(&self) -> &'static str { + "counter" + } + + fn run( + self: Box, + _scope: &Scope<'_>, + _id: core_runtime::event_loop::TaskId, + ) -> Result<(), ExnThrown> { + self.counter.set(self.counter.get() + 1); + Ok(()) + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[test] +fn test_event_loop() { + let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + + // Install timer globals for timer tests. + install_timer_globals(&scope, scope.global()); + + // ---- Basic EventLoop operations ---- + { + let el = EventLoop::new(); + + // Empty loop. + assert!(el.is_empty()); + assert!(!el.has_pending()); + assert!(!el.has_ready()); + assert_eq!(el.len(), 0); + + // Queue a task. + let counter = Rc::new(Cell::new(0u32)); + let id = el.queue(Box::new(CounterTask { + counter: counter.clone(), + })); + + assert!(el.has_pending()); + assert!(!el.has_ready()); + assert_eq!(el.len(), 1); + + // The task isn't ready yet, so `pop_ready` returns `None`. + assert!(el.pop_ready().is_none()); + + // Signal it ready. + el.signal_ready(id); + assert!(el.has_ready()); + + // Pop and run it. + let (popped_id, task) = el.pop_ready().unwrap(); + assert_eq!(popped_id, id); + assert_eq!(counter.get(), 0); + task.run(&scope, popped_id).unwrap(); + assert_eq!(counter.get(), 1); + + // Now the loop is empty. + assert!(el.is_empty()); + } + + // ---- Cancel ---- + { + let el = EventLoop::new(); + let counter = Rc::new(Cell::new(0u32)); + + let id = el.queue_ready(Box::new(CounterTask { + counter: counter.clone(), + })); + + assert!(el.cancel_if_queued(id)); + assert!(el.is_empty()); + assert_eq!(counter.get(), 0); + + // Cancel of non-existent ID returns false. + assert!(!el.cancel_if_queued(id)); + } + + // ---- Timer tasks ---- + { + let el = EventLoop::new(); + let counter = Rc::new(Cell::new(0u32)); + + // Queue a timer 5ms in the future. + let _id = el.queue_timer( + Box::new(CounterTask { + counter: counter.clone(), + }), + Instant::now() + Duration::from_millis(5), + ); + + assert!(el.has_pending()); + assert!(!el.has_ready()); + + // advance_timers should NOT mark it ready yet. + assert_eq!(el.advance_timers(), 0); + assert!(!el.has_ready()); + + // time_to_next_timer should be Some and > 0. + let wait = el.time_to_next_timer(); + assert!(wait.is_some()); + + // Sleep past the deadline. + std::thread::sleep(Duration::from_millis(10)); + + // Now advance_timers should mark it ready. + assert_eq!(el.advance_timers(), 1); + assert!(el.has_ready()); + + let (id, task) = el.pop_ready().unwrap(); + task.run(&scope, id).unwrap(); + assert_eq!(counter.get(), 1); + } + + // ---- timer ids are unique per global across concurrent loops ---- + // + // Two event loops driving the same global (the WASIp3 concurrent-invocation + // shape) share the global's id namespace, so their `setTimeout` ids must not + // collide. + { + let el_a = EventLoop::new(); + let el_b = EventLoop::new(); + + let start = || eval(&scope, "setTimeout(function() {}, 100000)").to_number(); + let id_a = with_event_loop(&el_a, |_| start()); + let id_b = with_event_loop(&el_b, |_| start()); + assert_ne!( + id_a, id_b, + "timer ids from concurrent event loops on the same global must be unique" + ); + } + + // ---- another loop clears a loop's JS timers ---- + #[cfg(not(target_arch = "wasm32"))] + { + let owner = EventLoop::new(); + let other = EventLoop::new(); + let eval = |source: &str| eval(&scope, source); + let start = |source: &str| with_event_loop(&owner, |_| eval(source)).to_number() as u64; + + let interval = start("setInterval(function() {}, 100000)"); + with_event_loop(&other, |_| eval(&format!("clearInterval({interval})"))); + assert!( + owner.is_empty(), + "clearInterval from another loop left the interval queued" + ); + + let timeout = start("setTimeout(function() {}, 100000)"); + with_event_loop(&other, |_| eval(&format!("clearTimeout({timeout})"))); + assert!( + owner.is_empty(), + "clearTimeout from another loop left the timeout queued" + ); + + // The loop that holds a timer is woken when another loop clears it: `run_to_completion` + // returns rather than waiting for the cleared interval. + let interval = start("setInterval(function() {}, 100000)"); + tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .unwrap() + .block_on(async { + // SAFETY: as in `block_on_event_loop`. + let raw_cx = unsafe { scope.raw_cx_no_gc() }; + let drive = async { + unsafe { run_to_completion(raw_cx, &owner, tokio::time::sleep).await } + }; + let clear = async { + tokio::time::sleep(Duration::from_millis(10)).await; + with_event_loop(&other, |_| eval(&format!("clearInterval({interval})"))); + }; + tokio::time::timeout( + Duration::from_secs(10), + futures_lite::future::zip(drive, clear), + ) + .await + .expect("the loop that held a cleared interval kept waiting for it"); + }); + + // A timer whose loop ended is ignored. + let orphan = start("setTimeout(function() {}, 100000)"); + drop(owner); + with_event_loop(&other, |el| el.clear_js_timer(&scope, orphan)); + assert!(other.is_empty()); + } + + // ---- another global's clearTimeout leaves a global's JS timers alone ---- + { + let el = EventLoop::new(); + let timeout = with_event_loop(&el, |_| { + eval(&scope, "setTimeout(function() {}, 100000)").to_number() + }); + let interval = with_event_loop(&el, |_| { + eval(&scope, "setInterval(function() {}, 100000)").to_number() + }); + { + let other_global = rt.new_global(); + with_event_loop(&el, |_| { + eval( + &other_global, + &format!("clearTimeout({timeout}); clearInterval({interval})"), + ) + }); + } + assert_eq!(el.len(), 2, "another global cleared this global's timers"); + with_event_loop(&el, |_| { + eval( + &scope, + &format!("clearTimeout({timeout}); clearInterval({interval})"), + ) + }); + assert!(el.is_empty(), "the creating global's clears were ignored"); + } + + // ---- setTimeout forwards extra arguments to the callback ---- + #[cfg(not(target_arch = "wasm32"))] + { + let mut el = EventLoop::new(); + + with_event_loop(&el, |_| { + eval( + &scope, + "globalThis._timerArgs = null; \ + setTimeout(function(a, b, c) { globalThis._timerArgs = [a, b, c].join(','); }, 1, 'x', 42, true);", + ); + }); + + block_on_event_loop(&scope, &mut el); + + let s = eval_string(&scope, "String(globalThis._timerArgs)"); + assert_eq!( + s, "x,42,true", + "extra setTimeout arguments must be forwarded" + ); + } + + // ---- handler is converted before the timeout (WebIDL order) ---- + { + let el = EventLoop::new(); + + with_event_loop(&el, |_| { + eval( + &scope, + "globalThis._convOrder = []; \ + globalThis._convTid = setTimeout(\ + { toString() { globalThis._convOrder.push('handler'); return ''; } }, \ + { valueOf() { globalThis._convOrder.push('timeout'); return 1e9; } });", + ); + }); + + let s = eval_string(&scope, "globalThis._convOrder.join(',')"); + assert_eq!( + s, "handler,timeout", + "WebIDL requires left-to-right argument conversion" + ); + + // Drain the queued (far-future) timer so it can't leak into later tests. + with_event_loop(&el, |_| eval(&scope, "clearTimeout(globalThis._convTid)")); + assert!(el.is_empty(), "cleanup clearTimeout should empty the loop"); + } + + // ---- a throwing interval callback keeps repeating ---- + #[cfg(not(target_arch = "wasm32"))] + { + let mut el = EventLoop::new(); + + with_event_loop(&el, |_| { + eval( + &scope, + "globalThis._ivTicks = 0; \ + globalThis._iv = setInterval(function() { \ + globalThis._ivTicks++; \ + if (globalThis._ivTicks === 1) { throw new Error('boom'); } \ + if (globalThis._ivTicks >= 3) { clearInterval(globalThis._iv); } \ + }, 1);", + ); + }); + + block_on_event_loop(&scope, &mut el); + + assert_eq!( + eval_string(&scope, "String(globalThis._ivTicks)"), + "3", + "a throwing callback must not kill the interval (spec: report + repeat)" + ); + } + + // ---- JS timer ids are spec-shaped, and clearing reaches only timers ---- + #[cfg(not(target_arch = "wasm32"))] + { + let mut el = EventLoop::new(); + let counter = Rc::new(Cell::new(0)); + + // An internal (non-JS) timer task on the same loop: clearTimeout must + // not be able to reach it, whatever ids JS throws at it. + el.queue_timer( + Box::new(CounterTask { + counter: Rc::clone(&counter), + }), + Instant::now(), + ); + + with_event_loop(&el, |_| { + eval( + &scope, + "globalThis._t1Fired = false; \ + globalThis._tid1 = setTimeout(function() { globalThis._t1Fired = true; }, 1); \ + if (globalThis._tid1 <= 0) throw new Error('timer id must be > 0, got ' + globalThis._tid1); \ + clearTimeout(globalThis._tid1 + 1); \ + globalThis._ivTicks2 = 0; \ + globalThis._iv2 = setInterval(function() { \ + globalThis._ivTicks2++; \ + if (globalThis._ivTicks2 >= 2) clearInterval(globalThis._iv2); \ + }, 1); \ + clearTimeout(-0.5); clearTimeout(0); clearTimeout(999999); clearTimeout(-7); \ + globalThis._t3Fired = false; \ + globalThis._tid3 = setTimeout(function() { globalThis._t3Fired = true; }, 1); \ + clearTimeout(String(globalThis._tid3));", + ); + }); + + block_on_event_loop(&scope, &mut el); + + assert_eq!( + counter.get(), + 1, + "internal tasks must be unreachable from clearTimeout" + ); + let s = eval_string( + &scope, + "[globalThis._t1Fired, globalThis._ivTicks2, globalThis._t3Fired].join(',')", + ); + assert_eq!( + s, "true,2,false", + "junk clears must be no-ops, a pre-cleared id must not poison a \ + later interval, and a string id must coerce and clear" + ); + } + + // ---- expired timers fire in deadline order ---- + #[cfg(not(target_arch = "wasm32"))] + { + let mut el = EventLoop::new(); + + with_event_loop(&el, |_| { + eval( + &scope, + // `late` is registered last so that its deadline follows the + // others' however long each `setTimeout` call itself takes. + "globalThis._fireOrder = []; \ + setTimeout(function() { globalThis._fireOrder.push('early'); }, 1); \ + globalThis._goner = setTimeout(function() { globalThis._fireOrder.push('goner'); }, 1); \ + setTimeout(function() { globalThis._fireOrder.push('early2'); }, 1); \ + setTimeout(function() { globalThis._fireOrder.push('late'); }, 2); \ + clearTimeout(globalThis._goner);", + ); + }); + + // Let every deadline pass before the loop runs, so one `advance_timers` + // call marks them all ready at once. The order then comes from the + // deadlines, not from queue positions, which the `clearTimeout`'s + // `swap_remove` perturbed. + std::thread::sleep(Duration::from_millis(10)); + block_on_event_loop(&scope, &mut el); + + let s = eval_string(&scope, "globalThis._fireOrder.join(',')"); + assert_eq!( + s, "early,early2,late", + "expired timers must fire in deadline order, ties in scheduling \ + order, regardless of cancellations" + ); + } + + // ---- interval arguments survive moving GC across re-queues ---- + // While a task runs it is out of the loop's task list and untraced, so the + // re-queued interval must rebuild its argument Heaps from scope roots. + // Compacting on every allocation makes a stale Heap fail deterministically. + #[cfg(not(target_arch = "wasm32"))] + { + let mut el = EventLoop::new(); + + with_event_loop(&el, |_| { + eval( + &scope, + "globalThis._gcArgSeen = []; \ + globalThis._gcIv = setInterval(function(tag) { \ + globalThis._gcArgSeen.push(tag.name); \ + if (globalThis._gcArgSeen.length >= 3) clearInterval(globalThis._gcIv); \ + }, 1, { name: 'payload' });", + ); + }); + #[cfg(feature = "debugmozjs")] + unsafe { + js::gc::SetGCZeal(scope.raw_cx_no_gc(), 14, 1) + }; + block_on_event_loop(&scope, &mut el); + #[cfg(feature = "debugmozjs")] + unsafe { + js::gc::SetGCZeal(scope.raw_cx_no_gc(), 0, 0) + }; + + let s = eval_string(&scope, "globalThis._gcArgSeen.join(',')"); + assert_eq!( + s, "payload,payload,payload", + "interval arguments must survive compacting GC between re-queues" + ); + } + + // ---- step() returns Idle when tasks exist but none ready ---- + { + let el = EventLoop::new(); + let counter = Rc::new(Cell::new(0u32)); + assert!(!el.is_alive()); + + // Queue a task but don't signal it ready. The pending task keeps the loop alive. + let _id = el.queue(Box::new(CounterTask { + counter: counter.clone(), + })); + assert!(el.is_alive()); + + let outcome = el.step(&scope); + assert_eq!(outcome, StepOutcome::Idle); + assert_eq!(counter.get(), 0); + } + + // ---- step() returns Idle with interest but no tasks ---- + { + let el = EventLoop::new(); + + let interest = el.acquire_interest_handle(); + assert!(el.has_interest()); + assert!(el.is_alive()); + + let outcome = el.step(&scope); + assert_eq!(outcome, StepOutcome::Idle); + + interest.release(); + assert!(!el.has_interest()); + + let outcome = el.step(&scope); + assert_eq!(outcome, StepOutcome::Done); + } + + // ---- step() runs timer tasks after advance ---- + { + let el = EventLoop::new(); + let counter = Rc::new(Cell::new(0u32)); + + // Queue a timer with deadline in the past. + el.queue_timer( + Box::new(CounterTask { + counter: counter.clone(), + }), + Instant::now(), + ); + + // step() advances timers internally. + let outcome = el.step(&scope); + assert_eq!(outcome, StepOutcome::Progressed); + assert_eq!(counter.get(), 1); + assert!(el.is_empty()); + } + + // ---- run_to_completion respects interest tracking ---- + #[cfg(not(target_arch = "wasm32"))] + { + let mut el = EventLoop::new(); + + // Acquire interest, queue a task that releases it (by dropping the handle). + let interest = el.acquire_interest_handle(); + + struct ReleaseInterestTask { + interest: Option, + } + impl Task for ReleaseInterestTask { + fn kind(&self) -> &'static str { + "release-interest" + } + fn run( + mut self: Box, + _scope: &Scope<'_>, + _id: core_runtime::event_loop::TaskId, + ) -> Result<(), ExnThrown> { + drop(self.interest.take()); + Ok(()) + } + } + + el.queue_ready(Box::new(ReleaseInterestTask { + interest: Some(interest), + })); + + // With interest, the native driver should run the task and then + // exit once interest drops to zero and no tasks remain. + block_on_event_loop(&scope, &mut el); + assert!(!el.has_interest()); + assert!(el.is_empty()); + } +} diff --git a/crates/core-runtime/tests/js_api.rs b/crates/core-runtime/tests/runtime/js_api.rs similarity index 85% rename from crates/core-runtime/tests/js_api.rs rename to crates/core-runtime/tests/runtime/js_api.rs index caf7b86..0e65aff 100644 --- a/crates/core-runtime/tests/js_api.rs +++ b/crates/core-runtime/tests/runtime/js_api.rs @@ -1,9 +1,6 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception //! Integration tests for the `js` API. -//! -//! Tests that need a JS runtime are grouped in a single test because -//! `JSEngine` can only be initialized once per process. use core_runtime::config::RuntimeConfig; use core_runtime::runtime::Runtime; @@ -46,27 +43,6 @@ fn test_value_constructors() { assert!((v.to_double() - 2.5).abs() < f64::EPSILON); } -#[test] -fn test_value_constructors_extra() { - // value:: constructors cover the same ground as the old IntoJSVal impls. - let v = value::from_bool(true); - assert!(v.is_boolean()); - assert!(v.to_boolean()); - - let v = value::from_i32(42); - assert!(v.is_int32()); - assert_eq!(v.to_int32(), 42); - - let v = value::from_u32(99); - assert!(v.is_int32() || v.is_double()); - - let v = value::from_f64(2.71); - assert!(v.is_double()); - - let v = value::undefined(); - assert!(v.is_undefined()); -} - #[test] fn test_jserror_check() { assert!(ExnThrown::check(true).is_ok()); @@ -265,17 +241,10 @@ fn test_js_api_with_runtime() { assert_eq!(set.size(&scope), 0); } - // --- Array builtins --- - { - let arr = Array::new(&scope, 3).unwrap(); - let len = arr.length(&scope).unwrap(); - assert_eq!(len, 3); - } - // --- Date builtins --- { let rval = js::compile::evaluate(&scope, "new Date(2024, 0, 15)").unwrap(); - let date_obj = Object::from_value(&scope, rval.get()).unwrap(); + let date_obj = Object::from_raw_value(&scope, rval.get()).unwrap(); assert!(Date::is_date(&scope, date_obj.handle()).unwrap()); let js_date = date_obj.cast::().unwrap(); assert!(js_date.is_valid(&scope).unwrap()); @@ -285,7 +254,7 @@ fn test_js_api_with_runtime() { { let rval = js::compile::evaluate(&scope, "new Promise(function(resolve) { resolve(42) })") .unwrap(); - let promise_obj = Object::from_value(&scope, rval.get()).unwrap(); + let promise_obj = Object::from_raw_value(&scope, rval.get()).unwrap(); assert!(Promise::is_promise(promise_obj.handle())); let js_promise = promise_obj.cast::().unwrap(); let _id = js_promise.id(); @@ -295,7 +264,7 @@ fn test_js_api_with_runtime() { // A pending promise has no result yet (the engine asserts on the // access in debug builds). let rval = js::compile::evaluate(&scope, "new Promise(function() {})").unwrap(); - let pending = Object::from_value(&scope, rval.get()).unwrap(); + let pending = Object::from_raw_value(&scope, rval.get()).unwrap(); let pending = pending.cast::().unwrap(); assert!(pending.is_pending()); assert!(pending.result(&scope).is_none()); @@ -687,8 +656,7 @@ fn array_buffer_transfer_moves_the_contents() { let moved = source.transfer(&scope).unwrap(); assert!(source.is_detached()); assert_eq!(moved.byte_length(), 4); - // SAFETY: nothing allocates while the slice is read. - assert_eq!(unsafe { moved.bytes() }, &[1, 2, 3, 4]); + assert_eq!(moved.with_bytes(<[u8]>::to_vec), [1, 2, 3, 4]); let empty = js::ArrayBuffer::new(&scope, 0).unwrap(); let moved_empty = empty.transfer(&scope).unwrap(); @@ -696,6 +664,30 @@ fn array_buffer_transfer_moves_the_contents() { assert_eq!(moved_empty.byte_length(), 0); } +/// Byte access to a cross-compartment wrapper of an `ArrayBuffer` or a typed array reads the +/// wrapped object's bytes. +#[test] +fn byte_access_reads_through_cross_compartment_wrappers() { + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + let value = js::compile::evaluate(&scope, "new Uint8Array([1, 2, 3, 4])").unwrap(); + let array = Object::from_value(&scope, value).unwrap(); + let value = js::compile::evaluate(&scope, "new Uint8Array([5, 6]).buffer").unwrap(); + let buffer = Object::from_value(&scope, value).unwrap(); + + let other = rt.new_global(); + let wrapped_array = array.wrap_object(&other).unwrap(); + let wrapped_buffer = buffer.wrap_object(&other).unwrap(); + assert_ne!(wrapped_array.as_raw(), array.as_raw()); + assert_ne!(wrapped_buffer.as_raw(), buffer.as_raw()); + let array = wrapped_array.cast::().unwrap(); + assert_eq!(array.len(), 4); + assert_eq!(array.with_slice(<[u8]>::to_vec), [1, 2, 3, 4]); + let buffer = wrapped_buffer.cast::().unwrap(); + assert_eq!(buffer.with_bytes(<[u8]>::to_vec), [5, 6]); + assert_eq!(buffer.copy_bytes(&other).unwrap(), [5, 6]); +} + /// Strings created from Rust text hold the same UTF-16 code units as the text, whether it is /// ASCII, Latin-1, other BMP characters, or characters outside the BMP, and whether the /// non-ASCII part starts at the beginning or after an ASCII prefix longer than the conversion @@ -761,3 +753,67 @@ fn strings_convert_to_utf8_from_either_representation() { assert_eq!(s.to_utf8(&scope).unwrap(), *expected, "{source}"); } } + +/// Calling a value that isn't callable from native code throws a `TypeError` naming the method or +/// the value's source. +#[test] +fn native_calls_of_values_that_are_not_functions_name_them() { + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + let message = |scope: &js::gc::scope::Scope<'_>| { + let error = js::exception::take_pending(scope).expect("a pending exception"); + let error = Object::from_value(scope, error).expect("an error object"); + let message = error.get_property(scope, c"message").unwrap(); + String::from_jsval(scope, message, ()).unwrap() + }; + + let object = Object::new_plain(&scope).unwrap(); + let result = + js::Function::call_by_name(&scope, object.handle(), c"nope", js::function::EmptyArgs); + assert!(result.is_err()); + assert_eq!(message(&scope), "nope is not a function"); + + let five = scope.root_value(value::from_i32(5)); + let result = js::Function::call(&scope, five, five, js::function::EmptyArgs); + assert!(result.is_err()); + assert_eq!(message(&scope), "5 is not a function"); + + let undefined = scope.root_value(value::undefined()); + let result = js::Function::call(&scope, undefined, undefined, js::function::EmptyArgs); + assert!(result.is_err()); + assert_eq!(message(&scope), "undefined is not a function"); + + let value = js::compile::evaluate(&scope, "({ a: [1, 'b'] })").unwrap(); + let result = js::Function::call_value(&scope, object.handle(), value, js::function::EmptyArgs); + assert!(result.is_err()); + assert_eq!(message(&scope), r#"({a:[1, "b"]}) is not a function"#); +} + +/// Setting and defining a property under a name that isn't interned yet stores the given value +/// when the GC that interning the name runs moves it. +#[cfg(feature = "debugmozjs")] +#[test] +fn named_property_writes_store_a_value_that_interning_the_name_moves() { + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + let target = Object::new_plain(&scope).unwrap(); + let value = Object::new_plain(&scope).unwrap(); + + // Zeal mode 14 runs a compacting GC on every allocation, which moves `value`. + unsafe { js::gc::SetGCZeal(scope.raw_cx_no_gc(), 14, 1) }; + let set = target.set_property(&scope, c"setPropertyFreshName", value); + let defined = target.define_property( + &scope, + c"definePropertyFreshName", + value, + js::class_spec::JSPROP_ENUMERATE as u32, + ); + unsafe { js::gc::SetGCZeal(scope.raw_cx_no_gc(), 0, 0) }; + set.unwrap(); + defined.unwrap(); + + for name in [c"setPropertyFreshName", c"definePropertyFreshName"] { + let stored = target.get_property(&scope, name).unwrap(); + assert_eq!(stored.to_object(), value.as_raw(), "{name:?}"); + } +} diff --git a/crates/core-runtime/tests/runtime/main.rs b/crates/core-runtime/tests/runtime/main.rs new file mode 100644 index 0000000..b9d46b2 --- /dev/null +++ b/crates/core-runtime/tests/runtime/main.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception + +//! The integration tests that also run on wasm targets. They share one test binary, so a wasm test +//! run compiles the engine once for all of them. + +mod event_loop; +mod js_api; +mod snapshot_idle; diff --git a/crates/core-runtime/tests/snapshot_idle.rs b/crates/core-runtime/tests/runtime/snapshot_idle.rs similarity index 96% rename from crates/core-runtime/tests/snapshot_idle.rs rename to crates/core-runtime/tests/runtime/snapshot_idle.rs index dffdd9c..08c5100 100644 --- a/crates/core-runtime/tests/snapshot_idle.rs +++ b/crates/core-runtime/tests/runtime/snapshot_idle.rs @@ -2,9 +2,6 @@ //! `EventLoop::ensure_idle_for_snapshot`, and the origins a runtime configured for a snapshot //! records for the work it reports. -//! -//! The cases are grouped in a single test function because `JSEngine` can only be initialized -//! once per process. use core_runtime::config::RuntimeConfig; use core_runtime::event_loop::timer::install_timer_globals; @@ -31,7 +28,7 @@ fn pending_work_is_reported_with_its_origin() { let scope = rt.default_global(); install_timer_globals(&scope, scope.global()); - // A loop that holds nothing can be wizened. + // A loop without tasks or futures can be snapshotted. assert!(EventLoop::new().ensure_idle_for_snapshot().is_ok()); // A timer is reported with the stack that created it, and clearing it leaves the loop idle. diff --git a/crates/core-runtime/tests/scope.rs b/crates/core-runtime/tests/scope.rs index 1205623..3331c12 100644 --- a/crates/core-runtime/tests/scope.rs +++ b/crates/core-runtime/tests/scope.rs @@ -1,9 +1,6 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception //! Integration tests for scope-based rooting (`RootScope`). -//! -//! Tests that need a JS runtime are grouped in a single test because -//! `JSEngine` can only be initialized once per process. // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] @@ -17,51 +14,6 @@ fn test_scope_rooting() { let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); let scope = rt.default_global(); - // --- Basic rooting --- - { - // Root a string via the scope. - let s = js::JSString::from_str(&scope, "scope test").unwrap(); - let result = s.to_utf8(&scope).unwrap(); - assert_eq!(result, "scope test"); - - // Root an object via the scope. - let obj = js::Object::new(&scope, None).unwrap(); - assert!(!obj.as_raw().is_null()); - - // Root a value via the scope. - let val = scope.root_value(value::from_i32(42)); - assert_eq!(val.get().to_int32(), 42); - - // Global handle works. - let g = scope.global(); - assert!(!g.as_raw().is_null()); - } - - // --- Inner scope --- - { - // Root something in the outer scope. - let outer_str = js::JSString::from_str(&scope, "outer").unwrap(); - - { - // Create an inner scope. - let inner = scope.inner_scope(); - - // Root something in the inner scope. - let inner_str = js::JSString::from_str(&inner, "inner").unwrap(); - let result = inner_str.to_utf8(&inner).unwrap(); - assert_eq!(result, "inner"); - - // Outer values are still accessible via the inner scope. - let result = outer_str.to_utf8(&inner).unwrap(); - assert_eq!(result, "outer"); - } - // Inner scope dropped — values rooted there are released. - - // Outer-scope values are still valid. - let result = outer_str.to_utf8(&scope).unwrap(); - assert_eq!(result, "outer"); - } - // --- GC survival --- { // Root some values. @@ -80,24 +32,17 @@ fn test_scope_rooting() { assert_eq!(val.get().to_int32(), 99); } - // --- Evaluate script --- - { - let rval = js::compile::evaluate(&scope, "1 + 2 + 3").unwrap(); - assert_eq!(rval.to_int32(), 6); - } - // --- Many roots of the same type --- { - // Root many strings — tests that the arena grows correctly. + // Root more strings than one 128-slot page of the root stack holds. let mut handles = Vec::new(); - for i in 0..100 { + for i in 0..130 { let s = js::JSString::from_str(&scope, &format!("string_{i}")).unwrap(); handles.push(s); } - // Trigger GC to verify all roots are traced. - js::gc::prepare_for_full_gc(&scope); - js::gc::maybe_gc(&scope); + // Trigger a compacting GC to verify all roots are traced. + js::gc::non_incremental_gc(&scope, js::gc::GCOptions::Shrink, js::gc::GCReason::API); // Verify all strings survived. for (i, h) in handles.iter().enumerate() { @@ -133,37 +78,10 @@ fn test_scope_rooting() { assert_eq!(s0.to_utf8(&scope).unwrap(), "level0"); } - // --- Parent-scope rooting during inner scope lifetime --- - // - // This test verifies that rooting on the parent scope while an inner - // scope is alive does NOT corrupt the parent's roots. With per-scope - // page ownership, the parent and inner scope have disjoint storage, - // so dropping the inner scope cannot affect the parent's handles. - { - let inner = scope.inner_scope(); - - // Root on the parent scope while inner scope exists. - let parent_str = js::JSString::from_str(&scope, "parent-rooted").unwrap(); - - // Root on the inner scope. - let _inner_str = js::JSString::from_str(&inner, "inner-rooted").unwrap(); - - // Drop the inner scope — only inner-scope roots should be freed. - drop(inner); - - // Trigger GC — parent_str must still be traced and survive. - js::gc::prepare_for_full_gc(&scope); - js::gc::maybe_gc(&scope); - - // parent_str should still be valid. - let result = parent_str.to_utf8(&scope).unwrap(); - assert_eq!(result, "parent-rooted"); - } - // --- Interleaved parent/inner rooting with GC --- // - // A more aggressive version: interleave parent and inner allocations, - // drop the inner scope, GC, then verify parent handles. + // Rooting on the parent scope while an inner scope is alive must not let + // dropping the inner scope release the parent's roots. { let inner = scope.inner_scope(); diff --git a/crates/core-runtime/tests/scope_gc_zeal.rs b/crates/core-runtime/tests/scope_gc_zeal.rs index f152894..86ee8a4 100644 --- a/crates/core-runtime/tests/scope_gc_zeal.rs +++ b/crates/core-runtime/tests/scope_gc_zeal.rs @@ -6,8 +6,7 @@ //! compaction (which moves objects), generational/nursery collections, and //! incremental marking. //! -//! Grouped in a single test because `JSEngine` can only be initialized once -//! per process. Each section sets its own GC zeal mode. +//! Each section sets its own GC zeal mode. // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] @@ -44,12 +43,12 @@ fn test_scope_gc_zeal() { unsafe { // ===================================================================== - // Verification #5: 1000 scopes in a loop — no heap growth + // Verification #5: roots in repeated inner scopes stay valid // ===================================================================== // Pages are reused when cursor rewinds. After the first iteration // allocates a page, subsequent iterations should reuse it. { - for i in 0..1000 { + for i in 0..3 { let inner = scope.inner_scope(); let s = js::JSString::from_str(&inner, &format!("scope_{i}")).unwrap(); let obj = Object::new(&inner, None).unwrap(); @@ -65,39 +64,39 @@ fn test_scope_gc_zeal() { } // ===================================================================== - // Verification #6: 500 roots across nested scopes + GC + // Verification #6: roots across nested scopes + GC // ===================================================================== { - // Root 100 strings in the outer scope. + // Root 3 strings in the outer scope. let mut outer_handles = Vec::new(); - for i in 0..100 { + for i in 0..3 { let s = js::JSString::from_str(&scope, &format!("root_{i}")).unwrap(); outer_handles.push((i, s)); } { let inner = scope.inner_scope(); - // Root 200 strings in the inner scope. + // Root 130 strings in the inner scope, more than one 128-slot page holds. let mut inner_handles = Vec::new(); - for i in 100..300 { + for i in 3..133 { let s = js::JSString::from_str(&inner, &format!("root_{i}")).unwrap(); inner_handles.push((i, s)); } { let inner2 = inner.inner_scope(); - // Root 200 strings in the innermost scope. + // Root 10 strings in the innermost scope. let mut inner2_handles = Vec::new(); - for i in 300..500 { + for i in 133..143 { let s = js::JSString::from_str(&inner2, &format!("root_{i}")).unwrap(); inner2_handles.push((i, s)); } - // Trigger GC with all 500 roots live. + // Trigger GC with all 143 roots live. gc::prepare_for_full_gc(&inner2); gc::non_incremental_gc(&inner2, GCOptions::Normal, GCReason::API); - // All 500 should survive. + // All 143 should survive. for (i, h) in &outer_handles { let result = h.to_utf8(&inner2).unwrap(); assert_eq!(result, format!("root_{i}")); @@ -111,7 +110,7 @@ fn test_scope_gc_zeal() { assert_eq!(result, format!("root_{i}")); } } - // inner2 drops — roots 300-499 released + // inner2 drops, releasing roots 133-142. // inner + outer roots still valid. for (i, h) in &inner_handles { @@ -119,9 +118,9 @@ fn test_scope_gc_zeal() { assert_eq!(result, format!("root_{i}")); } } - // inner drops — roots 100-299 released + // inner drops, releasing roots 3-132. - // Roots 0-99 still valid. + // Roots 0-2 still valid. for (i, h) in &outer_handles { let result = h.to_utf8(&scope).unwrap(); assert_eq!(result, format!("root_{i}")); @@ -163,7 +162,7 @@ fn test_scope_gc_zeal() { // Root many values — GC fires on every JS allocation. let mut string_handles = Vec::new(); - for i in 0..50 { + for i in 0..5 { let s = js::JSString::from_str(&inner, &format!("alloc_{i}")).unwrap(); string_handles.push(s); } @@ -171,10 +170,10 @@ fn test_scope_gc_zeal() { // Evaluate JS that allocates heavily. let rval = js::compile::evaluate( &inner, - "var arr = []; for (var i = 0; i < 100; i++) arr.push({x: i}); arr.length", + "var arr = []; for (var i = 0; i < 5; i++) arr.push({x: i}); arr.length", ) .unwrap(); - assert_eq!(rval.to_int32(), 100); + assert_eq!(rval.to_int32(), 5); // All string roots must survive. for (i, h) in string_handles.iter().enumerate() { @@ -311,7 +310,7 @@ fn test_scope_gc_zeal() { // Create objects and arrays that may be moved by compaction. let mut objects = Vec::new(); - for i in 0..30 { + for i in 0..5 { let obj = Object::new(&inner, None).unwrap(); let val = inner.root_value(value::from_i32(i * 10)); obj.set_property(&inner, c"val", val).unwrap(); @@ -323,7 +322,7 @@ fn test_scope_gc_zeal() { // Trigger operations that cause allocations (and thus compacting // GCs with zeal mode 14). - for i in 0..10 { + for i in 0..5 { let s = js::JSString::from_str(&inner, &format!("compact_{i}")).unwrap(); let result = s.to_utf8(&inner).unwrap(); assert_eq!(result, format!("compact_{i}")); @@ -436,7 +435,7 @@ fn test_scope_gc_zeal() { // ===================================================================== // GC Zeal #15: Mode 2 loop stress test // ===================================================================== - // 100 iterations: create inner scope, root, trigger GC, drop scope. + // 5 iterations: create inner scope, root, trigger GC, drop scope. { set_zeal(&scope, 2, 1); @@ -444,7 +443,7 @@ fn test_scope_gc_zeal() { // all the inner scope churn. let outer_str = js::JSString::from_str(&scope, "outer_survivor").unwrap(); - for i in 0..100 { + for i in 0..5 { let inner = scope.inner_scope(); let s = js::JSString::from_str(&inner, &format!("loop_{i}")).unwrap(); @@ -525,7 +524,7 @@ fn scope_roots_survive_nesting_out_of_order_drops_and_page_spans() { // More roots than fit in one page of the root stack. let inner = scope.inner_scope(); - let values: Vec<_> = (0..300) + let values: Vec<_> = (0..130) .map(|i| js::JSString::from_str(&inner, &format!("value {i}")).unwrap()) .collect(); compact(&inner); diff --git a/crates/core-runtime/tests/scope_root_enum.rs b/crates/core-runtime/tests/scope_root_enum.rs index 13b01a9..a28bbde 100644 --- a/crates/core-runtime/tests/scope_root_enum.rs +++ b/crates/core-runtime/tests/scope_root_enum.rs @@ -1,10 +1,7 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception -//! `#[derive(ScopeRoot)]` must handle a unit enum variant. -//! -//! The mirror/pattern builder treated a unit variant as the parenthesized -//! (tuple) case, emitting `Variant()` patterns that fail to compile against a -//! unit variant (E0532). A unit variant now carries no field group. +//! `#[derive(ScopeRoot)]` handles a unit enum variant. A `Variant()` pattern for one fails to +//! compile (E0532). // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] diff --git a/crates/core-runtime/tests/traceable_enum.rs b/crates/core-runtime/tests/traceable_enum.rs index 94a7cf8..7a81a7e 100644 --- a/crates/core-runtime/tests/traceable_enum.rs +++ b/crates/core-runtime/tests/traceable_enum.rs @@ -1,12 +1,8 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception -//! `#[derive(Traceable)]` on an enum must trace each variant's GC fields. -//! -//! The derive previously panicked on enums (builtins hand-wrote `Trace` -//! impls). It now generates a `match` that traces each variant's non-`#[no_trace]` -//! fields. A compacting GC relocates the held object; the `Heap` inside the -//! variant must be traced so its stored pointer is updated, or a later read -//! returns a stale/wrong object. +//! `#[derive(Traceable)]` on an enum traces each variant's non-`#[no_trace]` fields. A compacting +//! GC relocates the held object, so the `Heap` inside the variant must be traced for its stored +//! pointer to be updated. // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] @@ -75,7 +71,7 @@ fn traceable_enum_traces_variant_fields() { match &*slot { Slot::Held(h) => { let rooted = h.get(&scope); - let recovered = Object::from_value(&scope, *rooted).expect("still an object"); + let recovered = Object::from_raw_value(&scope, rooted.get()).expect("still an object"); let got = recovered.get_property(&scope, c"marker").unwrap(); assert_eq!(got.to_int32(), 7777); } diff --git a/crates/core-runtime/tests/typedef_macros.rs b/crates/core-runtime/tests/typedef_macros.rs index 8316bc1..defd177 100644 --- a/crates/core-runtime/tests/typedef_macros.rs +++ b/crates/core-runtime/tests/typedef_macros.rs @@ -1,9 +1,6 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception -//! Integration tests for the `js` API. -//! -//! Tests that need a JS runtime are grouped in a single test because -//! `JSEngine` can only be initialized once per process. +//! Tests of the class, namespace and dictionary macros. // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] @@ -45,59 +42,37 @@ mod js_proto_tests { } #[test] - fn inherits_from_error_prototype() { - assert_eq!(eval("new CustomError('x') instanceof Error"), "true"); - } - - #[test] - fn is_custom_error_instance() { - assert_eq!(eval("new CustomError('x') instanceof CustomError"), "true"); - } - - #[test] - fn prototype_chain() { - assert_eq!( - eval("Object.getPrototypeOf(CustomError.prototype) === Error.prototype"), - "true" - ); - } - - #[test] - fn getter_works() { - assert_eq!(eval("new CustomError('test detail').detail"), "test detail"); - } - - #[test] - fn not_array_instance() { - assert_eq!(eval("new CustomError('x') instanceof Array"), "false"); - } - - #[test] - fn to_string_tag() { + fn js_proto_classes_inherit_from_the_builtin_prototype() { assert_eq!( - eval("Object.prototype.toString.call(new CustomError('x'))"), - "[object CustomError]" + eval( + "const e = new CustomError('d'); + [e instanceof CustomError, Object.getPrototypeOf(e) === CustomError.prototype, + Object.getPrototypeOf(CustomError.prototype) === Error.prototype, + e.detail, Object.prototype.toString.call(e), + CustomError.prototype[Symbol.toStringTag]].join()" + ), + "true,true,true,d,[object CustomError],CustomError" ); } + /// `#[jsclass]` accessors are non-enumerable. A call of the constructor without `new` throws + /// the same `TypeError` as a JS class constructor. #[test] - fn to_string_tag_on_prototype() { + fn jsclass_accessors_are_not_enumerable_and_constructors_require_new() { assert_eq!( - eval("CustomError.prototype[Symbol.toStringTag]"), - "CustomError" + eval( + "class Plain {} + const error = (f) => { + try { f(); return 'no throw'; } catch (e) { return `${e.constructor.name}: ${e.message}`; } + }; + const own = error(() => CustomError('x')); + [Object.getOwnPropertyDescriptor(CustomError.prototype, 'detail').enumerable, + own, own === error(() => Plain())].join('|')" + ), + "false|TypeError: class constructors must be invoked with 'new'|true" ); } - #[test] - fn has_stack_property() { - assert_eq!(eval("typeof new CustomError('x').stack"), "string"); - } - - #[test] - fn stack_is_nonempty() { - assert_eq!(eval("new CustomError('x').stack.length > 0"), "true"); - } - /// The stack is the one at construction, formatted when it is first read. #[test] fn stack_is_captured_at_construction() { @@ -113,7 +88,8 @@ mod js_proto_tests { } /// Like `Error.prototype.stack`, `stack` is an accessor on the prototype: instances have no - /// own `stack` until one is assigned, and the getter returns `undefined` for other objects. + /// own `stack` until one is assigned, and the getter returns `undefined` for other objects and + /// throws for primitives. #[test] fn stack_is_a_prototype_accessor() { assert_eq!( @@ -123,22 +99,13 @@ mod js_proto_tests { const own = Object.getOwnPropertyDescriptor(e, 'stack'); e.stack = 'replaced'; const after = Object.getOwnPropertyDescriptor(e, 'stack'); + let primitive; + try { d.get.call(1); primitive = 'no throw'; } catch (e) { primitive = e.constructor.name; } [typeof d.get, typeof d.set, d.enumerable, d.configurable, own, after.value, after.enumerable, after.writable, - d.get.call(CustomError.prototype), d.get.call({})].join()" + d.get.call(CustomError.prototype), d.get.call({}), primitive].join()" ), - "function,function,false,true,,replaced,true,true,," - ); - } - - #[test] - fn stack_getter_throws_on_primitives() { - assert_eq!( - eval( - "const get = Object.getOwnPropertyDescriptor(CustomError.prototype, 'stack').get; - try { get.call(1); 'no throw' } catch (e) { e.constructor.name }" - ), - "TypeError" + "function,function,false,true,,replaced,true,true,,,TypeError" ); } } @@ -175,57 +142,34 @@ mod constant_tests { ) } + /// Constants are read-only, enumerable properties of the constructor only. #[test] - fn constant_on_constructor() { - assert_eq!(eval("StatusCode.OK"), "200"); - } - - #[test] - fn constant_not_found() { - assert_eq!(eval("StatusCode.NOT_FOUND"), "404"); - } - - #[test] - fn constant_internal_server_error() { - assert_eq!(eval("StatusCode.INTERNAL_SERVER_ERROR"), "500"); - } - - #[test] - fn constant_is_readonly() { + fn constants_are_defined_on_the_constructor() { assert_eq!( - eval("'use strict'; try { StatusCode.OK = 999; 'changed' } catch(e) { 'error' }"), - "error" + eval( + "'use strict'; + let assigned; + try { StatusCode.OK = 999; assigned = 'changed'; } catch (e) { assigned = 'error'; } + [StatusCode.OK, StatusCode.NOT_FOUND, StatusCode.INTERNAL_SERVER_ERROR, assigned, + Object.keys(StatusCode).includes('OK'), StatusCode.prototype.OK, + new StatusCode().OK].map(String).join()" + ), + "200,404,500,error,true,undefined,undefined" ); } - - #[test] - fn constant_is_enumerable() { - assert_eq!(eval("Object.keys(StatusCode).includes('OK')"), "true"); - } - - #[test] - fn constant_not_on_prototype() { - assert_eq!(eval("StatusCode.prototype.OK"), "undefined"); - } - - #[test] - fn constant_not_on_instance() { - assert_eq!(eval("new StatusCode().OK"), "undefined"); - } } /// Tests for `#[webidl_interface]`: auto Symbol.toStringTag and /// constants on both constructor AND prototype. mod webidl_interface_tests { - use core_runtime::jsmethods; use core_runtime::test_util::eval_with_setup; - use core_runtime::webidl_interface; + use core_runtime::{webidl_interface, webidl_methods}; - /// A WebIDL-style interface with constants and methods. + /// A WebIDL-style interface with constants and an attribute. #[webidl_interface] struct MediaError {} - #[jsmethods] + #[webidl_methods] impl MediaError { pub const MEDIA_ERR_ABORTED: u16 = 1; pub const MEDIA_ERR_NETWORK: u16 = 2; @@ -236,6 +180,11 @@ mod webidl_interface_tests { fn construct() -> Self { Self {} } + + #[getter] + fn code(&self) -> u16 { + 1 + } } fn eval(code: &str) -> String { @@ -249,82 +198,38 @@ mod webidl_interface_tests { ) } - // --- Auto Symbol.toStringTag --- - - #[test] - fn auto_to_string_tag() { - assert_eq!( - eval("Object.prototype.toString.call(new MediaError())"), - "[object MediaError]" - ); - } - - #[test] - fn to_string_tag_on_prototype() { - assert_eq!( - eval("MediaError.prototype[Symbol.toStringTag]"), - "MediaError" - ); - } - - // --- Constants on constructor --- - - #[test] - fn constant_on_constructor() { - assert_eq!(eval("MediaError.MEDIA_ERR_ABORTED"), "1"); - } - - #[test] - fn constant_network_on_constructor() { - assert_eq!(eval("MediaError.MEDIA_ERR_NETWORK"), "2"); - } - - // --- Constants on prototype (WebIDL §3.7.3) --- - - #[test] - fn constant_on_prototype() { - assert_eq!(eval("MediaError.prototype.MEDIA_ERR_ABORTED"), "1"); - } - - #[test] - fn constant_decode_on_prototype() { - assert_eq!(eval("MediaError.prototype.MEDIA_ERR_DECODE"), "3"); - } - - #[test] - fn constant_src_not_supported_on_prototype() { - assert_eq!( - eval("MediaError.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED"), - "4" - ); - } - - // --- Constants on instances (inherited via prototype) --- - - #[test] - fn constant_on_instance() { - assert_eq!(eval("new MediaError().MEDIA_ERR_ABORTED"), "1"); - } - - // --- Constants are read-only --- - + /// The constants are read-only properties of the constructor and of the prototype (WebIDL + /// §3.7.3), so instances inherit them. #[test] - fn constant_readonly_on_constructor() { + fn interfaces_get_a_to_string_tag_and_constants_on_the_prototype() { assert_eq!( eval( - "'use strict'; try { MediaError.MEDIA_ERR_ABORTED = 99; 'changed' } catch(e) { 'error' }" + "'use strict'; + const assign = (target) => { + try { target.MEDIA_ERR_ABORTED = 99; return 'changed'; } catch (e) { return 'error'; } + }; + [Object.prototype.toString.call(new MediaError()), + MediaError.prototype[Symbol.toStringTag], MediaError.MEDIA_ERR_ABORTED, + MediaError.MEDIA_ERR_NETWORK, MediaError.prototype.MEDIA_ERR_DECODE, + MediaError.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED, new MediaError().MEDIA_ERR_ABORTED, + assign(MediaError), assign(MediaError.prototype)].join()" ), - "error" + "[object MediaError],MediaError,1,2,3,4,1,error,error" ); } + /// Attributes are enumerable (WebIDL §3.7.6), and a call of the constructor without `new` + /// throws a `TypeError`. #[test] - fn constant_readonly_on_prototype() { + fn interface_attributes_are_enumerable_and_constructors_require_new() { assert_eq!( eval( - "'use strict'; try { MediaError.prototype.MEDIA_ERR_ABORTED = 99; 'changed' } catch(e) { 'error' }" + "let call; + try { MediaError(); call = 'no throw'; } catch (e) { call = e.constructor.name; } + [Object.getOwnPropertyDescriptor(MediaError.prototype, 'code').enumerable, + new MediaError().code, call].join()" ), - "error" + "true,1,TypeError" ); } } @@ -365,55 +270,19 @@ mod jsnamespace_tests { ) } + /// The namespace is a plain object without a `Symbol.toStringTag`, and a non-enumerable + /// property of the global (WebIDL §3.13). Its functions aren't constructors. #[test] - fn namespace_exists() { - assert_eq!(eval("typeof math"), "object"); - } - - #[test] - fn namespace_function_call() { - assert_eq!(eval("math.add(2, 3)"), "5"); - } - - #[test] - fn namespace_multiply() { - assert_eq!(eval("math.multiply(4, 5)"), "20"); - } - - #[test] - fn namespace_function_with_scope() { - assert_eq!(eval("math.greet('World')"), "Hello, World!"); - } - - #[test] - fn namespace_not_constructable() { + fn namespaces_are_plain_objects_with_functions() { assert_eq!( - eval("try { new math.add(); 'ok' } catch(e) { 'error' }"), - "error" - ); - } - - #[test] - fn namespace_no_to_string_tag() { - assert_eq!( - eval("Object.prototype.toString.call(math)"), - "[object Object]" - ); - } - - #[test] - fn namespace_constant_exposed() { - // `pub const` items are installed on the namespace object. - assert_eq!(eval("math.PI"), "3.5"); - } - - #[test] - fn namespace_object_is_not_enumerable() { - // WebIDL §3.13: the namespace object is a non-enumerable property of - // the global (installed via define, not [[Set]]). - assert_eq!( - eval("Object.getOwnPropertyDescriptor(globalThis, 'math').enumerable"), - "false" + eval( + "let constructed; + try { new math.add(); constructed = 'ok'; } catch (e) { constructed = 'error'; } + [typeof math, math.add(2, 3), math.multiply(4, 5), math.greet('World'), math.PI, + constructed, Object.prototype.toString.call(math), + Object.getOwnPropertyDescriptor(globalThis, 'math').enumerable].join()" + ), + "object,5,20,Hello, World!,3.5,error,[object Object],false" ); } } @@ -442,23 +311,14 @@ mod webidl_namespace_tests { } #[test] - fn namespace_exists() { - assert_eq!(eval("typeof CSS"), "object"); - } - - #[test] - fn namespace_function_call() { - assert_eq!(eval("CSS.escape('hello')"), "hello"); - } - - #[test] - fn auto_to_string_tag() { - assert_eq!(eval("Object.prototype.toString.call(CSS)"), "[object CSS]"); - } - - #[test] - fn to_string_tag_on_namespace() { - assert_eq!(eval("CSS[Symbol.toStringTag]"), "CSS"); + fn webidl_namespaces_get_a_to_string_tag() { + assert_eq!( + eval( + "[typeof CSS, CSS.escape('hello'), Object.prototype.toString.call(CSS), + CSS[Symbol.toStringTag]].join()" + ), + "object,hello,[object CSS],CSS" + ); } } @@ -541,48 +401,24 @@ mod setup_style_inheritance { } #[test] - fn pet_constructor() { - assert_eq!(eval("new Pet('Cat').kind"), "Cat"); - } - - #[test] - fn pet_method() { - assert_eq!(eval("new Pet('Some dog').sound()"), ""); - } - - #[test] - fn lily_constructor() { - assert_eq!(eval("new Lily(5).kind"), "The very cutest"); - } - - #[test] - fn lily_payload() { - assert_eq!(eval("new Lily(5).cuteness"), "5"); - } - - #[test] - fn lily_overrides_method() { - assert_eq!(eval("new Lily(5).sound()"), "The cutest sound!"); - } - - #[test] - fn lily_instanceof() { - assert_eq!(eval("new Lily(5) instanceof Pet"), "true"); - } - - #[test] - fn lily_describe() { + fn setup_style_constructors_initialize_subclasses() { assert_eq!( - eval("new Lily(5).describe()"), - "The very cutest Lily, cuteness: 5/5" + eval( + "const pet = new Pet('Cat'); + const lily = new Lily(5); + [pet.kind, pet.sound(), lily.kind, lily.cuteness, lily.sound(), lily instanceof Pet, + lily.describe()].join('|')" + ), + "Cat||The very cutest|5|The cutest sound!|true|\ + The very cutest Lily, cuteness: 5/5" ); } } /// Tests for `#[webidl_dictionary]` — WebIDL dictionary conversion. mod webidl_dictionary_tests { - use core_runtime::test_util::{eval_with_setup, throws_with_setup}; - use core_runtime::{jsmethods, webidl_dictionary, webidl_interface}; + use core_runtime::test_util::eval_with_setup; + use core_runtime::{webidl_dictionary, webidl_interface, webidl_methods}; use js::gc::scope::Scope; use js::Object; @@ -663,7 +499,7 @@ mod webidl_dictionary_tests { members: String, } - #[jsmethods] + #[webidl_methods] impl Inherited { #[constructor] fn new(init: &DerivedInit<'_>) -> Self { @@ -713,8 +549,8 @@ mod webidl_dictionary_tests { self.data_mut().members.push('!'); } - /// `&str` parameters. A call from JS still has to copy the string out of the engine, so - /// this buys nothing there; what it buys is Rust callers, which can pass a borrow. + /// `&str` parameters. A call from JS still has to copy the string out of the engine. Rust + /// callers can pass a borrow. #[method] fn label(&self, prefix: &str, suffix: Option<&str>) -> String { format!("{prefix}{}{}", self.data().members, suffix.unwrap_or("")) @@ -740,7 +576,7 @@ mod webidl_dictionary_tests { } } - #[jsmethods] + #[webidl_methods] impl Greeter { #[constructor] fn new(person: PersonInit, options: Option) -> Self { @@ -768,7 +604,7 @@ mod webidl_dictionary_tests { label: String, } - #[jsmethods] + #[webidl_methods] impl Config { #[constructor] fn new(init: ConfigInit) -> Self { @@ -801,87 +637,62 @@ mod webidl_dictionary_tests { eval_with_setup(setup, code) } - fn throws(code: &str) -> bool { - throws_with_setup(setup, code) - } - - // Required members - - #[test] - fn required_members_extracted() { - assert_eq!( - eval("new Greeter({ name: 'Alice', age: 30 }).greeting"), - "Hello, Alice (age 30)." - ); - } - - #[test] - fn required_member_missing_throws() { - // Missing 'age' should throw. - assert!(throws("new Greeter({ name: 'Alice' })")); - } - + /// A `null` optional dictionary argument converts like an absent one. #[test] - fn required_member_all_missing_throws() { - assert!(throws("new Greeter({})")); - } - - #[test] - fn non_object_dict_throws() { - assert!(throws("new Greeter(42)")); - } - - // Optional members - - #[test] - fn optional_members_present() { + fn dictionary_members_are_extracted() { assert_eq!( - eval("new Greeter({ name: 'Bob', age: 25 }, { prefix: 'Hi', excited: true }).greeting"), - "Hi, Bob (age 25)!" + eval( + "[new Greeter({ name: 'Alice', age: 30 }).greeting, + new Greeter({ name: 'Bob', age: 25 }, { prefix: 'Hi', excited: true }).greeting, + new Greeter({ name: 'Dana', age: 18 }, null).greeting].join('|')" + ), + "Hello, Alice (age 30).|Hi, Bob (age 25)!|Hello, Dana (age 18)." ); } #[test] - fn optional_members_absent() { + fn missing_required_members_and_non_objects_throw() { assert_eq!( - eval("new Greeter({ name: 'Charlie', age: 20 }).greeting"), - "Hello, Charlie (age 20)." + eval( + "[{ name: 'Alice' }, {}, 42].map(init => { + try { new Greeter(init); return 'no throw'; } catch (e) { return e.constructor.name; } + }).join()" + ), + "TypeError,TypeError,TypeError" ); } + /// A `null` or `undefined` dictionary converts like an empty one. #[test] - fn optional_dict_param_null() { - // Second param (GreetingOptions) is optional and null → treated as None. + fn absent_members_take_their_defaults() { assert_eq!( - eval("new Greeter({ name: 'Dana', age: 18 }, null).greeting"), - "Hello, Dana (age 18)." + eval( + "[new Config({}).timeout, new Config({ timeout: 42 }).timeout, new Config({}).label, + new Config({ label: 'custom' }).label, new Config(null).timeout, + new Config(undefined).timeout].join()" + ), + "10,42,default,custom,10,10" ); } - // Inheritance - #[test] fn inherited_members_are_read_through_the_chain() { assert_eq!( eval( - "new Inherited({ top: 'T', base: 'B', middle: 'M', bottom: 'b', extra: {} }) - .members" + "[new Inherited({ top: 'T', base: 'B', middle: 'M', bottom: 'b', extra: {} }).members, + new Inherited({}).members].join('|')" ), - "T B M b true" + "T B M b true| false" ); } - #[test] - fn inherited_members_absent_take_their_defaults() { - assert_eq!(eval("new Inherited({}).members"), " false"); - } - #[test] fn inherited_dictionaries_convert_least_derived_first() { // Every member is a property get, so an accessor on the source object records the order - // the conversion reads them in. WebIDL: the inherited dictionaries first, least derived - // first, each one's own members lexicographically. Sorting all five as one flat set would - // give `base, bottom, extra, middle, top` — this asserts the chain is walked instead. + // in which the conversion reads them. WebIDL: the inherited dictionaries first, least + // derived first, each one's own members lexicographically. Sorting all five as one flat + // set would give `base, bottom, extra, middle, top`. This asserts the chain is walked + // instead. assert_eq!( eval( r#" @@ -902,115 +713,37 @@ mod webidl_dictionary_tests { ); } + /// Operations take dictionaries, `&str` and `&[T]` parameters by reference, plain and in an + /// `Option`, and a promise-returning operation takes a dictionary by reference too. A + /// non-string argument to a `&str` parameter still coerces. #[test] - fn dictionaries_can_be_taken_by_reference() { - // Both the plain and the `Option` form, and the absent case for the latter. - assert_eq!( - eval("new Inherited({}).describe({ top: 'T', base: 'B' }, { top: 't', base: 'b' })"), - "T/B t/b" - ); - assert_eq!( - eval("new Inherited({}).describe({ top: 'T', base: 'B' })"), - "T/B" - ); - assert_eq!( - eval("new Inherited({}).describe({ top: 'T', base: 'B' }, undefined)"), - "T/B" - ); - } - - #[test] - fn a_getter_can_lend_a_string_instead_of_copying_it() { - // What script sees is an ordinary JS string, identical to the copying getter's. + fn parameters_can_be_taken_by_reference() { assert_eq!( eval( - "const i = new Inherited({ top: 'T', base: 'B' }); - `${typeof i.borrowedMembers} ${i.borrowedMembers === i.members}`" + "const i = new Inherited({ top: 'T' }); + [i.describe({ top: 'T', base: 'B' }, { top: 't', base: 'b' }), + i.describe({ top: 'T', base: 'B' }), i.describe({ top: 'T', base: 'B' }, undefined), + i.label('[', ']'), i.label('['), i.label(1, 2), i.tally([1, 2, 3], ['a', 'b']), + i.tally([]), i.describeLater({ top: 'T' }) instanceof Promise].join('|')" ), - "string true" - ); - } - - #[test] - fn methods_can_take_str_by_reference() { - // The `Option<&str>` form covers both present and absent, since `as_deref` is what makes - // the present case type-check at all. - assert_eq!( - eval("new Inherited({ top: 'T' }).label('[', ']')"), - "[T false]" - ); - assert_eq!( - eval("new Inherited({ top: 'T' }).label('[')"), - "[T false" + "T/B t/b|T/B|T/B|[T false]|[T false|1T false2|3 6 2|0 0 0|true" ); - // Conversion is unchanged by the borrow: a non-string argument still coerces. - assert_eq!(eval("new Inherited({}).label(1, 2)"), "1 false2"); } + /// The lent string is an ordinary JS string, and the shared borrow is released after the + /// call: `touch` borrows the data mutably, which panics otherwise. #[test] - fn methods_can_take_slices_by_reference() { - assert_eq!( - eval("new Inherited({}).tally([1, 2, 3], ['a', 'b'])"), - "3 6 2" - ); - assert_eq!(eval("new Inherited({}).tally([])"), "0 0 0"); - } - - #[test] - fn lending_a_string_gives_the_borrow_back() { - // `touch` borrows the data mutably, which panics if the projected guard held its shared - // borrow open past the call that lent it. + fn a_getter_can_lend_a_string_instead_of_copying_it() { assert_eq!( eval( - "const i = new Inherited({ top: 'T' }); - i.borrowedMembers; + "const i = new Inherited({ top: 'T', base: 'B' }); + const lent = i.borrowedMembers; i.touch(); - i.borrowedMembers.endsWith('!')" + [typeof lent, lent === 'T B false', i.borrowedMembers.endsWith('!')].join()" ), - "true" + "string,true,true" ); } - - #[test] - fn a_lent_dictionary_survives_a_promise_returning_call() { - // The value is extracted inside the closure that turns a failure into a rejection; the - // borrow of it has to reach the call in the same block. - assert_eq!( - eval("new Inherited({}).describeLater({ top: 'T' }) instanceof Promise"), - "true" - ); - } - - // Default values - - #[test] - fn default_value_used_when_missing() { - assert_eq!(eval("new Config({}).timeout"), "10"); - } - - #[test] - fn default_value_overridden() { - assert_eq!(eval("new Config({ timeout: 42 }).timeout"), "42"); - } - - #[test] - fn default_and_optional_combined() { - assert_eq!(eval("new Config({}).label"), "default"); - assert_eq!(eval("new Config({ label: 'custom' }).label"), "custom"); - } - - // Empty/null/undefined dictionaries - - #[test] - fn null_dict_uses_defaults() { - // null → all members get their defaults. - assert_eq!(eval("new Config(null).timeout"), "10"); - } - - #[test] - fn undefined_dict_uses_defaults() { - assert_eq!(eval("new Config(undefined).timeout"), "10"); - } } /// Tests for Rust-side factory functions: unannotated constructor-shaped fns @@ -1065,3 +798,88 @@ mod implicit_factory_tests { ); } } + +/// Tests of class registration. +mod registration_tests { + use core_runtime::config::RuntimeConfig; + use core_runtime::runtime::Runtime; + use core_runtime::test_util::eval_with_setup; + use core_runtime::{jsclass, jsmethods}; + use js::conversion::ToJSVal; + + #[jsclass] + struct Registered { + value: i32, + } + + #[jsmethods] + impl Registered { + #[constructor] + fn construct() -> Self { + Self { value: 1 } + } + } + + #[test] + fn class_constructor_ignores_a_replaced_prototype_constructor() { + core_runtime::runtime::register_global_initializer(|scope, global| { + Registered::add_to_global(scope, global); + }); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + let original = js::compile::evaluate( + &scope, + "const original = Registered; + Registered.prototype.constructor = function Replaced() {}; + original", + ) + .expect("the script runs"); + let ctor = js::class::class_constructor::(&scope).expect("registered"); + let ctor = ctor.to_jsval(&scope).expect("an object converts"); + assert!(js::comparison::same_value(&scope, ctor, original).expect("comparing succeeds")); + } + + #[jsclass] + struct Visible {} + + #[jsmethods] + impl Visible { + #[constructor] + fn construct() -> Self { + Self {} + } + } + + #[jsclass(hidden, extends = Visible)] + struct HiddenChild { + parent: VisibleImpl, + } + + #[jsmethods] + impl HiddenChild { + #[constructor] + fn construct() -> Self { + Self { + parent: VisibleImpl::construct(), + } + } + } + + /// Registering a hidden class before its parent defines the parent's constructor on the + /// global and leaves the hidden class's constructor off it. + #[test] + fn a_hidden_class_registered_first_leaves_its_parent_visible() { + assert_eq!( + eval_with_setup( + || { + core_runtime::runtime::register_global_initializer(|scope, global| { + HiddenChild::add_to_global(scope, global); + Visible::add_to_global(scope, global); + }); + }, + "[typeof Visible, typeof HiddenChild].join()" + ), + "function,undefined" + ); + } +} diff --git a/crates/js/src/class.rs b/crates/js/src/class.rs index a993a73..eaa736f 100644 --- a/crates/js/src/class.rs +++ b/crates/js/src/class.rs @@ -311,8 +311,7 @@ impl JSType for T { /// /// Each element is converted through `FromJSVal`, so a `Promise<'_>` element is /// a *brand check*, unlike a `Promise<'_>` parameter, which is the WebIDL -/// `Promise` conversion that accepts any value (see [`PromiseOf`]). WebIDL -/// has no variadic promise argument, so nothing needs the other behavior. +/// `Promise` conversion that accepts any value (see [`PromiseOf`]). /// /// Note that instead of `HandleValue<'_>`, you can also use `&CallArgs`, which gives you access /// to the raw, already rooted, arguments vector and count. @@ -845,16 +844,12 @@ struct ClassRegistry { /// plumbing objects reused across calls (e.g. a resolved promise serving /// as a microtask trigger). Traced like prototypes. shared_objects: FxHashMap>>, - /// Per-global monotonic `u64` counters keyed by an arbitrary `usize`. Used - /// where a spec requires ids unique across a whole global rather than per - /// call site — e.g. HTML's setTimeout/setInterval ids, which must not - /// collide across the concurrent event loops that share one global. Plain - /// integers, so no GC tracing. - counters: FxHashMap, /// Per-global interned property names keyed by an arbitrary `usize` (by /// convention the address of a `static` unique to the call site), so a hot /// property name is interned once per global. Traced like prototypes. property_atoms: FxHashMap>>, + /// Per-global Rust state that holds no GC things, keyed by its type (see [`global_state`]). + state: FxHashMap>, /// Type-erased GC glue for the global's private data, /// installed by [`set_global_private`]. /// @@ -874,21 +869,12 @@ impl ClassRegistry { Self { map: FxHashMap::default(), shared_objects: FxHashMap::default(), - counters: FxHashMap::default(), property_atoms: FxHashMap::default(), + state: FxHashMap::default(), global_private: None, } } - /// Return the counter for `key` and advance it by one (wrapping on - /// overflow). Counters start at 0. - fn bump_counter(&mut self, key: usize) -> u64 { - let slot = self.counters.entry(key).or_insert(0); - let value = *slot; - *slot = slot.wrapping_add(1); - value - } - fn register(&mut self, type_id: TypeId, proto: *mut JSObject) { let entry = self .map @@ -1127,7 +1113,7 @@ unsafe extern "C" fn global_private_trace(trc: *mut JSTracer, global: *mut JSObj /// /// Must be called on the [`RealmOptions`](crate::engine::RealmOptions) used to /// create a [`STARLING_GLOBAL_CLASS`] global, before `JS_NewGlobalObject`. The -/// op is a no-op until [`set_global_private`] populates the global, so it is +/// op does nothing until [`set_global_private`] populates the global, so it is /// safe to install unconditionally for Starling globals. pub fn install_global_private_trace(options: &mut crate::engine::RealmOptions) { options.creationOptions_.traceGlobal_ = Some(global_private_trace); @@ -1167,14 +1153,12 @@ pub(crate) fn inherits_from(concrete_tag: usize, target_tag: usize) -> bool { }) } -// Registry mapping child type tag → parent info. -// Thread-local because the SpiderMonkey runtime is single-threaded. /// Register a child→parent derivation in the inheritance registry, keyed by the /// child's runtime `JSClass` tag. /// /// `accessor`/`accessor_mut` map a pointer to the child's private data onto a -/// pointer to the embedded parent data, so [`get_private_or_ancestor`] can reach -/// a `Parent`'s data through a concrete child instance. +/// pointer to the embedded parent data, so [`borrow`] can reach a `Parent`'s data through a +/// concrete child instance. fn register_parent_info_raw( child_tag: usize, parent_tag: usize, @@ -1343,33 +1327,6 @@ fn step_to_parent_mut(info: &InheritanceInfo, ptr: *mut c_void) -> *mut c_void { unsafe { (info.accessor_mut)(ptr) } } -/// Inheritance-aware immutable private data access. -/// -/// If the object's concrete type matches T, returns a direct reference. -/// If the concrete type derives from T (via HasParent chain), walks the -/// parent accessor chain to find the T reference within the concrete data. -/// Returns `None` for any other object, and for objects without private data. -/// -/// # Safety -/// -/// - `obj` must be a valid JS object. -/// - The returned reference is only valid as long as the JS object is alive. -pub unsafe fn get_private_or_ancestor<'a, T: ClassDef>(obj: *mut JSObject) -> Option<&'a T> { - private_data_ptr::(obj, step_to_parent).map(|ptr| &*(ptr as *const T)) -} - -/// Inheritance-aware mutable private data access. -/// -/// # Safety -/// -/// - `obj` must be a valid JS object. -/// - No other references to the data may exist simultaneously. -pub unsafe fn get_private_or_ancestor_mut<'a, T: ClassDef>( - obj: *mut JSObject, -) -> Option<&'a mut T> { - private_data_ptr::(obj, step_to_parent_mut).map(|ptr| &mut *(ptr as *mut T)) -} - // ============================================================================ // ClassDef trait // ============================================================================ @@ -1410,6 +1367,10 @@ pub trait ClassDef: Sized + Trace + 'static { /// Generated by the proc macro as a compile-time C string literal. const PROTOTYPE_CALL_ERROR: &'static CStr; + /// The message of the `TypeError` a call of the constructor without `new` throws. + /// `#[jsclass]` overrides it with the message SpiderMonkey throws for a JS class constructor. + const CALL_WITHOUT_NEW_ERROR: &'static CStr = c"Constructor must be called with 'new'"; + /// Return the `JSClass` descriptor for this type. /// /// Each `ClassDef` type gets a unique `static JSClass` generated by the @@ -1491,11 +1452,12 @@ pub trait ClassDef: Sized + Trace + 'static { /// Override by using `#[jsclass(extends = ParentType)]`. fn register_inheritance() {} - /// Ensure the parent class is registered on `global` before `Self` is. + /// Ensure the parent class is registered on the current global before `Self` is. The + /// parent's constructor is defined on the global object unless the parent is hidden. /// /// Called automatically by [`register_class`] — you never need to call /// this directly. Override by using `#[jsclass(extends = ParentType)]`. - fn ensure_parent_registered(_scope: &Scope<'_>, _global: Object<'_>) {} + fn ensure_parent_registered(_scope: &Scope<'_>) {} /// The `Symbol.toStringTag` value for this class (empty = none). /// @@ -1674,17 +1636,19 @@ impl ClassBuilder { /// `name` must be a null-terminated C string (use `c"name"`). /// `getter` is the getter callback (`JSNative` is `Option`: pass `None` for write-only). /// `setter` is the setter callback (`JSNative` is `Option`: pass `None` for read-only). + /// `flags` are the property attributes, such as `JSPROP_ENUMERATE`. pub fn property( mut self, name: &'static std::ffi::CStr, getter: JSNative, setter: JSNative, + flags: u8, ) -> Self { self.properties.push(JSPropertySpec { name: crate::class_spec::JSPropertySpec_Name { string_: name.as_ptr(), }, - attributes_: crate::class_spec::JSPROP_ENUMERATE, + attributes_: flags, kind_: crate::class_spec::JSPropertySpec_Kind::NativeAccessor, u: crate::class_spec::JSPropertySpec_AccessorsOrValue { accessors: crate::class_spec::JSPropertySpec_AccessorsOrValue_Accessors { @@ -1773,14 +1737,45 @@ fn spec_tables_for() -> SpecTables { }) } -/// Register a class on the global object, making it available as a constructor. +/// The registry key of the constructor of class `T`. +struct ConstructorOf(PhantomData); + +/// The constructor of class `T` in the current realm, or a `TypeError` if `T` isn't registered in +/// it. The constructor is the one [`register_class`] created, whatever the prototype's +/// `constructor` property holds. +pub fn class_constructor<'s, T: ClassDef>(scope: &'s Scope<'_>) -> Result, ExnThrown> { + registered_prototype::(scope)?; + let ctor = get_prototype::>(scope.global()) + .expect("a registered class has a constructor"); + // SAFETY: the registry holds live objects, which the global's trace keeps alive. + Ok(unsafe { Object::from_raw(scope, ctor) }.expect("the constructor is non-null")) +} + +/// Register class `T` on the current global without defining its constructor on the global +/// object, and return its prototype. A class registered before keeps whatever global property it +/// has. +/// +/// # Safety +/// +/// The same as for [`register_class`]. +pub unsafe fn register_hidden_class<'s, T: ClassDef>(scope: &'s Scope<'s>) -> Object<'s> { + // An unregistered class's constructor is defined on a new object that nothing else + // references, so the global's properties stay as they are. Its parent class is registered on + // the global by `ensure_parent_registered`. + let target = Object::new_plain(scope).expect("creating an object succeeds outside of OOM"); + // SAFETY: the caller upholds `register_class`'s requirements. + unsafe { register_class::(scope, target) } +} + +/// Register a class on the current global, and define its constructor as a property of +/// `global`, which is the global object unless [`register_hidden_class`] registers the class. /// /// This creates the class's JSClass, constructor, prototype, and methods, /// and stores the prototype for later use with [`create_instance`]. /// /// # Safety /// -/// - `global` must be a global object. +/// - `global` must be the current global or an ordinary object. /// - Must be called within an appropriate realm/compartment. /// /// # Returns @@ -1801,7 +1796,7 @@ pub unsafe fn register_class<'s, T: ClassDef>( } // Ensure the parent class is registered before we register ourselves. - T::ensure_parent_registered(scope, global); + T::ensure_parent_registered(scope); // The leaked method/property tables, shared across globals. let tables = spec_tables_for::(); @@ -1844,7 +1839,8 @@ pub unsafe fn register_class<'s, T: ClassDef>( ) .expect("Constructor was just installed"); - register_prototype::(global, proto.handle().get()); + register_prototype::(scope.global(), proto.handle().get()); + register_prototype::>(scope.global(), ctor.as_raw()); // Define Symbol.toStringTag if the class specifies one. if !T::TO_STRING_TAG.is_empty() { @@ -1905,7 +1901,7 @@ unsafe extern "C" fn generic_constructor( let args = CallArgs::from_vp(vp, argc); if !args.is_constructing() { - crate::error::throw_type_error(&scope, c"Constructor must be called with 'new'"); + crate::error::throw_type_error(&scope, T::CALL_WITHOUT_NEW_ERROR); return false; } @@ -2089,8 +2085,8 @@ pub unsafe fn get_arg_with_config<'s, 'v, T: FromJSVal<'s, 'v>>( /// Extract a WebIDL `Promise` argument from CallArgs. /// /// Performs an eager argc check that throws on missing arguments, and then -/// registers a type check for `T` on the value a promise resolved with the -/// argument resolves to. +/// registers a type check for `T` on the value to which a promise resolved +/// with the argument resolves. /// /// # Safety /// @@ -2139,54 +2135,14 @@ unsafe fn promise_arg_value<'s>( Ok(Handle::from_raw(args.get(index))) } -/// Extract a stack newtype argument from CallArgs. -/// -/// Verifies that the argument is an object with the correct class, roots it, -/// and wraps it in the stack newtype `T`. Used by generated constructor and -/// method trampolines for parameters typed as stack newtypes (e.g. `Item<'_>`). -/// -/// # Safety -/// -/// - `scope` must be in a valid realm. -/// - `args` must be from a valid JSNative call. -#[doc(hidden)] -pub unsafe fn get_stack_arg<'s, T: StackType<'s>>( - scope: &'s Scope<'_>, - args: &CallArgs, - index: u32, -) -> Result { - if index >= args.argc_ { - return Err(crate::error::throw_type_error( - scope, - c"Not enough arguments", - )); - } - let val = *args.get(index); - if !val.is_object() || !::is_instance(val.to_object()) { - let msg = CString::new(format!( - "argument {} is not an instance of {}", - index, - T::Inner::NAME, - )) - .unwrap_or_else(|_| c"argument is not the expected class".into()); - return Err(crate::error::throw_type_error(scope, &msg)); - } - let obj = val.to_object(); - let nn = NonNull::new(obj).unwrap(); - Ok(unsafe { T::from_handle_unchecked(scope.root_object(nn)) }) -} - /// Resolve a WebIDL operation's receiver object from the call's `this`. /// -/// Returns the `this` value as an object, except that — per WebIDL `[Global]` -/// operation semantics — a null or undefined `this` resolves to the global -/// object when the global implements `target_tag`. This is what lets a bare, -/// unqualified `addEventListener('fetch', …)` in a ServiceWorker script reach -/// the global's own `EventTarget` data: an unqualified sloppy-mode call passes -/// `this = undefined`, and SpiderMonkey performs no global substitution for -/// native callees. The substitution is gated on the same inheritance registry -/// the brand check uses, so for any interface the global does not implement, a -/// non-object `this` is rejected exactly as before. +/// Returns the `this` value as an object, except that a null or undefined `this` resolves to the +/// global object when the global implements `target_tag`, per WebIDL `[Global]` operation +/// semantics. An unqualified sloppy-mode call, such as `addEventListener('fetch', …)` in a +/// ServiceWorker script, passes `this = undefined`, and SpiderMonkey performs no global +/// substitution for native callees. For an interface the global does not implement, a non-object +/// `this` is rejected. /// /// # Safety /// @@ -2535,8 +2491,25 @@ pub fn try_create_instance_with<'s, T: ClassDef>( init: impl FnOnce(Object<'s>) -> Result, ) -> Result, ExnThrown> { let proto = registered_prototype::(scope)?; - let class = T::class(); - let obj = Object::new_with_proto(scope, class, proto)?; + create_instance_in(scope, proto, init) +} + +/// Like [`create_instance_with`], for an object whose prototype is `proto` instead of `T`'s +/// registered prototype. +pub fn create_instance_with_proto<'s, T: ClassDef>( + scope: &'s Scope<'_>, + proto: Object<'s>, + init: impl FnOnce(Object<'s>) -> T, +) -> Result, ExnThrown> { + create_instance_in(scope, proto, |obj| Ok(init(obj))) +} + +fn create_instance_in<'s, T: ClassDef>( + scope: &'s Scope<'_>, + proto: Object<'s>, + init: impl FnOnce(Object<'s>) -> Result, +) -> Result, ExnThrown> { + let obj = Object::new_with_proto(scope, T::class(), proto)?; T::install_unforgeable(scope, obj)?; let data = init(obj)?; unsafe { @@ -2584,7 +2557,10 @@ pub fn get_or_init_shared_object<'s>( /// Return, and create on the first call in a given global, an interned property key for `name`. /// -/// `name` must not be an array index, whose property key is an integer rather than a string. +/// # Panics +/// +/// Panics if `name` is an array index, whose property key is an integer rather than a string, or +/// isn't UTF-8. pub fn get_or_init_property_id<'s>( scope: &'s Scope<'_>, name: &'static CStr, @@ -2632,34 +2608,22 @@ pub fn get_or_init_shared_function<'s>( unsafe { crate::Function::from_raw(scope, obj.as_raw()) }.ok_or(ExnThrown) } -/// Read-and-increment a per-global monotonic `u64` counter keyed by `key`. -/// -/// Returns the counter's current value and advances it (wrapping on overflow); -/// counters start at 0. The counter lives on the global's [`ClassRegistry`], so -/// every caller on a given global draws from one sequence — use this for ids a -/// spec requires to be unique per global rather than per call site, such as -/// HTML's setTimeout/setInterval ids, which must not collide across the -/// concurrent event loops that share a single global. +/// Return the scope's global's `T`, which the first call on the global creates with +/// `T::default()`. /// -/// `key` is an arbitrary namespace token; pass the address of a dedicated -/// `static` to get a process-unique key, mirroring how shared functions key on -/// their native pointer. -/// -/// Note: the counter is a u64 instead of a u32 because otherwise it'd be possible -/// (though implausible) for a long-running script to wrap the counter and reuse ids, -/// which would violate the uniqueness guarantee. We convert the id to a JS value, -/// which can represent about 53 bits of integer precision, making wrap-around -/// effectively impossible. -pub fn next_global_counter(scope: &Scope<'_>, key: usize) -> u64 { - // The bump is a HashMap entry plus an integer add — no JS-GC-capable call - // in between — so briefly holding the `&mut ClassRegistry` aliases nothing. +/// The global holds the `T` until it is finalized. `T` must not hold GC things, since the global +/// doesn't trace it. +pub fn global_state(scope: &Scope<'_>) -> std::rc::Rc { + // SAFETY: the scope's global is a live StarlingMonkey global. Nothing in between can run JS + // or GC, so the `&mut ClassRegistry` isn't aliased. let registry = unsafe { get_or_init_class_registry(scope.global().as_raw()) }; - let counter = registry.bump_counter(key); - assert!( - counter < crate::conversion::Number::MAX, - "counter for key {key} wrapped around" - ); - counter + let state = registry + .state + .entry(TypeId::of::()) + .or_insert_with(|| std::rc::Rc::new(T::default())); + std::rc::Rc::clone(state) + .downcast::() + .expect("global state is keyed by its type") } // --------------------------------------------------------------------------- @@ -2810,8 +2774,11 @@ unsafe extern "C" fn error_stack_getter( return false; } let formatted = unsafe { mozjs::jsval::StringValue(&*formatted.get()) }; - // SAFETY: as above. - unsafe { crate::object::set_reserved_slot(obj, ERROR_STACK_SLOT, &formatted) }; + // SAFETY: as above. `obj` is read again from the rooted `this`, since formatting the stack can + // move it. + unsafe { + crate::object::set_reserved_slot(args.thisv().to_object(), ERROR_STACK_SLOT, &formatted) + }; args.rval().set(formatted); true } @@ -2831,7 +2798,7 @@ unsafe extern "C" fn error_stack_setter(cx: *mut RawJSContext, argc: u32, vp: *m let obj = Object::from_raw(&scope, this.to_object()).expect("objects are non-null"); let attrs = crate::class_spec::JSPROP_ENUMERATE as std::ffi::c_uint; if obj - .define_property(&scope, c"stack", *args.get(0), attrs) + .define_property(&scope, c"stack", Handle::from_raw(args.get(0)), attrs) .is_err() { return false; @@ -2929,7 +2896,7 @@ pub fn define_well_known_symbol_property<'s>( /// Adds an alias to the property given by `property` under the symbol `symbol` to the /// prototype of the builtin class `T`. -pub fn add_symbol_alias(scope: &Scope, property: &CStr, symbol: SymbolCode) { +pub fn add_symbol_alias(scope: &Scope, property: &'static CStr, symbol: SymbolCode) { let proto = get_prototype_object_for::(scope).expect("prototype not found"); let values = proto .get_property(scope, property) diff --git a/crates/js/src/compartment.rs b/crates/js/src/compartment.rs index b8a2f2f..bfa6d16 100644 --- a/crates/js/src/compartment.rs +++ b/crates/js/src/compartment.rs @@ -1,11 +1,6 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception -//! Cross-compartment wrapper operations. -//! -//! SpiderMonkey uses compartments to isolate different security domains. -//! When an object from one compartment needs to be accessed from another, -//! a cross-compartment wrapper (CCW) is used. This module provides safe -//! wrappers for CCW management and compartment queries. +//! Cross-compartment wrapper management and compartment queries. use std::ptr::NonNull; @@ -13,19 +8,9 @@ use crate::gc::scope::Scope; use mozjs::gc::HandleObject; use mozjs::jsapi::{CompartmentFilter, JSClass, JSObject}; use mozjs::rust::wrappers2; -use mozjs::rust::MutableHandleObject; use super::error::ExnThrown; -/// Wrap an object for use in the current compartment. -/// -/// If the object is already in the current compartment, this is a no-op. -/// Otherwise, a cross-compartment wrapper is created or reused. -pub fn wrap_object(scope: &Scope<'_>, objp: MutableHandleObject) -> Result<(), ExnThrown> { - let ok = unsafe { wrappers2::JS_WrapObject(scope.cx_mut(), objp) }; - ExnThrown::check(ok) -} - /// Refresh existing cross-compartment wrappers to an object. pub fn refresh_cross_compartment_wrappers( scope: &Scope<'_>, diff --git a/crates/js/src/compile.rs b/crates/js/src/compile.rs index e2b991c..d712313 100644 --- a/crates/js/src/compile.rs +++ b/crates/js/src/compile.rs @@ -84,25 +84,22 @@ fn evaluate_with_options<'s>( lineno: u32, non_syntactic_scope: bool, ) -> Result, ExnThrown> { - let filename_cstr = - CString::new(filename).unwrap_or_else(|_| CString::new("").unwrap()); - let options = options(scope, filename_cstr, lineno); + let options = options(scope, filename_or_unknown(filename), lineno); if non_syntactic_scope { - // SAFETY: `options.ptr` is a valid pointer created by `NewCompileOptions`. - // We are setting the `nonSyntacticScope` field on the base - // `TransitiveCompileOptions` struct, which is safe because the pointer - // is valid and we own it exclusively. + // SAFETY: `options.ptr` is a valid pointer created by `NewCompileOptions`, owned by + // `options`. unsafe { (*options.ptr)._base.nonSyntacticScope = non_syntactic_scope; } } let mut source = transform_str_to_source_text(script); - // SAFETY: we're calling into SpiderMonkey with valid compile options and source. - let mut rval = scope.root_value_mut(crate::value::undefined()); - let ok = - unsafe { wrappers2::Evaluate2(scope.cx_mut(), options.ptr, &mut source, rval.reborrow()) }; - ExnThrown::check(ok)?; - Ok(rval.handle()) + // SAFETY: the options and the source are valid. + unsafe { evaluate_utf8_raw(scope, options.ptr, &mut source) } +} + +/// Returns `filename` as a C string, or `` if it contains a NUL. +fn filename_or_unknown(filename: &str) -> CString { + CString::new(filename).unwrap_or_else(|_| c"".into()) } /// Evaluate a UTF-16 script and return its completion value. @@ -190,14 +187,10 @@ pub fn compile_with_filename<'s>( filename: &str, lineno: u32, ) -> Result, ExnThrown> { - let filename_cstr = - CString::new(filename).unwrap_or_else(|_| CString::new("").unwrap()); - let options = options(scope, filename_cstr, lineno); + let options = options(scope, filename_or_unknown(filename), lineno); let mut source = transform_str_to_source_text(script); - let script = unsafe { wrappers2::Compile1(scope.cx_mut(), options.ptr, &mut source) }; - NonNull::new(script) - .map(|p| scope.root_script(p)) - .ok_or(ExnThrown) + // SAFETY: the options and the source are valid. + unsafe { compile_raw(scope, options.ptr, &mut source) } } /// Compile a UTF-16 script into a `JSScript` without executing it. diff --git a/crates/js/src/conversion.rs b/crates/js/src/conversion.rs index 112859e..e0752d5 100644 --- a/crates/js/src/conversion.rs +++ b/crates/js/src/conversion.rs @@ -50,7 +50,7 @@ use std::{mem, ptr}; use crate::error::{throw_type_error, ExnThrown, ThrowException}; use crate::gc::handle::Heap; -use crate::heap::{MozHeap, Trace}; +use crate::heap::Trace; use crate::prelude::Scope; use crate::value; @@ -132,6 +132,17 @@ impl_num!(f32, 0.0, f32::MIN, f32::MAX); impl_num!(f64, 0.0, f64::MIN, f64::MAX); /// A trait to convert Rust types to `JSVal`s. +/// +/// Every implementing type is rooted or holds no GC thing, so a function that takes +/// `impl ToJSVal` may run code that can GC before it converts the value. A raw `Value` doesn't +/// implement it: root it first, or pass it to a function that takes a raw value, such as +/// [`Object::from_raw_value`](crate::Object::from_raw_value). +/// +/// ```compile_fail,E0277 +/// fn store(scope: &js::gc::scope::Scope<'_>, obj: js::Object<'_>, value: js::native::Value) { +/// obj.set_property(scope, c"name", value).unwrap(); +/// } +/// ``` pub trait ToJSVal<'s> { /// Convert `self` to a rooted `HandleValue`. /// @@ -283,7 +294,7 @@ where let rounded = d.signum() * d.abs().floor(); if D::MIN.cast() <= rounded && rounded <= D::MAX.cast() { - Ok(rounded.cast()) + Ok(As::::cast(rounded)) } else { throw_type_error(scope, c"value out of range in an EnforceRange argument"); Err(()) @@ -325,7 +336,7 @@ where // the correct result. If N is even, plus or minus N is the correct result. let to_truncate = if d < 0.0 { d - 0.5 } else { d + 0.5 }; - let mut truncated: D = to_truncate.cast(); + let mut truncated: D = As::::cast(to_truncate); if truncated.cast() == to_truncate { // It was a tie (since moving away from 0 by 0.5 gave us the exact integer @@ -362,13 +373,6 @@ impl FromJSVal<'_, '_> for JSVal { } } -impl<'s> ToJSVal<'s> for JSVal { - #[inline] - fn to_jsval_raw(&self, _scope: &'_ Scope<'_>) -> Result { - Ok(*self) - } -} - impl<'s> FromJSVal<'s, 's> for HandleValue<'s> { type Config = (); fn from_jsval( @@ -392,18 +396,6 @@ impl<'s> ToJSVal<'s> for HandleValue<'s> { } } -impl<'s> ToJSVal<'s> for MozHeap { - #[inline] - fn to_jsval(&self, scope: &'s Scope<'_>) -> Result, ConversionError> { - Ok(scope.root_value(self.get())) - } - - #[inline] - fn to_jsval_raw(&self, _scope: &'_ Scope<'_>) -> Result { - Ok(self.get()) - } -} - #[inline] fn convert_int_from_jsval( scope: &Scope<'_>, @@ -1004,10 +996,8 @@ fn for_of_iterator_slot(scope: &Scope<'_>) -> ForOfIterator { /// Drive the ES `for...of` protocol over `value`, calling `f` for each element. /// -/// This owns the backing [`ForOfIterator`], so it pins and roots it correctly -/// for the whole iteration; callers never touch the raw rooting machinery and -/// cannot observe a half-rooted iterator. SpiderMonkey's array fast path is -/// preserved, since the engine's own iterator is used. +/// SpiderMonkey's array fast path applies, since the engine's own iterator is +/// used. /// /// Returns `Ok(true)` once iteration completes, or `Ok(false)` if `value` is /// not iterable. `f` receives a handle to a value slot that is reused across diff --git a/crates/js/src/error.rs b/crates/js/src/error.rs index 10fef11..590ebb8 100644 --- a/crates/js/src/error.rs +++ b/crates/js/src/error.rs @@ -39,13 +39,6 @@ use mozjs::rust::Runtime; /// This error type carries no data — it simply signals that a SpiderMonkey /// operation failed and an exception is pending. Use [`ExnThrown::capture`] to /// extract details into a [`CapturedError`]. -/// -/// # Why a unit struct? -/// -/// Most code only needs to propagate errors with `?`. The heavy lifting of -/// extracting error messages, filenames, and stack traces is deferred to the -/// few call sites that actually need it (e.g., error reporters, REPL shells). -/// This keeps `Result` zero-cost in the common case. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ExnThrown; diff --git a/crates/js/src/function.rs b/crates/js/src/function.rs index 4f6543c..6e64229 100644 --- a/crates/js/src/function.rs +++ b/crates/js/src/function.rs @@ -38,7 +38,7 @@ use std::ffi::CStr; use std::os::raw::c_uint; use std::ptr::NonNull; -use super::error::{report_error_ascii, ExnThrown}; +use super::error::{report_error_ascii, ExnThrown, ThrowException}; use crate::builtins::JSType; use crate::conversion::ToJSVal; use crate::gc::handle::Stack; @@ -181,13 +181,17 @@ impl<'s> Stack<'s, Function> { // Function calling // --------------------------------------------------------------------------- - /// Call a function value with the given `this` object and arguments. + /// Call a function value with the given `this` object and arguments. A `fval` that isn't + /// callable throws the `TypeError` of [`throw_not_callable`]. pub fn call_value<'a>( scope: &'a Scope<'_>, this: HandleObject, fval: HandleValue, args: &[impl ToJSVal<'a>], ) -> Result, ExnThrown> { + if !is_callable(fval) { + return Err(throw_not_callable(scope, fval)); + } let mut args_root = ValueArrayRooter::new(args.len()); let args = args_root.root(scope, args)?; let mut rval = scope.root_value_mut(UndefinedValue()); @@ -204,21 +208,32 @@ impl<'s> Stack<'s, Function> { Ok(rval.handle()) } - /// Call a named method on an object. + /// Call a named method on an object. The method is looked up through the global's interned + /// key for `name` (see [`get_or_init_property_id`](crate::class::get_or_init_property_id)). A + /// property that isn't callable throws a `TypeError` that `name` is not a function. Panics if + /// `name` is an array index. pub fn call_by_name<'a>( scope: &'a Scope<'_>, obj: HandleObject, - name: &CStr, + name: &'static CStr, args: &[impl ToJSVal<'a>], ) -> Result, ExnThrown> { let mut args_root = ValueArrayRooter::new(args.len()); let args = args_root.root(scope, args)?; + let id = crate::class::get_or_init_property_id(scope, name)?; + let mut fval = scope.root_value_mut(UndefinedValue()); + let ok = unsafe { wrappers2::JS_GetPropertyById(scope.cx_mut(), obj, id, fval.reborrow()) }; + ExnThrown::check(ok)?; + if !is_callable(fval.handle()) { + let message = format!("{} is not a function", name.to_string_lossy()); + return Err(crate::error::TypeError(message).throw(scope)); + } let mut rval = scope.root_value_mut(UndefinedValue()); let ok = unsafe { - wrappers2::JS_CallFunctionName( + wrappers2::JS_CallFunctionValue( scope.cx_mut(), obj, - name.as_ptr(), + fval.handle(), &args.handles(), rval.reborrow(), ) @@ -227,25 +242,24 @@ impl<'s> Stack<'s, Function> { Ok(rval.handle()) } - /// Call a function object with a given `this` value. + /// Call a function object with a given `this` value. A `fun` that isn't callable throws the + /// `TypeError` of [`throw_not_callable`]. pub fn call<'a>( scope: &'a Scope<'_>, this: impl ToJSVal<'a>, fun: impl ToJSVal<'a>, args: &[impl ToJSVal<'a>], ) -> Result, ExnThrown> { + let this = this.to_jsval_throwing(scope)?; + let fun = fun.to_jsval_throwing(scope)?; + if !is_callable(fun) { + return Err(throw_not_callable(scope, fun)); + } let mut args_root = ValueArrayRooter::new(args.len()); let args = args_root.root(scope, args)?; let mut rval = scope.root_value_mut(UndefinedValue()); - let ok = unsafe { - wrappers2::Call( - scope.cx_mut(), - this.to_jsval_throwing(scope)?, - fun.to_jsval_throwing(scope)?, - &args.handles(), - rval.reborrow(), - ) - }; + let ok = + unsafe { wrappers2::Call(scope.cx_mut(), this, fun, &args.handles(), rval.reborrow()) }; ExnThrown::check(ok)?; Ok(rval.handle()) } @@ -256,16 +270,12 @@ impl<'s> Stack<'s, Function> { fun: impl ToJSVal<'s>, args: &[impl ToJSVal<'s>], ) -> Result, ExnThrown> { + let fun = fun.to_jsval_throwing(scope)?; let mut args_root = ValueArrayRooter::new(args.len()); let args = args_root.root(scope, args)?; let mut result = scope.root_object_mut(std::ptr::null_mut()); let ok = unsafe { - wrappers2::Construct1( - scope.cx_mut(), - fun.to_jsval_throwing(scope)?, - &args.handles(), - result.reborrow(), - ) + wrappers2::Construct1(scope.cx_mut(), fun, &args.handles(), result.reborrow()) }; ExnThrown::check(ok)?; Object::from_handle(result.handle()).ok_or(ExnThrown) @@ -278,13 +288,14 @@ impl<'s> Stack<'s, Function> { new_target: HandleObject, args: &[impl ToJSVal<'s>], ) -> Result, ExnThrown> { + let fun = fun.to_jsval_throwing(scope)?; let mut args_root = ValueArrayRooter::new(args.len()); let args = args_root.root(scope, args)?; let mut result = scope.root_object_mut(std::ptr::null_mut()); let ok = unsafe { wrappers2::Construct( scope.cx_mut(), - fun.to_jsval_throwing(scope)?, + fun, new_target, &args.handles(), result.reborrow(), @@ -383,6 +394,7 @@ impl<'s> Stack<'s, Function> { cb: Callback, payload: impl ToJSVal<'s>, ) -> Result { + let payload = payload.to_jsval_throwing(scope)?; let fun = Self::new_with_reserved(scope, Some(callback_trampoline), nargs, flags, name)?; unsafe { @@ -390,7 +402,7 @@ impl<'s> Stack<'s, Function> { ReservedSlot::Slot0, mozjs::jsval::PrivateValue(cb as *const std::ffi::c_void), ); - fun.set_reserved(ReservedSlot::Slot1, payload.to_jsval_raw_throwing(scope)?); + fun.set_reserved(ReservedSlot::Slot1, payload.get()); } Ok(fun) @@ -569,6 +581,29 @@ impl<'a> CallbackArgs<'a> { } } +/// Whether `value` is a callable object, as ECMAScript's `IsCallable` tests. +#[inline] +pub fn is_callable(value: HandleValue<'_>) -> bool { + // SAFETY: `value` is rooted, so an object it holds is live. + value.is_object() && unsafe { mozjs::jsapi::JS::IsCallable(value.to_object()) } +} + +/// Throw the `TypeError` for a call of `value`, which isn't callable: `undefined`, or `value`'s +/// source as `JS_ValueToSource` gives it, followed by `is not a function`. +pub fn throw_not_callable(scope: &Scope<'_>, value: HandleValue<'_>) -> ExnThrown { + let source = if value.is_undefined() { + "undefined".to_string() + } else { + // SAFETY: `value` is rooted. + let source = unsafe { wrappers2::JS_ValueToSource(scope.cx_mut(), value) }; + let Some(source) = NonNull::new(source) else { + return ExnThrown; + }; + crate::conversion::jsstr_to_string(scope, source) + }; + crate::error::TypeError(format!("{source} is not a function")).throw(scope) +} + /// Type-erased callback stored in a helper function's reserved slot. pub type Callback = fn(&Scope<'_>, CallbackArgs<'_>, HandleValue) -> Result; @@ -577,12 +612,12 @@ pub type Callback = fn(&Scope<'_>, CallbackArgs<'_>, HandleValue) -> Result(scope: &'r Scope<'_>, payload: HandleValue<'_>) -> T where T: crate::builtins::CastTarget<'r, Output = T>, { - Object::from_value(scope, *payload) + Object::from_raw_value(scope, payload.get()) .expect("callback payload is an object") .cast::() .unwrap_or_else(|e| panic!("callback payload: {e}")) diff --git a/crates/js/src/gc/handle.rs b/crates/js/src/gc/handle.rs index 29396c8..965b52d 100644 --- a/crates/js/src/gc/handle.rs +++ b/crates/js/src/gc/handle.rs @@ -184,7 +184,7 @@ macro_rules! from_jsval_via_cast { val: mozjs::gc::HandleValue<'v>, _option: Self::Config, ) -> Result { - $crate::Object::from_value(scope, *val)? + $crate::Object::from_raw_value(scope, val.get())? .cast::() .map_err(|_| { $crate::conversion::ConversionError::Failure(std::borrow::Cow::Borrowed( diff --git a/crates/js/src/gc/pool.rs b/crates/js/src/gc/pool.rs index 8055c3f..a17482c 100644 --- a/crates/js/src/gc/pool.rs +++ b/crates/js/src/gc/pool.rs @@ -154,7 +154,7 @@ impl Drop for ScopeAlloc { /// registered in the pool's intrusive linked list of active allocators so that the GC tracer can /// discover its slots. pub struct PrivatePages { - /// The pool this allocator borrows pages from. + /// The pool from which this allocator borrows pages. pool: *const HandlePool, /// Current page being allocated into. `None` until the first allocation. @@ -392,7 +392,7 @@ pub struct HandlePool { /// The number of slots in use on the root stack. top: Cell, - /// One entry per scope, innermost last. Entries of dropped scopes stay until every scope + /// One entry per scope, innermost last. Entries of dropped scopes are kept until every scope /// created after them has dropped. levels: UnsafeCell>, @@ -412,7 +412,7 @@ pub struct HandlePool { root_tracers: UnsafeCell>, } -/// The most released private allocations the pool keeps. +/// The maximum number of released private allocations the pool keeps. const MAX_FREE_ALLOCS: usize = 32; /// This module's share of the crate's thread-local state. See [`crate::tls`]. diff --git a/crates/js/src/gc/scope.rs b/crates/js/src/gc/scope.rs index 2783cb7..0de4a3a 100644 --- a/crates/js/src/gc/scope.rs +++ b/crates/js/src/gc/scope.rs @@ -96,7 +96,6 @@ pub struct EnteredRealm(()); /// `'cx` is the lifetime of the parent context or scope. /// /// [`ScopeAlloc`]: mozjs::gc::pool::ScopeAlloc -/// [`HandlePool`]: mozjs::gc::pool::HandlePool pub struct Scope<'cx> { /// Raw pointer to the JSContext, wrapped in `UnsafeCell` for interior /// mutability (same rationale as `RootScope`). diff --git a/crates/js/src/id.rs b/crates/js/src/id.rs index 2bd7651..fe3ae45 100644 --- a/crates/js/src/id.rs +++ b/crates/js/src/id.rs @@ -57,7 +57,7 @@ pub fn mark_cross_zone_id(scope: &Scope<'_>, id: jsid) { unsafe { wrappers2::JS_MarkCrossZoneId(scope.cx(), id) } } -/// Convert getter/setter property ids. +/// Convert to getter property id. pub fn to_getter_id(scope: &Scope<'_>, id: Handle) -> Result { rooted!(in(unsafe { scope.raw_cx_no_gc() }) let mut getter_id = PropertyKey::default()); let ok = unsafe { wrappers2::ToGetterId(scope.cx_mut(), id, getter_id.handle_mut()) }; diff --git a/crates/js/src/iteration.rs b/crates/js/src/iteration.rs index 1e13ca0..73e78eb 100644 --- a/crates/js/src/iteration.rs +++ b/crates/js/src/iteration.rs @@ -17,7 +17,7 @@ //! value on the record and chaining the wrap continuation on the shared //! resolved trigger ([`Promise::shared_resolved_undefined`]). -use crate::class::{get_or_init_shared_function, get_prototype_for}; +use crate::class::get_or_init_shared_function; use crate::conversion::{FromJSVal, ToJSVal}; use crate::error::ExnThrown; use crate::function::{cast_payload, EmptyArgs}; @@ -64,8 +64,8 @@ pub struct AsyncIteratorRecord { impl AsyncIteratorRecord<'_> { /// `GetIterator(iterable, async)`: populate the iterator record, falling /// back to the sync iterator (wrapped via `afs_next`) when there is no - /// `Symbol.asyncIterator` method. Use [`get_async_iterator`], which also - /// registers this hidden class on the global. + /// `Symbol.asyncIterator` method. Use [`get_async_iterator`]. The global + /// must have registered this hidden class with `add_to_global`. fn new(&self, scope: &Scope<'_>, iterable: HandleValue<'_>) -> Result<(), ExnThrown> { // The method lookups use the boxed value (so primitive iterables such as // strings work), but each iterator factory is called with the original @@ -120,15 +120,12 @@ impl AsyncIteratorRecord<'_> { /// `GetIterator(iterable, async)` with the `CreateAsyncFromSyncIterator` /// fallback: build an [`AsyncIteratorRecord`] for `iterable`, throwing a -/// `TypeError` when it is not (async or sync) iterable. Registers the hidden -/// record class on the scope's global on first use. +/// `TypeError` when it is not (async or sync) iterable. The global must have +/// registered the hidden record class with `AsyncIteratorRecord::add_to_global`. pub fn get_async_iterator<'s>( scope: &'s Scope<'_>, iterable: HandleValue<'_>, ) -> Result, ExnThrown> { - if get_prototype_for::(scope).is_none() { - AsyncIteratorRecord::add_to_global(scope, scope.global()); - } AsyncIteratorRecord::new(scope, iterable) } @@ -154,9 +151,9 @@ impl<'s> AsyncIteratorRecord<'s> { let return_method = iterator.get_property(scope, c"return")?; // If undefined, return a promise resolved with undefined. if return_method.is_null_or_undefined() { - return Promise::new_resolved_with_value(scope, value::undefined()); + return Promise::new_resolved_with_value(scope, HandleValue::undefined()); } - if !is_callable(scope, return_method) { + if !crate::function::is_callable(return_method) { crate::error::throw_type_error(scope, c"iterator return is not callable"); return Promise::new_rejected_with_pending_error(scope); } @@ -203,12 +200,6 @@ fn return_result_check( Ok(value::undefined()) } -/// Whether `v` is callable, without throwing — the `IsCallable` test for the -/// `GetMethod`-style method lookups. -fn is_callable(scope: &Scope<'_>, v: HandleValue<'_>) -> bool { - Object::from_value(scope, *v).is_ok_and(|o| o.is_callable()) -} - /// `GetMethod`-style check: undefined/null yields `None`; a non-callable /// throws a `TypeError`; a callable is returned. pub(crate) fn ensure_is_method<'r>( @@ -218,7 +209,7 @@ pub(crate) fn ensure_is_method<'r>( if v.is_null_or_undefined() { return Ok(None); } - if !is_callable(scope, v) { + if !crate::function::is_callable(v) { return Err(crate::error::throw_type_error( scope, c"iterator method is not callable", @@ -331,11 +322,11 @@ fn afs_value_rejected( let record = cast_payload::(scope, payload); let error = args.get(0); let sync_iter = record.data().iterator.get(scope); - // `IteratorClose`: call the iterator's `return` (a missing `return` is a no-op). + // `IteratorClose`: call the iterator's `return` (nothing happens if it is missing). // Its outcome is discarded — when the incoming completion is a throw, the // original error wins, so a throwing or absent `return` cannot mask it. if let Ok(return_method) = sync_iter.get_property(scope, c"return") { - if is_callable(scope, return_method) { + if crate::function::is_callable(return_method) { let _ = Function::call(scope, sync_iter, return_method, EmptyArgs); } } @@ -402,6 +393,7 @@ pub fn create_iter_result<'r>( done: bool, ) -> Result, ExnThrown> { let attrs = crate::class_spec::JSPROP_ENUMERATE as std::ffi::c_uint; + let value = value.to_jsval_throwing(scope)?; let obj = Object::new_plain(scope)?; let value_id = crate::class::get_or_init_property_id(scope, c"value")?; obj.define_value_by_id(scope, value_id, value, attrs)?; @@ -507,7 +499,7 @@ pub fn iter_result_object<'r>( scope: &'r Scope<'_>, result: HandleValue<'_>, ) -> Result, ExnThrown> { - Object::from_value(scope, *result) + Object::from_raw_value(scope, result.get()) .map_err(|_| crate::error::throw_type_error(scope, c"iterator result is not an object")) } diff --git a/crates/js/src/jobs.rs b/crates/js/src/jobs.rs index 6a20c1e..d460bc8 100644 --- a/crates/js/src/jobs.rs +++ b/crates/js/src/jobs.rs @@ -5,16 +5,6 @@ //! SpiderMonkey requires a job queue to execute promise continuations and other //! microtasks. This module provides wrappers for configuring and draining the //! queue. -//! -//! # Quick Start -//! -//! ```ignore -//! // Use SpiderMonkey's built-in job queue (simplest option): -//! jobs::use_internal_job_queues(cx)?; -//! -//! // After evaluating JS that creates promises, drain the queue: -//! jobs::run_jobs(cx); -//! ``` use std::cell::RefCell; @@ -109,7 +99,7 @@ unsafe extern "C" fn track_rejection( _data: *mut std::ffi::c_void, ) { // SAFETY: the engine calls the tracker with a valid context. A realm is entered whenever a - // promise is rejected or gets a handler, and the check covers any other call. + // promise is rejected or gets a handler. The check covers any other call. if unsafe { mozjs::jsapi::GetCurrentRealmOrNull(cx) }.is_null() { return; } diff --git a/crates/js/src/json.rs b/crates/js/src/json.rs index 5d87503..5be889f 100644 --- a/crates/js/src/json.rs +++ b/crates/js/src/json.rs @@ -79,7 +79,7 @@ pub fn parse_js_string_with_reviver<'r>( /// /// [stringify]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify /// -/// Returns `None` where `JSON.stringify` returns `undefined`. +/// Returns `None` if `JSON.stringify` returns `undefined`. // TODO: consider turning `replacer` and `space` into enums. pub fn stringify( scope: &Scope<'_>, diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index 9a060f7..7833047 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -47,7 +47,6 @@ //! | [`date`] | Date object creation and queries | //! | [`compile`] | Script compilation and evaluation | //! | [`module`] | ES module compilation, linking, evaluation | -//! | [`realm`] | Realm context trait, utilities, and built-in prototypes | //! | [`gc`] | Garbage collection control | //! | [`exception`] | Pending exception management | //! | [`class`] | JSClass definition and standard classes | @@ -154,7 +153,7 @@ pub mod engine { /// The monotonic clock the engine reads. /// /// Only on wasm, where the clock is scoped to the instance and an instance resuming from a Wizer -/// snapshot has to be given the reading the snapshot ended at. +/// snapshot has to be given the clock reading at which the snapshot ended. #[cfg(target_arch = "wasm32")] pub mod clock { /// Add `nanoseconds` to every monotonic clock reading the engine takes afterwards. @@ -222,15 +221,10 @@ pub mod heap { pub use mozjs::rust::Trace; } -/// Module compilation and source text utilities. -/// -/// Re-exports of low-level module compilation functions that are not -/// (yet) wrapped by the higher-level `js::module` API. +/// Module and source text functions that `js::module` doesn't wrap. pub mod module_raw { - pub use mozjs::jsapi::{ - CompileModule1, GetModuleRequestSpecifier, JSRuntime, SetModulePrivate, - }; - pub use mozjs::rust::{transform_str_to_source_text, wrappers2::GetModuleEnvironment}; + pub use mozjs::jsapi::SetModulePrivate; + pub use mozjs::rust::transform_str_to_source_text; } pub use macros::must_root; diff --git a/crates/js/src/object.rs b/crates/js/src/object.rs index 28a2a0e..1a24091 100644 --- a/crates/js/src/object.rs +++ b/crates/js/src/object.rs @@ -121,7 +121,14 @@ impl<'s> Stack<'s, Object> { scope: &'s Scope<'_>, val: impl ToJSVal<'s>, ) -> Result { - let val = val.to_jsval_raw(scope)?; + Self::from_raw_value(scope, val.to_jsval_raw(scope)?) + } + + /// Root the object `val` holds, or fail if it holds something else. + /// + /// `val` must be read from a rooted location, such as a handle, right before the call, with + /// nothing that can GC in between, or hold no GC thing. + pub fn from_raw_value(scope: &'s Scope<'_>, val: Value) -> Result { if val.is_object() { // SAFETY: the value holds an object, so the pointer is a non-null JSObject. Ok(unsafe { Self::from_raw(scope, val.to_object()) }.unwrap()) @@ -146,9 +153,22 @@ impl<'s> Stack<'s, Object> { // Property access // ----------------------------------------------------------------------- - /// Get a property by name. + /// Get a property by name, through the global's interned key for `name` (see + /// [`get_or_init_property_id`](crate::class::get_or_init_property_id)). Panics if `name` is an + /// array index. #[inline] pub fn get_property<'a>( + &self, + scope: &'a Scope<'_>, + name: &'static CStr, + ) -> Result, ExnThrown> { + let id = crate::class::get_or_init_property_id(scope, name)?; + self.get_property_by_id(scope, id) + } + + /// Get a property by a name that isn't `'static`. The name is atomized on each call. + #[inline] + pub fn get_property_by_name<'a>( &self, scope: &'a Scope<'_>, name: &CStr, @@ -196,9 +216,23 @@ impl<'s> Stack<'s, Object> { Ok(rval.handle()) } - /// Set a property by name. + /// Set a property by name, through the global's interned key for `name` (see + /// [`get_or_init_property_id`](crate::class::get_or_init_property_id)). Panics if `name` is an + /// array index. #[inline] pub fn set_property<'v>( + &self, + scope: &'v Scope<'_>, + name: &'static CStr, + value: impl ToJSVal<'v>, + ) -> Result<(), ExnThrown> { + let id = crate::class::get_or_init_property_id(scope, name)?; + self.set_property_by_id(scope, id, value) + } + + /// Set a property by a name that isn't `'static`. The name is atomized on each call. + #[inline] + pub fn set_property_by_name<'v>( &self, scope: &'v Scope<'_>, name: &CStr, @@ -253,9 +287,19 @@ impl<'s> Stack<'s, Object> { ExnThrown::check(ok) } - /// Check whether this object has a property with the given name. + /// Check whether this object has a property with the given name, through the global's + /// interned key for `name` (see [`get_or_init_property_id`](crate::class::get_or_init_property_id)). + /// Panics if `name` is an array index. + #[inline] + pub fn has_property(&self, scope: &Scope<'_>, name: &'static CStr) -> Result { + let id = crate::class::get_or_init_property_id(scope, name)?; + self.has_property_by_id(scope, id) + } + + /// Check whether this object has a property with a name that isn't `'static`. The name is + /// atomized on each call. #[inline] - pub fn has_property(&self, scope: &Scope<'_>, name: &CStr) -> Result { + pub fn has_property_by_name(&self, scope: &Scope<'_>, name: &CStr) -> Result { let mut found = false; let ok = unsafe { wrappers2::JS_HasProperty(scope.cx_mut(), self.handle(), name.as_ptr(), &mut found) @@ -284,9 +328,27 @@ impl<'s> Stack<'s, Object> { Ok(found) } - /// Check whether this object has an own property with the given name. + /// Check whether this object has an own property with the given name, through the global's + /// interned key for `name` (see [`get_or_init_property_id`](crate::class::get_or_init_property_id)). + /// Panics if `name` is an array index. + #[inline] + pub fn has_own_property( + &self, + scope: &Scope<'_>, + name: &'static CStr, + ) -> Result { + let id = crate::class::get_or_init_property_id(scope, name)?; + self.has_own_property_by_id(scope, id) + } + + /// Check whether this object has an own property with a name that isn't `'static`. The name + /// is atomized on each call. #[inline] - pub fn has_own_property(&self, scope: &Scope<'_>, name: &CStr) -> Result { + pub fn has_own_property_by_name( + &self, + scope: &Scope<'_>, + name: &CStr, + ) -> Result { let mut found = false; let ok = unsafe { wrappers2::JS_HasOwnProperty(scope.cx_mut(), self.handle(), name.as_ptr(), &mut found) @@ -310,9 +372,23 @@ impl<'s> Stack<'s, Object> { Ok(found) } - /// Delete a property by name. + /// Delete a property by key. + pub fn delete_property_by_id(&self, scope: &Scope<'_>, id: HandleId) -> Result<(), ExnThrown> { + let ok = unsafe { wrappers2::JS_DeletePropertyById1(scope.cx_mut(), self.handle(), id) }; + ExnThrown::check(ok) + } + + /// Delete a property by name, through the global's interned key for `name`. Panics if `name` + /// is an array index. #[inline] - pub fn delete_property(&self, scope: &Scope<'_>, name: &CStr) -> Result<(), ExnThrown> { + pub fn delete_property(&self, scope: &Scope<'_>, name: &'static CStr) -> Result<(), ExnThrown> { + let id = crate::class::get_or_init_property_id(scope, name)?; + self.delete_property_by_id(scope, id) + } + + /// Delete a property by a name that isn't `'static`. The name is atomized on each call. + #[inline] + pub fn delete_property_by_name(&self, scope: &Scope<'_>, name: &CStr) -> Result<(), ExnThrown> { let ok = unsafe { wrappers2::JS_DeleteProperty1(scope.cx_mut(), self.handle(), name.as_ptr()) }; ExnThrown::check(ok) @@ -343,9 +419,24 @@ impl<'s> Stack<'s, Object> { // Property definition // ----------------------------------------------------------------------- - /// Define a property with a JS value and attribute flags. + /// Define a property with a JS value and attribute flags, through the global's interned key + /// for `name`. Panics if `name` is an array index. #[inline] pub fn define_property<'v>( + &self, + scope: &'v Scope<'_>, + name: &'static CStr, + value: impl ToJSVal<'v>, + attrs: c_uint, + ) -> Result<(), ExnThrown> { + let id = crate::class::get_or_init_property_id(scope, name)?; + self.define_value_by_id(scope, id, value, attrs) + } + + /// Like [`define_property`](Self::define_property), for a name that isn't + /// `'static`. The name is atomized on each call. + #[inline] + pub fn define_property_by_name<'v>( &self, scope: &'v Scope<'_>, name: &CStr, @@ -640,21 +731,6 @@ impl<'s> Stack<'s, Object> { ExnThrown::check(ok) } - /// Get the property keys of this object. - /// - /// `flags` controls which properties to include (e.g., - /// `JSITER_OWNONLY`, `JSITER_HIDDEN`). - // TODO: this should return a vector of ids instead of taking a MutableHandle outparam. - pub fn get_property_keys( - &self, - scope: &Scope<'_>, - flags: c_uint, - props: mozjs::jsapi::MutableHandleIdVector, - ) -> Result<(), ExnThrown> { - let ok = unsafe { wrappers2::GetPropertyKeys(scope.cx_mut(), self.handle(), flags, props) }; - ExnThrown::check(ok) - } - /// Wrap this object for use in the current compartment. pub fn wrap_object(&self, scope: &'s Scope<'_>) -> Result, ExnThrown> { let mut wrapped = scope.root_object_mut(self.handle().get()); @@ -684,7 +760,7 @@ impl<'s, 'v> FromJSVal<'s, 'v> for Stack<'s, Object> { val: HandleValue<'v>, _option: Self::Config, ) -> Result { - Self::from_value(scope, *val) + Self::from_raw_value(scope, val.get()) } } diff --git a/crates/js/src/prelude.rs b/crates/js/src/prelude.rs index 23c2aa2..5a1d74b 100644 --- a/crates/js/src/prelude.rs +++ b/crates/js/src/prelude.rs @@ -31,6 +31,3 @@ pub use super::try_catch::TryCatch; // Re-export the callback args type for closure-based callbacks. pub use super::function::CallbackArgs; - -// Re-export the runtime / engine types for convenience. -pub use mozjs::rust::{JSEngine, Runtime}; diff --git a/crates/js/src/promise.rs b/crates/js/src/promise.rs index 98347bc..2844998 100644 --- a/crates/js/src/promise.rs +++ b/crates/js/src/promise.rs @@ -116,7 +116,7 @@ impl<'s> Stack<'s, Promise> { scope, std::ptr::from_ref(&KEY) as usize, |scope| { - let p = Self::new_resolved_with_value(scope, crate::value::undefined())?; + let p = Self::new_resolved_with_value(scope, HandleValue::undefined())?; Ok(*p) }, )?; @@ -378,8 +378,8 @@ impl<'s> Stack<'s, Promise> { pending_futures(|f| f.borrow_mut().push(pending)); } - /// [`Promise::spawn`], after polling the future once, now, with a waker that - /// does nothing. The work of the future's first poll, such as starting host + /// Like [`Promise::spawn`], but polls the future once right away, with a waker + /// that does nothing. The work of the future's first poll, such as starting host /// I/O, happens before this returns. The event loop's later polls register its /// own waker, and settle the promise as for [`Promise::spawn`]. /// @@ -445,7 +445,7 @@ impl<'s, T> PromiseOf<'s, T> { // microtask; resolving with a thenable defers it to that thenable's resolution. let on_fulfilled = crate::class::get_or_init_shared_function(scope, check as usize, |scope| { - crate::Function::new_callback(scope, c"", 1, check, crate::value::undefined()) + crate::Function::new_callback(scope, c"", 1, check, HandleValue::undefined()) })?; // A `None` rejection reaction forwards the rejection unchanged, which is what the spec's // `onRejectedSteps` amount to for a caller that supplies no rejection steps. diff --git a/crates/js/src/stack.rs b/crates/js/src/stack.rs index be24d14..33b7dc4 100644 --- a/crates/js/src/stack.rs +++ b/crates/js/src/stack.rs @@ -29,8 +29,8 @@ use std::rc::Rc; use super::error::ExnThrown; crate::instance_local! { - /// The context [`current_origin`] captures stacks on, and null while origins are not being - /// recorded. + /// The context on which [`current_origin`] captures stacks, and null while origins are not + /// being recorded. static ORIGIN_CX: Cell<*mut RawJSContext> = const { Cell::new(std::ptr::null_mut()) }; } @@ -44,14 +44,14 @@ pub unsafe fn record_origins(cx: Option<*mut RawJSContext>) { ORIGIN_CX.with(|cell| cell.set(cx.unwrap_or(std::ptr::null_mut()))); } -/// The current JS call stack, formatted as `Error.prototype.stack` formats one, for describing +/// The current JS call stack, in the format of `Error.prototype.stack`, for describing /// where asynchronous work being created comes from. /// /// Returns `None` while [`record_origins`] is not recording, when no realm is entered or no /// script is running, and when capturing the stack fails. A failed capture's exception is /// cleared. pub fn current_origin() -> Option> { - // The most frames an origin holds, counted from the call that created the work. + // The maximum number of frames an origin holds, counted from the call that created the work. const MAX_FRAMES: u32 = 16; let cx = ORIGIN_CX.with(Cell::get); diff --git a/crates/js/src/tls.rs b/crates/js/src/tls.rs index 76a347a..974031b 100644 --- a/crates/js/src/tls.rs +++ b/crates/js/src/tls.rs @@ -4,8 +4,7 @@ //! //! Each module that needs thread-local state owns a struct of it here and //! reaches it through [`with`]. One key for the crate means one destructor, so -//! the fields below drop in the order they are declared and anything reasoning -//! about teardown order has one `js` entry to consider rather than eight. +//! the fields below drop in the order they are declared. //! //! Every field keeps its own `RefCell` or `Cell`, so a borrow covers one piece //! of state rather than all of it. @@ -16,9 +15,6 @@ use crate::module::ModuleTls; use crate::promise::PromiseTls; /// The whole crate's thread-local state. -/// -/// The key's destructor drops these fields in declaration order, so ordering -/// requirements are easy to express and reason about. #[crate::allow_unrooted_interior] pub(crate) struct JsTls { pub(crate) promise: PromiseTls, diff --git a/crates/js/src/typedarray.rs b/crates/js/src/typedarray.rs index cf4b657..c487854 100644 --- a/crates/js/src/typedarray.rs +++ b/crates/js/src/typedarray.rs @@ -23,12 +23,23 @@ //! let buf = ArrayBuffer::new(&scope, 1024)?; //! ``` //! -//! # Safety +//! # Byte access //! -//! Methods that hand out raw slices into a buffer's backing store -//! (`data`, `data_mut`) are `unsafe`: callers must ensure that no GC runs -//! and the buffer is not detached or transferred while the slice is live. -//! Use [`copy_bytes`](Stack::copy_bytes) when you want an owned copy. +//! A buffer's bytes are borrowed only for the duration of a closure: `with_bytes` and +//! `with_bytes_mut` on the handles, and `with_slice` and `with_slice_mut` on typed arrays. The +//! closure must be `Send`. A `Scope`, a rooted handle, a `Heap` and a borrow of a class's private +//! data are not `Send`, so the closure can't capture them, and no GC, JS call or detach can happen +//! while it runs. The slice can't outlive the closure. Closures that need a fallible copy use +//! [`try_copy`]. `copy_bytes` returns an owned copy and reports out of memory if allocating it +//! fails. +//! +//! A closure that uses the scope doesn't compile: +//! +//! ```compile_fail,E0277 +//! fn latin1<'s>(scope: &'s js::gc::scope::Scope<'_>, view: js::ArrayBufferView<'_>) { +//! view.with_bytes(|bytes| js::JSString::from_latin1(scope, bytes).is_ok()); +//! } +//! ``` //! //! The underlying SpiderMonkey element-type tags //! (`mozjs::typedarray::{Uint8, Int32, …}`) and the generic @@ -103,6 +114,13 @@ impl JSType for ArrayBuffer { fn js_class() -> *const JSClass { crate::class::proto_key_to_class(JSProtoKey::JSProto_ArrayBuffer) } + + /// Fixed-length and resizable `ArrayBuffer`s have different classes. + #[inline] + unsafe fn is_instance(obj: *mut JSObject) -> bool { + // SAFETY: `obj` is a valid object pointer, per the trait's contract. + unsafe { JS::IsArrayBufferObject(obj) } + } } impl<'s> Stack<'s, ArrayBuffer> { @@ -171,8 +189,8 @@ impl<'s> Stack<'s, ArrayBuffer> { // External ArrayBuffer contents must be aligned to the engine's `ARRAY_BUFFER_ALIGNMENT`. const ARRAY_BUFFER_ALIGNMENT: usize = 8; let mut boxed = Box::new(data); - // The contents pointer is read off the *boxed* value — the address the free - // callback's box will own — never the pre-move one. + // The contents pointer is read off the boxed value, never the pre-move one: the free + // callback's box owns the boxed address. let slice = boxed.as_mut_slice(); let len = slice.len(); // An external buffer needs non-null contents; an empty body is a plain empty buffer. @@ -227,50 +245,66 @@ impl<'s> Stack<'s, ArrayBuffer> { unsafe { JS::IsDetachedArrayBufferObject(self.as_raw()) } } - /// Borrow the buffer's backing bytes. - /// - /// # Safety - /// - /// No GC may run and the buffer must not be detached or transferred - /// while the returned slice is live. - pub unsafe fn bytes(self) -> &'s [u8] { - let (ptr, len) = array_buffer_length_and_data(self.as_raw()); - if ptr.is_null() || len == 0 { - &[] - } else { - std::slice::from_raw_parts(ptr, len) - } + /// Call `f` with the buffer's bytes, which are empty for a detached buffer. See the + /// [module docs](self#byte-access). + pub fn with_bytes(self, f: impl FnOnce(&[u8]) -> R + Send) -> R { + // SAFETY: `self` is a live `ArrayBuffer`, and `f` can't run a GC or detach it. + f(unsafe { slice_from_parts(array_buffer_length_and_data(self.as_raw())) }) + } + + /// Call `f` with the buffer's bytes, mutably. See [`Self::with_bytes`]. + pub fn with_bytes_mut(self, f: impl FnOnce(&mut [u8]) -> R + Send) -> R { + // SAFETY: as in `with_bytes`. No other reference to the bytes exists while `f` runs. + f(unsafe { slice_from_parts_mut(array_buffer_length_and_data(self.as_raw())) }) + } + + /// A copy of the buffer's bytes. Failing to allocate it reports out of memory. + pub fn copy_bytes(self, scope: &Scope<'_>) -> Result, ExnThrown> { + copy_or_report(scope, self.with_bytes(try_copy)) } - /// Mutably borrow the buffer's backing bytes. + /// Copy `count` bytes from `src` at `src_start` into this buffer at `dest_start`. The buffers + /// can be the same buffer, and the ranges can overlap. /// - /// # Safety + /// # Panics /// - /// No GC may run and the buffer must not be detached or transferred - /// while the returned slice is live. Concurrent access via another - /// reference is undefined behaviour. - #[allow(clippy::mut_from_ref)] - pub unsafe fn bytes_mut(self) -> &'s mut [u8] { - let (ptr, len) = array_buffer_length_and_data(self.as_raw()); - if ptr.is_null() || len == 0 { - &mut [] - } else { - std::slice::from_raw_parts_mut(ptr, len) + /// Panics if either range doesn't lie within its buffer. + pub fn copy_region_from( + self, + dest_start: usize, + src: Stack<'_, ArrayBuffer>, + src_start: usize, + count: usize, + ) { + // SAFETY: both are live `ArrayBuffer`s, and nothing runs between reading the pointers and + // copying. + let ((dest, dest_len), (from, src_len)) = unsafe { + ( + array_buffer_length_and_data(self.as_raw()), + array_buffer_length_and_data(src.as_raw()), + ) + }; + assert!(dest_start + .checked_add(count) + .is_some_and(|end| end <= dest_len)); + assert!(src_start + .checked_add(count) + .is_some_and(|end| end <= src_len)); + if count != 0 { + // SAFETY: both ranges lie within their buffers, which are non-empty, so neither + // pointer is null. `ptr::copy` allows the ranges to overlap. + unsafe { std::ptr::copy(from.add(src_start), dest.add(dest_start), count) }; } } - /// Copy the buffer's contents into an owned `Vec`. - pub fn copy_bytes(self) -> Vec { - unsafe { self.bytes() }.to_vec() - } - /// Transfer this buffer's contents into a new `ArrayBuffer`, detaching the /// receiver. /// /// This is the WHATWG abstract operation `TransferArrayBuffer`: the returned /// buffer holds the receiver's bytes and the receiver is left detached - /// (zero-length). The receiver must not already be detached. The contents are - /// moved, not copied. + /// (zero-length). The receiver must not already be detached. Contents the + /// engine allocated are moved. The engine copies the contents of an external, + /// inline or user-owned buffer, such as one created by [`Self::from_external`]. pub fn transfer(self, scope: &'s Scope<'_>) -> Result { let length = self.byte_length(); // A zero-length buffer may have no contents to steal. Hand back a fresh empty buffer @@ -308,16 +342,7 @@ impl<'s> Stack<'s, ArrayBuffer> { length: usize, ) -> Result { let out = Self::new(scope, length)?; - if length != 0 { - // SAFETY: both buffers are live and non-detached, the source region - // is within bounds (caller-validated), and no GC runs between the - // two borrows because no allocation happens here. - unsafe { - let src = self.bytes(); - let dst = out.bytes_mut(); - dst.copy_from_slice(&src[byte_offset..byte_offset + length]); - } - } + out.copy_region_from(0, self, byte_offset, length); Ok(out) } } @@ -507,53 +532,71 @@ impl<'s> Stack<'s, ArrayBufferView> { self.byte_length() / self.view_kind().element_size() } - /// Borrow the view's bytes. + /// Call `f` with the view's bytes, which are empty for a view of a detached buffer. See the + /// [module docs](self#byte-access). + pub fn with_bytes(self, f: impl FnOnce(&[u8]) -> R + Send) -> R { + // SAFETY: `self` is a live view, and `f` can't run a GC or detach its buffer. + f(unsafe { slice_from_parts((self.data_ptr(), self.byte_length())) }) + } + + /// Call `f` with the view's bytes, mutably. See [`Self::with_bytes`]. + pub fn with_bytes_mut(self, f: impl FnOnce(&mut [u8]) -> R + Send) -> R { + // SAFETY: as in `with_bytes`. No other reference to the bytes exists while `f` runs. + f(unsafe { slice_from_parts_mut((self.data_ptr(), self.byte_length())) }) + } + + /// A copy of the view's bytes. Failing to allocate it reports out of memory. + pub fn copy_bytes(self, scope: &Scope<'_>) -> Result, ExnThrown> { + copy_or_report(scope, self.with_bytes(try_copy)) + } + + /// Copy `count` bytes from `src` at `src_start` into this view at `dest_start`. The views can + /// share a buffer, and the ranges can overlap. /// - /// # Safety + /// # Panics /// - /// No GC may run and the buffer must not be detached while the - /// returned slice is live. - pub unsafe fn bytes(self) -> &'s [u8] { - let length = self.byte_length(); - if length == 0 { - return &[]; - } - let mut is_shared = false; - let nogc = JS::AutoRequireNoGC { _address: 0 }; - let data = mozjs::jsapi::JS_GetArrayBufferViewData(self.as_raw(), &mut is_shared, &nogc); - if data.is_null() { - &[] - } else { - std::slice::from_raw_parts(data as *const u8, length) + /// Panics if either range doesn't lie within its view. + pub fn copy_region_from( + self, + dest_start: usize, + src: Stack<'_, ArrayBufferView>, + src_start: usize, + count: usize, + ) { + let (dest_len, src_len) = (self.byte_length(), src.byte_length()); + assert!(dest_start + .checked_add(count) + .is_some_and(|end| end <= dest_len)); + assert!(src_start + .checked_add(count) + .is_some_and(|end| end <= src_len)); + if count != 0 { + // SAFETY: both ranges lie within their views, which are non-empty, so neither pointer + // is null, and nothing runs between reading the pointers and copying. `ptr::copy` + // allows the ranges to overlap. + unsafe { + std::ptr::copy( + src.data_ptr().add(src_start), + self.data_ptr().add(dest_start), + count, + ) + }; } } - /// Mutably borrow the view's bytes. - /// - /// # Safety - /// - /// No GC may run and the buffer must not be detached while the - /// returned slice is live. Concurrent access via another reference is - /// undefined behaviour. - #[allow(clippy::mut_from_ref)] - pub unsafe fn bytes_mut(self) -> &'s mut [u8] { - let length = self.byte_length(); - if length == 0 { - return &mut []; + /// A pointer to the view's first byte, or a null pointer for an empty view. The pointer is + /// valid for `byte_length()` bytes until the next GC or until the buffer is detached. + fn data_ptr(self) -> *mut u8 { + if self.byte_length() == 0 { + return std::ptr::null_mut(); } let mut is_shared = false; let nogc = JS::AutoRequireNoGC { _address: 0 }; - let data = mozjs::jsapi::JS_GetArrayBufferViewData(self.as_raw(), &mut is_shared, &nogc); - if data.is_null() { - &mut [] - } else { - std::slice::from_raw_parts_mut(data as *mut u8, length) - } - } - - /// Copy the view's bytes into an owned `Vec`. - pub fn copy_bytes(self) -> Vec { - unsafe { self.bytes() }.to_vec() + // SAFETY: `self` is a live view. The pointer is only returned. + let data = unsafe { + mozjs::jsapi::JS_GetArrayBufferViewData(self.as_raw(), &mut is_shared, &nogc) + }; + data as *mut u8 } } @@ -605,26 +648,12 @@ crate::gc::handle::deref_to_object!(ArrayBufferView); crate::gc::handle::from_jsval_via_cast!(ArrayBufferView, c"Value isn't a typed array or DataView"); -/// Copy the bytes held by any `BufferSource` (an `ArrayBuffer`, -/// `SharedArrayBuffer`, or `ArrayBufferView`) into an owned `Vec`. -/// -/// Returns `None` if `obj` is none of the above. -pub fn copy_bytes_if_buffer_source(obj: Object<'_>) -> Option> { - // SAFETY: All functions called following JSAPI's contract. - unsafe { - if JS::IsArrayBufferObject(obj.as_raw()) { - let (ptr, len) = array_buffer_length_and_data(obj.as_raw()); - if ptr.is_null() || len == 0 { - return Some(Vec::new()); - } - return Some(std::slice::from_raw_parts(ptr, len).to_vec()); - } - if mozjs::jsapi::JS_IsArrayBufferViewObject(obj.as_raw()) { - let view: Stack<'_, ArrayBufferView> = Stack::from_handle_unchecked(obj.handle()); - return Some(view.copy_bytes()); - } - } - None +/// A copy of `bytes`, or `None` if allocating it fails. +pub fn try_copy(bytes: &[u8]) -> Option> { + let mut copy = Vec::new(); + copy.try_reserve_exact(bytes.len()).ok()?; + copy.extend_from_slice(bytes); + Some(copy) } // --------------------------------------------------------------------------- @@ -652,7 +681,7 @@ pub trait TypedArrayKind: JSType { /// /// # Safety /// - /// `obj` must be a non-null typed array of this kind. + /// `obj` must be a non-null typed array of this kind, or a cross-compartment wrapper of one. unsafe fn length_and_data(obj: *mut JSObject) -> (*mut Self::Element, usize); } @@ -695,36 +724,18 @@ impl<'s, T: TypedArrayKind> Stack<'s, T> { unsafe { mozjs::jsapi::JS_GetArrayBufferViewByteLength(self.as_raw()) } } - /// Borrow the typed array's elements. - /// - /// # Safety - /// - /// No GC may run and the underlying buffer must not be detached while - /// the returned slice is live. - pub unsafe fn as_slice(self) -> &'s [T::Element] { - let (ptr, len) = T::length_and_data(self.as_raw()); - if ptr.is_null() || len == 0 { - &[] - } else { - std::slice::from_raw_parts(ptr, len) - } + /// Call `f` with the typed array's elements, which are empty for a detached buffer. See the + /// [module docs](self#byte-access). + pub fn with_slice(self, f: impl FnOnce(&[T::Element]) -> R + Send) -> R { + // SAFETY: `self` is a live typed array of kind `T`, and `f` can't run a GC or detach its + // buffer. + f(unsafe { slice_from_parts(T::length_and_data(self.as_raw())) }) } - /// Mutably borrow the typed array's elements. - /// - /// # Safety - /// - /// No GC may run and the underlying buffer must not be detached while - /// the returned slice is live. Concurrent access via another reference - /// is undefined behaviour. - #[allow(clippy::mut_from_ref)] - pub unsafe fn as_mut_slice(self) -> &'s mut [T::Element] { - let (ptr, len) = T::length_and_data(self.as_raw()); - if ptr.is_null() || len == 0 { - &mut [] - } else { - std::slice::from_raw_parts_mut(ptr, len) - } + /// Call `f` with the typed array's elements, mutably. See [`Self::with_slice`]. + pub fn with_slice_mut(self, f: impl FnOnce(&mut [T::Element]) -> R + Send) -> R { + // SAFETY: as in `with_slice`. No other reference to the elements exists while `f` runs. + f(unsafe { slice_from_parts_mut(T::length_and_data(self.as_raw())) }) } /// View this typed array through the abstract `ArrayBufferView` handle. @@ -772,6 +783,13 @@ macro_rules! typed_array_marker { fn js_class() -> *const JSClass { crate::class::proto_key_to_class(JSProtoKey::$proto) } + + /// Views of fixed-length and of resizable buffers have different classes. Both are + /// instances. + unsafe fn is_instance(obj: *mut JSObject) -> bool { + // SAFETY: `obj` is a valid object pointer, per the trait's contract. + !unsafe { <$tag as TypedArrayElement>::unwrap_array(obj) }.is_null() + } } impl TypedArrayKind for $Marker { @@ -782,6 +800,9 @@ macro_rules! typed_array_marker { } unsafe fn length_and_data(obj: *mut JSObject) -> (*mut Self::Element, usize) { + // `length_and_data` requires an unwrapped typed array. + let obj = <$tag as TypedArrayElement>::unwrap_array(obj); + debug_assert!(!obj.is_null(), concat!("expected a ", $name)); match <$tag as TypedArrayElement>::length_and_data(obj) { Some(data) => (data.as_ptr().cast(), data.len()), None => (std::ptr::null_mut(), 0), @@ -806,7 +827,7 @@ macro_rules! typed_array_marker { val: HandleValue<'v>, _option: Self::Config, ) -> Result { - Object::from_value(scope, *val)? + Object::from_raw_value(scope, val.get())? .cast::() .map_err(|_| { const MSG: &std::ffi::CStr = unsafe { @@ -840,7 +861,7 @@ typed_array_marker!(Float64Array, "Float64Array", JSProto_Float64Array, Float64) /// so the marker is written out against the JSAPI entry points directly. // TODO: add element tags to mozjs and remove the need for this. macro_rules! bigint_typed_array_marker { - ($Marker:ident, $name:literal, $proto:ident, $elem:ty, $new:ident, $data:ident) => { + ($Marker:ident, $name:literal, $proto:ident, $elem:ty, $new:ident, $data:ident, $unwrap:ident) => { #[doc = concat!("Marker type for JavaScript `", $name, "` objects.")] pub struct $Marker; @@ -851,6 +872,13 @@ macro_rules! bigint_typed_array_marker { fn js_class() -> *const JSClass { crate::class::proto_key_to_class(JSProtoKey::$proto) } + + /// Views of fixed-length and of resizable buffers have different classes. Both are + /// instances. + unsafe fn is_instance(obj: *mut JSObject) -> bool { + // SAFETY: `obj` is a valid object pointer, per the trait's contract. + !unsafe { mozjs::jsapi::js::$unwrap(obj) }.is_null() + } } impl TypedArrayKind for $Marker { @@ -891,7 +919,7 @@ macro_rules! bigint_typed_array_marker { val: HandleValue<'v>, _option: Self::Config, ) -> Result { - Object::from_value(scope, *val)? + Object::from_raw_value(scope, val.get())? .cast::() .map_err(|_| { const MSG: &std::ffi::CStr = unsafe { @@ -912,7 +940,8 @@ bigint_typed_array_marker!( JSProto_BigInt64Array, i64, JS_NewBigInt64Array, - JS_GetBigInt64ArrayData + JS_GetBigInt64ArrayData, + UnwrapBigInt64Array ); bigint_typed_array_marker!( BigUint64Array, @@ -920,7 +949,8 @@ bigint_typed_array_marker!( JSProto_BigUint64Array, u64, JS_NewBigUint64Array, - JS_GetBigUint64ArrayData + JS_GetBigUint64ArrayData, + UnwrapBigUint64Array ); // --------------------------------------------------------------------------- @@ -940,9 +970,12 @@ fn root_or_throw<'s, T: JSType>( /// /// # Safety /// -/// `obj` must be a live `ArrayBuffer` object. The returned pointer is valid -/// only until the next GC or detach. +/// `obj` must be a live `ArrayBuffer` object or a cross-compartment wrapper of one. The returned +/// pointer is valid only until the next GC or detach. unsafe fn array_buffer_length_and_data(obj: *mut JSObject) -> (*mut u8, usize) { + // `GetArrayBufferLengthAndData` requires an unwrapped buffer. + let obj = JS::UnwrapArrayBuffer(obj); + debug_assert!(!obj.is_null(), "expected an ArrayBuffer"); let mut length = 0usize; let mut is_shared = false; let mut data: *mut u8 = std::ptr::null_mut(); @@ -951,6 +984,42 @@ unsafe fn array_buffer_length_and_data(obj: *mut JSObject) -> (*mut u8, usize) { (data, length) } +/// The slice of `len` elements at `ptr`, or an empty slice if `ptr` is null or `len` is 0. +/// +/// # Safety +/// +/// A non-null `ptr` must be valid for reads of `len` elements for `'a`, and nothing may write to +/// them for `'a`. +unsafe fn slice_from_parts<'a, T>((ptr, len): (*mut T, usize)) -> &'a [T] { + if ptr.is_null() || len == 0 { + &[] + } else { + std::slice::from_raw_parts(ptr, len) + } +} + +/// The mutable slice of `len` elements at `ptr`, or an empty slice if `ptr` is null or `len` is 0. +/// +/// # Safety +/// +/// A non-null `ptr` must be valid for reads and writes of `len` elements for `'a`, and nothing else +/// may access them for `'a`. +unsafe fn slice_from_parts_mut<'a, T>((ptr, len): (*mut T, usize)) -> &'a mut [T] { + if ptr.is_null() || len == 0 { + &mut [] + } else { + std::slice::from_raw_parts_mut(ptr, len) + } +} + +/// Returns `copy`, or, if it is `None`, an out-of-memory error reported on `scope`. +fn copy_or_report(scope: &Scope<'_>, copy: Option>) -> Result, ExnThrown> { + copy.ok_or_else(|| { + crate::exception::report_out_of_memory(scope); + ExnThrown + }) +} + /// Get the data pointer of a freshly created (and therefore non-detached, /// non-shared) `ArrayBuffer`. /// diff --git a/crates/js/src/webidl_enum.rs b/crates/js/src/webidl_enum.rs index 273ce09..c87baab 100644 --- a/crates/js/src/webidl_enum.rs +++ b/crates/js/src/webidl_enum.rs @@ -5,8 +5,7 @@ /// Define a WebIDL enumeration: the Rust enum plus `as_str`, `Display`, /// `FromStr`, and the JS value conversions (`FromJSVal`/`ToJSVal`), all -/// generated from a single `Variant => "string"` list — so the four string -/// tables cannot drift, and adding a variant is a one-line change. +/// generated from a single `Variant => "string"` list. /// /// ```ignore /// js::webidl_enum! { diff --git a/crates/libstarling/tests/serve.rs b/crates/libstarling/tests/serve.rs index d4069a7..cbf6a1f 100644 --- a/crates/libstarling/tests/serve.rs +++ b/crates/libstarling/tests/serve.rs @@ -1639,7 +1639,7 @@ fn a_response_body_timeout_truncates_a_body_that_never_ends() { eval_script: Some(handler.to_string()), legacy_script: true, serve: Some(18217), - response_body_timeout: Some(1), + response_body_timeout: Some(Duration::from_secs(1)), ..Default::default() }, 18217, @@ -1700,7 +1700,7 @@ fn a_response_body_timeout_aborts_the_request_signal() { eval_script: Some(handler.to_string()), legacy_script: true, serve: Some(18219), - response_body_timeout: Some(1), + response_body_timeout: Some(Duration::from_secs(1)), ..Default::default() }, 18219, @@ -1752,10 +1752,10 @@ fn wait_until_gets_its_own_window_after_the_body_is_truncated() { eval_script: Some(handler.to_string()), legacy_script: true, serve: Some(18233), - response_body_timeout: Some(1), + response_body_timeout: Some(Duration::from_secs(1)), // The bound only has to clear the 2s of lifetime work, with room for a loaded // machine. We're testing that truncating the body left the window intact. - waituntil_timeout: Some(20), + waituntil_timeout: Some(Duration::from_secs(20)), ..Default::default() }, 18233, @@ -1794,7 +1794,7 @@ fn a_response_body_timeout_truncates_a_response_the_client_will_not_read() { eval_script: Some(handler), legacy_script: true, serve: Some(18221), - response_body_timeout: Some(1), + response_body_timeout: Some(Duration::from_secs(1)), ..Default::default() }, 18221, @@ -1833,7 +1833,7 @@ fn a_response_body_timeout_truncates_an_endless_proxied_body() { eval_script: Some(handler), legacy_script: true, serve: Some(18235), - response_body_timeout: Some(1), + response_body_timeout: Some(Duration::from_secs(1)), ..Default::default() }, 18235, @@ -1872,7 +1872,7 @@ fn an_end_to_end_timeout_truncates_a_streaming_body() { eval_script: Some(handler.to_string()), legacy_script: true, serve: Some(18223), - end_to_end_timeout: Some(1), + end_to_end_timeout: Some(Duration::from_secs(1)), ..Default::default() }, 18223, @@ -1902,7 +1902,7 @@ fn an_end_to_end_timeout_answers_a_never_settling_dispatch() { eval_script: Some(handler.to_string()), legacy_script: true, serve: Some(18225), - end_to_end_timeout: Some(1), + end_to_end_timeout: Some(Duration::from_secs(1)), ..Default::default() }, 18225, @@ -1943,7 +1943,7 @@ fn an_end_to_end_timeout_stops_leftover_lifetime_work() { eval_script: Some(handler.to_string()), legacy_script: true, serve: Some(18227), - end_to_end_timeout: Some(1), + end_to_end_timeout: Some(Duration::from_secs(1)), ..Default::default() }, 18227, @@ -1984,7 +1984,7 @@ fn an_end_to_end_timeout_bounds_isolated_startup() { legacy_script: true, serve: Some(18231), serve_isolated: true, - end_to_end_timeout: Some(1), + end_to_end_timeout: Some(Duration::from_secs(1)), ..Default::default() }, 18231, @@ -2074,8 +2074,8 @@ fn a_contradictory_timeout_config_refuses_to_serve() { eval_script: Some("addEventListener('fetch', () => {});".to_string()), legacy_script: true, serve: Some(18341), - end_to_end_timeout: Some(5), - waituntil_timeout: Some(6), + end_to_end_timeout: Some(Duration::from_secs(5)), + waituntil_timeout: Some(Duration::from_secs(6)), ..Default::default() }; let error = @@ -2096,8 +2096,8 @@ fn a_dispatch_timeout_answers_a_never_settling_respond_with() { eval_script: Some(handler.to_string()), legacy_script: true, serve: Some(18213), - dispatch_timeout: Some(1), - waituntil_timeout: Some(1), + dispatch_timeout: Some(Duration::from_secs(1)), + waituntil_timeout: Some(Duration::from_secs(1)), ..Default::default() }, 18213, @@ -2129,7 +2129,7 @@ fn a_dispatch_timeout_frees_the_slot_it_held() { legacy_script: true, serve: Some(18289), serve_isolated: true, - dispatch_timeout: Some(1), + dispatch_timeout: Some(Duration::from_secs(1)), ..Default::default() }, 18289, diff --git a/crates/starling-macro/src/lib.rs b/crates/starling-macro/src/lib.rs index 4ca09c1..f650647 100644 --- a/crates/starling-macro/src/lib.rs +++ b/crates/starling-macro/src/lib.rs @@ -18,8 +18,8 @@ use syn::{ // Attribute option parsing (shared across all macros) // ============================================================================ -/// Parsed key-value options from attribute arguments. -/// Used by `#[jsclass]`, `#[jsmethods]`, `#[jsmodule]`, and `#[method]`. +/// The options of the attribute macros and method markers. [`AttrOpts::reject_all_but`] rejects +/// the ones an attribute doesn't take. #[derive(Default)] struct AttrOpts { /// Optional name override for the JS class or method. By default, a @@ -63,31 +63,24 @@ impl Parse for AttrOpts { while !input.is_empty() { let key: Ident = input.parse()?; // Bare flags take no `= value`. - if key == "hidden" { - opts.hidden = true; - if !input.is_empty() { - let _: Token![,] = input.parse()?; - } - continue; - } - if key == "unforgeable" { - opts.unforgeable = true; - if !input.is_empty() { - let _: Token![,] = input.parse()?; - } - continue; - } - if input.parse::().is_err() { + let flag = match key.to_string().as_str() { + "hidden" => Some(&mut opts.hidden), + "unforgeable" => Some(&mut opts.unforgeable), + _ => None, + }; + if let Some(flag) = flag { + *flag = true; + } else if input.parse::().is_err() { return Err(syn::Error::new(key.span(), "unknown option")); - } - - match key.to_string().as_str() { - "name" => opts.name = Some(input.parse::()?.value()), - "length" => opts.length = Some(input.parse::()?.base10_parse()?), - "extends" => opts.extends = Some(input.parse()?), - "js_proto" => opts.js_proto = Some(input.parse::()?.value()), - "to_string_tag" => opts.to_string_tag = Some(input.parse::()?.value()), - _ => return Err(syn::Error::new(key.span(), "unknown option")), + } else { + match key.to_string().as_str() { + "name" => opts.name = Some(input.parse::()?.value()), + "length" => opts.length = Some(input.parse::()?.base10_parse()?), + "extends" => opts.extends = Some(input.parse()?), + "js_proto" => opts.js_proto = Some(input.parse::()?.value()), + "to_string_tag" => opts.to_string_tag = Some(input.parse::()?.value()), + _ => return Err(syn::Error::new(key.span(), "unknown option")), + } } if !input.is_empty() { let _: Token![,] = input.parse()?; @@ -108,10 +101,7 @@ impl Parse for AttrOpts { /// `#[method(...)]` or `#[getter(...)]`. /// /// A bare marker (`#[method]`, no parentheses) yields default options. -/// Malformed arguments — or options that aren't valid in this context (a -/// typo'd key, `#[getter(length = 2)]`, a class-level option on a method) — -/// produce a spanned compile error rather than being silently dropped, which -/// would otherwise register the member under the wrong JS name or `.length`. +/// Malformed arguments and options not in `allowed` are a compile error. fn parse_marker_opts(attr: &syn::Attribute, allowed: &[&str]) -> syn::Result { let opts = match &attr.meta { syn::Meta::Path(_) => return Ok(AttrOpts::default()), @@ -129,12 +119,6 @@ fn parse_marker_opts(attr: &syn::Attribute, allowed: &[&str]) -> syn::Result syn::Result<()> { let set: [(bool, &str); 7] = [ (self.name.is_some(), "name"), @@ -220,8 +204,12 @@ struct ClassConfig { /// so that `pub const` items are installed on both constructor and /// prototype (per WebIDL §3.7.3). constants_on_prototype: bool, - /// JS builtins' methods aren't enumerable, but WebIDL interfaces' are, so we have to use different flags for them. + /// The property flags of methods and accessors. JS builtins' methods and accessors aren't + /// enumerable, but WebIDL interfaces' are, so we have to use different flags for them. method_flags: u16, + /// The message of the `TypeError` a call of the constructor without `new` throws, if it + /// differs from `ClassDef::CALL_WITHOUT_NEW_ERROR`'s default. + call_without_new_error: Option<&'static str>, /// The attribute this configures, for use in error messages. attribute: &'static str, } @@ -233,6 +221,8 @@ impl ClassConfig { auto_to_string_tag: false, constants_on_prototype: false, method_flags: 0, + // SpiderMonkey's `JSMSG_CANT_CALL_CLASS_CONSTRUCTOR`. + call_without_new_error: Some("class constructors must be invoked with 'new'"), attribute: "jsclass", }; @@ -242,6 +232,7 @@ impl ClassConfig { auto_to_string_tag: true, constants_on_prototype: true, method_flags: 1, // js::class_spec::JSPROP_ENUMERATE + call_without_new_error: None, attribute: "webidl_interface", }; } @@ -262,13 +253,10 @@ fn process_class_def(attr: TokenStream, item: TokenStream, config: ClassConfig) let class_static = format_ident!("__{}_CLASS", struct_name.to_string().to_uppercase()); let js_name_cstr_lit = cstr_literal(&js_name); - // If extends is set, compute the inner parent name and rewrite the parent field type - let opts_extends_ident = opts.extends.clone(); let inner_parent = opts.extends.as_ref().map(|p| format_ident!("{}Impl", p)); - if let Some(ref inner_parent_name) = inner_parent { + if let (Some(extends), Some(inner_parent_name)) = (&opts.extends, &inner_parent) { // A class embeds its parent's data, so the `parent` field holds the parent's `Impl`. - let extends = opts_extends_ident.as_ref().expect("extends is set"); if let Fields::Named(ref fields) = input.fields { if let Err(e) = parent_field( &struct_name, @@ -319,13 +307,14 @@ fn process_class_def(attr: TokenStream, item: TokenStream, config: ClassConfig) ::js::class::register_parent_info::(); } - fn ensure_parent_registered( - scope: &::js::gc::scope::Scope<'_>, - global: ::js::Object<'_>, - ) { + fn ensure_parent_registered(scope: &::js::gc::scope::Scope<'_>) { + // SAFETY: `scope.global()` is the current global. unsafe { - // SAFETY: register_class is safe to call if scope and global are valid. - ::js::class::register_class::<#inner_parent_name>(scope, global); + if <#inner_parent_name as ::js::class::ClassDef>::HIDDEN { + ::js::class::register_hidden_class::<#inner_parent_name>(scope); + } else { + ::js::class::register_class::<#inner_parent_name>(scope, scope.global()); + } } } } @@ -363,16 +352,9 @@ fn process_class_def(attr: TokenStream, item: TokenStream, config: ClassConfig) // to the JS class name. let effective_tag = opts .to_string_tag - .as_deref() - .or({ - if config.auto_to_string_tag { - Some(js_name.as_str()) - } else { - None - } - }) - .map(|t| t.to_owned()); - let to_string_tag_const = if let Some(ref tag) = effective_tag { + .as_ref() + .or(config.auto_to_string_tag.then_some(&js_name)); + let to_string_tag_const = if let Some(tag) = effective_tag { quote! { const TO_STRING_TAG: &'static str = #tag; } @@ -483,6 +465,13 @@ fn process_class_def(attr: TokenStream, item: TokenStream, config: ClassConfig) let cast_error_lit = cstr_literal(&format!("Value is not an instance of {}", struct_name)); let not_type_error_lit = cstr_literal(&format!("'this' is not of type {}", js_name)); + let no_new_error = match config.call_without_new_error { + Some(message) => { + let message = cstr_literal(message); + quote! { const CALL_WITHOUT_NEW_ERROR: &'static ::core::ffi::CStr = #message; } + } + None => quote! {}, + }; let prototype_call_error_lit = cstr_literal(&format!( "\"{}\" getter/method called on prototype or uninitialized object", js_name @@ -543,6 +532,7 @@ fn process_class_def(attr: TokenStream, item: TokenStream, config: ClassConfig) const NAME_CSTR: &'static ::core::ffi::CStr = #js_name_cstr_lit; const NOT_TYPE_ERROR: &'static ::core::ffi::CStr = #not_type_error_lit; const PROTOTYPE_CALL_ERROR: &'static ::core::ffi::CStr = #prototype_call_error_lit; + #no_new_error fn class() -> &'static ::js::class_spec::JSClass { &#class_static @@ -750,55 +740,55 @@ fn process_class_def(attr: TokenStream, item: TokenStream, config: ClassConfig) // If extends is specified, append inheritance impls and set Deref target // to the parent type. Otherwise, Deref targets Object. - let output = if let Some(ref inner_parent_name) = inner_parent { - let parent_name = opts_extends_ident.as_ref().unwrap(); - quote! { - #output + let output = + if let (Some(parent_name), Some(inner_parent_name)) = (&opts.extends, &inner_parent) { + quote! { + #output - // Deref: Foo<'s> -> Parent<'s> - impl<'s> ::std::ops::Deref for #struct_name<'s> { - type Target = #parent_name<'s>; - fn deref(&self) -> &Self::Target { - unsafe { ::std::mem::transmute(self) } + // Deref: Foo<'s> -> Parent<'s> + impl<'s> ::std::ops::Deref for #struct_name<'s> { + type Target = #parent_name<'s>; + fn deref(&self) -> &Self::Target { + unsafe { ::std::mem::transmute(self) } + } } - } - // Deref: FooImpl -> ParentImpl - impl ::std::ops::Deref for #inner_name { - type Target = #inner_parent_name; - fn deref(&self) -> &Self::Target { - &self.parent + // Deref: FooImpl -> ParentImpl + impl ::std::ops::Deref for #inner_name { + type Target = #inner_parent_name; + fn deref(&self) -> &Self::Target { + &self.parent + } } - } - // Deref: FooImpl -> ParentImpl - impl ::std::ops::DerefMut for #inner_name { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.parent + // Deref: FooImpl -> ParentImpl + impl ::std::ops::DerefMut for #inner_name { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.parent + } } - } - impl ::js::class::HasParent for #inner_name { - type Parent = #inner_parent_name; - fn as_parent(&self) -> &#inner_parent_name { &self.parent } - fn as_parent_mut(&mut self) -> &mut #inner_parent_name { &mut self.parent } - } + impl ::js::class::HasParent for #inner_name { + type Parent = #inner_parent_name; + fn as_parent(&self) -> &#inner_parent_name { &self.parent } + fn as_parent_mut(&mut self) -> &mut #inner_parent_name { &mut self.parent } + } - impl ::js::class::DerivedFrom<#inner_parent_name> for #inner_name {} - } - } else { - quote! { - #output + impl ::js::class::DerivedFrom<#inner_parent_name> for #inner_name {} + } + } else { + quote! { + #output - // Deref: Foo<'s> -> Object<'s> (base case, no parent) - impl<'s> ::std::ops::Deref for #struct_name<'s> { - type Target = ::js::Object<'s>; - fn deref(&self) -> &Self::Target { - unsafe { ::std::mem::transmute(self) } + // Deref: Foo<'s> -> Object<'s> (base case, no parent) + impl<'s> ::std::ops::Deref for #struct_name<'s> { + type Target = ::js::Object<'s>; + fn deref(&self) -> &Self::Target { + unsafe { ::std::mem::transmute(self) } + } } } - } - }; + }; output.into() } @@ -855,12 +845,84 @@ enum ReturnStyle { InstanceValue, } +/// The parameters of a callable other than its receiver. +struct Inputs { + /// The parameters converted from JS arguments, excluding the scope, `&CallArgs` and + /// `RestArgs` parameters. + params: Vec<(Ident, Type)>, + /// Whether the callable takes a `&Scope` (or `&mut JSContext`). + has_cx: bool, + /// Whether the callable takes `&CallArgs` and reads its arguments itself. + is_raw: bool, + /// The name and element type of the `RestArgs` parameter. + rest: Option<(Ident, Type)>, +} + +impl Inputs { + /// Classify the parameters `inputs` of a signature. Receivers are skipped. A bare `RestArgs` + /// without an element type is skipped too, as [`rewrite_rest_args_in_sig`] rejects it. + fn parse<'a>(inputs: impl IntoIterator) -> Self { + let mut parsed = Inputs { + params: Vec::new(), + has_cx: false, + is_raw: false, + rest: None, + }; + for arg in inputs { + let FnArg::Typed(pat_ty) = arg else { + continue; + }; + if is_cx_param_type(&pat_ty.ty) { + parsed.has_cx = true; + continue; + } + if is_callargs_param_type(&pat_ty.ty) { + parsed.is_raw = true; + continue; + } + let Pat::Ident(pat_ident) = pat_ty.pat.as_ref() else { + continue; + }; + // A variadic tail is collected from the arguments after the fixed ones, so it stays + // out of `params` for the index arithmetic in `gen_rest_setup` to line up. + if is_rest_args_type(&pat_ty.ty) { + parsed.rest = type_arg(&pat_ty.ty, "RestArgs", 0) + .map(|inner| (pat_ident.ident.clone(), inner.clone())); + continue; + } + parsed + .params + .push((pat_ident.ident.clone(), (*pat_ty.ty).clone())); + } + parsed + } + + /// The arguments a trampoline passes to the Rust fn after its receiver: `scope` and `args` + /// for a raw fn, and otherwise `scope` if the fn takes one, then the converted parameters and + /// the variadic tail. + fn forwarded_args( + &self, + scope: proc_macro2::TokenStream, + args: proc_macro2::TokenStream, + ) -> Vec { + if self.is_raw { + return vec![scope, args]; + } + let names = self.params.iter().map(|(name, _)| name); + let names = names.chain(self.rest.as_ref().map(|(name, _)| name)); + self.has_cx + .then_some(scope) + .into_iter() + .chain(names.map(|name| quote!(#name))) + .collect() + } +} + /// Info about a parsed method. struct MethodInfo { kind: MethodKind, fn_item: ImplItemFn, - /// Parameter names and types (excluding self/cx/args) - params: Vec<(Ident, Type)>, + inputs: Inputs, /// Number of required arguments: the explicit `length = N` override when /// given, otherwise the count of non-`Option` (and non-`RestArgs`) params. /// This is both the JS-visible `.length` and the threshold at and beyond @@ -873,16 +935,6 @@ struct MethodInfo { has_self: bool, /// Whether the method takes &mut self has_mut_self: bool, - /// Whether the method takes cx: &mut JSContext - has_cx: bool, - /// Whether the method has raw cx/args params for low-level access - is_raw: bool, - /// Whether the method has a variadic rest parameter (last param) - has_rest_args: bool, - /// Name of the rest parameter, if any - rest_arg_name: Option, - /// Inner type of RestArgs, or None for bare RestArgs (defaults to Value) - rest_inner_type: Option, } /// Attribute macro for an `impl` block that generates JSNative wrappers. @@ -1035,15 +1087,12 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - continue; } - let const_name = const_item.ident.to_string(); - let const_name_bytes = format!("{const_name}\0"); - let const_name_cstr = - proc_macro2::Literal::byte_string(const_name_bytes.as_bytes()); + let const_name_cstr = cstr_literal(&const_item.ident.to_string()); let const_ident = &const_item.ident; constant_builder_calls.push(quote! { .constant( - unsafe { ::std::ffi::CStr::from_bytes_with_nul_unchecked(#const_name_cstr) }, + #const_name_cstr, #inner_name::#const_ident as i32, ) }); @@ -1237,9 +1286,9 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - MethodKind::Constructor if Some(i) == js_ctor_index => { if method.has_self || method.has_mut_self { // Setup-style: constructor runs on the stack newtype after - // allocation. ConstructorRegistrar returns FooImpl::default(), - // and PostInitRegistrar handles arg extraction + calling - // the user's constructor body. + // allocation. ConstructorRegistrar returns FooImpl::default(). + // PostInitRegistrar extracts the arguments and calls the + // user's constructor body. setup_ctor_info = Some(i); } else { // Old-style: constructor returns Self directly. @@ -1325,11 +1374,13 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - None => quote! { None }, }; + let flags = u8::try_from(config.method_flags).expect("accessor flags fit in a `u8`"); builder_calls.push(quote! { .property( #js_name_cstr_lit, #getter, #setter, + #flags, ) }); } @@ -1507,14 +1558,15 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - let post_init_impl = if let Some(idx) = setup_ctor_info { let info = &methods[idx]; let setup_fn_name = setup_fn_ident(&info.fn_item.sig.ident); - let arg_extractions = - gen_arg_extractions(&info.params, quote!(args), true, quote!(scope), info.nargs); - let arg_names: Vec<_> = info.params.iter().map(|(name, _)| quote!(#name)).collect(); - let call = if info.has_cx { - quote! { #type_name::#setup_fn_name(&__typed, scope, #(#arg_names),*) } - } else { - quote! { #type_name::#setup_fn_name(&__typed, #(#arg_names),*) } - }; + let arg_extractions = gen_arg_extractions( + &info.inputs.params, + quote!(args), + true, + quote!(scope), + info.nargs, + ); + let args = info.inputs.forwarded_args(quote!(scope), quote!(args)); + let call = quote! { #type_name::#setup_fn_name(&__typed, #(#args),*) }; quote! { #[allow(clippy::not_unsafe_ptr_arg_deref)] impl ::js::class::__PostInitRegistrar<#inner_name> for ::js::class::__PostInitReg<#inner_name> { @@ -1541,9 +1593,10 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - /// If the class is marked as hidden `#[{jsclass,webidl_interface}(hidden)]`, the /// constructor isn't installed on the global object, so it is not visible to content. pub fn add_to_global<'scope>(scope: &'scope ::js::gc::scope::Scope<'_>, global: ::js::Object<'scope>) { - unsafe { ::js::class::register_class::<#inner_name>(scope, global); } if <#inner_name as ::js::class::ClassDef>::HIDDEN { - global.delete_property(scope, <#inner_name as ::js::class::ClassDef>::NAME_CSTR).expect("delete_property failed"); + unsafe { ::js::class::register_hidden_class::<#inner_name>(scope); } + } else { + unsafe { ::js::class::register_class::<#inner_name>(scope, global); } } } }; @@ -1554,7 +1607,7 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - let method = &methods[*i]; if method.has_self || method.has_mut_self { ctor_items.push(gen_setup_factory(method, &type_name, &inner_name)); - } else if !method.is_raw { + } else if !method.inputs.is_raw { // A raw (`&CallArgs`) constructor is callable only from a JSNative // wrapper, so it gets no Rust-side factory. ctor_items.push(gen_instance_factory( @@ -1574,67 +1627,64 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - }; // Generate forwarding methods on Foo<'s> for InstanceValue methods - // (which stay on FooImpl). All other instance methods are now directly + // (which stay on FooImpl). All other instance methods are directly // on the newtype via `newtype_items`. let mut forwarding_methods: Vec = Vec::new(); for method in &methods { - match &method.kind { - MethodKind::Method { .. } - if matches!(method.return_style, ReturnStyle::InstanceValue) => - { - if !method.has_self && !method.has_mut_self { - continue; - } - - let fn_name = &method.fn_item.sig.ident; - let fn_generics = &method.fn_item.sig.generics; - let param_decls: Vec<_> = method - .params - .iter() - .map(|(name, ty)| quote! { #name: #ty }) - .collect(); - let param_names: Vec<_> = method - .params - .iter() - .map(|(name, _)| quote! { #name }) - .collect(); + if !matches!(method.kind, MethodKind::Method { .. }) + || !matches!(method.return_style, ReturnStyle::InstanceValue) + || !(method.has_self || method.has_mut_self) + { + continue; + } + let fn_name = &method.fn_item.sig.ident; + let fn_generics = &method.fn_item.sig.generics; + let param_decls: Vec<_> = method + .inputs + .params + .iter() + .map(|(name, ty)| quote! { #name: #ty }) + .collect(); + let param_names: Vec<_> = method + .inputs + .params + .iter() + .map(|(name, _)| quote! { #name }) + .collect(); - // `data()`/`data_mut()` return borrow guards; reborrow through - // them when passing the receiver to the inner method. - let (get_inner, inner_arg) = if method.has_mut_self { - ( - quote! { let mut inner = self.data_mut(); }, - quote! { &mut *inner }, - ) - } else { - (quote! { let inner = self.data(); }, quote! { &*inner }) - }; + // `data()`/`data_mut()` return borrow guards. The receiver passed to + // the inner method is reborrowed through them. + let (get_inner, inner_arg) = if method.has_mut_self { + ( + quote! { let mut inner = self.data_mut(); }, + quote! { &mut *inner }, + ) + } else { + (quote! { let inner = self.data(); }, quote! { &*inner }) + }; - // InstanceValue: always needs a scope to create the JS object. - let cx_param = quote! { scope: &'s ::js::gc::scope::Scope<'_>, }; - let cx_arg = if method.has_cx { - quote! { scope, } - } else { - quote! {} - }; + // InstanceValue: always needs a scope to create the JS object. + let cx_param = quote! { scope: &'s ::js::gc::scope::Scope<'_>, }; + let cx_arg = if method.inputs.has_cx { + quote! { scope, } + } else { + quote! {} + }; - forwarding_methods.push(quote! { - pub fn #fn_name #fn_generics (&self, #cx_param #(#param_decls),*) - -> ::std::result::Result<#type_name<'s>, ::js::error::ExnThrown> - { - #get_inner - unsafe { - let __typed = ::js::class::create_instance_with::<#inner_name>(scope, |_| { - #inner_name::#fn_name(#inner_arg, #cx_arg #(#param_names),*) - })?; - Ok(__typed) - } - } - }); + forwarding_methods.push(quote! { + pub fn #fn_name #fn_generics (&self, #cx_param #(#param_decls),*) + -> ::std::result::Result<#type_name<'s>, ::js::error::ExnThrown> + { + #get_inner + unsafe { + let __typed = ::js::class::create_instance_with::<#inner_name>(scope, |_| { + #inner_name::#fn_name(#inner_arg, #cx_arg #(#param_names),*) + })?; + Ok(__typed) + } } - _ => continue, - } + }); } // Generate the UnforgeableRegistrar impl when the interface has @@ -1646,13 +1696,12 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - let installs: Vec<_> = unforgeable_getters .iter() .map(|(js_name, native_name)| { - let name_bytes = format!("{js_name}\0"); - let name_cstr = proc_macro2::Literal::byte_string(name_bytes.as_bytes()); + let name_cstr = cstr_literal(js_name); quote! { ::js::class::define_unforgeable_accessor( scope, obj, - unsafe { ::std::ffi::CStr::from_bytes_with_nul_unchecked(#name_cstr) }, + #name_cstr, Some(#native_name), )?; } @@ -1688,12 +1737,6 @@ fn process_methods(_attr: TokenStream, item: TokenStream, config: ClassConfig) - #(#forwarding_methods)* } } - } else if !forwarding_methods.is_empty() { - quote! { - impl<'s> #type_name<'s> { - #(#forwarding_methods)* - } - } } else { quote! {} }; @@ -1748,62 +1791,21 @@ fn parse_method_info( ) -> MethodInfo { let method_name = unraw(&fn_item.sig.ident); - // Determine self receiver - let has_self = fn_item - .sig - .inputs - .first() - .map(|a| matches!(a, FnArg::Receiver(r) if r.mutability.is_none())) - .unwrap_or(false); - let has_mut_self = fn_item - .sig - .inputs - .first() - .map(|a| matches!(a, FnArg::Receiver(r) if r.mutability.is_some())) - .unwrap_or(false); - - // Collect non-self parameters, detecting cx and raw params - let mut params = Vec::new(); - let mut required_args = 0; - let mut has_cx = false; - let mut is_raw = false; - let mut has_rest_args = false; - let mut rest_arg_name = None; - let mut rest_inner_type = None; - let skip_first = if has_self || has_mut_self { 1 } else { 0 }; - - for arg in fn_item.sig.inputs.iter().skip(skip_first) { - if let FnArg::Typed(pat_ty) = arg { - if is_cx_param_type(&pat_ty.ty) { - has_cx = true; - continue; - } - if is_callargs_param_type(&pat_ty.ty) { - is_raw = true; - continue; - } - // Check for RestArgs marker type - if is_rest_args_type(&pat_ty.ty) { - if let Pat::Ident(pat_ident) = pat_ty.pat.as_ref() { - has_rest_args = true; - rest_arg_name = Some(pat_ident.ident.clone()); - rest_inner_type = extract_rest_args_inner_type(&pat_ty.ty); - } - continue; - } - if let Pat::Ident(pat_ident) = pat_ty.pat.as_ref() { - params.push((pat_ident.ident.clone(), (*pat_ty.ty).clone())); - if !is_option_type(&pat_ty.ty) { - required_args += 1; - } - } - } - } + let receiver_mutable = match fn_item.sig.inputs.first() { + Some(FnArg::Receiver(receiver)) => Some(receiver.mutability.is_some()), + _ => None, + }; + let inputs = Inputs::parse(&fn_item.sig.inputs); + let required_args = required_arg_count(&inputs.params); // Determine return style let is_constructor = matches!(kind, MethodKind::Constructor); - let return_style = - classify_return_style(&fn_item.sig.output, Some(type_name), is_constructor, is_raw); + let return_style = classify_return_style( + &fn_item.sig.output, + Some(type_name), + is_constructor, + inputs.is_raw, + ); // Compute JS name: custom name overrides, otherwise default to camelCase. // For setters, derive the property name by stripping "set_" prefix. @@ -1817,34 +1819,21 @@ fn parse_method_info( }); match &mut kind { - MethodKind::Method { js_name: n } => { - *n = js_name; - } - MethodKind::StaticMethod { js_name: n } => { - *n = js_name; - } - MethodKind::Getter { js_name: n, .. } => { - *n = js_name; - } - MethodKind::Setter { js_name: n } => { - *n = js_name; - } - _ => {} + MethodKind::Method { js_name: n } + | MethodKind::StaticMethod { js_name: n } + | MethodKind::Getter { js_name: n, .. } + | MethodKind::Setter { js_name: n } => *n = js_name, + MethodKind::Constructor | MethodKind::Destructor => {} } MethodInfo { kind, fn_item, - params, - nargs: custom_nargs.unwrap_or(required_args) as u32, + inputs, + nargs: custom_nargs.map_or(required_args, |n| n as u32), return_style, - has_self, - has_mut_self, - has_cx, - is_raw, - has_rest_args, - rest_arg_name, - rest_inner_type, + has_self: receiver_mutable == Some(false), + has_mut_self: receiver_mutable == Some(true), } } @@ -1870,10 +1859,7 @@ fn is_method_on_newtype(method: &MethodInfo) -> bool { // Methods named data/data_mut would conflict with the generated accessors // on the newtype. let name = method.fn_item.sig.ident.to_string(); - if matches!(name.as_str(), "data" | "data_mut") { - return false; - } - true + !matches!(name.as_str(), "data" | "data_mut") } fn last_segment_is(ty: &Type, name: &str) -> bool { @@ -1911,51 +1897,17 @@ fn is_rest_args_type(ty: &Type) -> bool { || s.contains("::RestArgs<") } -/// Extract the inner type from `RestArgs`. Returns `None` for a bare -/// `RestArgs`. -fn extract_rest_args_inner_type(ty: &Type) -> Option { - if let Type::Path(type_path) = ty { - let last_seg = type_path.path.segments.last()?; - let ident = last_seg.ident.to_string(); - if ident == "RestArgs" { - if let syn::PathArguments::AngleBracketed(args) = &last_seg.arguments { - if let Some(syn::GenericArgument::Type(inner)) = args.args.first() { - return Some(inner.clone()); - } - } - } - } - None -} - -/// The element type a `RestArgs` collects into. -/// -/// [`rewrite_rest_args_in_sig`] rejects both a bare `RestArgs` and an owned -/// `Value` element before any codegen runs, so every callable that reaches this -/// point declared a usable element type. A `None` here would mean that check was -/// bypassed, which is a bug in this macro rather than in the user's code. -/// (Changing the callsites to unwrap the `Option` would create more noise than -/// it's worth, so an unwrap here it is.) -fn rest_args_element_type(declared: Option<&Type>) -> Type { - declared - .cloned() - .expect("bare `RestArgs` must be rejected by `rewrite_rest_args_in_sig` before codegen") -} - /// Rewrite every `RestArgs` parameter in `sig` to the fully-qualified /// `::js::class::RestArgs`, and reject the element types that can't work: /// a missing one (bare `RestArgs`) and the GC-unsafe owned `Value`. /// -/// Shared by `#[jsmethods]` and by the `mod`-block macros (`#[jsmodule]`, -/// `#[jsglobals]`, `#[jsnamespace]`, `#[webidl_namespace]`). -/// /// Errors are spanned on the name of the function or method whose signature this /// is, because the parameter type itself has usually already been replaced by /// the time a caller reports one. fn rewrite_rest_args_in_sig(sig: &mut syn::Signature, name: &Ident) -> syn::Result<()> { let declared = sig.inputs.iter().find_map(|arg| match arg { FnArg::Typed(pat_ty) if is_rest_args_type(&pat_ty.ty) => { - Some(extract_rest_args_inner_type(&pat_ty.ty)) + Some(type_arg(&pat_ty.ty, "RestArgs", 0).cloned()) } _ => None, }); @@ -2005,34 +1957,23 @@ fn rewrite_rest_args_in_sig(sig: &mut syn::Signature, name: &Ident) -> syn::Resu Ok(()) } -/// Emit the statement that collects a variadic tail into the `RestArgs` bound -/// to `rest_name`, converting each argument from index `start_idx` onwards with -/// `FromJSVal`. Returns an empty token stream when the callable doesn't use rest -/// args. -/// -/// Takes the same context parameters as [`gen_arg_extractions`], and with the -/// same meaning: a JSNative trampoline names its argument vector `__args` and -/// owns its `scope`, while a constructor registrar names it `args` and receives -/// `scope` by reference; `use_question_mark` picks the early return that -/// context needs (`Err(ExnThrown)` for the registrar, whose body is a `Result`, -/// versus `false` for the trampoline, which returns `bool` to SpiderMonkey). +/// Emit the statement that collects the variadic tail of `inputs` into its `RestArgs`, +/// converting each argument after the fixed parameters with `FromJSVal`. Returns an empty token +/// stream when there is no tail. The other parameters are as for [`gen_arg_extractions`]. fn gen_rest_setup( - rest_name: Option<&Ident>, - rest_inner_type: Option<&Type>, - start_idx: usize, + inputs: &Inputs, args_expr: &proc_macro2::TokenStream, use_question_mark: bool, scope_expr: &proc_macro2::TokenStream, ) -> proc_macro2::TokenStream { - let Some(rest_name) = rest_name else { + let Some((rest_name, inner_ty)) = &inputs.rest else { return quote! {}; }; - let start_idx = start_idx as u32; - let inner_ty = rest_args_element_type(rest_inner_type); + let start_idx = inputs.params.len() as u32; // Integer element types (and integer-element containers) convert with // `ConversionBehavior`, everything else uses `()`. Mirrors the config // selection in `gen_arg_extractions` for ordinary parameters. - let rest_config = if is_integer_type(&inner_ty) || is_int_container_type(&inner_ty) { + let rest_config = if is_integer_type(inner_ty) || is_int_container_type(inner_ty) { quote! { ::js::conversion::ConversionBehavior::Default } } else { quote! { () } @@ -2071,7 +2012,6 @@ fn gen_rest_setup( } } -/// Walk a `UseTree` to find the leaf `Ident` (e.g., `super::Vec2` → `Vec2`). /// Collect the class identifiers a `pub use` brings into scope, for /// registration on the global. `use A`, `use path::A`, `use path::A as B` /// (registers the alias `B`, which is what's in scope), and `use path::{A, B}` @@ -2274,86 +2214,58 @@ fn is_integer_type(ty: &Type) -> bool { ) } -/// Whether a type is a primitive WebIDL type whose `null` value should be -/// converted through the normal path rather than treated as absent. -/// Detect `Vec` types (WebIDL sequence parameters). -fn is_vec_type(ty: &Type) -> bool { - if let Type::Path(tp) = ty { - if let Some(seg) = tp.path.segments.last() { - return seg.ident == "Vec" - && matches!(seg.arguments, syn::PathArguments::AngleBracketed(_)); - } - } - false -} - -/// Extract the inner type from `Vec`. -fn extract_vec_inner_type(ty: &Type) -> Option { - if let Type::Path(type_path) = ty { - let last_seg = type_path.path.segments.last()?; - if last_seg.ident == "Vec" { - if let syn::PathArguments::AngleBracketed(args) = &last_seg.arguments { - if let Some(syn::GenericArgument::Type(inner)) = args.args.first() { - return Some(inner.clone()); - } - } - } - } - None -} - -/// Detect `Record` types (WebIDL record parameters). -fn is_record_type(ty: &Type) -> bool { - if let Type::Path(tp) = ty { - if let Some(seg) = tp.path.segments.last() { - return seg.ident == "Record" - && matches!(seg.arguments, syn::PathArguments::AngleBracketed(_)); - } +/// The generic arguments of `ty` when its last path segment is `name` with angle-bracketed +/// arguments. +fn generic_args<'a>( + ty: &'a Type, + name: &str, +) -> Option<&'a syn::punctuated::Punctuated> { + let Type::Path(tp) = ty else { + return None; + }; + let seg = tp.path.segments.last()?; + match &seg.arguments { + syn::PathArguments::AngleBracketed(args) if seg.ident == name => Some(&args.args), + _ => None, } - false } -/// Extract the value type from `Record` (the last type argument). -fn extract_record_inner_type(ty: &Type) -> Option { - if let Type::Path(type_path) = ty { - let last_seg = type_path.path.segments.last()?; - if last_seg.ident == "Record" { - if let syn::PathArguments::AngleBracketed(args) = &last_seg.arguments { - return args.args.iter().rev().find_map(|arg| match arg { - syn::GenericArgument::Type(inner) => Some(inner.clone()), - _ => None, - }); - } - } +/// The generic argument at `index` of the `name` type `ty`, if it is a type. +fn type_arg<'a>(ty: &'a Type, name: &str, index: usize) -> Option<&'a Type> { + match generic_args(ty, name)?.iter().nth(index)? { + syn::GenericArgument::Type(inner) => Some(inner), + _ => None, } - None } -/// Check whether a container type (`Vec` or `Record`) has an integer +/// Check whether a container type (`Vec` or `Record`) has an integer /// inner type, requiring `ConversionBehavior` config instead of `()`. fn is_int_container_type(ty: &Type) -> bool { - if is_vec_type(ty) { - extract_vec_inner_type(ty).is_some_and(|inner| is_integer_type(&inner)) - } else if is_record_type(ty) { - extract_record_inner_type(ty).is_some_and(|inner| is_integer_type(&inner)) - } else { - false - } + let record_value = || { + generic_args(ty, "Record")? + .iter() + .rev() + .find_map(|arg| match arg { + syn::GenericArgument::Type(inner) => Some(inner), + _ => None, + }) + }; + type_arg(ty, "Vec", 0) + .or_else(record_value) + .is_some_and(is_integer_type) } /// Detect `Option` types for optional parameter handling. fn is_option_type(ty: &Type) -> bool { - if let Type::Path(tp) = ty { - if let Some(seg) = tp.path.segments.last() { - return seg.ident == "Option" - && matches!(seg.arguments, syn::PathArguments::AngleBracketed(_)); - } - } - false + generic_args(ty, "Option").is_some() +} + +/// Extract the inner type from `Option`. +fn extract_option_inner_type(ty: &Type) -> Option<&Type> { + type_arg(ty, "Option", 0) } -/// JS-visible `.length` for a free function exported by `#[jsmodule]`, -/// `#[jsglobals]`, `#[webidl_methods]`, or a namespace: the number of required arguments, i.e. +/// JS-visible `.length` for a callable: the number of required arguments, i.e. /// parameters that aren't `Option`. fn required_arg_count(params: &[(Ident, Type)]) -> u32 { params.iter().filter(|(_, ty)| !is_option_type(ty)).count() as u32 @@ -2373,55 +2285,17 @@ fn is_any_value_type(ty: &Type) -> bool { /// Check whether a type is a none-optional `Heap`. fn is_bare_heap_type(ty: &Type) -> bool { - if let Type::Path(tp) = ty { - // Only match single-segment `Heap` (our js::gc::handle::Heap), - // not multi-segment paths like `js::gc::handle::MozHeap` (mozjs Heap). - if tp.path.segments.len() == 1 { - if let Some(seg) = tp.path.segments.last() { - return seg.ident == "Heap" - && matches!(seg.arguments, syn::PathArguments::AngleBracketed(_)); - } - } - } - false -} - -/// Extract the inner type from `Option`. -fn extract_option_inner_type(ty: &Type) -> Option { - if let Type::Path(type_path) = ty { - let last_seg = type_path.path.segments.last()?; - if last_seg.ident == "Option" { - if let syn::PathArguments::AngleBracketed(args) = &last_seg.arguments { - if let Some(syn::GenericArgument::Type(inner)) = args.args.first() { - return Some(inner.clone()); - } - } - } - } - None + // Only match single-segment `Heap` (our js::gc::handle::Heap), + // not multi-segment paths like `js::gc::handle::MozHeap` (mozjs Heap). + matches!(ty, Type::Path(tp) if tp.path.segments.len() == 1) + && generic_args(ty, "Heap").is_some() } -/// Extract the error type `E` from a `Result` return type. -fn extract_result_error_type(output: &ReturnType) -> Option { - if let ReturnType::Type(_, ty) = output { - if let Type::Path(tp) = ty.as_ref() { - if let Some(seg) = tp.path.segments.last() { - if seg.ident == "Result" { - if let syn::PathArguments::AngleBracketed(args) = &seg.arguments { - if let Some(syn::GenericArgument::Type(err_ty)) = args.args.iter().nth(1) { - return Some(err_ty.clone()); - } - } - } - } - } - } - None +/// Whether a return type is a `Result`. +fn returns_result(output: &ReturnType) -> bool { + matches!(output, ReturnType::Type(_, ty) if type_arg(ty, "Result", 1).is_some()) } -/// Detect stack newtype types like `Item<'_>` or `Item<'s>`. -/// -/// Stack newtypes are generated by `#[jsclass]` and always carry a single /// Check if a type string is exactly `Result < () , ::js::error::ExnThrown >` fn is_result_unit_jserror(ty_str: &str) -> bool { let normalized: String = ty_str.chars().filter(|c| !c.is_whitespace()).collect(); @@ -2593,7 +2467,7 @@ fn gen_arg_extractions( let optional = is_option_type(ty); let inner = optional.then(|| extract_option_inner_type(ty).expect("Option has an inner type")); - let declared = inner.as_ref().unwrap_or(ty); + let declared = inner.unwrap_or(ty); let borrow = BorrowedParam::of(declared); let convert_ty = borrow.as_ref().map_or(declared, |b| &b.convert_ty); let hidden = format_ident!("__arg_owned_{}", idx); @@ -2720,46 +2594,8 @@ fn gen_typed_arg_getter( } } -/// The declarations and names of a constructor's parameters, with a trailing -/// `RestArgs` appended when the constructor is variadic. -fn ctor_param_lists( - info: &MethodInfo, -) -> (Vec, Vec) { - let mut decls: Vec<_> = info - .params - .iter() - .map(|(name, ty)| quote! { #name: #ty }) - .collect(); - let mut names: Vec<_> = info - .params - .iter() - .map(|(name, _)| quote! { #name }) - .collect(); - if let Some(rest_name) = info.rest_arg_name.as_ref() { - let inner_ty = rest_args_element_type(info.rest_inner_type.as_ref()); - decls.push(quote! { #rest_name: ::js::class::RestArgs<#inner_ty> }); - names.push(quote! { #rest_name }); - } - (decls, names) -} - -/// The call to an old-style constructor fn on the inner data struct, which -/// yields the `FooImpl` value to wrap in a fresh JS object. -fn ctor_call( - info: &MethodInfo, - inner_name: &Ident, - param_names: &[proc_macro2::TokenStream], -) -> proc_macro2::TokenStream { - let ctor_fn = &info.fn_item.sig.ident; - if info.has_cx { - quote! { #inner_name::#ctor_fn(scope, #(#param_names),*) } - } else { - quote! { #inner_name::#ctor_fn(#(#param_names),*) } - } -} - -/// The `__ctor_setup_*` name a setup-style constructor's body is moved to on -/// the stack newtype. +/// The name of the `__ctor_setup_*` method on the stack newtype that a +/// setup-style constructor's body is moved into. fn setup_fn_ident(ident: &Ident) -> Ident { format_ident!("__ctor_setup_{}", unraw(ident)) } @@ -2776,33 +2612,20 @@ fn gen_setup_factory( let factory_name = &info.fn_item.sig.ident; let setup_ident = setup_fn_ident(factory_name); let param_decls: Vec<_> = info + .inputs .params .iter() .map(|(name, ty)| quote! { #name: #ty }) .collect(); - let param_names: Vec<_> = info - .params - .iter() - .map(|(name, _)| quote! { #name }) - .collect(); + let args = info.inputs.forwarded_args(quote!(scope), quote!(args)); + let call = quote! { #type_name::#setup_ident(&__typed, #(#args),*) }; // Always returns Result, ExnThrown> because create_instance_with // allocates a JS object, which is fallible. - let err_ty = extract_result_error_type(&info.fn_item.sig.output); - let setup_call = if err_ty.is_some() { - if info.has_cx { - quote! { #type_name::#setup_ident(&__typed, scope, #(#param_names),*).map_err(|e| { - ::js::error::ThrowException::throw(e, scope) - })?; } - } else { - quote! { #type_name::#setup_ident(&__typed, #(#param_names),*).map_err(|e| { - ::js::error::ThrowException::throw(e, scope) - })?; } - } - } else if info.has_cx { - quote! { #type_name::#setup_ident(&__typed, scope, #(#param_names),*); } + let setup_call = if returns_result(&info.fn_item.sig.output) { + quote! { #call.map_err(|e| ::js::error::ThrowException::throw(e, scope))?; } } else { - quote! { #type_name::#setup_ident(&__typed, #(#param_names),*); } + quote! { #call; } }; // Extract the constructor body and rename it. @@ -2844,9 +2667,21 @@ fn gen_instance_factory( type_name: &Ident, inner_name: &Ident, ) -> proc_macro2::TokenStream { - let (param_decls, param_names) = ctor_param_lists(info); - let call = ctor_call(info, inner_name, ¶m_names); - let create = if extract_result_error_type(&info.fn_item.sig.output).is_some() { + let param_decls = info + .inputs + .params + .iter() + .map(|(name, ty)| quote! { #name: #ty }) + .chain( + info.inputs + .rest + .iter() + .map(|(name, ty)| quote! { #name: ::js::class::RestArgs<#ty> }), + ); + let ctor_fn = &info.fn_item.sig.ident; + let args = info.inputs.forwarded_args(quote!(scope), quote!(args)); + let call = quote! { #inner_name::#ctor_fn(#(#args),*) }; + let create = if returns_result(&info.fn_item.sig.output) { quote! { ::js::class::try_create_instance_with::<#inner_name>(scope, |_| { #call.map_err(|e| ::js::error::ThrowException::throw(e, scope)) @@ -2879,31 +2714,16 @@ fn gen_instance_factory( /// Generate the constructor body that extracts args and calls the Rust constructor fn. fn gen_constructor_body(info: &MethodInfo, type_name: &Ident) -> proc_macro2::TokenStream { let ctor_fn = &info.fn_item.sig.ident; - let arg_extractions = - gen_arg_extractions(&info.params, quote!(args), true, quote!(scope), info.nargs); - let mut arg_names: Vec<_> = info.params.iter().map(|(name, _)| quote!(#name)).collect(); - - let rest_setup = gen_rest_setup( - info.rest_arg_name.as_ref(), - info.rest_inner_type.as_ref(), - info.params.len(), - "e!(args), + let arg_extractions = gen_arg_extractions( + &info.inputs.params, + quote!(args), true, - "e!(scope), + quote!(scope), + info.nargs, ); - if let Some(rest_name) = info.rest_arg_name.as_ref() { - arg_names.push(quote!(#rest_name)); - } - - // Build the constructor call, passing scope and/or args if the Rust - // constructor requested them via `scope: &Scope<'_>` or `args: &CallArgs`. - let call = if info.is_raw { - quote! { #type_name::#ctor_fn(scope, args) } - } else if info.has_cx { - quote! { #type_name::#ctor_fn(scope, #(#arg_names),*) } - } else { - quote! { #type_name::#ctor_fn(#(#arg_names),*) } - }; + let rest_setup = gen_rest_setup(&info.inputs, "e!(args), true, "e!(scope)); + let args = info.inputs.forwarded_args(quote!(scope), quote!(args)); + let call = quote! { #type_name::#ctor_fn(#(#args),*) }; // Constructors returning `Result` need error conversion: // the error is thrown as a JS exception via `ThrowException`. @@ -2928,11 +2748,6 @@ fn gen_constructor_body(info: &MethodInfo, type_name: &Ident) -> proc_macro2::To } } -/// Generate a JSNative wrapper function and the corresponding ClassBuilder call. -/// -/// When `on_newtype` is true, the method lives on the stack newtype `Foo<'s>`. -/// The wrapper extracts `this` as the rooted newtype and calls the method -/// via method-call syntax. /// Emit the complete JSNative trampoline (`extern "C" fn`) for a callable: a /// method, a static method, or a free function exposed by a module, the global, /// or a namespace. This is the single place where argument extraction, return- @@ -2947,9 +2762,7 @@ fn gen_constructor_body(info: &MethodInfo, type_name: &Ident) -> proc_macro2::To /// - `this_extraction` runs before the body for receiver-taking callables (empty /// for free functions, and for `ResultPromise` where it moves into the closure). /// - `this_in_closure` is the `?`-style receiver extraction for `ResultPromise`. -/// - `rest_arg` names the variadic tail's parameter and element type, if there is -/// one. The collection is emitted here rather than by the caller so that it -/// lands in the same place as the fixed-argument extractions. +/// - `inputs` are the parameters to convert, including the variadic tail. /// - `instance_type` is the class to mint for `InstanceValue`; `None` for free /// functions, which `classify_return_style` never classifies as `InstanceValue`. #[allow(clippy::too_many_arguments)] @@ -2957,23 +2770,16 @@ fn emit_native_fn( native_name: &Ident, name_str: &str, return_style: &ReturnStyle, - params: &[(Ident, Type)], + inputs: &Inputs, nargs: u32, call: &proc_macro2::TokenStream, this_extraction: &proc_macro2::TokenStream, this_in_closure: &proc_macro2::TokenStream, - rest_arg: Option<(&Ident, Option<&Type>)>, instance_type: Option<&Ident>, ) -> proc_macro2::TokenStream { + let params = &inputs.params; let gen_rest = |use_question_mark: bool| { - gen_rest_setup( - rest_arg.map(|(name, _)| name), - rest_arg.and_then(|(_, ty)| ty), - params.len(), - "e!(__args), - use_question_mark, - "e!(&scope), - ) + gen_rest_setup(inputs, "e!(__args), use_question_mark, "e!(&scope)) }; // For `ResultPromise` the argument extractions live inside the rejecting @@ -3124,6 +2930,30 @@ fn emit_native_fn( } } +/// The call of a member's Rust fn from its trampoline: a method call on `__self` when the member +/// lives on the stack newtype, and otherwise a call of `type_name`'s associated fn, with `self_arg` +/// as the first argument when it is given. +fn member_call( + info: &MethodInfo, + type_name: &Ident, + on_newtype: bool, + self_arg: Option, +) -> proc_macro2::TokenStream { + let fn_name = &info.fn_item.sig.ident; + let args = info.inputs.forwarded_args(quote!(&scope), quote!(&__args)); + if on_newtype { + quote! { __self.#fn_name(#(#args),*) } + } else { + let args = self_arg.into_iter().chain(args); + quote! { #type_name::#fn_name(#(#args),*) } + } +} + +/// Generate a JSNative wrapper function and the corresponding ClassBuilder call. +/// +/// When `on_newtype` is true, the method lives on the stack newtype `Foo<'s>`. +/// The wrapper extracts `this` as the rooted newtype and calls the method +/// via method-call syntax. fn gen_method_native( info: &MethodInfo, type_name: &Ident, @@ -3182,20 +3012,6 @@ fn gen_method_native( quote! {} }; - let call_args: Vec<_> = info.params.iter().map(|(name, _)| quote!(#name)).collect(); - let rest_arg = info - .rest_arg_name - .as_ref() - .map(|name| (name, info.rest_inner_type.as_ref())); - - // Build rest arg value for method call - let rest_arg_expr: Vec = if info.has_rest_args { - let rest_name = info.rest_arg_name.as_ref().unwrap(); - vec![quote! { #rest_name }] - } else { - vec![] - }; - // When `this` is the private data (not the newtype), `__self` is a borrow // guard; reborrow through it to pass the receiver to the inner method. let self_arg = if info.has_mut_self { @@ -3203,51 +3019,20 @@ fn gen_method_native( } else { quote! { &*__self } }; - let call = if on_newtype && (info.has_self || info.has_mut_self) { - // Method lives on the stack newtype — use method-call syntax. - if info.is_raw { - quote! { __self.#fn_name(&scope, &__args) } - } else if info.has_cx { - let all_args: Vec<_> = call_args.iter().chain(rest_arg_expr.iter()).collect(); - quote! { __self.#fn_name(&scope, #(#all_args),*) } - } else { - let all_args: Vec<_> = call_args.iter().chain(rest_arg_expr.iter()).collect(); - quote! { __self.#fn_name(#(#all_args),*) } - } - } else if info.has_self || info.has_mut_self { - if info.is_raw { - quote! { #type_name::#fn_name(#self_arg, &scope, &__args) } - } else if info.has_cx { - let all_args: Vec<_> = call_args.iter().chain(rest_arg_expr.iter()).collect(); - quote! { #type_name::#fn_name(#self_arg, &scope, #(#all_args),*) } - } else { - let all_args: Vec<_> = call_args.iter().chain(rest_arg_expr.iter()).collect(); - quote! { #type_name::#fn_name(#self_arg, #(#all_args),*) } - } - } else if info.is_raw { - quote! { #type_name::#fn_name(&scope, &__args) } - } else if info.has_cx { - let all_args: Vec<_> = call_args.iter().chain(rest_arg_expr.iter()).collect(); - quote! { #type_name::#fn_name(&scope, #(#all_args),*) } - } else { - let all_args: Vec<_> = call_args.iter().chain(rest_arg_expr.iter()).collect(); - quote! { #type_name::#fn_name(#(#all_args),*) } - }; + let call = member_call(info, type_name, on_newtype, has_this.then_some(self_arg)); let native_fn = emit_native_fn( &native_name, &name_str, &info.return_style, - &info.params, + &info.inputs, info.nargs, &call, &this_extraction, &this_in_closure, - rest_arg, Some(type_name), ); // Generate: .method(c"jsName", nargs, Some(native_fn), flags) - // We need a &'static CStr. Use an unsafe trick with a byte string literal. let builder_call = quote! { .method( #js_name_cstr_lit, @@ -3333,24 +3118,8 @@ fn gen_accessor_native( quote! { &mut *__self } }; + let call = member_call(info, type_name, on_newtype, Some(self_arg)); let body = if is_getter { - // Getter: call method, set return value - let call = if on_newtype { - if info.is_raw { - quote! { __self.#fn_name(&scope, &__args) } - } else if info.has_cx { - quote! { __self.#fn_name(&scope) } - } else { - quote! { __self.#fn_name() } - } - } else if info.is_raw { - quote! { #type_name::#fn_name(#self_arg, &scope, &__args) } - } else if info.has_cx { - quote! { #type_name::#fn_name(#self_arg, &scope) } - } else { - quote! { #type_name::#fn_name(#self_arg) } - }; - match &info.return_style { ReturnStyle::Raw => quote! { match #call { @@ -3358,11 +3127,6 @@ fn gen_accessor_native( Err(::js::error::ExnThrown) => ::js::exception::check_fn_return(&scope, false, &#name_str), } }, - ReturnStyle::Value => quote! { - let __result = #call; - let __set_ok = ::js::class::set_return(&scope, &__args, &__result); - ::js::exception::check_fn_return(&scope, __set_ok, &#name_str) - }, ReturnStyle::ResultValue => quote! { match #call { Ok(__v) => { @@ -3406,30 +3170,13 @@ fn gen_accessor_native( } else { // Setter: extract arg[0], call method let arg_extractions = gen_arg_extractions( - &info.params, + &info.inputs.params, quote!(&__args), false, quote!(&scope), info.nargs, ); - let call_args: Vec<_> = info.params.iter().map(|(name, _)| quote!(#name)).collect(); - let call = if on_newtype { - if info.is_raw { - quote! { __self.#fn_name(&scope, &__args) } - } else if info.has_cx { - quote! { __self.#fn_name(&scope, #(#call_args),*) } - } else { - quote! { __self.#fn_name(#(#call_args),*) } - } - } else if info.is_raw { - quote! { #type_name::#fn_name(#self_arg, &scope, &__args) } - } else if info.has_cx { - quote! { #type_name::#fn_name(#self_arg, &scope, #(#call_args),*) } - } else { - quote! { #type_name::#fn_name(#self_arg, #(#call_args),*) } - }; - match &info.return_style { ReturnStyle::Raw => quote! { #(#arg_extractions)* @@ -3705,27 +3452,6 @@ fn trace_variant_arm( // #[derive(ScopeRoot)] — scope-rooted mirror for `#[must_root]` aggregates // ============================================================================ -/// If `ty` is `Heap`, returns `INNER`. -fn heap_inner_ty(ty: &Type) -> Option<&Type> { - let Type::Path(tp) = ty else { return None }; - let seg = tp.path.segments.last()?; - if seg.ident != "Heap" { - return None; - } - let syn::PathArguments::AngleBracketed(args) = &seg.arguments else { - return None; - }; - match args.args.first()? { - syn::GenericArgument::Type(inner) => Some(inner), - _ => None, - } -} - -/// Whether `ty`'s final path segment is `Value` (the `js::native::Value` JSVal). -fn is_value_ty(ty: &Type) -> bool { - matches!(ty, Type::Path(tp) if tp.path.segments.last().is_some_and(|s| s.ident == "Value")) -} - /// One field's classification for `#[derive(ScopeRoot)]`. struct RootField { /// Binding identifier used in destructuring patterns. @@ -3746,8 +3472,8 @@ fn classify_root_fields(fields: &Fields) -> Vec { .iter() .enumerate() .map(|(i, f)| { - let (is_heap, is_object_heap, mirror_ty) = match heap_inner_ty(&f.ty) { - Some(inner) if is_value_ty(inner) => { + let (is_heap, is_object_heap, mirror_ty) = match type_arg(&f.ty, "Heap", 0) { + Some(inner) if last_segment_is(inner, "Value") => { (true, false, quote! { ::js::prelude::HandleValue<'s> }) } Some(inner) => ( @@ -3798,16 +3524,14 @@ fn build_root_group( None => quote! { #ty }, } }); + // A named field's binding has the field's name, so a plain named field uses the shorthand. let fast_args = infos.iter().map(|f| { let bind = &f.bind; - let expr = if f.is_heap { - quote! { #bind.take(scope) } - } else { - quote! { #bind } - }; - match &f.name { - Some(n) => quote! { #n: #expr }, - None => quote! { #expr }, + match (&f.name, f.is_heap) { + (Some(n), true) => quote! { #n: #bind.take(scope) }, + (Some(n), false) => quote! { #n }, + (None, true) => quote! { #bind.take(scope) }, + (None, false) => quote! { #bind }, } }); let traced_args = infos.iter().map(|f| { @@ -3971,51 +3695,13 @@ pub fn derive_scope_root(input: TokenStream) -> TokenStream { /// /// The function name is camelCased. fn parse_free_fn_export(fn_item: &syn::ItemFn) -> ModuleFnExport { - let fn_name = &fn_item.sig.ident; - let js_name = unraw(fn_name).to_lower_camel_case(); - - let mut params: Vec<(Ident, Type)> = Vec::new(); - let mut has_cx = false; - let mut is_raw = false; - let mut rest_arg_name = None; - let mut rest_inner_type = None; - for arg in &fn_item.sig.inputs { - if let FnArg::Typed(pat_ty) = arg { - if is_cx_param_type(&pat_ty.ty) { - has_cx = true; - continue; - } - if is_callargs_param_type(&pat_ty.ty) { - is_raw = true; - continue; - } - // A variadic tail is not an ordinary parameter: it is collected from - // whatever arguments follow the fixed ones, so it must stay out of - // `params` for the index arithmetic in `gen_rest_setup` to line up. - if is_rest_args_type(&pat_ty.ty) { - if let Pat::Ident(pat_ident) = pat_ty.pat.as_ref() { - rest_arg_name = Some(pat_ident.ident.clone()); - rest_inner_type = extract_rest_args_inner_type(&pat_ty.ty); - } - continue; - } - if let Pat::Ident(pat_ident) = pat_ty.pat.as_ref() { - params.push((pat_ident.ident.clone(), (*pat_ty.ty).clone())); - } - } - } - - let return_style = classify_return_style(&fn_item.sig.output, None, false, is_raw); - + let inputs = Inputs::parse(&fn_item.sig.inputs); + let return_style = classify_return_style(&fn_item.sig.output, None, false, inputs.is_raw); ModuleFnExport { - fn_name: fn_name.clone(), - js_name, - params, + fn_name: fn_item.sig.ident.clone(), + js_name: unraw(&fn_item.sig.ident).to_lower_camel_case(), + inputs, return_style, - has_cx, - is_raw, - rest_arg_name, - rest_inner_type, } } @@ -4053,8 +3739,8 @@ struct ModExports { /// export or how its JS name and signature are derived, so they share this /// walk. /// -/// `collect_classes` enables the `pub use Foo;` arm, which only `#[jsglobals]` -/// acts on; elsewhere a `use` is passed through as an ordinary item. +/// `collect_classes` enables the `pub use Foo;` arm. Without it, a `use` is +/// passed through as an ordinary item. /// `macro_name` is the spelling used in diagnostics, e.g. `"#[jsmodule]"`. fn collect_mod_exports( input: &syn::ItemMod, @@ -4120,34 +3806,19 @@ fn gen_free_fn_native( ) -> proc_macro2::TokenStream { let fn_name = &exp.fn_name; let name_str = fn_name.to_string(); - let mut call_args: Vec<_> = exp.params.iter().map(|(name, _)| quote!(#name)).collect(); - let rest_arg = exp - .rest_arg_name - .as_ref() - .map(|name| (name, exp.rest_inner_type.as_ref())); - if let Some(rest_name) = exp.rest_arg_name.as_ref() { - call_args.push(quote!(#rest_name)); - } - - let call = if exp.is_raw { - quote! { #call_path::#fn_name(&scope, &__args) } - } else if exp.has_cx { - quote! { #call_path::#fn_name(&scope, #(#call_args),*) } - } else { - quote! { #call_path::#fn_name(#(#call_args),*) } - }; + let args = exp.inputs.forwarded_args(quote!(&scope), quote!(&__args)); + let call = quote! { #call_path::#fn_name(#(#args),*) }; emit_native_fn( native_name, &name_str, &exp.return_style, - &exp.params, + &exp.inputs, nargs, &call, // Free functions don't have a receiver, so `this` extraction is empty "e! {}, "e! {}, - rest_arg, None, ) } @@ -4176,7 +3847,7 @@ fn gen_free_fn_native( /// } /// /// // Register: -/// unsafe { core_runtime::module::register_module::(scope) }; +/// core_runtime::module::register_module::(scope); /// /// // Call from Rust: /// assert_eq!(my_math::add(1.0, my_math::PI), 4.14159); @@ -4215,7 +3886,7 @@ pub fn jsmodule(attr: TokenStream, item: TokenStream) -> TokenStream { for exp in &fn_exports { let native_name = format_ident!("__native_module_{}", unraw(&exp.fn_name)); let js_name = &exp.js_name; - let nargs = required_arg_count(&exp.params); + let nargs = required_arg_count(&exp.inputs.params); native_fns.push(gen_free_fn_native( exp, @@ -4246,14 +3917,8 @@ pub fn jsmodule(attr: TokenStream, item: TokenStream) -> TokenStream { // Generate the evaluate function that sets constant values let mut value_setters: Vec = Vec::new(); for exp in &const_exports { - let const_name = &exp.const_name; let js_name = &exp.js_name; - // For reference-type constants (e.g. `&str`), pass directly. - let value_expr = if exp.is_ref_type { - quote! { super::#mod_name::#const_name } - } else { - quote! { &super::#mod_name::#const_name } - }; + let value_expr = exp.value_ref(quote!(super::#mod_name::)); value_setters.push(quote! { if !::core_runtime::module::set_module_export( scope, env, #js_name, #value_expr, @@ -4295,7 +3960,7 @@ pub fn jsmodule(attr: TokenStream, item: TokenStream) -> TokenStream { /// Register this native module so it can be imported from JS. /// /// Equivalent to `register_module::(scope)`. - pub unsafe fn register( + pub fn register( scope: &::js::gc::scope::Scope<'_>, ) -> bool { ::core_runtime::module::register_module::(scope) @@ -4309,14 +3974,8 @@ pub fn jsmodule(attr: TokenStream, item: TokenStream) -> TokenStream { struct ModuleFnExport { fn_name: Ident, js_name: String, - params: Vec<(Ident, Type)>, + inputs: Inputs, return_style: ReturnStyle, - has_cx: bool, - is_raw: bool, - /// The variadic tail's parameter name and `RestArgs` element type, if the - /// function declared one. - rest_arg_name: Option, - rest_inner_type: Option, } struct ModuleConstExport { @@ -4396,7 +4055,7 @@ pub fn jsglobals(attr: TokenStream, item: TokenStream) -> TokenStream { for exp in &fn_exports { let native_name = format_ident!("__native_global_{}", unraw(&exp.fn_name)); let js_name = &exp.js_name; - let nargs = required_arg_count(&exp.params); + let nargs = required_arg_count(&exp.inputs.params); native_fns.push(gen_free_fn_native( exp, @@ -4421,16 +4080,8 @@ pub fn jsglobals(attr: TokenStream, item: TokenStream) -> TokenStream { // Generate install calls for constants for exp in &const_exports { - let const_name = &exp.const_name; let js_name_cstr_lit = cstr_literal(&exp.js_name); - - // For reference-type constants (e.g. `&str`), pass the value directly - // since it's already a reference. For value types, add `&`. - let value_expr = if exp.is_ref_type { - quote! { super::#mod_name::#const_name } - } else { - quote! { &super::#mod_name::#const_name } - }; + let value_expr = exp.value_ref(quote!(super::#mod_name::)); install_calls.push(quote! { global.define_property( @@ -4568,7 +4219,7 @@ fn process_namespace(opts: AttrOpts, input: syn::ItemMod, config: NamespaceConfi for exp in &fn_exports { let native_name = format_ident!("__native_ns_{}", unraw(&exp.fn_name)); - let nargs = required_arg_count(&exp.params); + let nargs = required_arg_count(&exp.inputs.params); native_fns.push(gen_free_fn_native( exp, @@ -4594,13 +4245,8 @@ fn process_namespace(opts: AttrOpts, input: syn::ItemMod, config: NamespaceConfi // Install constants on the namespace object. Namespace members are // enumerable, configurable, writable data properties (WebIDL §3.13). for exp in &const_exports { - let const_name = &exp.const_name; let js_name_cstr_lit = cstr_literal(&exp.js_name); - let value_expr = if exp.is_ref_type { - quote! { #const_name } - } else { - quote! { &#const_name } - }; + let value_expr = exp.value_ref(quote!()); install_calls.push(quote! { ns_obj.define_property( scope, @@ -4620,8 +4266,7 @@ fn process_namespace(opts: AttrOpts, input: syn::ItemMod, config: NamespaceConfi quote! {} }; - let js_ns_name_bytes = format!("{js_ns_name}\0"); - let js_ns_name_cstr = proc_macro2::Literal::byte_string(js_ns_name_bytes.as_bytes()); + let js_ns_name_cstr = cstr_literal(&js_ns_name); let output = quote! { #mod_vis mod #mod_name { @@ -4648,11 +4293,7 @@ fn process_namespace(opts: AttrOpts, input: syn::ItemMod, config: NamespaceConfi // Install the namespace object on the global as a // non-enumerable, configurable, writable data property (WebIDL // §3.13 — defined, not assigned via [[Set]]). - let ns_name = unsafe { - ::std::ffi::CStr::from_bytes_with_nul_unchecked(#js_ns_name_cstr) - }; - let ns_val = unsafe { ::js::value::from_object(ns_obj.as_raw()) }; - global.define_property(scope, ns_name, &ns_val, 0) + global.define_property(scope, #js_ns_name_cstr, ns_obj, 0) .expect("failed to define namespace on global"); } } @@ -4811,25 +4452,14 @@ pub fn webidl_dictionary(attr: TokenStream, item: TokenStream) -> TokenStream { } } - let js_name = js_name_override.unwrap_or_else(|| { - let name = ident.to_string(); - // Strip raw identifier prefix (e.g. `r#type` → `type`). - let name = name.strip_prefix("r#").unwrap_or(&name); - name.to_lower_camel_case() - }); - let optional = is_option_type(&ty); - let inner_ty = if optional { - extract_option_inner_type(&ty) - } else { - None - }; + let js_name = js_name_override.unwrap_or_else(|| unraw(&ident).to_lower_camel_case()); + let option_inner = extract_option_inner_type(&ty).cloned(); members.push(DictMember { ident, ty, - inner_ty, + option_inner, js_name, - optional, default_expr, }); } @@ -4905,7 +4535,7 @@ pub fn webidl_dictionary(attr: TokenStream, item: TokenStream) -> TokenStream { None } else { Some( - ::js::Object::from_value(scope, *val) + ::js::Object::from_raw_value(scope, val.get()) .map_err(|_| ::js::conversion::ConversionError::Failure( ::std::borrow::Cow::Borrowed(c"dictionary value is not an object"), ))? @@ -4923,41 +4553,15 @@ pub fn webidl_dictionary(attr: TokenStream, item: TokenStream) -> TokenStream { } }; // Strip #[webidl(...)] attributes from the output struct's fields. - let mut output_struct = input.clone(); - if let Fields::Named(ref mut named) = output_struct.fields { - for field in &mut named.named { - field.attrs.retain(|a| !a.path().is_ident("webidl")); - } + let mut output_fields = fields.clone(); + for field in &mut output_fields { + field.attrs.retain(|a| !a.path().is_ident("webidl")); } - let cleaned_fields = if let Fields::Named(ref named) = output_struct.fields { - let field_tokens: Vec<_> = named - .named - .iter() - .map(|f| { - let field_attrs: Vec<_> = f - .attrs - .iter() - .filter(|a| !a.path().is_ident("webidl")) - .collect(); - let field_vis = &f.vis; - let field_ident = &f.ident; - let field_ty = &f.ty; - quote! { - #(#field_attrs)* - #field_vis #field_ident: #field_ty - } - }) - .collect(); - field_tokens - } else { - vec![] - }; - let output = quote! { #(#attrs)* #vis struct #struct_name #impl_generics #where_clause { - #(#cleaned_fields),* + #output_fields } #from_jsval_impl @@ -5028,12 +4632,10 @@ fn parent_field<'a>( struct DictMember { ident: Ident, ty: Type, - /// The inner type if this is `Option`. - inner_ty: Option, + /// The `T` of an `Option` field, which is an optional member. + option_inner: Option, /// The JS property name (camelCase). js_name: String, - /// Whether this is an `Option` field (optional member). - optional: bool, /// Default expression, if any (from `#[webidl(default = ...)]`). default_expr: Option, } @@ -5047,9 +4649,8 @@ fn gen_dict_member_extraction(member: &DictMember) -> proc_macro2::TokenStream { // (not `undefined`). The three member kinds differ only in the binding (a // type annotation drives `from_jsval` inference for non-optional members), // the conversion type, and what an absent property yields. - let (binding, conv_ty, absent) = if member.optional { + let (binding, conv_ty, absent) = if let Some(inner_ty) = &member.option_inner { // Optional member (Option): None when property is missing/undefined. - let inner_ty = member.inner_ty.as_ref().unwrap_or(&member.ty); (quote! { let #ident }, inner_ty, quote! { None }) } else if let Some(default_expr) = &member.default_expr { // Required member with a default: use the default when missing/undefined. @@ -5097,7 +4698,7 @@ fn gen_dict_member_extraction(member: &DictMember) -> proc_macro2::TokenStream { quote! { ::js::conversion::FromJSVal::from_jsval(scope, __prop, ())? } } }; - let convert = if member.optional { + let convert = if member.option_inner.is_some() { quote! { Some(#core) } } else { core @@ -5175,6 +4776,19 @@ pub fn allow_unrooted_interior_in_rc(_attr: TokenStream, item: TokenStream) -> T .into() } +impl ModuleConstExport { + /// A reference to the constant, which `prefix` qualifies: the constant itself for a + /// reference-typed constant such as a `&str`, and a borrow of it otherwise. + fn value_ref(&self, prefix: proc_macro2::TokenStream) -> proc_macro2::TokenStream { + let const_name = &self.const_name; + if self.is_ref_type { + quote! { #prefix #const_name } + } else { + quote! { &#prefix #const_name } + } + } +} + // ============================================================================ // WebIDL Union types — `#[webidl_union]` // ============================================================================ @@ -5239,7 +4853,7 @@ fn classify_union_variant(ty: &Type) -> UnionCategory { if s == "String" { return UnionCategory::String; } - if is_vec_type(ty) { + if generic_args(ty, "Vec").is_some() { return UnionCategory::Sequence; } UnionCategory::Object @@ -5251,6 +4865,7 @@ fn process_webidl_union(input: ItemEnum) -> TokenStream { let attrs = &input.attrs; let generics = &input.generics; let where_clause = &generics.where_clause; + let variants = &input.variants; // Unions whose variants reference scope-rooted types declare a single // lifetime parameter. Reject anything beyond one lifetime / type parameter @@ -5533,18 +5148,6 @@ fn process_webidl_union(input: ItemEnum) -> TokenStream { }) .collect(); - // Rebuild the enum variants for the output. - let variant_defs: Vec<_> = input - .variants - .iter() - .map(|v| { - let id = &v.ident; - let fields = &v.fields; - let attrs = &v.attrs; - quote! { #(#attrs)* #id #fields } - }) - .collect(); - // When the enum carries a lifetime parameter, reuse it as the scope // lifetime so variants like `Stack<'a, T>` line up with the trait's `'s`. let scope_lt = match &lifetime { @@ -5559,7 +5162,7 @@ fn process_webidl_union(input: ItemEnum) -> TokenStream { let output = quote! { #(#attrs)* #vis enum #enum_name #generics #where_clause { - #(#variant_defs,)* + #variants } impl<#scope_lt, 'v> ::js::conversion::FromJSVal<#scope_lt, 'v> for #enum_ty { @@ -5706,44 +5309,43 @@ mod tests { #[test] fn is_result_promise_type_detects_promise_ok() { - let parse = |s: &str| syn::parse_str::(s).unwrap(); // The `Ok` type carries a lifetime argument, which the bare-identifier // `is_promise_type` rejects — the dedicated check must still match. - assert!(is_result_promise_type(&parse( + assert!(is_result_promise_type(&parse_ty( "Result, ExnThrown>" ))); - assert!(is_result_promise_type(&parse( + assert!(is_result_promise_type(&parse_ty( "Result, ExnThrown>" ))); - assert!(is_result_promise_type(&parse( + assert!(is_result_promise_type(&parse_ty( "Result" ))); } #[test] fn is_result_promise_type_rejects_non_promise() { - let parse = |s: &str| syn::parse_str::(s).unwrap(); - assert!(!is_result_promise_type(&parse("Result"))); - assert!(!is_result_promise_type(&parse("Result<(), ExnThrown>"))); - assert!(!is_result_promise_type(&parse("Promise<'r>"))); + assert!(!is_result_promise_type(&parse_ty( + "Result" + ))); + assert!(!is_result_promise_type(&parse_ty("Result<(), ExnThrown>"))); + assert!(!is_result_promise_type(&parse_ty("Promise<'r>"))); // A `Promise` nested inside another `Ok` type is not a promise return. - assert!(!is_result_promise_type(&parse( + assert!(!is_result_promise_type(&parse_ty( "Result>, ExnThrown>" ))); } #[test] fn promise_params_match_both_spellings() { - let parse = |s: &str| syn::parse_str::(s).unwrap(); - assert!(is_unchecked_promise_type(&parse("Promise<'_>"))); - assert!(is_unchecked_promise_type(&parse("js::Promise<'r>"))); + assert!(is_unchecked_promise_type(&parse_ty("Promise<'_>"))); + assert!(is_unchecked_promise_type(&parse_ty("js::Promise<'r>"))); // A future is a return type, never an argument, so it must not pick up // the `Promise` argument conversion the way `is_promise_type` does. - assert!(!is_unchecked_promise_type(&parse("PromiseFuture"))); - assert!(!is_unchecked_promise_type(&parse( + assert!(!is_unchecked_promise_type(&parse_ty("PromiseFuture"))); + assert!(!is_unchecked_promise_type(&parse_ty( "PromiseOf<'_, Response<'_>>" ))); - assert!(!is_unchecked_promise_type(&parse("Vec>"))); + assert!(!is_unchecked_promise_type(&parse_ty("Vec>"))); } #[test] @@ -5814,17 +5416,16 @@ mod tests { #[test] fn cx_and_callargs_params_match_exactly() { - let parse = |s: &str| syn::parse_str::(s).unwrap(); // The real context / raw-args parameter spellings are recognized. - assert!(is_cx_param_type(&parse("&Scope<'_>"))); - assert!(is_cx_param_type(&parse("&js::gc::scope::Scope<'_>"))); - assert!(is_cx_param_type(&parse("&mut JSContext"))); - assert!(is_callargs_param_type(&parse("&CallArgs"))); + assert!(is_cx_param_type(&parse_ty("&Scope<'_>"))); + assert!(is_cx_param_type(&parse_ty("&js::gc::scope::Scope<'_>"))); + assert!(is_cx_param_type(&parse_ty("&mut JSContext"))); + assert!(is_callargs_param_type(&parse_ty("&CallArgs"))); // A user parameter whose type merely contains the substring is not. - assert!(!is_cx_param_type(&parse("ScopeOptions"))); - assert!(!is_cx_param_type(&parse("Telescope"))); - assert!(!is_cx_param_type(&parse("Vec"))); - assert!(!is_callargs_param_type(&parse("MyCallArgs"))); - assert!(!is_callargs_param_type(&parse("CallArgsView"))); + assert!(!is_cx_param_type(&parse_ty("ScopeOptions"))); + assert!(!is_cx_param_type(&parse_ty("Telescope"))); + assert!(!is_cx_param_type(&parse_ty("Vec"))); + assert!(!is_callargs_param_type(&parse_ty("MyCallArgs"))); + assert!(!is_callargs_param_type(&parse_ty("CallArgsView"))); } } diff --git a/crown/src/common.rs b/crown/src/common.rs index 71a6a04..262f04a 100644 --- a/crown/src/common.rs +++ b/crown/src/common.rs @@ -11,17 +11,11 @@ use rustc_lint::{LateContext, Lint, LintContext}; use rustc_middle::ty::{self, GenericArg, TraitRef, Ty, TyCtxt, TypeVisitableExt}; use rustc_span::hygiene::{ExpnKind, MacroKind}; use rustc_span::symbol::Symbol; -use rustc_span::{Span, DUMMY_SP}; +use rustc_span::Span; use rustc_trait_selection::traits::query::evaluate_obligation::InferCtxtExt as _; use rustc_type_ir::Upcast as _; -/// Emit a lint whose diagnostic is built by a closure. -/// -/// rustc dropped the closure-based `LintContext::lint` in favour of -/// `opt_span_lint`, which takes a value implementing the `Diagnostic` trait. -/// This restores the ergonomic closure form the lint passes were written -/// against by wrapping the closure in [`DiagDecorator`]. As before, the span is -/// set inside the closure via `Diag::span`, so no span is passed here. +/// Emit a lint whose diagnostic `decorate` builds, including its span. pub trait LateContextExt { fn lint(&self, lint: &'static Lint, decorate: impl FnOnce(&mut Diag<'_, ()>)); } @@ -32,9 +26,6 @@ impl<'tcx> LateContextExt for LateContext<'tcx> { } } -/// check if a DefId's path matches the given absolute type path -/// usage e.g. with -/// `match_def_path(cx, id, &["core", "option", "Option"])` /// Attribute lookups on [`TyCtxt`]. pub trait TyCtxtExt { /// Whether the item `did` has an attribute whose path is `attrs`. @@ -50,13 +41,15 @@ impl TyCtxtExt for TyCtxt<'_> { /// The item an alias type names: the associated type, opaque type or type alias. pub fn alias_def_id(kind: ty::AliasTyKind<'_>) -> DefId { match kind { - ty::AliasTyKind::Projection { def_id } => def_id.into(), - ty::AliasTyKind::Inherent { def_id } => def_id.into(), - ty::AliasTyKind::Opaque { def_id } => def_id.into(), - ty::AliasTyKind::Free { def_id } => def_id.into(), + ty::AliasTyKind::Projection { def_id } + | ty::AliasTyKind::Inherent { def_id } + | ty::AliasTyKind::Opaque { def_id } + | ty::AliasTyKind::Free { def_id } => def_id, } } +/// Check if a DefId's path matches the given absolute type path, e.g. with +/// `match_def_path(cx, id, &["core", "option", "Option"])`. pub fn match_def_path(cx: &LateContext, def_id: DefId, path: &[Symbol]) -> bool { let def_path = cx.tcx.def_path(def_id); let krate = &cx.tcx.crate_name(def_path.krate); @@ -147,66 +140,23 @@ pub fn trait_in_crate<'tcx>( Stuff copied from clippy: */ -/// Checks whether a type implements a trait. -/// The function returns false in case the type contains an inference variable. +/// Checks whether a type implements a trait. Panics if the type contains an inference variable. /// /// See [Common tools for writing lints] for an example how to use this function and other options. /// /// [Common tools for writing lints]: https://github.com/rust-lang/rust-clippy/blob/master/book/src/development/common_tools_writing_lints.md#checking-if-a-type-implements-a-specific-trait -pub fn implements_trait<'tcx>( - cx: &LateContext<'tcx>, - ty: Ty<'tcx>, - trait_id: DefId, - args: &[GenericArg<'tcx>], -) -> bool { - implements_trait_with_env_from_iter( - cx.tcx, - cx.typing_env(), - ty, - trait_id, - None, - args.iter().map(|&x| Some(x)), - ) -} - -/// Same as `implements_trait_from_env` but takes the arguments as an iterator. -pub fn implements_trait_with_env_from_iter<'tcx>( - tcx: TyCtxt<'tcx>, - typing_env: ty::TypingEnv<'tcx>, - ty: Ty<'tcx>, - trait_id: DefId, - callee_id: Option, - args: impl IntoIterator>>>, -) -> bool { +pub fn implements_trait<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, trait_id: DefId) -> bool { + let tcx = cx.tcx; // Clippy shouldn't have infer types assert!(!ty.has_infer()); - // If a `callee_id` is passed, then we assert that it is a body owner - // through calling `body_owner_kind`, which would panic if the callee - // does not have a body. - if let Some(callee_id) = callee_id { - let _ = tcx.hir_body_owner_kind(callee_id); - } - let ty = tcx.erase_and_anonymize_regions(ty); if ty.has_escaping_bound_vars() { return false; } - let (infcx, param_env) = tcx.infer_ctxt().build_with_typing_env(typing_env); - let args = args - .into_iter() - .map(|arg| { - arg.into() - .unwrap_or_else(|| infcx.next_ty_var(DUMMY_SP).into()) - }) - .collect::>(); - - let trait_ref = TraitRef::new( - tcx, - trait_id, - [GenericArg::from(ty)].into_iter().chain(args), - ); + let (infcx, param_env) = tcx.infer_ctxt().build_with_typing_env(cx.typing_env()); + let trait_ref = TraitRef::new(tcx, trait_id, [GenericArg::from(ty)]); debug_assert!( matches!(tcx.def_kind(trait_id), DefKind::Trait | DefKind::TraitAlias), diff --git a/crown/src/trace_in_no_trace.rs b/crown/src/trace_in_no_trace.rs index e64da59..9b6d92c 100644 --- a/crown/src/trace_in_no_trace.rs +++ b/crown/src/trace_in_no_trace.rs @@ -109,7 +109,7 @@ fn find_jstraceable<'tcx>(cx: &LateContext<'tcx>) -> Option { fn is_jstraceable<'tcx>(cx: &LateContext<'tcx>, ty: ty::Ty<'tcx>) -> bool { if let Some(trait_id) = find_jstraceable(cx) { - return implements_trait(cx, ty, trait_id, &[]); + return implements_trait(cx, ty, trait_id); } panic!("JSTraceable not found"); } @@ -168,11 +168,6 @@ jsmanaged inside on {pos}-th position. Consider removing the wrapper." // as these are the only ones that are actually traced impl<'tcx> LateLintPass<'tcx> for NotracePass { fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item) { - // TODO: better performance if we limit with lint attr??? - /*let attrs = cx.tcx.hir().attrs(item.hir_id()); - if has_lint_attr(&self.symbols, &attrs, self.symbols.must_root) { - return; - }*/ if let hir::ItemKind::Struct(_, _, variant_data) = &item.kind { for field in variant_data.fields() { let field_type = cx.tcx.type_of(field.def_id); @@ -182,15 +177,11 @@ impl<'tcx> LateLintPass<'tcx> for NotracePass { } fn check_variant(&mut self, cx: &LateContext, var: &hir::Variant) { - #[allow(clippy::single_match)] - match var.data { - hir::VariantData::Tuple(fields, ..) => { - for field in fields { - let field_type = cx.tcx.type_of(field.def_id); - incorrect_no_trace(&self.symbols, cx, field_type.skip_binder(), field.ty.span); - } - }, - _ => (), // Struct variants already caught by check_struct_def + if let hir::VariantData::Tuple(fields, ..) = var.data { + for field in fields { + let field_type = cx.tcx.type_of(field.def_id); + incorrect_no_trace(&self.symbols, cx, field_type.skip_binder(), field.ty.span); + } } } } diff --git a/crown/src/unrooted_must_root.rs b/crown/src/unrooted_must_root.rs index d7bd55e..30518b0 100644 --- a/crown/src/unrooted_must_root.rs +++ b/crown/src/unrooted_must_root.rs @@ -144,6 +144,35 @@ fn is_unrooted_ty<'tcx>( // callers pass `false` for return-type checks. flag_value: bool, ) -> bool { + // Structures which are semantically similar to an &ptr. + let ref_like: &[&[Symbol]] = &[ + &[sym::core, sym.cell, sym.Ref], + &[sym::core, sym.cell, sym.RefMut], + &[sym::core, sym::slice, sym::iter, sym.Iter], + &[sym::core, sym::slice, sym::iter, sym.IterMut], + &[sym.accountable_refcell, sym.Ref], + &[sym.accountable_refcell, sym.RefMut], + &[sym.mozjs, sym.gc, sym.root, sym.Handle], + &[sym.js, sym.gc, sym.handle, sym.Stack], + &[sym.mozjs_sys, sym.generated, sym.root, sym.JS, sym.Handle], + &[sym::std, sym.collections, sym.hash, sym.map, sym.Entry], + &[ + sym::std, + sym.collections, + sym.hash, + sym.map, + sym.OccupiedEntry, + ], + &[ + sym::std, + sym.collections, + sym.hash, + sym.map, + sym.VacantEntry, + ], + &[sym::std, sym.collections, sym.hash, sym.map, sym.Iter], + &[sym::std, sym.collections, sym.hash, sym.set, sym.Iter], + ]; let mut ret = false; let mut walker = ty.walk(); while let Some(generic_arg) = walker.next() { @@ -182,62 +211,10 @@ fn is_unrooted_ty<'tcx>( }, _ => true, } - } else if match_def_path(cx, did.did(), &[sym::core, sym.cell, sym.Ref]) - || match_def_path(cx, did.did(), &[sym::core, sym.cell, sym.RefMut]) - || match_def_path(cx, did.did(), &[sym::core, sym::slice, sym::iter, sym.Iter]) - || match_def_path( - cx, - did.did(), - &[sym::core, sym::slice, sym::iter, sym.IterMut], - ) - || match_def_path(cx, did.did(), &[sym.accountable_refcell, sym.Ref]) - || match_def_path(cx, did.did(), &[sym.accountable_refcell, sym.RefMut]) - || match_def_path(cx, did.did(), &[sym.mozjs, sym.gc, sym.root, sym.Handle]) - || match_def_path(cx, did.did(), &[sym.js, sym.gc, sym.handle, sym.Stack]) - || match_def_path( - cx, - did.did(), - &[sym.mozjs_sys, sym.generated, sym.root, sym.JS, sym.Handle], - ) - || match_def_path( - cx, - did.did(), - &[sym::std, sym.collections, sym.hash, sym.map, sym.Entry], - ) - || match_def_path( - cx, - did.did(), - &[ - sym::std, - sym.collections, - sym.hash, - sym.map, - sym.OccupiedEntry, - ], - ) - || match_def_path( - cx, - did.did(), - &[ - sym::std, - sym.collections, - sym.hash, - sym.map, - sym.VacantEntry, - ], - ) - || match_def_path( - cx, - did.did(), - &[sym::std, sym.collections, sym.hash, sym.map, sym.Iter], - ) - || match_def_path( - cx, - did.did(), - &[sym::std, sym.collections, sym.hash, sym.set, sym.Iter], - ) + } else if ref_like + .iter() + .any(|path| match_def_path(cx, did.did(), path)) { - // Structures which are semantically similar to an &ptr. false } else if match_def_path( cx, @@ -493,14 +470,12 @@ impl<'tcx> LateLintPass<'tcx> for UnrootedPass { ) { let in_new_function = match kind { visit::FnKind::ItemFn(n, _, _) | visit::FnKind::Method(n, _) => { - n.as_str() == "new" - || n.as_str().starts_with("new_") - || n.as_str() == "from" - || n.as_str().starts_with("from_") - || n.as_str() == "default" - || n.as_str() == "Wrap" - || n.as_str() == "constructor" - || n.as_str() == "construct" + let n = n.as_str(); + matches!( + n, + "new" | "from" | "default" | "Wrap" | "constructor" | "construct" + ) || n.starts_with("new_") + || n.starts_with("from_") }, visit::FnKind::Closure => false, }; @@ -562,45 +537,18 @@ struct FnDefVisitor<'a, 'tcx: 'a> { } impl<'a, 'tcx> visit::Visitor<'tcx> for FnDefVisitor<'a, 'tcx> { - // TODO: https://github.com/servo/servo/issues/37330 - /* - type NestedFilter = rustc_middle::hir::nested_filter::OnlyBodies; - - fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt { - self.cx.tcx - } - */ - fn visit_expr(&mut self, expr: &'tcx hir::Expr) { let cx = self.cx; - let require_rooted = |cx: &LateContext, in_new_function: bool, subexpr: &hir::Expr| { + // Trait casts from #[js::must_root] types are not allowed + if let ExprKind::Cast(subexpr, _) = expr.kind { let ty = cx.typeck_results().expr_ty(subexpr); - if is_unrooted_ty(self.symbols, cx, ty, in_new_function, true) { + if is_unrooted_ty(self.symbols, cx, ty, self.in_new_function, true) { cx.lint(UNROOTED_MUST_ROOT, |lint| { lint.primary_message(format!("Expression of type {:?} must be rooted.", ty)); lint.span(subexpr.span); }) } - }; - - match expr.kind { - // Trait casts from #[js::must_root] types are not allowed - ExprKind::Cast(subexpr, _) => require_rooted(cx, self.in_new_function, subexpr), - // This catches assignments... the main point of this would be to catch mutable - // references to `JS`. - // FIXME: Enable this? Triggers on certain kinds of uses of DomRefCell. - // hir::ExprAssign(_, ref rhs) => require_rooted(cx, self.in_new_function, &*rhs), - // This catches calls; basically, this enforces the constraint that only constructors - // can call other constructors. - // FIXME: Enable this? Currently triggers with constructs involving DomRefCell, and - // constructs like Vec> and RootedVec>. - // hir::ExprCall(..) if !self.in_new_function => { - // require_rooted(cx, self.in_new_function, expr); - // } - _ => { - // TODO(pcwalton): Check generics with a whitelist of allowed generics. - }, } visit::walk_expr(self, expr); diff --git a/docs/rooting.md b/docs/rooting.md index ce6d573..31e3701 100644 --- a/docs/rooting.md +++ b/docs/rooting.md @@ -201,6 +201,12 @@ at compile time (`just crown`, and part of `just check-all`): When crown flags something, the fix is almost always one of: root the value, move it straight into a traced container, or wrap it while you need it. +Crown allows bare `Value`s inside the `js` crate. The types cover the case that matters there: a +raw `Value` doesn't implement `ToJSVal`, so an API taking `impl ToJSVal` only ever receives a +rooted value or one that holds no GC thing, and can GC before it converts it. Pass a raw value +read from a handle to an API that takes one directly, such as `Object::from_raw_value`, with +nothing that can GC in between. + ## Ensuring proper rooting through dynamic checks The tools and practices described here cut down on rooting complexity and catch most bugs statically, but they're not 100% foolproof. To further improve rooting correctness, StarlingMonkey CI runs parts of the test suite with dynamic checks enabled that are very highly likely to catch any remaining issues. These checks cause tests to run many (in some cases: hundreds of) times more slowly, so they're not enabled for the entire test suite. diff --git a/starling/examples/native_module.rs b/starling/examples/native_module.rs index d2006a0..95644b4 100644 --- a/starling/examples/native_module.rs +++ b/starling/examples/native_module.rs @@ -47,10 +47,7 @@ fn main() { let scope = rt.default_global(); // Register native module using the generated convenience function - assert!( - unsafe { math_utils::register(&scope) }, - "Failed to register module" - ); + assert!(math_utils::register(&scope), "Failed to register module"); // ==================================================================== // Test 1: Call functions from Rust diff --git a/starling/examples/showcase.rs b/starling/examples/showcase.rs index 598f6f7..11bf4d7 100644 --- a/starling/examples/showcase.rs +++ b/starling/examples/showcase.rs @@ -306,10 +306,7 @@ fn main() { app_globals::add_to_global(&scope, global); // Register native module. - assert!( - unsafe { math::register(&scope) }, - "Failed to register math module" - ); + assert!(math::register(&scope), "Failed to register math module"); // ==================================================================== // Part A: Rust-side usage of every feature diff --git a/tests/wpt-harness/run-wpt.mjs b/tests/wpt-harness/run-wpt.mjs index def09cc..5d38050 100644 --- a/tests/wpt-harness/run-wpt.mjs +++ b/tests/wpt-harness/run-wpt.mjs @@ -874,7 +874,7 @@ function runScriptPath(testPath) { /// restricted runtime — and `--update-expectations` would then write those results into the /// `permissive_status` fields, corrupting them for the runs that do disable the restrictions. function restrictionFlags() { - return config.permissive ? ["--enforce-fetch-restrictions=false"] : []; + return config.permissive ? ["--web-security-restrictions=false"] : []; } // Build the command that runs `scriptFile` as a one-shot command: the runtime binary directly on From 91fd43d6ab310a5583a19a38dc0c38b4a1d4e829 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Thu, 22 Oct 2026 15:32:17 +0200 Subject: [PATCH 04/11] Simplify the web builtins and trim their tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `btoa` and `TextDecoder.decode` report out-of-memory instead of aborting, reading a whole body rejects when its buffer can't grow, an outgoing body whose chunk can't be allocated fails its send, and all of them throw one `js::error::OutOfMemory`. The C++ `console` prints messages that contain NUL characters in full. `onabort` follows the `EventHandler` rules, and `performance.now()` and `Event.timeStamp` are coarsened to 100 µs while web security restrictions are on. Duplicated helpers and accessors are folded together, dead code (including the unused Rust `console`) is removed, and Rust tests that repeat passing WPT subtests are dropped. The streams tests are one binary. --- Cargo.lock | 2 - crates/builtins/cpp-builtins/Cargo.toml | 8 +- .../cpp-builtins/cpp/builtins/console.cpp | 48 +- crates/builtins/cpp-builtins/src/lib.rs | 59 -- crates/builtins/fetch-event/Cargo.toml | 4 + .../fetch-event/src/extendable_event.rs | 2 - .../builtins/fetch-event/src/fetch_event.rs | 41 +- crates/builtins/fetch-event/tests/dispatch.rs | 143 +---- crates/builtins/web-fetch/Cargo.toml | 2 +- crates/builtins/web-fetch/src/abort.rs | 14 +- crates/builtins/web-fetch/src/algorithms.rs | 15 +- crates/builtins/web-fetch/src/body_mixin.rs | 16 +- crates/builtins/web-fetch/src/body_proxy.rs | 7 +- crates/builtins/web-fetch/src/byte_string.rs | 6 - crates/builtins/web-fetch/src/globals.rs | 50 +- crates/builtins/web-fetch/src/headers.rs | 19 +- .../builtins/web-fetch/src/incoming_body.rs | 30 +- crates/builtins/web-fetch/src/lib.rs | 4 +- .../builtins/web-fetch/src/outgoing_body.rs | 79 ++- crates/builtins/web-fetch/src/request.rs | 39 +- crates/builtins/web-fetch/src/response.rs | 54 +- crates/builtins/web-fetch/src/transport.rs | 70 +-- .../builtins/web-fetch/tests/body_memory.rs | 4 +- crates/builtins/web-fetch/tests/config.rs | 15 +- .../builtins/web-fetch/tests/fetch_network.rs | 424 +++----------- crates/builtins/web-globals/Cargo.toml | 4 + crates/builtins/web-globals/src/base64.rs | 319 ++-------- crates/builtins/web-globals/src/console.rs | 79 --- .../builtins/web-globals/src/dom_exception.rs | 328 +---------- .../web-globals/src/encoding/text_decoder.rs | 182 ++++-- .../web-globals/src/events/algorithms.rs | 93 ++- .../builtins/web-globals/src/events/event.rs | 2 +- .../web-globals/src/events/event_target.rs | 44 +- crates/builtins/web-globals/src/lib.rs | 4 - crates/builtins/web-globals/src/navigator.rs | 2 - .../builtins/web-globals/src/performance.rs | 83 +-- .../web-globals/src/signals/abort_signal.rs | 55 +- .../web-globals/src/signals/algorithms.rs | 16 +- .../web-globals/src/worker_location.rs | 165 +----- .../builtins/web-globals/tests/dom_events.rs | 149 ++--- .../builtins/web-globals/tests/dom_signals.rs | 104 ++-- crates/builtins/web-globals/tests/encoding.rs | 345 +---------- crates/builtins/web-globals/tests/globals.rs | 280 +++++++++ .../web-globals/tests/structured_clone.rs | 297 ++-------- crates/builtins/web-streams/Cargo.toml | 2 +- crates/builtins/web-streams/src/lib.rs | 12 +- .../src/queuing/queue_with_sizes.rs | 7 - .../web-streams/src/readable/algorithms.rs | 152 ++--- .../src/readable/async_iterator.rs | 17 +- .../src/readable/byte_stream_controller.rs | 10 - .../builtins/web-streams/src/readable/mod.rs | 1 - .../src/readable/read_all_bytes.rs | 61 +- .../web-streams/src/readable/read_request.rs | 55 +- .../src/readable/readable_stream.rs | 27 +- crates/builtins/web-streams/src/support.rs | 17 +- .../builtins/web-streams/src/transform/mod.rs | 4 +- .../src/transform/transform_stream.rs | 9 +- .../src/writable/writable_stream.rs | 11 - .../web-streams/tests/async_iterator.rs | 172 ------ crates/builtins/web-streams/tests/body.rs | 81 --- crates/builtins/web-streams/tests/pipe_to.rs | 190 ------ .../web-streams/tests/readable_byob.rs | 346 ----------- .../web-streams/tests/readable_byte.rs | 180 ------ .../web-streams/tests/readable_default.rs | 182 ------ .../builtins/web-streams/tests/stream_from.rs | 111 ---- crates/builtins/web-streams/tests/streams.rs | 550 ++++++++++++++++++ .../builtins/web-streams/tests/tee_default.rs | 140 ----- .../web-streams/tests/transform_default.rs | 113 ---- .../web-streams/tests/writable_default.rs | 119 ---- crates/builtins/web-url/Cargo.toml | 2 +- crates/builtins/web-url/src/lib.rs | 7 +- .../builtins/web-url/src/url_search_params.rs | 9 - crates/builtins/web-url/tests/url.rs | 535 +---------------- crates/js/src/error.rs | 11 + crates/js/src/promise.rs | 5 +- crates/js/src/typedarray.rs | 7 +- 76 files changed, 1773 insertions(+), 5078 deletions(-) delete mode 100644 crates/builtins/web-globals/src/console.rs create mode 100644 crates/builtins/web-globals/tests/globals.rs delete mode 100644 crates/builtins/web-streams/tests/async_iterator.rs delete mode 100644 crates/builtins/web-streams/tests/body.rs delete mode 100644 crates/builtins/web-streams/tests/pipe_to.rs delete mode 100644 crates/builtins/web-streams/tests/readable_byob.rs delete mode 100644 crates/builtins/web-streams/tests/readable_byte.rs delete mode 100644 crates/builtins/web-streams/tests/readable_default.rs delete mode 100644 crates/builtins/web-streams/tests/stream_from.rs create mode 100644 crates/builtins/web-streams/tests/streams.rs delete mode 100644 crates/builtins/web-streams/tests/tee_default.rs delete mode 100644 crates/builtins/web-streams/tests/transform_default.rs delete mode 100644 crates/builtins/web-streams/tests/writable_default.rs diff --git a/Cargo.lock b/Cargo.lock index dc055e6..9690d47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -409,9 +409,7 @@ name = "cpp-builtins" version = "0.3.0" dependencies = [ "cc", - "core-runtime", "js", - "libstarling", "mozjs", "mozjs_sys", ] diff --git a/crates/builtins/cpp-builtins/Cargo.toml b/crates/builtins/cpp-builtins/Cargo.toml index 2d1ae96..0cb7b7c 100644 --- a/crates/builtins/cpp-builtins/Cargo.toml +++ b/crates/builtins/cpp-builtins/Cargo.toml @@ -6,6 +6,10 @@ license.workspace = true authors.workspace = true edition.workspace = true +[lib] +# No unit tests. This skips building the (SpiderMonkey-sized) lib test harness binary. +test = false + [features] debugmozjs = [] @@ -25,7 +29,3 @@ mozjs_sys = { git = "https://github.com/tschneidereit/mozjs.git", branch = "main [build-dependencies] cc = "1.2.56" - -[dev-dependencies] -core-runtime.workspace = true -libstarling.workspace = true diff --git a/crates/builtins/cpp-builtins/cpp/builtins/console.cpp b/crates/builtins/cpp-builtins/cpp/builtins/console.cpp index 33f9c1e..a522ca0 100644 --- a/crates/builtins/cpp-builtins/cpp/builtins/console.cpp +++ b/crates/builtins/cpp-builtins/cpp/builtins/console.cpp @@ -2,6 +2,7 @@ #include "encode.h" #include #include +#include #include #include @@ -406,23 +407,28 @@ JS::Result ToSource(JSContext *cx, std::string &sourceOut, JS::Hand namespace builtins::web::console { // `log`, `info` and `debug` print the bare message on stdout. `warn` and `error` print it on -// stderr, prefixed with the level. +// stderr, prefixed with the level. The message is written with its length, so NUL characters in +// it are printed too. __attribute__((weak)) -void builtin_impl_console_log(Console::LogType log_ty, const char *msg) { +void builtin_impl_console_log(Console::LogType log_ty, std::string_view msg) { + FILE *stream = stdout; + const char *prefix = ""; switch (log_ty) { case Console::LogType::Warn: - fprintf(stderr, "Warn: %s\n", msg); - fflush(stderr); - return; + stream = stderr; + prefix = "Warn: "; + break; case Console::LogType::Error: - fprintf(stderr, "Error: %s\n", msg); - fflush(stderr); - return; + stream = stderr; + prefix = "Error: "; + break; default: - fprintf(stdout, "%s\n", msg); - fflush(stdout); - return; + break; } + fputs(prefix, stream); + fwrite(msg.data(), 1, msg.size(), stream); + fputc('\n', stream); + fflush(stream); } template @@ -450,7 +456,7 @@ static bool console_out(JSContext *cx, unsigned argc, JS::Value *vp) { } } - builtin_impl_console_log(log_ty, fullLogLine.c_str()); + builtin_impl_console_log(log_ty, fullLogLine); args.rval().setUndefined(); return true; @@ -502,7 +508,7 @@ static bool assert_(JSContext *cx, unsigned argc, JS::Value *vp) { } // 5. Perform Logger("assert", data). - builtin_impl_console_log(Console::LogType::Error, message.c_str()); + builtin_impl_console_log(Console::LogType::Error, message); return true; } @@ -535,7 +541,7 @@ static bool count(JSContext *cx, unsigned argc, JS::Value *vp) { concat += ": "; concat += std::to_string(count); // 5. Perform Logger("count", « concat »). - builtin_impl_console_log(Console::LogType::Log, concat.c_str()); + builtin_impl_console_log(Console::LogType::Log, concat); args.rval().setUndefined(); return true; } @@ -566,7 +572,7 @@ static bool countReset(JSContext *cx, unsigned argc, JS::Value *vp) { std::string message = "Count for '"; message += label; message += "' does not exist"; - builtin_impl_console_log(Console::LogType::Warn, message.c_str()); + builtin_impl_console_log(Console::LogType::Warn, message); } args.rval().setUndefined(); @@ -623,7 +629,7 @@ static bool timeLog(JSContext *cx, unsigned argc, JS::Value *vp) { std::string message = "No such label '"; message += label; message += "' for console.timeLog()"; - builtin_impl_console_log(Console::LogType::Warn, message.c_str()); + builtin_impl_console_log(Console::LogType::Warn, message); args.rval().setUndefined(); return true; } @@ -672,7 +678,7 @@ static bool timeLog(JSContext *cx, unsigned argc, JS::Value *vp) { concat += data; // 6. Perform Printer("timeLog", « concat »). - builtin_impl_console_log(Console::LogType::Log, concat.c_str()); + builtin_impl_console_log(Console::LogType::Log, concat); args.rval().setUndefined(); return true; @@ -696,7 +702,7 @@ static bool timeEnd(JSContext *cx, unsigned argc, JS::Value *vp) { std::string message = "No such label '"; message += label; message += "' for console.timeEnd()"; - builtin_impl_console_log(Console::LogType::Warn, message.c_str()); + builtin_impl_console_log(Console::LogType::Warn, message); args.rval().setUndefined(); return true; } @@ -717,7 +723,7 @@ static bool timeEnd(JSContext *cx, unsigned argc, JS::Value *vp) { concat += "ms"; // 6. Perform Printer("timeEnd", « concat »). - builtin_impl_console_log(Console::LogType::Info, concat.c_str()); + builtin_impl_console_log(Console::LogType::Info, concat); args.rval().setUndefined(); return true; @@ -744,7 +750,7 @@ static bool dir(JSContext *cx, unsigned argc, JS::Value *vp) { fullLogLine += source; } - builtin_impl_console_log(Console::LogType::Log, fullLogLine.c_str()); + builtin_impl_console_log(Console::LogType::Log, fullLogLine); args.rval().setUndefined(); return true; @@ -795,7 +801,7 @@ static bool trace(JSContext *cx, unsigned argc, JS::Value *vp) { fullLogLine += stack_string.begin(); // 3. Perform Printer("trace", « trace »). - builtin_impl_console_log(Console::LogType::Log, fullLogLine.c_str()); + builtin_impl_console_log(Console::LogType::Log, fullLogLine); args.rval().setUndefined(); return true; } diff --git a/crates/builtins/cpp-builtins/src/lib.rs b/crates/builtins/cpp-builtins/src/lib.rs index a8838d1..6e108fd 100644 --- a/crates/builtins/cpp-builtins/src/lib.rs +++ b/crates/builtins/cpp-builtins/src/lib.rs @@ -13,62 +13,3 @@ unsafe extern "C" { pub unsafe fn install(cx: *mut JSContext, global: HandleObject) -> bool { unsafe { console_install(cx, global.get()) } } - -#[cfg(test)] -mod tests { - mod console_integration { - use core_runtime::test_util::throws_with_setup; - - fn eval(code: &str) -> bool { - libstarling::register_builtins(); - let rt = - libstarling::runtime::Runtime::init(&libstarling::config::RuntimeConfig::default()) - .expect("runtime init"); - let scope = rt.default_global(); - let rval = js::compile::evaluate_with_filename(&scope, code, "test.js", 1) - .expect("eval failed"); - rval.is_undefined() || rval.to_boolean() - } - - fn eval_throws(code: &str) -> bool { - throws_with_setup(libstarling::register_builtins, code) - } - - #[test] - fn console_log_exists() { - assert!(eval("typeof console === 'object'")); - } - - #[test] - fn console_methods_exist() { - assert!(eval("typeof console.log === 'function'")); - assert!(eval("typeof console.warn === 'function'")); - assert!(eval("typeof console.error === 'function'")); - assert!(eval("typeof console.info === 'function'")); - assert!(eval("typeof console.debug === 'function'")); - } - - #[test] - fn console_log_returns_undefined() { - assert!(eval("console.log('test') === undefined")); - } - - #[test] - fn console_log_no_args() { - // Should not throw with zero arguments. - assert!(!eval_throws("console.log()")); - } - - #[test] - fn console_log_multiple_args() { - assert!(!eval_throws("console.log('a', 'b', 'c')")); - } - - #[test] - fn console_log_non_string_args() { - assert!(!eval_throws( - "console.log(42, true, null, undefined, {x: 1})" - )); - } - } -} diff --git a/crates/builtins/fetch-event/Cargo.toml b/crates/builtins/fetch-event/Cargo.toml index 5ae0c83..e75c959 100644 --- a/crates/builtins/fetch-event/Cargo.toml +++ b/crates/builtins/fetch-event/Cargo.toml @@ -6,6 +6,10 @@ license.workspace = true authors.workspace = true edition.workspace = true +[lib] +# No unit tests. This skips building the (SpiderMonkey-sized) lib test harness binary. +test = false + [features] # For `cargo test --features debugmozjs`. debugmozjs = ["js/debugmozjs"] diff --git a/crates/builtins/fetch-event/src/extendable_event.rs b/crates/builtins/fetch-event/src/extendable_event.rs index 3c283b9..6c207c2 100644 --- a/crates/builtins/fetch-event/src/extendable_event.rs +++ b/crates/builtins/fetch-event/src/extendable_event.rs @@ -96,8 +96,6 @@ impl ExtendableEvent { &format!("`{caller}`"), )); }; - // Note: if the allocation of `on_settled` were to fail, this would leak `handle` until - // the event loop is dropped. Working around that would be more trouble than it's worth. let holder = LifetimePromisePayload::new(scope, handle, *self)?; let on_settled = Function::new_callback(scope, c"", 1, lifetime_promise_settled_cb, holder)?; diff --git a/crates/builtins/fetch-event/src/fetch_event.rs b/crates/builtins/fetch-event/src/fetch_event.rs index 8800373..141ab25 100644 --- a/crates/builtins/fetch-event/src/fetch_event.rs +++ b/crates/builtins/fetch-event/src/fetch_event.rs @@ -23,9 +23,7 @@ use web_globals::events::algorithms::ScriptStackState; use web_globals::events::EventTarget; /// -// Note: the missing fields are intentionally omitted. The only difference that makes is that -// validation will accept things the spec wouldn't, but that's really marginal: it'd reject -// e.g. a `resultingClientId` property where `toString()` throws, but not much else. +// Note: the other members are omitted, so their values are not converted. #[webidl_dictionary(extends = ExtendableEventInit)] pub struct FetchEventInit<'a> { parent: ExtendableEventInit, @@ -63,8 +61,8 @@ pub struct FetchEvent { wait_to_respond: bool, /// respond_with_error: bool, - // Note: `preloadResponse`, `resultingClientId`, and `replacesClientId` omitted intentionally. - // The first isn't supported anywhere, and all three don't really matter to our use cases. + // Note: `preloadResponse` and `replacesClientId` are not implemented, and `resultingClientId` + // is always empty. } #[webidl_methods] @@ -76,7 +74,7 @@ impl FetchEvent { r#type: &str, event_init_dict: &FetchEventInit<'_>, ) -> Result { - // The spec-defined script constructor; the runtime creates its events via + // The spec-defined script constructor. The runtime creates its events with // `create_for_request` instead, and only those events are ever dispatched. // WPT still contains tests for the JS constructor though. let handled = Promise::new_pending(scope)?; @@ -196,9 +194,9 @@ impl FetchEvent { /// // `r` is a WebIDL `Promise`: the trampoline accepts any value, hands over a promise // resolved with it, and rejects that promise if what it settles with isn't a `Response`. That - // rejection lands in step 9 below, which sets the `respond-with error flag` — the same flag - // step 10.1 sets for a non-`Response` fulfillment, which is why step 10.1 is unreachable here - // (see `on_respond_fulfilled`). + // rejection lands in step 9 below, which sets the `respond-with error flag`. Step 10.1 sets the + // same flag for a non-`Response` fulfillment, which is why step 10.1 is unreachable here (see + // `on_respond_fulfilled`). #[method] pub fn respond_with( &self, @@ -295,8 +293,8 @@ fn on_respond_fulfilled( let unusable = response.is_body_unusable(scope); if !unusable { // Steps 10.2.2 to 10.2.5 are implemented at the platform layer instead of here, except - // for the effect step 10.2.2's transform has on the response handed in, which is to take - // its body, and step 10.2.5.1's getting a reader for the body's stream, which + // for the effect that step 10.2.2's transform has on the response handed in, which is to + // take its body, and step 10.2.5.1's getting a reader for the body's stream, which // `reserve_body_for_sending` acquires now so author code can no longer read from it. response.reserve_body_for_sending(scope)?; } @@ -373,20 +371,15 @@ impl<'s> FetchEvent<'s> { self.data().respond_with_entered } - /// Settle the `handled` promise: steps 21–23 of - /// [Create Fetch Event and Dispatch](https://w3c.github.io/ServiceWorker/#create-fetch-event-and-dispatch), - /// where the event's outcome becomes final. `responded` says whether a `Response` is on its way - /// to the client. + /// Settle the `handled` promise, per steps 21–23 of + /// [Create Fetch Event and Dispatch](https://w3c.github.io/ServiceWorker/#create-fetch-event-and-dispatch): + /// resolve it if `responded` (a `Response` is on its way to the client), and reject it with a + /// "NetworkError" `DOMException` otherwise. Does nothing once `handled` is settled. /// - /// The spec settles `handled` three ways: resolve once a response is on its way (step 23), - /// reject when the event failed (steps 21.1.1 and 22.1), and, when the handler never called - /// `respondWith`, resolve (step 21.2), because that case returns null, which tells `Fetch` - /// to go get the response from the network itself. - /// - /// There is no network to fall back to here, so that third case ends in a `500` like the - /// failures do, and resolving would tell the handler its request was served. So this resolves - /// only when `responded`, and rejects otherwise. Which failure it was only affects the - /// rejection's message, which [`respond_with_error`](Self::respond_with_error_set) selects. + /// When the handler never called `respondWith`, the spec resolves `handled` (step 21.2), since + /// `Fetch` then falls back to the network. There is no network fallback here, so that case ends + /// in a `500` and rejects like the failures. The rejection's message depends on + /// [`Self::respond_with_error_set`]. pub fn settle_handled(&self, scope: &'s Scope<'_>, responded: bool) { let handled = self.data().handled.get(scope); if !handled.is_pending() { diff --git a/crates/builtins/fetch-event/tests/dispatch.rs b/crates/builtins/fetch-event/tests/dispatch.rs index 845aa54..098eafd 100644 --- a/crates/builtins/fetch-event/tests/dispatch.rs +++ b/crates/builtins/fetch-event/tests/dispatch.rs @@ -2,9 +2,7 @@ //! The server-side dispatch path: create a `FetchEvent` for a request, run a JS //! `fetch` handler against it, drive a **per-request** event loop, and read the -//! response the handler produced with `respondWith`. Exercises the M2/M3 event -//! bodies end to end, including `waitUntil` keeping the loop alive and that -//! concurrent requests run on isolated event loops. +//! response the handler produced with `respondWith`. #![cfg(not(target_arch = "wasm32"))] @@ -30,8 +28,8 @@ fn eval<'s>(scope: &'s Scope, src: &str) -> HandleValue<'s> { /// A runtime with the builtins registered, ready for a global to be created from it. Every test /// here starts this way, and each has to clear the initializers first: they accumulate in a -/// process-wide registry, so a test that inherited the previous one's would register each builtin -/// twice over. +/// process-wide registry, so a test that inherited the previous test's initializers would register +/// each builtin twice over. /// /// The caller enters the global itself and calls [`install_globals`] on it — the scope borrows the /// runtime, so the two cannot be returned together. @@ -138,16 +136,6 @@ fn dispatch(handler_src: &str, request_src: &str) -> (String, String) { (response, probe) } -#[test] -fn respond_with_captures_the_response() { - // The handler responds synchronously; the response is the value passed to respondWith. - let (response, _) = dispatch( - "(event) => { event.respondWith(mark('handled: ' + event.request.url)); }", - "new Request('http://example.com/')", - ); - assert_eq!(response, "handled: http://example.com/"); -} - #[test] fn responding_with_something_that_is_not_a_response_is_a_network_error() { // `respondWith` takes a WebIDL `Promise`, so a value of the wrong type is accepted at @@ -161,7 +149,6 @@ fn responding_with_something_that_is_not_a_response_is_a_network_error() { for handler in [ "(e) => e.respondWith('a string is not a Response')", - "(e) => e.respondWith(Promise.resolve(42))", "(e) => e.respondWith(new Promise(r => setTimeout(() => r({}), 2)))", "(e) => e.respondWith(Promise.reject(new RangeError('the author rejected')))", ] { @@ -186,9 +173,7 @@ fn describing_a_rejection_runs_author_code_only_after_the_respond_state_is_settl // The event is nonetheless still *active*: the reaction runs in the microtask checkpoint after // the listener returns, which is inside the dispatch, so the `dispatch flag` has not been // cleared yet and `waitUntil` legitimately succeeds (WPT - // `extendable-event-async-waituntil.js`, `no-current-extension-different-microtask`). Extending - // the lifetime of an event whose response already failed is allowed — `waitUntil` is about the - // worker staying alive for background work, not about the response. + // `extendable-event-async-waituntil.js`, `no-current-extension-different-microtask`). let rt = test_runtime(); let scope = rt.default_global(); install_globals(&scope); @@ -221,45 +206,6 @@ fn describing_a_rejection_runs_author_code_only_after_the_respond_state_is_settl assert_eq!(log, "extended,respondWith:InvalidStateError"); } -#[test] -fn respond_with_a_promise_resolves() { - // respondWith a promise that resolves after a microtask — the loop runs until it settles. - let (response, _) = dispatch( - "(event) => { event.respondWith(Promise.resolve(mark('async-response'))); }", - "new Request('http://example.com/')", - ); - assert_eq!(response, "async-response"); -} - -#[test] -fn respond_with_a_timer_backed_promise_runs_to_completion() { - // The response resolves only inside a setTimeout — the loop must stay alive and run the timer. - let (response, _) = dispatch( - "(event) => { event.respondWith(new Promise(r => setTimeout(() => r(mark('after-timer')), 5))); }", - "new Request('http://example.com/')", - ); - assert_eq!(response, "after-timer"); -} - -#[test] -fn wait_until_keeps_the_loop_alive_past_respond() { - // respondWith settles immediately, but waitUntil holds a setTimeout-backed promise; the loop - // must stay alive long enough to run the timer (proving waitUntil extends the lifetime past the - // response). If waitUntil did not hold the loop, it would exit before the timer and __probe - // would stay "". - let (response, probe) = dispatch( - r#"(event) => { - event.respondWith(mark("done")); - event.waitUntil(new Promise(resolve => { - setTimeout(() => { globalThis.__probe = "waited"; resolve(); }, 5); - })); - }"#, - "new Request('http://example.com/')", - ); - assert_eq!(response, "done"); - assert_eq!(probe, "waited"); -} - #[test] fn concurrent_requests_use_isolated_event_loops() { // Two requests, each dispatched on its own event loop, driven concurrently. Each handler @@ -298,37 +244,6 @@ fn concurrent_requests_use_isolated_event_loops() { assert_eq!(resp_b, "B:http://example.com/b"); } -#[test] -fn dispatch_delivers_to_a_script_registered_fetch_listener() { - // The full server entry point: script registers a handler with addEventListener("fetch", ...), - // and `dispatch` delivers an incoming request to it. - let rt = test_runtime(); - let scope = rt.default_global(); - install_globals(&scope); - let rawcx = unsafe { scope.cx_mut().raw_cx() }; - - let el = EventLoop::new(); - let event = { - with_event_loop(&el, |_| { - eval( - &scope, - "addEventListener('fetch', (e) => e.respondWith(mark('registered: ' + e.request.url)))", - ); - let request = eval(&scope, "new Request('http://example.com/req')"); - let request = Request::from_jsval(&scope, request, ()).unwrap(); - let event = - FetchEvent::dispatch(&scope, request, ScriptStackState::Empty).expect("dispatch"); - event - }) - }; - - tokio_rt() - .block_on(async { unsafe { run_to_completion(rawcx, &el, tokio::time::sleep).await } }); - - let response = response_marker(&scope, &event); - assert_eq!(response, "registered: http://example.com/req"); -} - #[test] fn the_promise_attributes_return_the_same_object_every_access() { // `preloadResponse` and `handled` are WebIDL promise attributes, which return the value they @@ -578,11 +493,8 @@ fn lifetime_interest_releases_on_the_acquiring_loop() { } /// `respondWith` stops propagation on its own: step 5 sets the `stop propagation` and `stop -/// immediate propagation` flags, so a later `fetch` listener never runs. The test above calls -/// `stopImmediatePropagation` by hand; this is the case WPT actually asserts -/// (`resources/fetch-event-respond-with-stops-propagation-worker.js`), where the handler only ever -/// calls `respondWith` — a runtime that delivered the event onward would let a second listener -/// overwrite the first's answer. +/// immediate propagation` flags, so a later `fetch` listener never runs. WPT +/// `resources/fetch-event-respond-with-stops-propagation-worker.js` asserts this. #[test] fn respond_with_alone_stops_listener_delivery() { let rt = test_runtime(); @@ -630,7 +542,7 @@ fn the_constructor_surface_matches_the_idl() { outcome(() => new FetchEvent('t', { request: null })), // ExtendableEvent takes a bare type. outcome(() => new ExtendableEvent('E').type), - // Dictionary defaults: an EventInit that says nothing leaves both flags false. + // Dictionary defaults: an EventInit without either flag leaves both flags false. outcome(() => new FetchEvent('t', { request }).bubbles), outcome(() => new FetchEvent('t', { request }).cancelable), outcome(() => new FetchEvent('t', { request, cancelable: true }).cancelable), @@ -673,34 +585,9 @@ fn the_constructor_surface_matches_the_idl() { ); } -#[test] -fn stop_immediate_propagation_stops_listener_delivery() { - let rt = test_runtime(); - let scope = rt.default_global(); - install_globals(&scope); - - eval( - &scope, - r#" - addEventListener('fetch', (e) => { e.stopImmediatePropagation(); e.respondWith(mark('first')); }); - addEventListener('fetch', () => { globalThis.secondRan = true; }); - "#, - ); - - let event = dispatch_once(&scope, "new Request('http://example.com/req')"); - let response = response_marker(&scope, &event); - assert_eq!(response, "first"); - - let second = eval(&scope, "typeof globalThis.secondRan"); - let second = String::from_jsval(&scope, second, ()).unwrap(); - assert_eq!(second, "undefined"); -} - /// A microtask queued by the handler is still inside the dispatch, so `respondWith` from one has /// to work: WPT `fetch-event-async-respond-with.https.html`, against the task case that must throw -/// (`respond_with_state_machine_guards`). Not a niche path — -/// `const body = await request.text(); event.respondWith(…)` lands in a microtask whenever the -/// awaited promise is already resolved. +/// (`respond_with_state_machine_guards`). /// /// Goes through `FetchEvent::dispatch` rather than the handler-called-directly shortcut, since the /// checkpoint lives in the dispatch itself. @@ -793,10 +680,6 @@ fn wait_until_from_a_later_task_throws() { /// author's reaction runs before the decrement that would end the extension, so the count never /// reaches zero in between. WPT `resources/extendable-event-async-waituntil.js`, /// `during-event-handler-and-microtask`. -/// -/// Pinned because that ordering is a property of how the settle reaction is wrapped rather than -/// something this code states outright (`scaffold-delta.md` §B) — the kind of invariant that breaks -/// quietly when the wrapping changes. #[test] fn wait_until_can_be_chained_off_a_settling_lifetime_promise() { let (response, probe) = dispatch( @@ -825,8 +708,7 @@ fn wait_until_can_be_chained_off_a_settling_lifetime_promise() { /// count keeps the event active regardless of the dispatch flag. /// /// WPT `resources/extendable-event-async-waituntil.js`, fetch case -/// `pending-respondwith-async-waituntil`. This passes today — and it is why the test above has to -/// avoid calling `respondWith` to isolate the flag. +/// `pending-respondwith-async-waituntil`. #[test] fn wait_until_in_a_microtask_is_allowed_while_respond_with_is_pending() { let (response, probe) = dispatch( @@ -934,7 +816,7 @@ fn handled_settles_by_whether_a_response_is_on_its_way() { } } -/// Settling twice is a no-op: the serve path settles `handled` once per request, but the event +/// Settling twice does nothing: the serve path settles `handled` once per request, but the event /// outlives that call through `waitUntil` work, and a second settle must not replace the outcome /// the client was told about. #[test] @@ -992,9 +874,8 @@ fn responding_with_an_unusable_body_is_a_network_error() { } /// Taking the response's body is step 10.2.2's transform, and it happens when the `respondWith` -/// promise settles rather than when the transport reads — so a handler that keeps a reference sees -/// `bodyUsed` flip before a single byte goes out. Pinned because it is the script-visible half of -/// the body deviation `docs/server-side-deviations.md` records. +/// promise settles rather than when the transport reads, so a handler that keeps a reference sees +/// `bodyUsed` flip before a single byte goes out. #[test] fn responding_marks_the_body_used_at_settle_time() { let rt = test_runtime(); diff --git a/crates/builtins/web-fetch/Cargo.toml b/crates/builtins/web-fetch/Cargo.toml index ef275fd..5ae19bd 100644 --- a/crates/builtins/web-fetch/Cargo.toml +++ b/crates/builtins/web-fetch/Cargo.toml @@ -7,7 +7,7 @@ authors.workspace = true edition.workspace = true [lib] -# No unit tests; skip building the (SpiderMonkey-sized) lib test harness binary. +# No unit tests. This skips building the (SpiderMonkey-sized) lib test harness binary. test = false [dependencies] diff --git a/crates/builtins/web-fetch/src/abort.rs b/crates/builtins/web-fetch/src/abort.rs index bfa7b57..8e63512 100644 --- a/crates/builtins/web-fetch/src/abort.rs +++ b/crates/builtins/web-fetch/src/abort.rs @@ -56,10 +56,10 @@ impl AbortFetchState<'_> { /// One with a body detaches it once that body has been read (see /// `Response::consume`), since aborting mid-read must still error the stream. pub(crate) fn deliver(&self, scope: &Scope<'_>, response: &Response<'_>) { - if response_body_is_abortable(scope, response) { + if response.body_is_abortable(scope) { debug_assert!(self.data().response.is_none()); self.data_mut().response = Some(Heap::from(*response)); - response.set_abort_state(self); + response.data_mut().abort_state = Some(Heap::from(*self)); } else { self.detach(scope); } @@ -83,14 +83,6 @@ impl AbortFetchState<'_> { } } -/// Whether a delivered response still has something an abort could act on: an -/// unread host body, or a `.body` stream that has not finished. -/// -/// Once neither is true, aborting is a no-op, so the algorithm can be detached. -pub(crate) fn response_body_is_abortable(scope: &Scope<'_>, response: &Response<'_>) -> bool { - response.has_unread_host_body() || response.body_stream_is_unfinished(scope) -} - /// /// To abort a fetch() call with a promise, request, responseObject, and an error: pub(crate) fn abort_fetch( @@ -101,7 +93,7 @@ pub(crate) fn abort_fetch( error: HandleValue<'_>, ) -> Result<(), ExnThrown> { // Step 1: `Reject` _promise_ with _error_. - // Note: This is a no-op if _promise_ has already fulfilled. + // Note: This does nothing if _promise_ has already fulfilled. promise.reject(scope, error)?; // Step 2: If _request_’s `body` is non-null and is `readable`, then `cancel` _request_’s `body` diff --git a/crates/builtins/web-fetch/src/algorithms.rs b/crates/builtins/web-fetch/src/algorithms.rs index d672a0e..0c93345 100644 --- a/crates/builtins/web-fetch/src/algorithms.rs +++ b/crates/builtins/web-fetch/src/algorithms.rs @@ -910,7 +910,7 @@ pub(crate) fn fill_headers( /// entries taken from an existing header list most of it is already known to /// hold, so this runs only what can still change the result: /// -/// - `append` Step 1 (`normalize`) and `validate` step 1 is a no-op: entries are already normalized. +/// - `append` Step 1 (`normalize`) and `validate` Step 1 are no-ops: entries are already normalized. /// - `validate` Steps 1 & 2 cannot throw: the guard here is "`request`" or /// "`request-no-cors`" (constructor steps 31–32), never "`immutable`". /// - `append` Step 5 (`remove privileged no-CORS request-headers`) is a no-op: @@ -988,10 +988,9 @@ pub(crate) fn get_the_mime_type() { /// The `convertBytesToJSValue` algorithm a `consume body` caller supplies: how the /// fully-read byte sequence becomes the resolved value. -#[derive(Clone, Copy, Default)] +#[derive(Clone, Copy)] pub(crate) enum ConsumeType { /// `arrayBuffer()` — an `ArrayBuffer` over the bytes. - #[default] ArrayBuffer, /// `bytes()` — a `Uint8Array` over the bytes. Bytes, @@ -999,10 +998,9 @@ pub(crate) enum ConsumeType { Text, /// `json()` — `parse JSON from bytes`. Json, - /// `blob()` — a `Blob` over the bytes. Blob is not yet available in this runtime, so the - /// conversion fails; the body is still consumed (disturbed) first, per `consume body`. + /// `blob()`: not implemented, so the conversion fails after the body is consumed. Blob, - /// `formData()` — parse the body as form data. Not yet supported; consumes then fails. + /// `formData()`: not implemented, so the conversion fails after the body is consumed. FormData, } @@ -1329,9 +1327,6 @@ fn main_fetch<'r>( // The HTTP(S) arm of Step 12 runs on a future; the "`data`" arm is synchronous and needs none. // Step 12: If _response_ is null, then set _response_ to the result of running the steps // corresponding to the first matching statement: - // The switch keys on `origin`, `response tainting` and `mode`. This runtime has no requester - // `origin`, so no arm that compares one can be evaluated; what is left is a dispatch on the - // `current URL`’s `scheme`, and only two arms are reachable. let current_url = request.current_url(); match current_url.scheme() { // Step 12 _request_’s `current URL`’s `scheme` is "`data`".1: Set _request_’s `response @@ -1356,7 +1351,7 @@ fn main_fetch<'r>( // status`, set _internalResponse_’s `body` to null and disregard any enqueuing // toward it (if any). // A `data:` response is always 200, so only the method matters here. - let method = request.http_method(); + let method = request.method(); if method.eq_ignore_ascii_case("HEAD") || method.eq_ignore_ascii_case("CONNECT") { response.clear_body(); } diff --git a/crates/builtins/web-fetch/src/body_mixin.rs b/crates/builtins/web-fetch/src/body_mixin.rs index 742faea..f2a4dec 100644 --- a/crates/builtins/web-fetch/src/body_mixin.rs +++ b/crates/builtins/web-fetch/src/body_mixin.rs @@ -90,14 +90,6 @@ pub(crate) trait BodyMixin: HostBackedBodyOwner + Sized { /// The `TypeError` message for the not-yet-supported `textStream()`. const TEXT_STREAM_UNSUPPORTED: &'static CStr; - /// Store the `.body` stream materialized from the body's byte source. - fn set_body_stream(&self, stream: ReadableStream<'_>) { - self.with_body(|body| { - debug_assert!(body.stream.is_none()); - body.stream = Some(Heap::from(stream)); - }); - } - /// Mark the body's byte source as read — the `disturbed` of the stream it /// would have materialized into. fn set_source_disturbed(&self) { @@ -164,7 +156,10 @@ pub(crate) trait BodyMixin: HostBackedBodyOwner + Sized { // body to expose. BodySource::Null => return Ok(None), }; - self.set_body_stream(stream); + self.with_body(|body| { + debug_assert!(body.stream.is_none()); + body.stream = Some(Heap::from(stream)); + }); Ok(Some(stream)) } @@ -280,8 +275,7 @@ pub(crate) trait BodyMixin: HostBackedBodyOwner + Sized { // value of a ``Content-Type`` header and regardless of the presence or the value of a // ``charset`` parameter. // Step 6: Return the result of _stream_, `piped through` _decoder_. - // `textStream()` depends on `TextDecoderStream` piping, deferred to a later milestone; - // throw for now. + // `textStream()` is not implemented. Err(throw_type_error(scope, Self::TEXT_STREAM_UNSUPPORTED)) } } diff --git a/crates/builtins/web-fetch/src/body_proxy.rs b/crates/builtins/web-fetch/src/body_proxy.rs index 9e1482c..4908275 100644 --- a/crates/builtins/web-fetch/src/body_proxy.rs +++ b/crates/builtins/web-fetch/src/body_proxy.rs @@ -159,7 +159,7 @@ fn proxy_close_step(scope: &Scope<'_>, payload: Object<'_>) -> Result<(), ExnThr .cast::() .expect("payload is a BodyProxySource"); let controller = state.controller(scope); - // The proxy can already be closed or cancelled by the time the stream it proxies closes: + // The proxy can already be closed or cancelled by the time the proxied stream closes: // `abort the fetch() call` cancels the request body, and cancelling the proxy cancels the // proxied stream, whose close re-enters here through any read request still pending on it // (a host-backed body always has one — its chunk read is in flight). Closing again is @@ -183,8 +183,9 @@ fn proxy_error_step( .cast::() .expect("payload is a BodyProxySource"); let controller = state.controller(scope); - // Already-closed/cancelled proxy: erroring it is a no-op that fails. Ignore it for the - // same reason as the close step — these are a read request's `error steps`. + // Erroring an already closed or cancelled proxy fails without any effect. The failure + // is ignored for the same reason as in the close step: these are a read request's + // `error steps`. if controller.error(scope, Some(error)).is_err() { js::exception::clear(scope); } diff --git a/crates/builtins/web-fetch/src/byte_string.rs b/crates/builtins/web-fetch/src/byte_string.rs index 9f15852..3959d38 100644 --- a/crates/builtins/web-fetch/src/byte_string.rs +++ b/crates/builtins/web-fetch/src/byte_string.rs @@ -38,12 +38,6 @@ impl AsRef for ByteString { } } -impl From for String { - fn from(value: ByteString) -> Self { - value.0 - } -} - impl<'s, 'v> FromJSVal<'s, 'v> for ByteString { type Config = (); diff --git a/crates/builtins/web-fetch/src/globals.rs b/crates/builtins/web-fetch/src/globals.rs index 786a35e..94d0a8d 100644 --- a/crates/builtins/web-fetch/src/globals.rs +++ b/crates/builtins/web-fetch/src/globals.rs @@ -70,9 +70,7 @@ pub mod globals { // Step 4.2: Return _p_. return Ok(p); } - let abort_state = Some(crate::abort::register_fetch_abort( - scope, signal, p, request, - )?); + let abort_state = crate::abort::register_fetch_abort(scope, signal, p, request)?; // Step 5: Let _globalObject_ be _request_’s `client`’s `global object`. // Step 6: If _globalObject_ is a `ServiceWorkerGlobalScope` object, then set _request_’s @@ -114,9 +112,7 @@ pub mod globals { // object, given _response_, "`immutable`", and _relevantRealm_ — done by `scheme // fetch`'s "`data`" arm — then `Resolve` _p_ with _responseObject_. crate::algorithms::FetchOutcome::Response(response_object) => { - if let Some(state) = &abort_state { - state.deliver(scope, &response_object); - } + abort_state.deliver(scope, &response_object); p.resolve(scope, response_object)?; return Ok(p); } @@ -124,9 +120,7 @@ pub mod globals { // `TypeError` and abort these steps. Throw one to set the pending exception, then // take it as the rejection value. crate::algorithms::FetchOutcome::NetworkError => { - if let Some(state) = &abort_state { - state.detach(scope); - } + abort_state.detach(scope); throw_type_error(scope, c"Failed to fetch"); p.reject_with_pending(scope)?; return Ok(p); @@ -142,17 +136,17 @@ pub mod globals { // error`, rejecting _p_ with a `TypeError`. let platform_request = request.platform_request(scope); let url = request.current_url(); - let redirect_mode = request.redirect_mode(); - let mode = request.request_mode(); + let redirect_mode = request.redirect(); + let mode = request.mode(); // The request's `origin` is "`client`", i.e. the requester's own origin, which is the - // worker location's, when one is set. It feeds `main fetch`'s origin/mode switch in + // worker location's origin when one is set. It feeds `main fetch`'s origin/mode switch in // `send_following_redirects`. let request_origin = web_globals::worker_location::current_location_url().map(|url| url.origin()); let method = platform_request.method.clone(); // Root the abort state across the host fetch, so the resolve callback can // deliver the response to it, or detach it if the response fails. - let abort_state = abort_state.map(RootedHeap::new); + let abort_state = RootedHeap::new(abort_state); let future = async move { match crate::transport::send_following_redirects( platform_request, @@ -173,20 +167,18 @@ pub mod globals { redirect_mode, tainting, ); - if let Some(state) = &abort_state { - match &response { - Ok(response) => state.get(scope).deliver(scope, response), - Err(_) => { - // Detaching runs with no exception pending. - let error = js::exception::take_pending(scope); - state.get(scope).detach(scope); - if let Some(error) = error { - js::exception::set_pending( - scope, - error, - js::native::ExceptionStackBehavior::DoNotCapture, - ); - } + match &response { + Ok(response) => abort_state.get(scope).deliver(scope, response), + Err(_) => { + // Detaching runs with no exception pending. + let error = js::exception::take_pending(scope); + abort_state.get(scope).detach(scope); + if let Some(error) = error { + js::exception::set_pending( + scope, + error, + js::native::ExceptionStackBehavior::DoNotCapture, + ); } } } @@ -194,9 +186,7 @@ pub mod globals { })) } Err(error) => PromiseOutcome::Resolve(Box::new(move |scope: &Scope<'_>| { - if let Some(state) = &abort_state { - state.get(scope).detach(scope); - } + abort_state.get(scope).detach(scope); Err(TypeError(format!("Failed to fetch: {error}")).throw(scope)) })), } diff --git a/crates/builtins/web-fetch/src/headers.rs b/crates/builtins/web-fetch/src/headers.rs index ded734e..9a7914f 100644 --- a/crates/builtins/web-fetch/src/headers.rs +++ b/crates/builtins/web-fetch/src/headers.rs @@ -138,6 +138,11 @@ impl Headers { } } + /// The header list: every header in the order it was added, duplicates included. + pub(crate) fn list(&self) -> Vec<(String, String)> { + self.data().header_list.to_vec() + } + /// #[method] pub fn append( @@ -314,17 +319,6 @@ impl Headers { } } -impl Headers<'_> { - /// Define `Symbol.iterator` on `Headers.prototype` (an alias of `entries`). - fn install_symbol_iterator(scope: &Scope<'_>) { - js::class::add_symbol_alias::( - scope, - c"entries", - js::native::SymbolCode::iterator, - ); - } -} - /// #[webidl_interface(hidden, name = "Headers Iterator")] pub struct HeadersIterator { @@ -374,7 +368,8 @@ impl HeadersIterator { pub(crate) fn add_to_global(scope: &Scope, global: Object) { Headers::add_to_global(scope, global); - Headers::install_symbol_iterator(scope); + // `Headers.prototype[Symbol.iterator]` is `entries`. + js::class::add_symbol_alias::(scope, c"entries", js::native::SymbolCode::iterator); HeadersIterator::add_to_global(scope, global); js::class::inherit_from_iterator_prototype::(scope) .expect("setting a class prototype's prototype can only fail due to OOM"); diff --git a/crates/builtins/web-fetch/src/incoming_body.rs b/crates/builtins/web-fetch/src/incoming_body.rs index ce3319a..a2b6360 100644 --- a/crates/builtins/web-fetch/src/incoming_body.rs +++ b/crates/builtins/web-fetch/src/incoming_body.rs @@ -81,8 +81,8 @@ pub(crate) fn consume_host_body<'r>( /// in-flight chunk read, kept so an abort can cancel that read (dropping it /// closes the host connection). /// -/// A pull can be deferred: if this body feeds an identity transform nobody has -/// read yet, the host body is left whole. If the TS's readable end ends up being +/// A pull can be deferred: if this body feeds an identity transform that has not +/// been read yet, the host body is left whole. If the TS's readable end ends up being /// used as an outgoing body (or otherwise as a stream passed to the host), this /// body's stream can be used directly. See [`host_pull`]. #[jsclass(hidden)] @@ -144,12 +144,6 @@ impl HostBodySource<'_> { self.data_mut().host_body = None; } - /// See [`ReadableStream::deferred_demand`]. - fn deferred_demand<'r>(&self, scope: &'r Scope<'_>) -> Option> { - let stream = self.data().stream.get(scope)?; - stream.deferred_demand(scope) - } - /// Settle a deferred pull, if there is one, so the stream stops waiting on a /// read that is no longer coming. fn settle_deferred_pull(&self, scope: &Scope<'_>, close: CloseStream) { @@ -201,15 +195,6 @@ pub(crate) trait HostBackedBodyOwner { self.with_body(|body| body.stream.is_none().then(|| body.host.take())?) } - /// Store the stream the host body materialized into, and its native source. - fn set_host_body_stream(&self, stream: ReadableStream<'_>, source: HostBodySource<'_>) { - self.with_body(|body| { - debug_assert!(body.stream.is_none()); - body.stream = Some(Heap::from(stream)); - body.source = Some(Heap::from(source)); - }); - } - /// The body record, if this object has a body. A copy: a [`BodySource::Bytes`] it carries is /// refcounted, so holding the copy holds the buffer. fn body_record(&self) -> Option { @@ -297,7 +282,11 @@ pub(crate) fn materialize_host_body( return Ok(()); }; let (stream, source) = host_body_stream(scope, host_body)?; - object.set_host_body_stream(stream, source); + object.with_body(|body| { + debug_assert!(body.stream.is_none()); + body.stream = Some(Heap::from(stream)); + body.source = Some(Heap::from(source)); + }); Ok(()) } @@ -334,12 +323,13 @@ fn host_pull( ) -> Result { let state = cast_payload::(scope, payload); let promise = Promise::new_pending(scope)?; - // Only an active event loop drives the host read, and a rejected pull errors the stream. + // Only an active event loop drives the host read. A rejected pull errors the stream. if core_runtime::event_loop::reject_without_event_loop(scope, &promise, "reading a body")? { return Ok(promise.as_value()); } let controller = ReadableStreamDefaultController::from_jsval_throwing(scope, args.get(0), ())?; - if let Some(demand) = state.deferred_demand(scope) { + let stream = state.data().stream.get(scope); + if let Some(demand) = stream.and_then(|stream| stream.deferred_demand(scope)) { state.data_mut().deferred_pull = Some(Heap::from(promise)); state.data_mut().deferred_controller = Some(Heap::from(controller)); let resume = Function::new_callback(scope, c"", 0, host_pull_resume, state)?; diff --git a/crates/builtins/web-fetch/src/lib.rs b/crates/builtins/web-fetch/src/lib.rs index 7cd48fa..9f058ee 100644 --- a/crates/builtins/web-fetch/src/lib.rs +++ b/crates/builtins/web-fetch/src/lib.rs @@ -26,8 +26,8 @@ pub fn add_to_global(scope: &Scope<'_>, global: Object<'_>) { // `fetchLater()` / `FetchLaterResult` are deliberately not exposed: the API is // `[Exposed=Window]`-only and unimplemented here. - // `AbortFetchState` is an internal state object (the fetch abort algorithm), created via - // `create_instance_with` (so its prototype must be registered) but not web-exposed. + // The internal classes below are registered so instances can be created, and are not + // web-exposed. abort::AbortFetchState::add_to_global(scope, global); incoming_body::HostBodySource::add_to_global(scope, global); outgoing_body::OutgoingBodyPump::add_to_global(scope, global); diff --git a/crates/builtins/web-fetch/src/outgoing_body.rs b/crates/builtins/web-fetch/src/outgoing_body.rs index a7bc955..5b7935f 100644 --- a/crates/builtins/web-fetch/src/outgoing_body.rs +++ b/crates/builtins/web-fetch/src/outgoing_body.rs @@ -17,7 +17,6 @@ use core_runtime::jsclass; use core_runtime::jsmethods; -use js::class::create_instance_with; use js::conversion::FromJSVal; use js::error::ExnThrown; use js::gc::handle::{Heap, RootedHeap}; @@ -34,16 +33,10 @@ use web_streams::readable::readable_stream::ReadableStream; use crate::incoming_body::HostBackedBodyOwner; -/// A running tally of how outgoing bodies reached the transport. +/// Counts of how outgoing bodies reached the transport, for tests. /// -/// Whether a host body is handed to the wire whole or pumped through JS chunk by chunk is -/// deliberately not observable from content — both deliver the same bytes, leave the donor stream -/// locked and disturbed, and read only internally. That also means losing the shortcut is invisible: -/// every byte would start travelling through JS and nothing would fail. These counters are the one -/// place that difference is visible, so a test can hold the choice in place. -/// -/// Whether an in-memory body's owner let go of its buffer on the way out is invisible the same way: -/// the same bytes reach the wire either way, and only how long the buffer outlives them differs. +/// Content cannot observe whether a host body was handed to the transport whole or pumped through +/// JS, or whether an in-memory body's owner released its buffer when handing it over. pub mod paths_taken { use std::sync::atomic::{AtomicUsize, Ordering}; @@ -85,8 +78,8 @@ pub mod paths_taken { match body { platform::http::OutgoingBody::Host(_) => &SHORTCUT, platform::http::OutgoingBody::Stream(_) => &PUMPED, - // Empty ones sit out: there is no buffer to hold on to, and `Bytes::is_unique` answers - // for the static one an empty body carries as though it were shared. + // Empty ones sit out: there is no buffer to hold on to, and `Bytes::is_unique` returns + // `false` for the static buffer of an empty body. platform::http::OutgoingBody::Bytes(bytes) if !bytes.is_empty() => { if bytes.is_unique() { &SOLE_BYTES @@ -166,12 +159,10 @@ enum StreamEnd { #[jsmethods] impl OutgoingBodyPump { - fn new() -> Self { - // Internal type, always created with a real reader/sender via `create_instance_with`; this - // hidden constructor exists only so the prototype is registered. + fn new(reader: DefaultReader, sender: BodySender) -> Self { OutgoingBodyPumpImpl { - reader: Heap::default(), - sender: None, + reader: Heap::from(reader), + sender: Some(sender), batch: Vec::new(), end: None, reading: false, @@ -183,7 +174,7 @@ impl OutgoingBodyPump { /// Turn a body's materialized `ReadableStream` into a [`OutgoingBody`]. /// /// A host body that has never been read is handed straight through; anything -/// else is pumped through a `DefaultReader`, or cancelled unsent where +/// else is pumped through a `DefaultReader`, or cancelled unsent if /// `start_reading` is false. The pump reads through `reader` when the caller /// already locked the stream with one, and acquires its own otherwise. /// @@ -247,19 +238,12 @@ fn pump_body_from_stream( Some(reader) => reader, None => acquire_native_reader(scope, &stream)?, }; - let state = create_instance_with::(scope, |_| OutgoingBodyPumpImpl { - reader: Heap::from(reader), - sender: Some(sender), - batch: Vec::new(), - end: None, - reading: false, - running: false, - })?; + let state = OutgoingBodyPump::new(scope, reader, sender)?; run(scope, &state); Ok(body) } -/// The most bytes the pump reads into one batch before sending it. +/// The maximum number of bytes the pump reads into one batch before sending it. const BATCH_LIMIT: usize = 64 * 1024; /// The pump's native read-request steps. Each records what it received in the pump's state and @@ -286,7 +270,11 @@ fn run(scope: &Scope<'_>, state: &OutgoingBodyPump<'_>) { if read_next(scope, state).is_err() { js::exception::report_and_clear(scope, "reading a request or response body"); state.data_mut().reading = false; - fail_body(state, "the body stream could not be read"); + // A failed body fails the send instead of completing it. + let sender = state.data_mut().sender.take(); + if let Some(sender) = sender { + sender.fail("the body stream could not be read".to_string()); + } } if state.data().reading { break; @@ -375,15 +363,25 @@ fn pump_chunk_step( // The spec's "a copy of chunk": the chunk's bytes are copied once, into the batch. Ok(array) => { let batch = &mut data.batch; - array.as_array_buffer_view().with_bytes(|bytes| { + let appended = array.with_slice(|bytes| { // Appending to a batch reserves room up to the limit, so the batch is copied once // rather than through every doubling of its capacity. - if !batch.is_empty() && batch.capacity() - batch.len() < bytes.len() { - let wanted = BATCH_LIMIT.max(batch.len() + bytes.len()) - batch.len(); - batch.reserve_exact(wanted); + if batch.capacity() - batch.len() < bytes.len() { + let wanted = if batch.is_empty() { + bytes.len() + } else { + BATCH_LIMIT.max(batch.len() + bytes.len()) - batch.len() + }; + batch.try_reserve_exact(wanted)?; } batch.extend_from_slice(bytes); + Ok::<(), std::collections::TryReserveError>(()) }); + if appended.is_err() { + data.end = Some(StreamEnd::Errored( + "The body chunk could not be allocated".to_string(), + )); + } } Err(_) => { data.end = Some(StreamEnd::Errored( @@ -426,28 +424,17 @@ fn pump_error_step( Ok(()) } -/// End the pump's body with an error, so the transport fails the send instead of treating the -/// body as complete. -fn fail_body(state: &OutgoingBodyPump<'_>, message: &str) { - if let Some(sender) = state.data_mut().sender.take() { - sender.fail(message.to_string()); - } -} - /// Send `item` to the body channel (taking the sender out of the pump), awaiting /// channel capacity for backpressure. After a batch is accepted, the pump runs again. /// After an error, the pump stops and the sender is dropped (closing the channel after /// the queued error). fn spawn_send(scope: &Scope<'_>, state: &OutgoingBodyPump<'_>, item: Send) { - if state.data().sender.is_none() { + let Some(sender) = state.data_mut().sender.take() else { return; - } + }; let Ok(promise) = Promise::new_pending(scope) else { js::exception::report_and_clear(scope, "sending a request or response body"); - fail_body(state, "the body could not be sent"); - return; - }; - let Some(sender) = state.data_mut().sender.take() else { + sender.fail("the body could not be sent".to_string()); return; }; let _ = promise.set_any_is_handled(scope); diff --git a/crates/builtins/web-fetch/src/request.rs b/crates/builtins/web-fetch/src/request.rs index f558088..3c70f4f 100644 --- a/crates/builtins/web-fetch/src/request.rs +++ b/crates/builtins/web-fetch/src/request.rs @@ -234,7 +234,7 @@ fn request_init_if_not_empty(init: Option) -> Option { #[webidl_interface] pub struct Request { /// - /// Note: The header list lives in `headers` and the body in `body`/`body_stream`. + /// Note: The header list lives in `headers` and the body in `body`. #[no_trace] request: RequestRecord, /// , with its stream and host body. @@ -260,7 +260,7 @@ pub type RequestInputAdapter = for<'s> fn(&'s Scope<'_>, HandleValue<'_>) -> Result>, ExnThrown>; js::instance_local! { - /// The adapter [`register_request_input_adapter`] registered. + /// The adapter registered by [`register_request_input_adapter`]. static REQUEST_INPUT_ADAPTER: std::cell::Cell> = const { std::cell::Cell::new(None) }; } @@ -565,11 +565,8 @@ impl Request { // Step 33.4: If _headers_ is a `Headers` object, then `for each` _header_ of its // `header list`, `append` _header_ to `this`’s `headers`. // Step 33.5: Otherwise, `fill` `this`’s `headers` with _headers_. - // (Filling from _init_["`headers`"] is user input and goes through the fully - // validating `fill`. Without it, steps 33.1/33.3/33.4 amount to re-filtering the - // headers' own former list — already validated and normalized, so it needs only the - // guard's policy filtering, which is skipped entirely when request restrictions are - // off; see `refill_headers_from_own_list`.) + // Without _init_["`headers`"], `refill_headers_from_own_list` runs steps 33.1, 33.3 + // and 33.4. match init.headers.take() { Some(init_headers) => { headers.data_mut().header_list.clear(); @@ -710,9 +707,7 @@ impl Request { // bytes, so the input's own handle on them holds the buffer for a body it can no // longer read. let _ = input_request.take_byte_source(); - if let Some(body) = input_request.data_mut().body.record.as_mut() { - body.source_disturbed = true; - } + input_request.set_source_disturbed(); } Ok(()) } @@ -760,7 +755,7 @@ impl Request { /// #[getter] - fn method(&self) -> Ref<'_, str> { + pub(crate) fn method(&self) -> Ref<'_, str> { // Step 1: Return `this`’s `request`’s `method`. Ref::map(self.data(), |data| data.request.method.as_str()) } @@ -809,7 +804,7 @@ impl Request { /// #[getter] - fn mode(&self) -> RequestMode { + pub(crate) fn mode(&self) -> RequestMode { // Step 1: Return `this`’s `request`’s `mode`. self.data().request.mode } @@ -830,7 +825,7 @@ impl Request { /// #[getter] - fn redirect(&self) -> RequestRedirect { + pub(crate) fn redirect(&self) -> RequestRedirect { // Step 1: Return `this`’s `request`’s `redirect mode`. self.data().request.redirect_mode } @@ -925,9 +920,8 @@ impl Request { // and `guard` is _guard_. let (header_list, guard) = { let headers = self.data().headers.get(scope); - let header_list = headers.data().header_list.clone(); let guard = headers.data().guard; - (header_list, guard) + (headers.list(), guard) }; let cloned_headers = Headers::from_list(scope, header_list, guard)?; // [inlined create a Request object] Steps 1, 2, 4: Let _requestObject_ be a `new` @@ -1056,21 +1050,6 @@ impl Request<'_> { pub(crate) fn current_url(&self) -> Url { self.data().request.current_url().clone() } - - /// The request's method (e.g. `"GET"`, `"HEAD"`). - pub(crate) fn http_method(&self) -> String { - self.data().request.method.clone() - } - - /// The request's redirect mode. - pub(crate) fn redirect_mode(&self) -> RequestRedirect { - self.data().request.redirect_mode - } - - /// The request's mode (the concept-record field, distinct from the JS `mode` getter). - pub(crate) fn request_mode(&self) -> RequestMode { - self.data().request.mode - } } impl BodyMixin for Request<'_> { diff --git a/crates/builtins/web-fetch/src/response.rs b/crates/builtins/web-fetch/src/response.rs index 94b8ebd..89a1466 100644 --- a/crates/builtins/web-fetch/src/response.rs +++ b/crates/builtins/web-fetch/src/response.rs @@ -196,7 +196,7 @@ impl Response { }) } - /// Returns a reference to the [`Url`] of this response. + /// The response's URL. pub fn url(&self) -> Option { self.data().response.url().cloned() } @@ -273,12 +273,11 @@ impl Response { // Step 3: Return the result of `creating` a `Response` object, given _clonedResponse_, // `this`’s `headers`’s `guard`, and `this`’s `relevant realm`. let headers = self.headers(scope); - let header_list = headers.data().header_list.clone(); let guard = headers.data().guard; algorithms::create_a_response_object( scope, new_response, - header_list, + headers.list(), guard, cloned_body, cloned_stream, @@ -458,33 +457,16 @@ impl Response<'_> { self.sync_body_accounting(); } - /// Whether a host body is still sitting unread on this response. - pub(crate) fn has_unread_host_body(&self) -> bool { - self.data().body.host.is_some() - } - - /// Remember the `fetch` abort algorithm's state, so consuming the body can detach it. - pub(crate) fn set_abort_state(&self, state: &crate::abort::AbortFetchState<'_>) { - self.data_mut().abort_state = Some(Heap::from(*state)); - } - - /// The body has been consumed, so an abort can no longer affect it: detach the `fetch` abort - /// algorithm from its signal. - fn detach_abort_state(&self, scope: &Scope<'_>) { - let state = self.data_mut().abort_state.take_rooted(scope); - if let Some(state) = state { - state.detach(scope); - } - } - - /// Whether this response's `.body` stream exists and has not finished, i.e. erroring it - /// would still have an observable effect. - pub(crate) fn body_stream_is_unfinished(&self, scope: &Scope<'_>) -> bool { - self.data() - .body - .stream - .get(scope) - .is_some_and(|stream| stream.is_readable()) + /// Whether an abort can still act on this response's body: it has an unread host body, or a + /// `.body` stream that is still readable. + pub(crate) fn body_is_abortable(&self, scope: &Scope<'_>) -> bool { + let data = self.data(); + data.body.host.is_some() + || data + .body + .stream + .get(scope) + .is_some_and(|stream| stream.is_readable()) } /// Abort this response's body: error its `.body` stream with `reason` (so pending reads @@ -671,7 +653,10 @@ impl BodyMixin for Response<'_> { /// The body is being read to completion, so an abort can no longer error it: /// drop the `fetch` abort algorithm that was keeping this response alive. fn on_body_consumed(&self, scope: &Scope<'_>) { - self.detach_abort_state(scope); + let state = self.data_mut().abort_state.take_rooted(scope); + if let Some(state) = state { + state.detach(scope); + } } } @@ -692,12 +677,7 @@ impl HostBackedBodyOwner for Response<'_> { impl Response<'_> { /// Returns a copy of the response's header list. pub fn headers_list(&self, scope: &Scope<'_>) -> Vec<(String, String)> { - self.headers(scope) - .data() - .header_list - .iter() - .map(|(name, value)| (name.clone(), value.clone())) - .collect() + self.headers(scope).list() } /// The response's body as a [`platform::http::OutgoingBody`] for sending on the wire, on the diff --git a/crates/builtins/web-fetch/src/transport.rs b/crates/builtins/web-fetch/src/transport.rs index 770e387..0ea2ae3 100644 --- a/crates/builtins/web-fetch/src/transport.rs +++ b/crates/builtins/web-fetch/src/transport.rs @@ -43,8 +43,8 @@ pub(crate) enum ResponseTainting { /// recurse through `main fetch`, so `send_following_redirects` re-runs it for /// the initial URL and for every redirect target. /// -/// The request's origin is its client's. For us: the worker location's. Without -/// one there is nothing to compare against, and in permissive server mode the +/// The request's origin is that of its client, which here is the worker location. +/// Without one there is nothing to compare against, and in permissive server mode the /// browser security policies are off wholesale, so in both cases every hop /// proceeds as "basic". fn apply_main_fetch_switch( @@ -66,8 +66,8 @@ fn apply_main_fetch_switch( // "`websocket`" → set _request_'s `response tainting` to "`basic`" (and run `scheme // fetch`). // `data:` URLs never reach the transport, "`navigate`" is rewritten to "`same-origin`" by - // the `Request` constructor, and there are no WebSockets, so the arm reduces to the origin - // check. + // the `Request` constructor, and no request has mode "`websocket`", so the arm reduces to the + // origin check. if *tainting == ResponseTainting::Basic && url.origin() == *request_origin { return Ok(()); } @@ -131,19 +131,15 @@ pub(crate) async fn send_following_redirects( let mut url_list = vec![initial_url]; loop { // Build this attempt's request, taking the body: a byte body is kept for replay on a - // body-preserving redirect (its clone is a refcount bump, not a copy); a stream body is - // moved (and cannot be replayed). - let body_is_stream = matches!( - request.body, - OutgoingBody::Stream(_) | OutgoingBody::Host(_) - ); - let send_body = match &mut request.body { - OutgoingBody::Bytes(bytes) => OutgoingBody::Bytes(bytes.clone()), - // A streamed or host-piped body is consumed by the send and cannot be replayed. - OutgoingBody::Stream(_) | OutgoingBody::Host(_) => { - std::mem::replace(&mut request.body, OutgoingBody::Consumed) - } - OutgoingBody::Consumed => OutgoingBody::Consumed, + // body-preserving redirect (its clone is a refcount bump, not a copy). A streamed or + // host-piped body is moved, since the send consumes it. + let (send_body, body_is_stream) = match &mut request.body { + OutgoingBody::Bytes(bytes) => (OutgoingBody::Bytes(bytes.clone()), false), + OutgoingBody::Stream(_) | OutgoingBody::Host(_) => ( + std::mem::replace(&mut request.body, OutgoingBody::Consumed), + true, + ), + OutgoingBody::Consumed => (OutgoingBody::Consumed, false), }; let attempt = Request { method: request.method.clone(), @@ -196,8 +192,8 @@ pub(crate) async fn send_following_redirects( // Established above: a non-redirect status already returned as the final response. // [inlined location URL] Step 2: Let _location_ be the result of `extracting header list // values` given `Location` and _response_'s `header list`. - // The `ABNF` for `Location` allows a single `header`, so a second one (identical or not) - // is an error. + // The `ABNF` for `Location` allows a single `header`, so a second one, identical or not, + // makes the location URL a failure. let mut locations = response .headers .iter() @@ -256,10 +252,8 @@ pub(crate) async fn send_following_redirects( // `includes credentials`, then return a `network error`. This catches a cross-origin // resource redirecting to a same-origin URL. // - // There is no origin or response-tainting model in this runtime — the requester has no - // origin — so the same-origin conditions cannot be evaluated. A credentialed Location on - // a cors-mode request is rejected outright: the strictest reading of steps 9–10, and what - // browsers observe for `fetch()` (always cors-mode here) of cross-origin resources. + // The origin conditions are not evaluated: a credentialed Location on a cors-mode request + // is rejected regardless of origin, the strictest reading of steps 9 and 10. let includes_credentials = !location_url.username().is_empty() || location_url.password().is_some(); if mode == RequestMode::Cors && includes_credentials { @@ -303,13 +297,9 @@ pub(crate) async fn send_following_redirects( // moment another origin is seen after the initial request, the `Authorization` header // is removed. if current_url.origin() != location_url.origin() { - // The spec's `CORS non-wildcard request-header name` list is just - // `Authorization`, because in a browser `Cookie`/`Proxy-Authorization` - // are forbidden request headers a script cannot set. This runtime's - // server mode lets a handler set them (forbidden-header enforcement - // off), so also strip them on a cross-origin redirect — defense in - // depth against leaking credentials to another origin. This only ever - // strips *more* than the spec, never fewer. + // `Cookie` and `Proxy-Authorization` are stripped as well. The spec leaves them out + // because they are forbidden request headers, but scripts can set them when + // forbidden-header enforcement is off. request.headers.retain(|(name, _)| { !name.eq_ignore_ascii_case("authorization") && !name.eq_ignore_ascii_case("cookie") @@ -347,21 +337,13 @@ pub(crate) async fn send_following_redirects( } } -/// Percent-encode the non-ASCII bytes of a `Location` header value, so the URL -/// parser resolves it as the byte sequence it is. -/// -/// A header value is bytes, and the platform layer hands it over isomorphic- -/// decoded, one code unit per byte. Passing that straight to the URL parser -/// would have it treat those code units as *text* and percent-encode each one's -/// UTF-8 encoding — so a `Location` of the three bytes `E2 98 83` would resolve -/// to `%C3%A2%C2%98%C2%83` rather than `%E2%98%83`, a different URL. Encoding -/// them here as the bytes they are avoids that. -/// -/// ASCII is left untouched, so an already-escaped `%e2` keeps its spelling and -/// a literal `%` stays literal — the URL parser does not re-encode either. +/// Percent-encode the non-ASCII code units of an isomorphic-decoded `Location` header value as the +/// bytes they represent, so the URL parser resolves the byte sequence rather than encoding each +/// code unit as UTF-8 (`E2 98 83` becomes `%E2%98%83`, not `%C3%A2%C2%98%C2%83`). ASCII, including +/// `%`, is left as is. /// -/// — the behaviour -/// `redirect-location-escape.tentative.any.js` covers, not yet in the spec. +/// , tested by +/// `redirect-location-escape.tentative.any.js`. fn percent_encode_location_bytes(location: &str) -> std::borrow::Cow<'_, str> { use std::fmt::Write; diff --git a/crates/builtins/web-fetch/tests/body_memory.rs b/crates/builtins/web-fetch/tests/body_memory.rs index 1a72169..83e279f 100644 --- a/crates/builtins/web-fetch/tests/body_memory.rs +++ b/crates/builtins/web-fetch/tests/body_memory.rs @@ -24,7 +24,7 @@ fn evaluate(scope: &js::gc::scope::Scope<'_>, code: &str) { } } -/// Builds 256 objects each holding a 1 MiB byte source and returns how many collections that +/// Builds 96 objects each holding a 1 MiB byte source and returns how many collections that /// caused. The objects are garbage as soon as they are built, and the JS heap sees only their /// small wrappers. fn collections_while_building(constructor: &str) -> u32 { @@ -36,7 +36,7 @@ fn collections_while_building(constructor: &str) -> u32 { let before = gc_count(&scope); evaluate( &scope, - &format!("for (let i = 0; i < 256; i++) {{ {constructor}; }}"), + &format!("for (let i = 0; i < 96; i++) {{ {constructor}; }}"), ); gc_count(&scope) - before } diff --git a/crates/builtins/web-fetch/tests/config.rs b/crates/builtins/web-fetch/tests/config.rs index 13591c7..8a8e29a 100644 --- a/crates/builtins/web-fetch/tests/config.rs +++ b/crates/builtins/web-fetch/tests/config.rs @@ -1,16 +1,9 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception -//! Keystone test for the request-restrictions configuration. -//! -//! The browser-security constraints — forbidden request headers, forbidden -//! methods, no-CORS safelisting, origin/mode enforcement — are off by default: -//! StarlingMonkey is primarily a server runtime, where a script must be able to -//! set `Host` or use `CONNECT`. WPT mode turns them on by default, and an -//! explicit `--web-security-restrictions[=bool]` overrides either default. -//! `Runtime::init` applies the configured value, which is what these tests -//! exercise end to end: with restrictions off, a forbidden header (`Host`) and -//! a forbidden method (`CONNECT`) are accepted; with them on, they are -//! filtered/rejected. +//! The web security restrictions configuration: off by default, on by default in WPT mode, and +//! overridable by `--web-security-restrictions[=bool]`. With restrictions off, a forbidden header +//! (`Host`) and a forbidden method (`CONNECT`) are accepted. With them on, they are filtered or +//! rejected. // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] diff --git a/crates/builtins/web-fetch/tests/fetch_network.rs b/crates/builtins/web-fetch/tests/fetch_network.rs index 3db7ab2..bb87410 100644 --- a/crates/builtins/web-fetch/tests/fetch_network.rs +++ b/crates/builtins/web-fetch/tests/fetch_network.rs @@ -5,39 +5,52 @@ //! Exercises the whole networking path: the `fetch` global builds a `Request`, //! the platform transport (reqwest) sends it over a real TCP socket, the //! async-promise event-loop driver settles the promise, and the `Response` is -//! consumed with `text()`. Networking is otherwise not covered by the offline -//! WPT suite. +//! consumed with `text()`. #![cfg(not(target_arch = "wasm32"))] use core_runtime::event_loop::{run_to_completion, with_event_loop, EventLoop}; use core_runtime::runtime::{clear_global_initializers, Runtime}; use js::conversion::FromJSVal; +use js::gc::scope::Scope; use std::io::{Read, Write}; use std::net::TcpListener; -/// Start a one-shot HTTP/1.1 server on `127.0.0.1` that replies to a single -/// request with `body`, and return its URL. -fn start_loopback_server(body: &'static str) -> String { +/// Start a server on `127.0.0.1` that accepts one connection per entry of `replies`, in order, +/// replies with that entry's bytes, and return its URL. +fn start_server(replies: Vec>) -> String { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let port = listener.local_addr().unwrap().port(); std::thread::spawn(move || { - if let Ok((mut stream, _)) = listener.accept() { - // Read the request headers (up to the blank line); we don't need the body. - let mut buf = [0u8; 1024]; - let _ = stream.read(&mut buf); - let response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", - body.len(), - body, - ); - let _ = stream.write_all(response.as_bytes()); - let _ = stream.flush(); + for reply in replies { + if let Ok((mut stream, _)) = listener.accept() { + let mut buf = [0u8; 4096]; + let _ = stream.read(&mut buf); + let _ = stream.write_all(&reply); + let _ = stream.flush(); + } } }); format!("http://127.0.0.1:{port}/") } +/// A `200` reply with `body`. +fn ok(body: &str) -> Vec { + format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ) + .into_bytes() +} + +/// A `302` reply to `location`. +fn found(location: &str) -> Vec { + format!( + "HTTP/1.1 302 Found\r\nLocation: {location}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + ) + .into_bytes() +} + /// Start a server that streams chunked data with a small delay between chunks, /// until the client disconnects, so a test can read some then abort mid-stream. fn start_streaming_server() -> String { @@ -162,24 +175,6 @@ fn start_echo_server() -> String { format!("http://127.0.0.1:{port}/") } -/// Start a one-shot server replying 302 with `location` (which may be a path, -/// an absolute URL, or a non-HTTP URL), and return its URL. -fn start_redirect_server(location: String) -> String { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let port = listener.local_addr().unwrap().port(); - std::thread::spawn(move || { - if let Ok((mut stream, _)) = listener.accept() { - let mut buf = [0u8; 4096]; - let _ = stream.read(&mut buf); - let response = format!( - "HTTP/1.1 302 Found\r\nLocation: {location}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", - ); - let _ = stream.write_all(response.as_bytes()); - } - }); - format!("http://127.0.0.1:{port}/") -} - /// Extract the `Authorization` header value out of a raw HTTP request, or "none". fn authorization_of(raw: &[u8]) -> String { String::from_utf8_lossy(raw) @@ -192,57 +187,31 @@ fn authorization_of(raw: &[u8]) -> String { .unwrap_or_else(|| "none".to_string()) } -/// Start a one-shot server that echoes the request's `Authorization` header as -/// `auth:`, and return its URL. -fn start_auth_echo_server() -> String { +/// Start a server that echoes the request's `Authorization` header as `auth:`, and +/// return its URL. With `redirect_first`, it first redirects to `/target` on the same origin. +fn start_auth_echo_server(redirect_first: bool) -> String { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let port = listener.local_addr().unwrap().port(); std::thread::spawn(move || { - if let Ok((mut stream, _)) = listener.accept() { - let mut buf = [0u8; 4096]; - let n = stream.read(&mut buf).unwrap_or(0); - let body = format!("auth:{}", authorization_of(&buf[..n])); - let response = format!( - "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", - body.len(), - body, - ); - let _ = stream.write_all(response.as_bytes()); - } - }); - format!("http://127.0.0.1:{port}/") -} - -/// Start a server that 302-redirects its first request to `/target` on the same -/// origin, and answers the second request with `auth:`. -fn start_same_origin_redirect_auth_server() -> String { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let port = listener.local_addr().unwrap().port(); - std::thread::spawn(move || { - if let Ok((mut stream, _)) = listener.accept() { - let mut buf = [0u8; 4096]; - let _ = stream.read(&mut buf); - let response = "HTTP/1.1 302 Found\r\nLocation: /target\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; - let _ = stream.write_all(response.as_bytes()); + if redirect_first { + if let Ok((mut stream, _)) = listener.accept() { + let mut buf = [0u8; 4096]; + let _ = stream.read(&mut buf); + let _ = stream.write_all(&found("/target")); + } } if let Ok((mut stream, _)) = listener.accept() { let mut buf = [0u8; 4096]; let n = stream.read(&mut buf).unwrap_or(0); - let body = format!("auth:{}", authorization_of(&buf[..n])); - let response = format!( - "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", - body.len(), - body, - ); - let _ = stream.write_all(response.as_bytes()); + let _ = stream.write_all(&ok(&format!("auth:{}", authorization_of(&buf[..n])))); } }); format!("http://127.0.0.1:{port}/") } -/// Evaluate `code` (which sets `globalThis.__out`), drive the event loop to -/// completion, and return `__out`. -fn run_and_get_out(code: &str) -> String { +/// Evaluate `code`, drive the event loop to completion, and return the result of `inspect`, which +/// runs while the runtime is still alive. +fn run(code: &str, inspect: impl FnOnce(&Scope<'_>) -> T) -> T { clear_global_initializers(); libstarling::register_builtins(); let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()).expect("runtime init"); @@ -250,11 +219,9 @@ fn run_and_get_out(code: &str) -> String { let rawcx = unsafe { scope.cx_mut().raw_cx() }; let el = EventLoop::new(); - { - with_event_loop(&el, |_| { - js::compile::evaluate_with_filename(&scope, code, "", 1).expect("eval failed"); - }); - } + with_event_loop(&el, |_| { + js::compile::evaluate_with_filename(&scope, code, "", 1).expect("eval failed"); + }); // Drain microtasks queued during top-level evaluation (e.g. a synchronously settled promise's // reactions), as the runtime does before stepping the event loop. js::jobs::run_jobs(&scope); @@ -264,25 +231,19 @@ fn run_and_get_out(code: &str) -> String { .build() .unwrap(); tokio_rt.block_on(async { unsafe { run_to_completion(rawcx, &el, tokio::time::sleep).await } }); + inspect(&scope) +} - let out = - js::compile::evaluate_with_filename(&scope, "globalThis.__out", "", 1).unwrap(); - String::from_jsval(&scope, out, ()).unwrap() +/// Evaluate `code` (which sets `globalThis.__out`), drive the event loop to completion, and +/// return `__out`. +fn run_and_get_out(code: &str) -> String { + run(code, read_out) } -#[test] -fn fetch_get_loopback_resolves_with_response_text() { - let url = start_loopback_server("hello from loopback"); - let out = run_and_get_out(&format!( - r#" - globalThis.__out = "pending"; - fetch("{url}") - .then(r => Promise.all([r.status, r.headers.get("content-type"), r.text()])) - .then(([status, ct, text]) => {{ globalThis.__out = `${{status}}|${{ct}}|${{text}}`; }}) - .catch(e => {{ globalThis.__out = "error:" + e; }}); - "# - )); - assert_eq!(out, "200|text/plain|hello from loopback"); +/// Returns `String(globalThis.__out)`. +fn read_out(scope: &Scope<'_>) -> String { + let out = js::compile::evaluate_with_filename(scope, "globalThis.__out", "", 1).unwrap(); + String::from_jsval(scope, out, ()).unwrap() } #[test] @@ -310,31 +271,12 @@ fn fetch_streams_a_readable_stream_request_body() { assert_eq!(out, "hello streamed body"); } -#[test] -fn fetch_pipes_a_response_body_into_a_request_body() { - // The incoming→outgoing shortcut: a response body used directly as another request's body is - // streamed straight through to the second server, which echoes it. - let source = start_loopback_server("piped through the shortcut"); - let echo = start_echo_server(); - let out = run_and_get_out(&format!( - r#" - globalThis.__out = "pending"; - fetch("{source}") - .then(response => fetch("{echo}", {{ method: "POST", body: response.body, duplex: "half" }})) - .then(r => r.text()) - .then(text => {{ globalThis.__out = text; }}) - .catch(e => {{ globalThis.__out = "error:" + e; }}); - "# - )); - assert_eq!(out, "piped through the shortcut"); -} - #[test] fn fetch_pipes_a_response_body_through_an_identity_transform() { // The complex incoming→outgoing shortcut: a response body piped through an identity // TransformStream into another request is still handed straight through (the native source is // propagated across the identity transform), so the second server echoes it unchanged. - let source = start_loopback_server("piped through identity transform"); + let source = start_server(vec![ok("piped through identity transform")]); let echo = start_echo_server(); let out = run_and_get_out(&format!( r#" @@ -383,10 +325,7 @@ fn fetch_pumps_a_piped_multichunk_body_without_dropping_chunks() { #[test] fn a_delayed_piped_identity_transform_still_delivers_the_whole_body() { // A pipe left running well ahead of the `fetch` that consumes it must still deliver every - // byte. The pipe's first pull parks rather than reading the host body, so this takes the - // shortcut where it once took the pump — whichever it picks, all 40 chunks must arrive in - // order. (Which path ran is deliberately not content-observable: both leave the donor stream - // locked and disturbed, and the pump reads internally so content cannot see its reads.) + // byte in order. The pipe's first pull parks rather than reading the host body. let (source, expected) = start_multichunk_server(40); let echo = start_echo_server(); let out = run_and_get_out(&format!( @@ -434,7 +373,7 @@ fn a_parked_body_read_resumes_when_the_transform_is_read() { fn fetch_applies_a_non_identity_transform_to_a_piped_body() { // A non-identity transform must NOT be shortcut: its transform runs, so the second server sees // the transformed (uppercased) body. - let source = start_loopback_server("transform me"); + let source = start_server(vec![ok("transform me")]); let echo = start_echo_server(); let out = run_and_get_out(&format!( r#" @@ -533,10 +472,9 @@ fn aborting_a_parked_body_read_rejects_rather_than_hanging() { #[test] fn transform_start_chunks_are_not_dropped_by_the_shortcut() { // A transformer with only a start callback is not an identity transform: its - // enqueued prefix must reach the destination. The incoming→outgoing shortcut - // previously treated any transformer without a `transform` callback as - // identity and handed the host body straight through, dropping the prefix. - let source = start_loopback_server("rest-of-body"); + // enqueued prefix must reach the destination rather than the shortcut handing the host + // body straight through. + let source = start_server(vec![ok("rest-of-body")]); let echo = start_echo_server(); let out = run_and_get_out(&format!( r#" @@ -561,8 +499,8 @@ fn transform_start_chunks_are_not_dropped_by_the_shortcut() { fn redirect_strips_authorization_cross_origin() { // HTTP-redirect fetch step 13: the moment another origin is seen, the Authorization header is // removed. Two loopback ports are two origins. - let target = start_auth_echo_server(); - let origin = start_redirect_server(target); + let target = start_auth_echo_server(false); + let origin = start_server(vec![found(&target)]); let out = run_and_get_out(&format!( r#" globalThis.__out = "pending"; @@ -578,7 +516,7 @@ fn redirect_strips_authorization_cross_origin() { #[test] fn redirect_keeps_authorization_same_origin() { // A same-origin redirect keeps the Authorization header. - let url = start_same_origin_redirect_auth_server(); + let url = start_auth_echo_server(true); let out = run_and_get_out(&format!( r#" globalThis.__out = "pending"; @@ -591,35 +529,15 @@ fn redirect_keeps_authorization_same_origin() { assert_eq!(out, "auth:Bearer s3cret"); } -#[test] -fn redirect_to_non_http_scheme_is_a_network_error() { - // HTTP-redirect fetch step 6: a Location whose scheme is not HTTP(S) is a network error. - // - // This confirms the observable outcome (a network error), but cannot isolate the step-6 check - // from the transport: the reqwest backend rejects every non-HTTP(S) scheme on its own, so a - // `file://` Location rejects either way. The step-6 check is the sole guard for a backend that - // would otherwise follow such a Location (e.g. the WASI HTTP path), which is what this asserts. - let origin = start_redirect_server("file:///etc/passwd".to_string()); - let out = run_and_get_out(&format!( - r#" - globalThis.__out = "pending"; - fetch("{origin}") - .then(r => {{ globalThis.__out = "resolved:" + r.status; }}) - .catch(e => {{ globalThis.__out = "rejected:" + e.constructor.name; }}); - "# - )); - assert_eq!(out, "rejected:TypeError"); -} - #[test] fn redirect_with_credentialed_location_is_a_network_error() { // HTTP-redirect fetch steps 9–10: a Location that includes credentials is a network error for // a cors-mode request (every fetch() here). The redirect target is a *reachable* server that // returns 200, so the oracle discriminates: with the check, the fetch rejects before following // (rejected:TypeError); without it, the fetch would follow and resolve (resolved:200). - let upstream = start_loopback_server("creds-followed"); + let upstream = start_server(vec![ok("creds-followed")]); let credentialed = upstream.replace("http://", "http://user:pass@"); - let origin = start_redirect_server(credentialed); + let origin = start_server(vec![found(&credentialed)]); let out = run_and_get_out(&format!( r#" globalThis.__out = "pending"; @@ -637,7 +555,7 @@ fn response_body_unaffected_by_patched_controller_enqueue() { // operation, not the author-visible ReadableStreamDefaultController.prototype // methods. Patching prototype.enqueue to drop chunks must not affect the // body's bytes. - let url = start_loopback_server("untampered body bytes"); + let url = start_server(vec![ok("untampered body bytes")]); let out = run_and_get_out(&format!( r#" globalThis.__out = "pending"; @@ -666,7 +584,7 @@ fn response_body_transmitted_via_shortcut_marks_it_used() { // to the transport (the incoming→outgoing shortcut). The donor stream must be // left locked and disturbed, as if the pump had read it: bodyUsed is true and // the stream is locked. - let source = start_loopback_server("shortcut body bytes"); + let source = start_server(vec![ok("shortcut body bytes")]); let echo = start_echo_server(); let out = run_and_get_out(&format!( r#" @@ -686,32 +604,15 @@ fn response_body_transmitted_via_shortcut_marks_it_used() { assert_eq!(out, "echoed=shortcut body bytes|used=true|locked=true"); } -/// Start a one-shot server that replies with `raw` verbatim, so a test can -/// control the status line and header bytes exactly (including bytes that are -/// not valid UTF-8, which no `format!`-built response could carry). -fn start_raw_server(raw: &'static [u8]) -> String { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let port = listener.local_addr().unwrap().port(); - std::thread::spawn(move || { - if let Ok((mut stream, _)) = listener.accept() { - let mut buf = [0u8; 1024]; - let _ = stream.read(&mut buf); - let _ = stream.write_all(raw); - let _ = stream.flush(); - } - }); - format!("http://127.0.0.1:{port}/") -} - #[test] fn status_text_is_empty_because_the_reason_phrase_is_not_recoverable() { // Neither transport surfaces the reason-phrase the peer sent, and it cannot be guessed from // the status code — a server may send any phrase, as this one does. Reporting the registered // phrase for the code would invent a value the peer never sent, so `statusText` stays empty. // Locks in the behaviour `fetch/api/basic/status.h2.any.js` depends on. - let url = start_raw_server( - b"HTTP/1.1 404 Totally Fine\r\nContent-Length: 2\r\nConnection: close\r\n\r\nhi", - ); + let url = start_server(vec![ + b"HTTP/1.1 404 Totally Fine\r\nContent-Length: 2\r\nConnection: close\r\n\r\nhi".to_vec(), + ]); let out = run_and_get_out(&format!( r#" globalThis.__out = "pending"; @@ -750,45 +651,13 @@ fn cancelling_a_derived_request_body_cancels_the_original() { assert_eq!(out, "cancelled=done here|derivedLocked=false"); } -/// Start a server that serves `count` sequential one-shot responses, so a test -/// can make several fetches against one URL. -fn start_repeating_server(count: usize, body: &'static str) -> String { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let port = listener.local_addr().unwrap().port(); - std::thread::spawn(move || { - for _ in 0..count { - if let Ok((mut stream, _)) = listener.accept() { - let mut buf = [0u8; 1024]; - let _ = stream.read(&mut buf); - let response = format!( - "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", - body.len(), - body, - ); - let _ = stream.write_all(response.as_bytes()); - let _ = stream.flush(); - } - } - }); - format!("http://127.0.0.1:{port}/") -} - #[test] fn a_reused_abort_signal_does_not_accumulate_fetch_abort_algorithms() { // `fetch` registers an abort algorithm on the request's signal. An `AbortSignal` outlives the // fetches made with it, so an algorithm that is never removed accumulates one entry per fetch // — each rooting that fetch's promise and Response, and through the Response its host body and // connection. Reading each body must detach it again. - // - // Inlined rather than run through `run_and_get_out` because the signal has to be inspected - // while its runtime is still alive. - let url = start_repeating_server(3, "body"); - clear_global_initializers(); - libstarling::register_builtins(); - let rt = Runtime::init(&core_runtime::config::RuntimeConfig::default()).expect("runtime init"); - let scope = rt.default_global(); - let rawcx = unsafe { scope.cx_mut().raw_cx() }; - let el = EventLoop::new(); + let url = start_server(vec![ok("body"); 3]); let code = format!( r#" globalThis.__out = "pending"; @@ -802,31 +671,25 @@ fn a_reused_abort_signal_does_not_accumulate_fetch_abort_algorithms() { }})().catch(e => {{ globalThis.__out = "error:" + e; }}); "# ); - with_event_loop(&el, |_| { - js::compile::evaluate_with_filename(&scope, &code, "", 1).expect("eval failed"); - }); - js::jobs::run_jobs(&scope); - let tokio_rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() + let (out, count) = run(&code, |scope| { + // The signal is still reachable from the global, so anything it holds is still alive. + let signal_value = js::compile::evaluate_with_filename( + scope, + "globalThis.__controller.signal", + "", + 1, + ) .unwrap(); - tokio_rt.block_on(async { unsafe { run_to_completion(rawcx, &el, tokio::time::sleep).await } }); - - let out = - js::compile::evaluate_with_filename(&scope, "globalThis.__out", "", 1).unwrap(); - assert_eq!(String::from_jsval(&scope, out, ()).unwrap(), "done"); - - // The signal is still reachable from the global, so anything it holds is still alive. - let signal_value = - js::compile::evaluate_with_filename(&scope, "globalThis.__controller.signal", "", 1) + let signal = js::Object::from_raw_value(scope, signal_value.get()) + .unwrap() + .cast::() .unwrap(); - let signal = js::Object::from_raw_value(&scope, signal_value.get()) - .unwrap() - .cast::() - .unwrap(); - let count = web_globals::signals::algorithms::abort_algorithm_count_including_dependents( - &scope, &signal, - ); + let count = web_globals::signals::algorithms::abort_algorithm_count_including_dependents( + scope, &signal, + ); + (read_out(scope), count) + }); + assert_eq!(out, "done"); assert_eq!( count, 0, "3 fetches left {count} abort algorithms registered on the reused signal's dependents" @@ -844,10 +707,7 @@ fn static_json_ignores_a_patched_json_stringify() { (async () => { let observed = false; JSON.stringify = () => { observed = true; return '"hijacked"'; }; - const patchedTotal = new Response(); // keep the patch live - const r = Response.json({ a: 1 }); - const text = await r.text(); - // Also check the not-serializable path still throws a TypeError. + const text = await Response.json({ a: 1 }).text(); let threw = "no"; try { Response.json(Symbol("nope")); } catch (e) { threw = e.constructor.name; } globalThis.__out = `${text}|observed=${observed}|threw=${threw}`; @@ -859,8 +719,8 @@ fn static_json_ignores_a_patched_json_stringify() { #[test] fn static_json_still_serializes_null_and_rejects_undefined() { - // A real null serializes to the string "null", while values `JSON.stringify` returns - // undefined for must be rejected — the two must not be conflated. + // A real null serializes to the string "null", while values for which + // `JSON.stringify` returns undefined must be rejected. The two must not be conflated. let out = run_and_get_out( r#" globalThis.__out = "pending"; @@ -887,43 +747,15 @@ fn static_json_still_serializes_null_and_rejects_undefined() { ); } -/// Start a one-shot server that captures the request head it receives and -/// replies `ok`, so a test can assert on what actually went on the wire. -fn start_capturing_server() -> (String, std::sync::mpsc::Receiver) { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let port = listener.local_addr().unwrap().port(); - let (tx, rx) = std::sync::mpsc::channel(); - std::thread::spawn(move || { - if let Ok((mut stream, _)) = listener.accept() { - // Note: long timeout because GC_ZEAL can make things real slow. - stream - .set_read_timeout(Some(std::time::Duration::from_secs(10))) - .ok(); - let mut buf = Vec::new(); - let mut tmp = [0u8; 4096]; - while let Ok(n) = stream.read(&mut tmp) { - if n == 0 { - break; - } - buf.extend_from_slice(&tmp[..n]); - if buf.windows(4).any(|w| w == b"\r\n\r\n") { - break; - } - } - let _ = tx.send(String::from_utf8_lossy(&buf).into_owned()); - let _ = stream - .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok"); - } - }); - (format!("http://127.0.0.1:{port}/"), rx) -} - /// The `Content-Length` header of a request made by running `code`, or `None`. fn content_length_of_fetch(code: &str) -> Option { - let (url, rx) = start_capturing_server(); + let (url, rx) = start_byte_capture_server( + b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok", + ); let out = run_and_get_out(&code.replace("URL", &url)); assert_eq!(out, "ok", "the fetch itself failed"); let head = rx.recv_timeout(std::time::Duration::from_secs(3)).unwrap(); + let head = String::from_utf8_lossy(&head); let found: Vec = head .lines() .filter(|line| { @@ -1079,9 +911,7 @@ fn header_values_are_isomorphic_encoded_on_the_wire() { #[test] fn header_values_are_isomorphic_decoded_from_the_wire() { // The mirror of the above: a response header byte of 0xE6 is the code unit U+00E6, so - // `headers.get` must yield "bæte". This has been wrong two ways: rejecting the value as - // not-UTF-8 dropped the header entirely, and decoding it as UTF-8 turned the lone 0xE6 into - // U+FFFD. Isomorphic decoding is total, so neither can happen. + // `headers.get` must yield "bæte", not drop the header or decode it to U+FFFD. let (url, _rx) = start_byte_capture_server( b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nX-Echo: b\xe6te\r\nConnection: close\r\n\r\nok", ); @@ -1099,63 +929,3 @@ fn header_values_are_isomorphic_decoded_from_the_wire() { )); assert_eq!(out, "62,e6,74,65"); } - -/// Start a one-shot server that answers the first request with `redirect` and -/// the second with a 200, so a test can observe where a redirect landed. -fn start_redirecting_server(redirect: &'static [u8]) -> String { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let port = listener.local_addr().unwrap().port(); - std::thread::spawn(move || { - for hop in 0..2 { - if let Ok((mut stream, _)) = listener.accept() { - let mut buf = [0u8; 2048]; - let _ = stream.read(&mut buf); - let reply: &[u8] = if hop == 0 { - redirect - } else { - b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok" - }; - let _ = stream.write_all(reply); - let _ = stream.flush(); - } - } - }); - format!("http://127.0.0.1:{port}/") -} - -#[test] -fn a_redirect_location_is_resolved_as_bytes_not_as_text() { - // A `Location` header value is a byte sequence. Resolving it as text percent-encodes each - // decoded code unit as UTF-8, so the three bytes E2 98 83 come back as %C3%A2%C2%98%C2%83 - // — six bytes, a different URL. They must be percent-encoded as the bytes they are. - let url = start_redirecting_server( - b"HTTP/1.1 302 Found\r\nLocation: /t?\xe2\x98\x83\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", - ); - let out = run_and_get_out(&format!( - r#" - globalThis.__out = "pending"; - fetch("{url}") - .then(r => {{ globalThis.__out = new URL(r.url).pathname + new URL(r.url).search; }}) - .catch(e => {{ globalThis.__out = "error:" + e; }}); - "# - )); - assert_eq!(out, "/t?%E2%98%83"); -} - -#[test] -fn a_redirect_location_preserves_already_escaped_and_literal_percents() { - // Escaped sequences keep their case and are not re-encoded, and a literal `%` that is not - // part of an escape survives — WPT's "Escaping produces double-percent" case. - let url = start_redirecting_server( - b"HTTP/1.1 302 Found\r\nLocation: /t?%\xe2\x98\x83%e2%98%83\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", - ); - let out = run_and_get_out(&format!( - r#" - globalThis.__out = "pending"; - fetch("{url}") - .then(r => {{ globalThis.__out = new URL(r.url).search; }}) - .catch(e => {{ globalThis.__out = "error:" + e; }}); - "# - )); - assert_eq!(out, "?%%E2%98%83%e2%98%83"); -} diff --git a/crates/builtins/web-globals/Cargo.toml b/crates/builtins/web-globals/Cargo.toml index da8ec28..979f8b7 100644 --- a/crates/builtins/web-globals/Cargo.toml +++ b/crates/builtins/web-globals/Cargo.toml @@ -6,6 +6,10 @@ license.workspace = true authors.workspace = true edition.workspace = true +[lib] +# No unit tests. This skips building the (SpiderMonkey-sized) lib test harness binary. +test = false + [dependencies] bitflags.workspace = true core-runtime.workspace = true diff --git a/crates/builtins/web-globals/src/base64.rs b/crates/builtins/web-globals/src/base64.rs index 50cbf22..ccb759b 100644 --- a/crates/builtins/web-globals/src/base64.rs +++ b/crates/builtins/web-globals/src/base64.rs @@ -8,23 +8,30 @@ //! //! [Forgiving Base64]: https://infra.spec.whatwg.org/#forgiving-base64 +use js::error::{ExnThrown, ThrowException}; +use js::gc::scope::Scope; + +use crate::dom_exception::DOMExceptionError; + #[core_runtime::jsglobals] pub mod base64_globals { + use super::BtoaError; use crate::dom_exception::DOMExceptionError; /// WHATWG `btoa(data)`: encode a byte string to base64. /// /// Coerces the argument to a string via `ToString`, then base64-encodes it. /// Throws `DOMException` with name `"InvalidCharacterError"` if the string - /// contains any code point above U+00FF. - pub fn btoa(data: String) -> Result { + /// contains any code point above U+00FF, and reports out of memory if the + /// encoded string can't be allocated. + pub fn btoa(data: String) -> Result { // : The `btoa(data)` method must throw an // "``InvalidCharacterError``" ``DOMException`` if _data_ contains any character whose code // point is greater than U+00FF. Otherwise, the user agent must convert _data_ to a byte // sequence whose _n_th byte is the eight-bit representation of the _n_th code point of // _data_, and then must apply `forgiving-base64 encode` to that byte sequence and return // the result. - super::btoa(&data).map_err(|msg| DOMExceptionError::new("InvalidCharacterError", msg)) + super::btoa(&data) } /// WHATWG `atob(data)`: forgiving-base64 decode. @@ -42,71 +49,75 @@ pub mod base64_globals { } } -// --------------------------------------------------------------------------- -// Base64 alphabet -// --------------------------------------------------------------------------- - -const BASE64_CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +/// Why [`btoa`] failed. +#[derive(Debug, PartialEq)] +pub enum BtoaError { + /// The input contains a code point above U+00FF. + InvalidCharacter, + /// Allocating the encoded text failed. + OutOfMemory, +} -// --------------------------------------------------------------------------- -// btoa implementation -// --------------------------------------------------------------------------- +impl ThrowException for BtoaError { + fn throw(self, scope: &Scope<'_>) -> ExnThrown { + match self { + BtoaError::InvalidCharacter => DOMExceptionError::new( + "InvalidCharacterError", + "The string to be encoded contains characters outside of the Latin1 range.", + ) + .throw(scope), + BtoaError::OutOfMemory => js::error::OutOfMemory.throw(scope), + } + } +} /// WHATWG `btoa`: encode a byte string to base64. /// /// The input string must contain only code points in the range U+0000..U+00FF /// (i.e., it must be a "byte string"). If any code point exceeds U+00FF, an /// `InvalidCharacterError` is thrown. -pub(crate) fn btoa(data: &str) -> Result { +pub(crate) fn btoa(data: &str) -> Result { + const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + // Validate that all characters are ≤ U+00FF (Latin-1 range). + let mut length = 0usize; for ch in data.chars() { if ch as u32 > 0x00FF { - return Err( - "The string to be encoded contains characters outside of the Latin1 range." - .to_string(), - ); - } - } - - // Collect the Latin-1 byte values. - let bytes: Vec = data.chars().map(|ch| ch as u8).collect(); - Ok(base64_encode(&bytes)) -} - -/// Standard base64 encoding with `=` padding. -fn base64_encode(input: &[u8]) -> String { - let mut output = String::with_capacity(input.len().div_ceil(3) * 4); - - for chunk in input.chunks(3) { - let b0 = chunk[0] as u32; - let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 }; - let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 }; - - let triple = (b0 << 16) | (b1 << 8) | b2; - - output.push(BASE64_CHARS[((triple >> 18) & 0x3F) as usize] as char); - output.push(BASE64_CHARS[((triple >> 12) & 0x3F) as usize] as char); - - if chunk.len() > 1 { - output.push(BASE64_CHARS[((triple >> 6) & 0x3F) as usize] as char); - } else { - output.push('='); - } - - if chunk.len() > 2 { - output.push(BASE64_CHARS[(triple & 0x3F) as usize] as char); - } else { - output.push('='); + return Err(BtoaError::InvalidCharacter); } + length += 1; } + // Encode the Latin-1 byte values with `=` padding. + let mut output = String::new(); + let encoded_length = length + .div_ceil(3) + .checked_mul(4) + .ok_or(BtoaError::OutOfMemory)?; output + .try_reserve_exact(encoded_length) + .map_err(|_| BtoaError::OutOfMemory)?; + let mut bytes = data.chars().map(|ch| ch as u8); + while let Some(b0) = bytes.next() { + let b1 = bytes.next(); + let b2 = bytes.next(); + let triple = + (u32::from(b0) << 16) | (u32::from(b1.unwrap_or(0)) << 8) | u32::from(b2.unwrap_or(0)); + + output.push(ALPHABET[((triple >> 18) & 0x3F) as usize] as char); + output.push(ALPHABET[((triple >> 12) & 0x3F) as usize] as char); + output.push(match b1 { + Some(_) => ALPHABET[((triple >> 6) & 0x3F) as usize] as char, + None => '=', + }); + output.push(match b2 { + Some(_) => ALPHABET[(triple & 0x3F) as usize] as char, + None => '=', + }); + } + Ok(output) } -// --------------------------------------------------------------------------- -// atob implementation -// --------------------------------------------------------------------------- - /// WHATWG `atob`: forgiving-base64 decode. /// /// Forgiving-base64 decodes `data` and returns the decoded byte string as a @@ -131,207 +142,3 @@ pub fn forgiving_base64_decode(data: &str) -> Result, String> { data_url::forgiving_base64::decode_to_vec(data.as_bytes()) .map_err(|_| "The string to be decoded is not correctly encoded.".to_string()) } - -// Nothing platform-specific in these tests, so skip them on wasm32. -#[cfg(all(test, not(target_arch = "wasm32")))] -mod tests { - use crate::base64::{atob, btoa}; - - // -- btoa tests -- - - #[test] - fn btoa_empty() { - assert_eq!(btoa("").unwrap(), ""); - } - - #[test] - fn btoa_basic() { - assert_eq!(btoa("ab").unwrap(), "YWI="); - assert_eq!(btoa("abc").unwrap(), "YWJj"); - assert_eq!(btoa("abcd").unwrap(), "YWJjZA=="); - assert_eq!(btoa("abcde").unwrap(), "YWJjZGU="); - } - - #[test] - fn btoa_latin1() { - assert_eq!(btoa("\u{FF}\u{FF}\u{C0}").unwrap(), "///A"); - } - - #[test] - fn btoa_null_bytes() { - assert_eq!(btoa("\0a").unwrap(), "AGE="); - assert_eq!(btoa("a\0b").unwrap(), "YQBi"); - } - - #[test] - fn btoa_coerced_types() { - // These test the JS-side coercion, but we can verify the Rust core: - assert_eq!(btoa("undefined").unwrap(), "dW5kZWZpbmVk"); - assert_eq!(btoa("null").unwrap(), "bnVsbA=="); - assert_eq!(btoa("7").unwrap(), "Nw=="); - assert_eq!(btoa("true").unwrap(), "dHJ1ZQ=="); - assert_eq!(btoa("false").unwrap(), "ZmFsc2U="); - assert_eq!(btoa("NaN").unwrap(), "TmFO"); - } - - #[test] - fn btoa_rejects_non_latin1() { - let result = btoa("עברית"); - assert!(result.is_err()); - assert!(result.unwrap_err().contains("outside of the Latin1 range")); - } - - #[test] - fn btoa_all_single_bytes() { - // U+0000..U+00FF should all succeed - for i in 0..=0xFF_u32 { - let ch = char::from_u32(i).unwrap(); - let s = ch.to_string(); - assert!(btoa(&s).is_ok(), "btoa should accept U+{i:04X}"); - } - } - - #[test] - fn btoa_first_non_latin1() { - // U+0100 should fail - let ch = char::from_u32(0x100).unwrap(); - assert!(btoa(&ch.to_string()).is_err()); - } - - // -- atob tests -- - - #[test] - fn atob_empty() { - assert_eq!(atob("").unwrap(), ""); - } - - #[test] - fn atob_basic() { - assert_eq!(atob("YWI=").unwrap(), "ab"); - assert_eq!(atob("YWJj").unwrap(), "abc"); - assert_eq!(atob("YWJjZA==").unwrap(), "abcd"); - assert_eq!(atob("YWJjZGU=").unwrap(), "abcde"); - } - - #[test] - fn atob_no_padding() { - // atob should work without padding too - assert_eq!(atob("YWI").unwrap(), "ab"); - assert_eq!(atob("YWJjZA").unwrap(), "abcd"); - } - - #[test] - fn atob_whitespace_stripping() { - assert_eq!(atob(" Y W J j ").unwrap(), "abc"); - assert_eq!(atob("\tYWJj\n").unwrap(), "abc"); - assert_eq!(atob("\r\nYWJj\r\n").unwrap(), "abc"); - } - - #[test] - fn atob_invalid_length() { - // After stripping whitespace and removing padding, length % 4 == 1 is invalid - let result = atob("A"); - assert!(result.is_err()); - assert!(result.unwrap_err().contains("not correctly encoded")); - } - - #[test] - fn atob_invalid_chars() { - let result = atob("===="); - assert!(result.is_err()); - } - - #[test] - fn atob_roundtrip() { - for s in &["", "a", "ab", "abc", "abcd", "hello world", "\0\x01\x7F"] { - let encoded = btoa(s).unwrap(); - let decoded = atob(&encoded).unwrap(); - assert_eq!(*s, decoded, "roundtrip failed for {:?}", s); - } - } - - // -- Integration tests (require JS engine) -- - - mod integration { - use core_runtime::test_util::{eval_with_setup, throws_with_setup}; - - fn eval(code: &str) -> String { - eval_with_setup( - || { - core_runtime::runtime::register_global_initializer(crate::add_to_global); - }, - code, - ) - } - - fn eval_throws(code: &str) -> bool { - throws_with_setup( - || { - core_runtime::runtime::register_global_initializer(crate::add_to_global); - }, - code, - ) - } - - #[test] - fn js_btoa_basic() { - assert_eq!(eval("btoa('abc')"), "YWJj"); - assert_eq!(eval("btoa('')"), ""); - assert_eq!(eval("btoa('abcde')"), "YWJjZGU="); - } - - #[test] - fn js_atob_basic() { - assert_eq!(eval("atob('YWJj')"), "abc"); - assert_eq!(eval("atob('')"), ""); - } - - #[test] - fn js_btoa_throws_on_non_latin1() { - assert!(eval_throws("btoa('\\u0100')")); - } - - #[test] - fn js_btoa_throws_dom_exception() { - // btoa must throw a DOMException with name "InvalidCharacterError" - assert_eq!( - eval( - "try { btoa('\\u0100'); 'no-throw' } catch(e) { \ - (e instanceof DOMException) + ',' + e.name + ',' + e.code }" - ), - "true,InvalidCharacterError,5" - ); - } - - #[test] - fn js_atob_throws_on_invalid() { - assert!(eval_throws("atob('====')")); - } - - #[test] - fn js_atob_throws_dom_exception() { - // atob must throw a DOMException with name "InvalidCharacterError" - assert_eq!( - eval( - "try { atob('===='); 'no-throw' } catch(e) { \ - (e instanceof DOMException) + ',' + e.name + ',' + e.code }" - ), - "true,InvalidCharacterError,5" - ); - } - - #[test] - fn js_roundtrip() { - assert_eq!(eval("atob(btoa('hello'))"), "hello"); - } - - #[test] - fn js_btoa_coercion() { - // JS coerces arguments to string: btoa(undefined) encodes "undefined" - assert_eq!(eval("btoa(undefined)"), "dW5kZWZpbmVk"); - assert_eq!(eval("btoa(null)"), "bnVsbA=="); - assert_eq!(eval("btoa(7)"), "Nw=="); - assert_eq!(eval("btoa(true)"), "dHJ1ZQ=="); - } - } -} diff --git a/crates/builtins/web-globals/src/console.rs b/crates/builtins/web-globals/src/console.rs deleted file mode 100644 index 1b57310..0000000 --- a/crates/builtins/web-globals/src/console.rs +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception - -//! Minimal `console` object implementation. -//! -//! Provides `console.log`, `console.warn`, `console.error`, `console.info`, -//! and `console.debug`. Each method converts its arguments to strings via -//! `ToString` and prints them space-separated to stdout (for `log`, `info`, -//! `debug`) or stderr (for `warn`, `error`). - -use std::io::Write; -use std::ptr::NonNull; - -use js::gc::scope::Scope; -use js::native::CallArgs; - -/// Format call arguments as a space-separated string. -/// -/// Each argument is converted to a string via the JS `ToString` operation. -fn format_args(scope: &Scope<'_>, args: &CallArgs) -> String { - let mut parts = Vec::with_capacity(args.argc_ as usize); - for i in 0..args.argc_ { - // SAFETY: `args.get(i)` dereferences vp which is valid for the duration - // of the native call. - let val = *args.get(i); - // Fast path for strings; slow path calls JS ToString. - let js_str = if val.is_string() { - NonNull::new(val.to_string()) - .map(|p| js::JSString::from_non_null(scope, p)) - } else { - js::JSString::from_value(scope, val).ok() - }; - match js_str { - Some(s) => match s.to_utf8(scope) { - Ok(utf8) => parts.push(utf8), - Err(_) => parts.push(String::from("[error converting to UTF-8]")), - }, - None => parts.push(String::from("[error converting to string]")), - } - } - parts.join(" ") -} - -/// Print to stdout (for `log`, `info`, `debug`). -fn print_stdout(scope: &Scope<'_>, args: &CallArgs) { - let output = format_args(scope, args); - let _ = writeln!(std::io::stdout(), "{output}"); -} - -/// Print to stderr (for `warn`, `error`). -fn print_stderr(scope: &Scope<'_>, args: &CallArgs) { - let output = format_args(scope, args); - let _ = writeln!(std::io::stderr(), "{output}"); -} - -#[core_runtime::jsnamespace(name = "console")] -pub mod console_ns { - use js::gc::scope::Scope; - use js::native::CallArgs; - - pub fn log(scope: &Scope<'_>, args: &CallArgs) { - super::print_stdout(scope, args); - } - - pub fn info(scope: &Scope<'_>, args: &CallArgs) { - super::print_stdout(scope, args); - } - - pub fn debug(scope: &Scope<'_>, args: &CallArgs) { - super::print_stdout(scope, args); - } - - pub fn warn(scope: &Scope<'_>, args: &CallArgs) { - super::print_stderr(scope, args); - } - - pub fn error(scope: &Scope<'_>, args: &CallArgs) { - super::print_stderr(scope, args); - } -} diff --git a/crates/builtins/web-globals/src/dom_exception.rs b/crates/builtins/web-globals/src/dom_exception.rs index b3a44d1..4c42cb6 100644 --- a/crates/builtins/web-globals/src/dom_exception.rs +++ b/crates/builtins/web-globals/src/dom_exception.rs @@ -6,9 +6,6 @@ //! including the constructor, prototype accessors, legacy error code constants, //! and the `Symbol.toStringTag` property. //! -//! Uses `#[webidl_interface]` and `#[webidl_methods]` proc macros for declarative -//! class registration. -//! //! [`DOMException`]: https://webidl.spec.whatwg.org/#idl-DOMException use core_runtime::{webidl_interface, webidl_methods}; @@ -18,10 +15,6 @@ use js::gc::scope::Scope; use js::native::ExceptionStackBehavior; use js::prelude::ToJSVal; -// --------------------------------------------------------------------------- -// DOMException names table -// --------------------------------------------------------------------------- - /// https://webidl.spec.whatwg.org/#dfn-error-names-table /// /// Maps DOMException error names to their legacy error code values. @@ -57,22 +50,13 @@ const ERROR_NAMES: &[(&str, u16)] = &[ /// "Return the legacy code indicated in the error names table for this /// DOMException object's name, or 0 if no such entry exists." fn legacy_code_for_name(name: &str) -> u16 { - for &(n, code) in ERROR_NAMES { - if n == name { - return code; - } - } - 0 + ERROR_NAMES + .iter() + .find(|(n, _)| *n == name) + .map_or(0, |&(_, code)| code) } -// --------------------------------------------------------------------------- -// DOMException class -// --------------------------------------------------------------------------- - -/// Internal data for a `DOMException` instance. -/// -/// Stores the exception name and message as Rust `String`s. The legacy -/// error code is computed from the name on demand via `legacy_code_for_name`. +/// #[webidl_interface(js_proto = "Error")] pub struct DOMException { name: String, @@ -81,9 +65,6 @@ pub struct DOMException { #[webidl_methods] impl DOMException { - // ----------------------------------------------------------------------- - // Legacy error code constants (WebIDL §2.8.1) - // ----------------------------------------------------------------------- pub const INDEX_SIZE_ERR: u16 = 1; pub const DOMSTRING_SIZE_ERR: u16 = 2; pub const HIERARCHY_REQUEST_ERR: u16 = 3; @@ -110,10 +91,6 @@ impl DOMException { pub const INVALID_NODE_TYPE_ERR: u16 = 24; pub const DATA_CLONE_ERR: u16 = 25; - // ----------------------------------------------------------------------- - // Constructor - // ----------------------------------------------------------------------- - /// https://webidl.spec.whatwg.org/#dom-domexception-domexception /// /// `constructor(optional DOMString message = "", optional DOMString name = "Error")` @@ -130,25 +107,21 @@ impl DOMException { /// Creates a new `DOMException`. /// /// In difference to the JS-facing constructor `new_js`, both arguments - /// to `new` are required, with no defaults.` + /// to `new` are required, with no defaults. pub fn new(message: String, name: String) -> Self { Self { name, message } } - // ----------------------------------------------------------------------- - // Getters - // ----------------------------------------------------------------------- - /// https://webidl.spec.whatwg.org/#dom-domexception-name #[getter] - fn name(&self) -> Ref<'_, str> { + pub fn name(&self) -> Ref<'_, str> { // Return `this`'s `name`. Ref::map(self.data(), |data| data.name.as_str()) } /// https://webidl.spec.whatwg.org/#dom-domexception-message #[getter] - fn message(&self) -> Ref<'_, str> { + pub fn message(&self) -> Ref<'_, str> { // Return `this`'s `message`. Ref::map(self.data(), |data| data.message.as_str()) } @@ -158,36 +131,26 @@ impl DOMException { /// Returns the legacy code indicated in the error names table for this /// DOMException object's name, or 0 if no such entry exists. #[getter] - fn code(&self) -> i32 { + pub fn code(&self) -> i32 { legacy_code_for_name(&self.data().name) as i32 } } -// --------------------------------------------------------------------------- -// Public API: throw_dom_exception -// --------------------------------------------------------------------------- - /// Throw a DOMException with the given name and message. /// /// This creates a new DOMException object and sets it as the pending exception. pub fn throw_dom_exception(scope: &Scope<'_>, name: &str, message: &str) -> ExnThrown { - // Get the DOMException constructor from the global. let exception = match DOMException::new(scope, message.into(), name.into()) { Ok(exc) => exc, Err(_) => return ExnThrown, }; - // Set the created DOMException as the pending exception. let exc_val = exception .to_jsval(scope) .expect("Value conversion doesn't fail"); js::exception::set_pending(scope, exc_val, ExceptionStackBehavior::Capture) } -// --------------------------------------------------------------------------- -// DOMExceptionError — typed error for use with ThrowException -// --------------------------------------------------------------------------- - /// A typed error representing a `DOMException` to be thrown. /// /// Use this as the `Err` type in `Result` in @@ -239,276 +202,3 @@ impl ThrowException for DOMExceptionError { throw_dom_exception(scope, self.name, &self.message) } } - -// Nothing platform-specific in these tests, so skip them on wasm32. -#[cfg(all(test, not(target_arch = "wasm32")))] -mod dom_exception_integration { - use core_runtime::{ - config::RuntimeConfig, - runtime::{self, Runtime}, - test_util::eval_with_setup, - }; - - fn eval(code: &str) -> String { - eval_with_setup( - || { - runtime::register_global_initializer(super::DOMException::add_to_global); - }, - code, - ) - } - - #[test] - fn constructor_no_args() { - assert_eq!(eval("let e = new DOMException(); e.name"), "Error"); - assert_eq!(eval("let e = new DOMException(); e.message"), ""); - assert_eq!(eval("let e = new DOMException(); e.code"), "0"); - } - - #[test] - fn rust_new() { - runtime::register_global_initializer(super::DOMException::add_to_global); - let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); - let scope = rt.default_global(); - let e = super::DOMException::new(&scope, String::new(), "Error".into()).unwrap(); - assert_eq!(&*e.name(), "Error"); - assert_eq!(&*e.message(), ""); - assert_eq!(e.code(), 0); - } - - #[test] - fn constructor_message_only() { - assert_eq!(eval("new DOMException('hello').message"), "hello"); - assert_eq!(eval("new DOMException('hello').name"), "Error"); - } - - #[test] - fn constructor_message_and_name() { - assert_eq!( - eval("new DOMException('msg', 'NotFoundError').name"), - "NotFoundError" - ); - assert_eq!(eval("new DOMException('msg', 'NotFoundError').code"), "8"); - } - - #[test] - fn instanceof() { - assert_eq!(eval("new DOMException() instanceof DOMException"), "true"); - assert_eq!(eval("new DOMException() instanceof Error"), "true"); - } - - #[test] - fn prototype_chain() { - assert_eq!( - eval("Object.getPrototypeOf(DOMException.prototype) === Error.prototype"), - "true" - ); - } - - #[test] - fn to_string_tag() { - assert_eq!( - eval("Object.prototype.toString.call(new DOMException())"), - "[object DOMException]" - ); - } - - #[test] - fn constants_on_constructor() { - assert_eq!(eval("DOMException.INDEX_SIZE_ERR"), "1"); - assert_eq!(eval("DOMException.NOT_FOUND_ERR"), "8"); - assert_eq!(eval("DOMException.NOT_SUPPORTED_ERR"), "9"); - assert_eq!(eval("DOMException.INVALID_STATE_ERR"), "11"); - assert_eq!(eval("DOMException.SYNTAX_ERR"), "12"); - } - - #[test] - fn constants_on_prototype() { - assert_eq!(eval("DOMException.prototype.INDEX_SIZE_ERR"), "1"); - assert_eq!(eval("DOMException.prototype.NOT_FOUND_ERR"), "8"); - } - - #[test] - fn code_not_affected_by_name_shadow() { - assert_eq!( - eval( - "let e = new DOMException('msg', 'InvalidCharacterError'); \ - Object.defineProperty(e, 'name', { value: 'WrongDocumentError' }); \ - e.code" - ), - "5" - ); - } - - #[test] - fn brand_check_rejects_non_instance() { - // Calling getter on a non-DOMException should throw TypeError. - assert_eq!( - eval( - "let getter = Object.getOwnPropertyDescriptor(\ - DOMException.prototype, 'name').get; \ - try { getter.call({}); 'no-throw' } catch(e) { e instanceof TypeError }" - ), - "true" - ); - } - - #[test] - fn requires_new() { - // DOMException() without `new` should throw TypeError. - assert_eq!( - eval("try { DOMException(); 'no-throw' } catch(e) { e instanceof TypeError }"), - "true" - ); - } - - #[test] - fn stack_property() { - assert_eq!(eval("typeof (new DOMException()).stack"), "string"); - } - - #[test] - fn inherits_tostring_from_error() { - assert_eq!( - eval("new DOMException('hello', 'TestError').toString()"), - "TestError: hello" - ); - } -} - -/// Tests for typed error dispatch via `ThrowException` trait. -/// -/// These tests use `#[jsglobals]` to register functions that return -/// `Result`, `Result`, `Result`, -/// `Result`, and `Result`. -// Nothing platform-specific in these tests, so skip them on wasm32. -#[cfg(all(test, not(target_arch = "wasm32")))] -mod throw_exception_integration { - use crate::dom_exception::DOMExceptionError; - use core_runtime::{jsglobals, test_util::eval_with_setup}; - use js::error::{RangeError, SyntaxError, TypeError}; - - #[jsglobals] - mod test_error_globals { - use super::*; - - /// Throws a TypeError. - pub fn throw_type_error() -> Result<(), TypeError> { - Err(TypeError("test type error".into())) - } - - /// Throws a RangeError. - pub fn throw_range_error() -> Result<(), RangeError> { - Err(RangeError("test range error".into())) - } - - /// Throws a SyntaxError. - pub fn throw_syntax_error() -> Result<(), SyntaxError> { - Err(SyntaxError("test syntax error".into())) - } - - /// Throws a TypeError from a String error. - pub fn throw_string_error() -> Result<(), String> { - Err("test string error".into()) - } - - /// Throws a DOMExceptionError. - pub fn throw_dom_exception_error() -> Result<(), DOMExceptionError> { - Err(DOMExceptionError::new( - "NotFoundError", - "test dom exception", - )) - } - - /// Succeeds. - pub fn no_error() -> Result { - Ok(42) - } - } - - fn eval(code: &str) -> String { - eval_with_setup( - || { - core_runtime::runtime::register_global_initializer( - crate::dom_exception::DOMException::add_to_global, - ); - core_runtime::runtime::register_global_initializer( - test_error_globals::add_to_global, - ); - }, - code, - ) - } - - /// Capturing the stack for a new `DOMException` is best-effort: a throwing - /// `Error.prototype.stack` accessor leaves the construction intact and no - /// exception pending. - #[test] - fn construction_survives_a_throwing_stack_accessor() { - assert_eq!( - eval( - "Object.defineProperty(Error.prototype, 'stack', { get() { throw 1; }, configurable: true }); - const e = new DOMException('m', 'NotFoundError'); - e.name + ',' + e.message" - ), - "NotFoundError,m" - ); - } - - #[test] - fn type_error_thrown() { - assert_eq!( - eval( - "try { throwTypeError(); 'no' } catch(e) { (e instanceof TypeError) + ',' + e.message }" - ), - "true,test type error" - ); - } - - #[test] - fn range_error_thrown() { - assert_eq!( - eval( - "try { throwRangeError(); 'no' } catch(e) { (e instanceof RangeError) + ',' + e.message }" - ), - "true,test range error" - ); - } - - #[test] - fn syntax_error_thrown() { - assert_eq!( - eval( - "try { throwSyntaxError(); 'no' } catch(e) { (e instanceof SyntaxError) + ',' + e.message }" - ), - "true,test syntax error" - ); - } - - #[test] - fn string_error_throws_type_error() { - // String errors throw TypeError. - assert_eq!( - eval( - "try { throwStringError(); 'no' } catch(e) { (e instanceof TypeError) + ',' + e.message }" - ), - "true,test string error" - ); - } - - #[test] - fn dom_exception_error_thrown() { - assert_eq!( - eval( - "try { throwDomExceptionError(); 'no' } catch(e) { \ - (e instanceof DOMException) + ',' + e.name + ',' + e.message }" - ), - "true,NotFoundError,test dom exception" - ); - } - - #[test] - fn success_returns_value() { - assert_eq!(eval("noError()"), "42"); - } -} diff --git a/crates/builtins/web-globals/src/encoding/text_decoder.rs b/crates/builtins/web-globals/src/encoding/text_decoder.rs index c9a738a..e7242a4 100644 --- a/crates/builtins/web-globals/src/encoding/text_decoder.rs +++ b/crates/builtins/web-globals/src/encoding/text_decoder.rs @@ -3,7 +3,7 @@ //! use core_runtime::{webidl_dictionary, webidl_interface, webidl_methods, webidl_union}; -use js::error::{ExnThrown, RangeError}; +use js::error::{ExnThrown, OutOfMemory, RangeError, ThrowException}; use js::gc::scope::Scope; use js::{ArrayBuffer, ArrayBufferView, JSString}; @@ -106,75 +106,74 @@ impl TextDecoder<'_> { let decoder = data.decoder.as_mut().expect("set by step 1"); // Step 3: If _input_ is given, then `push` a `copy of` _input_ to `this`'s `I/O queue`. - let bytes: &[u8] = &match input { - Some(ArrayBufferViewOrArrayBuffer::Buffer(input)) => input.copy_bytes(scope)?, - Some(ArrayBufferViewOrArrayBuffer::View(input)) => input.copy_bytes(scope)?, - None => Vec::new(), - }; - - // windows-1252 decodes every byte outside 0x80..=0x9F to the code point of the same - // value, and its decoder keeps no state between calls, so such input is its own - // Latin-1 output. - if encoding == encoding_rs::WINDOWS_1252 && !bytes.iter().any(|b| (0x80..=0x9F).contains(b)) - { - return JSString::from_latin1(scope, bytes); - } - // A fresh decoder that decodes its whole input at once turns ASCII into the same - // characters in every ASCII-compatible encoding. - if fresh - && last - && encoding.is_ascii_compatible() - && encoding_rs::Encoding::ascii_valid_up_to(bytes) == bytes.len() - { - return JSString::from_latin1(scope, bytes); - } - - // Step 4: Let _output_ be the `I/O queue` of scalar values « `end-of-queue` ». - // Step 5: While true: - // Step 5.1: Let _item_ be the result of `reading` from `this`'s `I/O queue`. - // Step 5.2: If _item_ is `end-of-queue` and `this`'s `do not flush` is true, then return - // the result of running `serialize I/O queue` with `this` and _output_. - // Step 5.3: Otherwise: - // Step 5.3.1: Let _result_ be the result of `processing an item` with _item_, `this`'s - // `decoder`, `this`'s `I/O queue`, _output_, and `this`'s `error mode`. - // Step 5.3.2: If _result_ is `finished`, then return the result of running `serialize - // I/O queue` with `this` and _output_. - // Step 5.3.3: Otherwise, if _result_ is `error`, `throw` a ``TypeError``. - // - // (One `encoding_rs` call processes every item. `last` is true when `end-of-queue` is - // processed rather than returned at step 5.2. The decoder's BOM removal performs - // `serialize I/O queue`'s BOM handling. We decode to UTF-16, which SpiderMonkey copies - // as is, or narrows to Latin-1 if every code unit fits.) - let max_len = decoder - .max_utf16_buffer_length(bytes.len()) - .unwrap_or(bytes.len().saturating_add(1)); - let mut output = vec![0u16; max_len]; - if fatal { - let (result, _read, written) = - decoder.decode_to_utf16_without_replacement(bytes, &mut output, last); - - match result { - encoding_rs::DecoderResult::InputEmpty => { - JSString::from_utf16(scope, &output[..written]) - } - encoding_rs::DecoderResult::Malformed(_, _) => Err(js::error::throw_type_error( - scope, - c"The encoded data was not valid.", - )), - encoding_rs::DecoderResult::OutputFull => { - unreachable!("output buffer was pre-allocated to max size"); - } + // (We decode directly from _input_'s bytes. Nothing retains them after this call.) + let mut decode = |bytes: &[u8]| { + // windows-1252 decodes every byte outside 0x80..=0x9F to the code point of the same + // value, and its decoder keeps no state between calls, so such input is its own + // Latin-1 output. + if encoding == encoding_rs::WINDOWS_1252 + && !bytes.iter().any(|b| (0x80..=0x9F).contains(b)) + { + return Decoded::Latin1(js::typedarray::try_copy(bytes)); + } + // A fresh decoder that decodes its whole input at once turns ASCII into the same + // characters in every ASCII-compatible encoding. + if fresh + && last + && encoding.is_ascii_compatible() + && encoding_rs::Encoding::ascii_valid_up_to(bytes) == bytes.len() + { + return Decoded::Latin1(js::typedarray::try_copy(bytes)); } - } else { - // Malformed sequences become U+FFFD. - let (_result, _read, written, _had_errors) = - decoder.decode_to_utf16(bytes, &mut output, last); - JSString::from_utf16(scope, &output[..written]) + // Step 4: Let _output_ be the `I/O queue` of scalar values « `end-of-queue` ». + // Step 5: While true: + // Step 5.1: Let _item_ be the result of `reading` from `this`'s `I/O queue`. + // Step 5.2: If _item_ is `end-of-queue` and `this`'s `do not flush` is true, then + // return the result of running `serialize I/O queue` with `this` and + // _output_. + // Step 5.3: Otherwise: + // Step 5.3.1: Let _result_ be the result of `processing an item` with _item_, + // `this`'s `decoder`, `this`'s `I/O queue`, _output_, and `this`'s `error + // mode`. + // Step 5.3.2: If _result_ is `finished`, then return the result of running + // `serialize I/O queue` with `this` and _output_. + // Step 5.3.3: Otherwise, if _result_ is `error`, `throw` a ``TypeError``. + // + // (One `encoding_rs` call processes every item. `last` is true when `end-of-queue` + // is processed rather than returned at step 5.2. The decoder's BOM removal performs + // `serialize I/O queue`'s BOM handling. We decode to UTF-16, which SpiderMonkey + // copies as is, or narrows to Latin-1 if every code unit fits.) + Decoded::Utf16(decode_to_utf16(decoder, bytes, last, fatal)) + }; + let decoded = match input { + Some(ArrayBufferViewOrArrayBuffer::Buffer(input)) => input.with_bytes(decode), + Some(ArrayBufferViewOrArrayBuffer::View(input)) => input.with_bytes(decode), + None => decode(&[]), + }; + drop(data); + match decoded { + Decoded::Latin1(Some(chars)) => JSString::from_latin1(scope, &chars), + Decoded::Latin1(None) => Err(OutOfMemory.throw(scope)), + Decoded::Utf16(output) => { + let output = output.map_err(|error| error.throw(scope))?; + JSString::from_utf16(scope, &output) + } } } } +/// The result of decoding the input of `TextDecoder.decode`. +enum Decoded { + /// A copy of the input, which is its own Latin-1 output. `None` means that allocating the + /// copy failed. + Latin1(Option>), + /// The UTF-16 output. + Utf16(Result, DecodeError>), +} + +/// A new decoder of `encoding`, which removes a byte order mark at the start of its input unless +/// `ignore_bom` is true. fn new_decoder(encoding: &'static encoding_rs::Encoding, ignore_bom: bool) -> encoding_rs::Decoder { if ignore_bom { encoding.new_decoder_without_bom_handling() @@ -183,6 +182,61 @@ fn new_decoder(encoding: &'static encoding_rs::Encoding, ignore_bom: bool) -> en } } +/// Why [`decode_to_utf16`] failed. +enum DecodeError { + /// The input is malformed, in the "fatal" error mode. + Malformed, + /// The output can't be allocated. + OutOfMemory, +} + +impl DecodeError { + /// Throw the `TypeError` of malformed input, or report the failed allocation. + fn throw(self, scope: &Scope<'_>) -> ExnThrown { + match self { + DecodeError::Malformed => { + js::error::throw_type_error(scope, c"The encoded data was not valid.") + } + DecodeError::OutOfMemory => OutOfMemory.throw(scope), + } + } +} + +/// Decode `bytes` with `decoder` to UTF-16. `last` is true for the end of the input, which the +/// decoder then flushes. With `fatal`, malformed input fails, and otherwise it becomes U+FFFD. +fn decode_to_utf16( + decoder: &mut encoding_rs::Decoder, + bytes: &[u8], + last: bool, + fatal: bool, +) -> Result, DecodeError> { + let max_len = decoder + .max_utf16_buffer_length(bytes.len()) + .ok_or(DecodeError::OutOfMemory)?; + let mut output = Vec::new(); + output + .try_reserve_exact(max_len) + .map_err(|_| DecodeError::OutOfMemory)?; + output.resize(max_len, 0); + let written = if fatal { + let (result, _read, written) = + decoder.decode_to_utf16_without_replacement(bytes, &mut output, last); + match result { + encoding_rs::DecoderResult::InputEmpty => written, + encoding_rs::DecoderResult::Malformed(_, _) => return Err(DecodeError::Malformed), + encoding_rs::DecoderResult::OutputFull => { + unreachable!("output buffer was pre-allocated to max size"); + } + } + } else { + let (_result, _read, written, _had_errors) = + decoder.decode_to_utf16(bytes, &mut output, last); + written + }; + output.truncate(written); + Ok(output) +} + #[webidl_union] pub enum ArrayBufferViewOrArrayBuffer<'a> { View(ArrayBufferView<'a>), diff --git a/crates/builtins/web-globals/src/events/algorithms.rs b/crates/builtins/web-globals/src/events/algorithms.rs index 0b84f2d..9d54424 100644 --- a/crates/builtins/web-globals/src/events/algorithms.rs +++ b/crates/builtins/web-globals/src/events/algorithms.rs @@ -24,8 +24,7 @@ use crate::signals::algorithms as signal_algorithms; /// /// /// -/// Note: while this *looks* like it should be a WebIDL dictionary, the coercion rules are -/// slightly different, which is why *flatten-more* is a thing in the spec and this implementation. +/// Note: this isn't a WebIDL dictionary because flatten-more converts its members differently. pub(crate) struct ListenerOptions<'s> { pub(crate) capture: bool, pub(crate) passive: Option, @@ -35,7 +34,6 @@ pub(crate) struct ListenerOptions<'s> { js::instance_local! { /// Monotonic source of event-listener ids. - /// Would theoretically overflow after 2^64 listeners, but that's not a realistic concern. static NEXT_LISTENER_ID: Cell = const { Cell::new(0) }; } @@ -49,9 +47,6 @@ fn next_listener_id() -> u64 { /// /// -/// (Note: while the spec presents this as a WebIDL spec concept, it's entirely -/// specific to event handling, so this file is indeed the right place for it.) -/// /// `options` is converted as the WebIDL union `(EventListenerOptions or boolean)`: /// `undefined`, `null` and objects become the dictionary, and any other value is /// converted with `ToBoolean`. @@ -225,7 +220,10 @@ pub(crate) fn add_an_event_listener( .event_listener_list .iter() .find(|l| { - l.event_type == event_type && l.capture == capture && l.callback.eq_stack(&callback) + !l.handler + && l.event_type == event_type + && l.capture == capture + && l.callback.eq_stack(&callback) }) .map(|l| l.id); let listener_id = match existing_id { @@ -239,6 +237,7 @@ pub(crate) fn add_an_event_listener( capture, passive, once, + handler: false, }); id } @@ -269,10 +268,67 @@ pub(crate) fn remove_an_event_listener( // Listeners have no `removed` flag: dispatch re-locates each listener in the live // list by id, so it skips any listener that is no longer in the list. target.event_listener_list.retain(|l| { - !(l.event_type == event_type && l.capture == capture && l.callback.eq_stack(callback)) + l.handler + || !(l.event_type == event_type + && l.capture == capture + && l.callback.eq_stack(callback)) }); } +/// : +/// the value of the event handler of `target` for `event_type`, which is the callback of its +/// listener, or `None` if the handler is null. +pub(crate) fn event_handler<'s>( + scope: &'s Scope<'_>, + target: &EventTarget<'_>, + event_type: &str, +) -> Option> { + let data = target.data(); + data.event_listener_list + .iter() + .find(|l| l.handler && l.event_type == event_type) + .map(|l| l.callback.get(scope)) +} + +/// The setter steps of an `EventHandler` IDL attribute of `target` for `event_type`, whose +/// callback type is `[LegacyTreatNonObjectAsNull]`: an object, callable or not, becomes the +/// handler, and any other value sets it to null. +/// +/// A handler set while the handler is null appends its listener to the event listener list +/// (). +/// Replacing a handler keeps the listener's place in the list, and setting it to null removes the +/// listener (). +/// A handler that isn't callable does nothing when an event is dispatched. +pub(crate) fn set_event_handler( + scope: &Scope<'_>, + target: &EventTarget<'_>, + event_type: &str, + value: HandleValue<'_>, +) { + let handler = Object::from_raw_value(scope, value.get()).ok(); + let mut data = target.data_mut(); + let list = &mut data.event_listener_list; + let position = list + .iter() + .position(|l| l.handler && l.event_type == event_type); + match (handler, position) { + (Some(handler), Some(position)) => list[position].callback.set(handler), + (Some(handler), None) => list.push(EventListener { + id: next_listener_id(), + event_type: event_type.to_string(), + callback: Heap::from(handler), + capture: false, + passive: None, + once: false, + handler: true, + }), + (None, Some(position)) => { + list.remove(position); + } + (None, None) => {} + } +} + /// Remove the event listener with the given id from an EventTarget. /// /// This is the concrete action of the abort algorithm registered by @@ -303,7 +359,8 @@ pub enum ScriptStackState { impl ScriptStackState { /// [Clean up after running script] step 3: if the JavaScript execution context stack is now /// empty, perform a microtask checkpoint. Run after every `call a user object's operation`. - /// The checkpoint also reports the promises rejected with no handler, as HTML's does. + /// The checkpoint also reports the promises rejected with no handler, as the HTML microtask + /// checkpoint does. /// /// [Clean up after running script]: https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script pub fn clean_up_after_running_script(self, scope: &Scope<'_>) { @@ -370,10 +427,9 @@ fn invoke_listeners( return; } - // Snapshot the listener list for a specific type to avoid issues with listeners added or - // removed during dispatch. We capture only listener `id`s and metadata; the - // live list is re-consulted by `id` each iteration, so callbacks are always - // fetched fresh and rooted (no stored GC pointers, no rooting gap). + // Snapshot the ids and options of the listeners for the event's type, so listeners added + // during dispatch don't run. Listeners removed during dispatch are skipped, and each callback + // is read from the live list when it runs. struct ListenerSnapshot { id: u64, passive: Option, @@ -408,7 +464,11 @@ fn invoke_listeners( }; // Root the callback before a `once` removal drops the listener's `Heap`. - let callback: Object<'_> = target.data().event_listener_list[pos].callback.get(scope); + let (callback, handler) = { + let data = target.data(); + let listener = &data.event_listener_list[pos]; + (listener.callback.get(scope), listener.handler) + }; // If listener's `once` is true, remove it. if snap.once { @@ -420,8 +480,9 @@ fn invoke_listeners( event.set_in_passive_listener(true); } - // Call the listener. - if call_event_listener(scope, target, callback, event).is_err() { + // Call the listener. An event handler that isn't callable does nothing. + let call = !handler || callback.is_callable(); + if call && call_event_listener(scope, target, callback, event).is_err() { // Per the spec, exceptions from event listeners are "reported" // but do not stop event dispatch. js::exception::report_and_clear(scope, "event listener"); diff --git a/crates/builtins/web-globals/src/events/event.rs b/crates/builtins/web-globals/src/events/event.rs index 7345a22..5fcd64c 100644 --- a/crates/builtins/web-globals/src/events/event.rs +++ b/crates/builtins/web-globals/src/events/event.rs @@ -284,7 +284,7 @@ impl Event { } /// Whether the event's dispatch flag is set (it is currently being - /// dispatched). Read by guards like `FetchEvent.respondWith` step 2. + /// dispatched). pub fn is_dispatching(&self) -> bool { self.data().flags.contains(EventFlags::DISPATCH) } diff --git a/crates/builtins/web-globals/src/events/event_target.rs b/crates/builtins/web-globals/src/events/event_target.rs index 6c5ffd4..b86c7a8 100644 --- a/crates/builtins/web-globals/src/events/event_target.rs +++ b/crates/builtins/web-globals/src/events/event_target.rs @@ -26,7 +26,7 @@ use crate::events::event::EventFlags; #[derive(core_runtime::Traceable)] pub(crate) struct EventListener { /// Identity used to re-locate this listener in the live list during - /// dispatch (the snapshot stores ids, not pointers — see `algorithms.rs`). + /// dispatch. #[no_trace] pub(crate) id: u64, #[no_trace] @@ -42,6 +42,11 @@ pub(crate) struct EventListener { pub(crate) passive: Option, #[no_trace] pub(crate) once: bool, + /// Whether this is the listener of an event handler, whose `callback` is the handler's value. + /// Such a listener is never `once`, `capture` or `passive`, and `addEventListener` and + /// `removeEventListener` neither match nor remove it. + #[no_trace] + pub(crate) handler: bool, } /// Convert an `EventListener?` argument: `None` for `null` and `undefined`, the @@ -181,37 +186,12 @@ impl EventTarget { .iter() .any(|listener| listener.event_type == event_type) } -} -// Nothing platform-specific in these tests, so skip them on wasm32. -#[cfg(all(test, not(target_arch = "wasm32")))] -mod tests { - use super::EventTarget; - use core_runtime::config::RuntimeConfig; - use core_runtime::runtime::{self, Runtime}; - use js::conversion::FromJSVal; - - /// A `once` listener leaves the event listener list when it runs. JS cannot observe the - /// list's length, so this reads it directly. - #[test] - fn dispatch_removes_once_listeners_from_the_list() { - runtime::register_global_initializer(crate::add_to_global); - let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); - let scope = rt.default_global(); - let value = js::compile::evaluate_with_filename( - &scope, - "const t = new EventTarget(); - t.addEventListener('x', () => {}); - for (let i = 0; i < 3; i++) { - t.addEventListener('x', () => {}, { once: true }); - t.dispatchEvent(new Event('x')); - } - t", - "test.js", - 1, - ) - .expect("script should evaluate"); - let target = EventTarget::from_jsval(&scope, value, ()).expect("an EventTarget"); - assert_eq!(target.data().event_listener_list.len(), 1); + /// The number of entries in the event listener list, including an event handler's. + /// + /// Used in some tests, but not part of the stable public API. + #[doc(hidden)] + pub fn listener_count(&self) -> usize { + self.data().event_listener_list.len() } } diff --git a/crates/builtins/web-globals/src/lib.rs b/crates/builtins/web-globals/src/lib.rs index 24d2e00..0033c2a 100644 --- a/crates/builtins/web-globals/src/lib.rs +++ b/crates/builtins/web-globals/src/lib.rs @@ -5,14 +5,12 @@ //! ## Globals //! //! - `btoa` and `atob`: [WHATWG Forgiving Base64] encoding/decoding -//! - `console`: Basic console logging (`log`, `info`, `debug`, `warn`, `error`) //! - `DOMException`: [WebIDL DOMException] interface //! //! [WHATWG Forgiving Base64]: https://infra.spec.whatwg.org/#forgiving-base64 //! [WebIDL DOMException]: https://webidl.spec.whatwg.org/#idl-DOMException pub mod base64; -// pub mod console; pub mod dom_exception; pub mod encoding; pub mod events; @@ -23,8 +21,6 @@ mod structured_clone; pub mod worker_location; pub fn add_to_global(scope: &js::prelude::Scope<'_>, global: js::Object<'_>) { - // Note: the Rust console builtin isn't currently used: we use the C++ version for now. - // console::console_ns::add_to_global(scope, global); dom_exception::DOMException::add_to_global(scope, global); encoding::add_to_global(scope, global); events::add_to_global(scope, global); diff --git a/crates/builtins/web-globals/src/navigator.rs b/crates/builtins/web-globals/src/navigator.rs index 07eb5a5..14472de 100644 --- a/crates/builtins/web-globals/src/navigator.rs +++ b/crates/builtins/web-globals/src/navigator.rs @@ -24,8 +24,6 @@ impl Navigator { pub fn add_to_global<'s>(scope: &'s Scope<'_>, global: Object<'s>) { Navigator::add_to_global(scope, global); - // SAFETY: `Navigator::add_to_global` registered the class on the - // current global immediately above. let navigator = js::class::create_instance_with::(scope, |_| NavigatorImpl {}) .expect("failed to allocate Navigator singleton"); diff --git a/crates/builtins/web-globals/src/performance.rs b/crates/builtins/web-globals/src/performance.rs index 1bd9e5e..702e317 100644 --- a/crates/builtins/web-globals/src/performance.rs +++ b/crates/builtins/web-globals/src/performance.rs @@ -21,11 +21,8 @@ struct TimeOriginSnapshot { epoch_ms: f64, } -/// Held behind a lock rather than in a `OnceLock` because it has to be replaceable: an origin -/// captured before a Wizer snapshot holds the wall-clock time of the machine that took the -/// snapshot, which bears no relation to when the resumed instance runs. [`reset_time_origin`] -/// re-establishes that half on resume. The monotonic half stays as captured, since -/// [`platform::clock`](platform::clock) already puts a resumed instance's readings past it. +/// Held behind a lock rather than in a `OnceLock` because [`reset_time_origin`] replaces its +/// wall-clock half. static TIME_ORIGIN: std::sync::RwLock> = std::sync::RwLock::new(None); /// The current wall-clock time as an ECMA-262 timestamp (ms since Unix epoch). @@ -49,8 +46,21 @@ fn elapsed_ms(origin: &TimeOriginSnapshot) -> f64 { platform::clock::monotonic_ns().saturating_sub(origin.monotonic_ns) as f64 / 1_000_000. } -fn init_time_origin() { - let _ = time_origin(); +/// The resolution, in nanoseconds, that [`relative_ms`] coarsens timestamps to: 100 +/// microseconds, the resolution of without +/// cross-origin isolated capability. +const COARSENED_RESOLUTION_NS: u64 = 100_000; + +/// The timestamp `ns` nanoseconds after the time origin, in milliseconds. While the web security +/// restrictions apply (see [`core_runtime::config::web_security_restrictions`]), it is coarsened +/// to [`COARSENED_RESOLUTION_NS`], rounding down. +fn relative_ms(ns: u64) -> f64 { + let ns = if core_runtime::config::web_security_restrictions() { + ns / COARSENED_RESOLUTION_NS * COARSENED_RESOLUTION_NS + } else { + ns + }; + ns as f64 / 1_000_000. } /// Re-anchor the time origin's wall clock to the instance running now. @@ -69,6 +79,7 @@ pub fn reset_time_origin() { } } +/// The time origin, captured on the first call. fn time_origin() -> TimeOriginSnapshot { if let Some(origin) = *TIME_ORIGIN.read().expect("time origin lock poisoned") { return origin; @@ -85,35 +96,29 @@ pub struct Performance { parent: EventTargetImpl, } +/// : the milliseconds since the +/// time origin, coarsened as [`relative_ms`] describes. pub fn now() -> f64 { - elapsed_ms(&time_origin()) + let origin = time_origin().monotonic_ns; + relative_ms(platform::clock::monotonic_ns().saturating_sub(origin)) } /// [`now`] from [`platform::clock::coarse_monotonic_ns`], for timestamps that don't need the /// precise clock, such as `Event.timeStamp`. pub fn coarse_now() -> f64 { - let origin = time_origin(); - platform::clock::coarse_monotonic_ns().saturating_sub(origin.monotonic_ns) as f64 / 1_000_000. + let origin = time_origin().monotonic_ns; + relative_ms(platform::clock::coarse_monotonic_ns().saturating_sub(origin)) } #[webidl_methods] impl Performance { /// - /// - /// Return the number of milliseconds since the time origin, as a `double`. - /// - /// The IDL defines the return as `DOMHighResTimeStamp` which is a - /// `double` in JS (64-bit float). #[method] pub fn now(&self) -> f64 { now() } /// - /// - /// Return the time origin as an ECMA-262 timestamp (ms since Unix epoch). - /// This is a fixed value: the wall-clock time (in ms since Unix epoch) - /// at which the runtime's time origin was established. #[getter] fn time_origin(&self) -> f64 { time_origin().epoch_ms @@ -135,9 +140,8 @@ impl Performance { /// Register the `Performance` class on `global` and install the singleton /// `performance` instance as a property on it. pub fn add_to_global<'s>(scope: &'s Scope<'_>, global: Object<'s>) { - // An embedder that snapshots the initialized runtime (Wizer) captures a time origin belonging - // to the snapshotting process; it calls `reset_time_origin` on resume to re-establish one. - init_time_origin(); + // The time origin is captured when the first global is set up, not at the first clock read. + time_origin(); Performance::add_to_global(scope, global); @@ -151,38 +155,3 @@ pub fn add_to_global<'s>(scope: &'s Scope<'_>, global: Object<'s>) { .set_property(scope, c"performance", performance) .expect("failed to define globalThis.performance"); } - -#[cfg(test)] -mod tests { - use core_runtime::{runtime, test_util::eval_with_setup}; - - fn eval(code: &str) -> String { - eval_with_setup( - || { - runtime::register_global_initializer(super::add_to_global); - }, - code, - ) - } - - #[test] - fn performance_to_string_tag() { - assert_eq!( - eval("Object.prototype.toString.call(performance)"), - "[object Performance]" - ); - } - - #[test] - fn to_json_returns_object() { - assert!(eval( - "(() => { - const json = performance.toJSON(); - return typeof json === 'object' && - typeof json.timeOrigin === 'number'; - })()" - ) - .parse::() - .unwrap()); - } -} diff --git a/crates/builtins/web-globals/src/signals/abort_signal.rs b/crates/builtins/web-globals/src/signals/abort_signal.rs index 5b6fb6c..8d54290 100644 --- a/crates/builtins/web-globals/src/signals/abort_signal.rs +++ b/crates/builtins/web-globals/src/signals/abort_signal.rs @@ -8,11 +8,11 @@ use core_runtime::{webidl_interface, webidl_methods}; use js::conversion::FromJSVal; use js::error::ExnThrown; -use js::gc::handle::{Heap, OptionHeapExt}; +use js::gc::handle::Heap; use js::gc::scope::Scope; use js::native::{ExceptionStackBehavior, Value}; use js::prelude::HandleValue; -use js::Callable; +use js::Object; use super::algorithms; use crate::dom_exception::DOMException; @@ -23,13 +23,8 @@ use crate::events::event_target::{EventTarget, EventTargetImpl}; /// /// /// -/// The only abort algorithm this runtime registers removes an event listener -/// added with the signal (per "add an event listener" step 6), so it is stored -/// concretely as the target plus the listener id rather than as a boxed -/// closure. A closure capturing a `Heap` could not be traced (it is opaque to -/// both the GC tracer and crown); keeping the GC pointer in an explicit field -/// makes it traceable. -/// +/// Algorithms are stored as data rather than as boxed closures, so that their GC pointers are +/// traced. #[js::must_root] pub(crate) enum AbortAlgorithm { /// Remove the event listener identified by `listener_id` from `held` (an @@ -76,11 +71,6 @@ pub struct AbortSignal { /// pub(crate) abort_algorithms: Vec, - - /// The `onabort` event handler attribute. - /// - /// - pub(crate) onabort_handler: Option>, } #[webidl_methods] @@ -93,7 +83,6 @@ impl AbortSignal { source_signals: Vec::new(), dependent_signals: Vec::new(), abort_algorithms: Vec::new(), - onabort_handler: None, } } @@ -130,42 +119,14 @@ impl AbortSignal { /// /// IDL event handler attribute: `attribute EventHandler onabort;` #[getter] - fn onabort<'r>(&self, scope: &'r Scope<'_>) -> Option> { - self.data().onabort_handler.get(scope) + fn onabort<'r>(&self, scope: &'r Scope<'_>) -> Option> { + event_algorithms::event_handler(scope, self, "abort") } /// #[setter] fn set_onabort(&self, scope: &Scope<'_>, val: HandleValue<'_>) { - // Remove the old handler listener, if any. Root it before taking the - // mutable borrow of `self`. - let old_callback = self.data().onabort_handler.get(scope); - if let Some(func) = old_callback { - event_algorithms::remove_an_event_listener( - &mut self.data_mut().parent, - "abort", - &func, - false, - ); - } - - // If the new value is a function, store it and add as listener. - match Callable::from_jsval(scope, val, ()) { - Ok(func) => { - // The onabort handler is registered without an abort signal. - event_algorithms::add_an_event_listener( - self, - "abort".to_string(), - *func, - false, - None, - false, - None, - ); - self.data_mut().onabort_handler.set(func); - } - Err(_) => self.data_mut().onabort_handler = None, - } + event_algorithms::set_event_handler(scope, self, "abort", val); } /// @@ -211,7 +172,7 @@ impl AbortSignal { // "``TimeoutError``" ``DOMException``. For the duration of this timeout, if // _signal_ has any event listeners registered for its ``abort`` event, there must // be a strong reference from _global_ to _signal_. - algorithms::schedule_abort_timeout(scope, &signal, milliseconds); + algorithms::schedule_abort_timeout(&signal, milliseconds); // Step 4: Return _signal_. Ok(signal) diff --git a/crates/builtins/web-globals/src/signals/algorithms.rs b/crates/builtins/web-globals/src/signals/algorithms.rs index 0168aa7..0a5be98 100644 --- a/crates/builtins/web-globals/src/signals/algorithms.rs +++ b/crates/builtins/web-globals/src/signals/algorithms.rs @@ -26,13 +26,9 @@ use crate::events::event_target::EventTarget; /// To create a dependent abort signal, given a list `signals`, returns a new /// `AbortSignal` that is aborted when any of the source signals is aborted. /// -/// The spec keeps the source/dependent links as *weak* references to avoid -/// keeping signals alive through each other. Here they are ordinary traced -/// `Heap` fields: because they are traced as object slots rather than held as -/// independent roots, SpiderMonkey's cycle-collecting GC still reclaims an -/// unreachable source↔dependent pair. The only difference is `WeakRef`/ -/// `FinalizationRegistry` observability, which is not exercised by the enabled -/// tests; revisit this if the GC-oriented abort-signal tests get enabled. +/// The spec's links between source and dependent signals are weak references. Here they are +/// traced `Heap` fields, which a tracing GC still collects when the linked signals are all +/// unreachable. A reachable source signal keeps every signal in its `dependent_signals` alive. pub fn create_dependent_abort_signal<'r>( scope: &'r Scope<'_>, signals: &[AbortSignal<'_>], @@ -330,11 +326,7 @@ impl Task for AbortTimeoutTask { /// /// Steps 2-3 of the `AbortSignal.timeout()` algorithm: run steps after a /// timeout to signal abort with a "TimeoutError" DOMException. -pub(crate) fn schedule_abort_timeout( - _scope: &Scope<'_>, - signal: &AbortSignal<'_>, - milliseconds: f64, -) { +pub(crate) fn schedule_abort_timeout(signal: &AbortSignal<'_>, milliseconds: f64) { let delay = Duration::from_millis(milliseconds.max(0.0) as u64); let deadline = Instant::now() + delay; let task = AbortTimeoutTask { diff --git a/crates/builtins/web-globals/src/worker_location.rs b/crates/builtins/web-globals/src/worker_location.rs index e1a0ce3..1f8d513 100644 --- a/crates/builtins/web-globals/src/worker_location.rs +++ b/crates/builtins/web-globals/src/worker_location.rs @@ -4,9 +4,8 @@ //! //! `WorkerLocation` exposes the runtime's current location URL via //! `globalThis.location`. The HTML spec sources this URL from the surrounding -//! `WorkerGlobalScope`. Starling has no `WorkerGlobalScope`, so a single -//! process-wide URL — typically read from an incoming HTTP request or configured -//! via `--init-location` — backs every `WorkerLocation` accessor instead. +//! `WorkerGlobalScope`. Starling has no `WorkerGlobalScope`, so the URL set with +//! [`set_init_location`] backs every `WorkerLocation` accessor instead. //! If no URL has been configured, every accessor throws a `TypeError`. use std::cell::RefCell; @@ -61,8 +60,7 @@ fn with_url(field: &str, f: impl FnOnce(&url::Url) -> T) -> Result(scope: &'s Scope<'_>, global: Object<'s>) { WorkerLocation::add_to_global(scope, global); - // SAFETY: `WorkerLocation::add_to_global` registered the class on the - // current global immediately above. let location = js::class::create_instance_with::(scope, |_| WorkerLocationImpl {}) .expect("failed to allocate WorkerLocation singleton"); @@ -172,158 +168,3 @@ pub fn add_to_global<'s>(scope: &'s Scope<'_>, global: Object<'s>) { .set_property(scope, c"location", location) .expect("failed to define globalThis.location"); } - -// Nothing platform-specific in these tests, so skip them on wasm32. -#[cfg(all(test, not(target_arch = "wasm32")))] -mod tests { - use core_runtime::{runtime, test_util::eval_with_setup}; - - fn eval(code: &str) -> String { - eval_with_setup( - || { - super::clear_init_location(); - runtime::register_global_initializer(super::add_to_global); - }, - code, - ) - } - - fn eval_with_url(location_url: &str, code: &str) -> String { - eval_with_setup( - || { - super::set_init_location(url::Url::parse(location_url).unwrap()); - runtime::register_global_initializer(super::add_to_global); - }, - code, - ) - } - - #[test] - fn location_exists_on_global() { - assert_eq!( - eval_with_url("https://example.com/", "typeof location"), - "object" - ); - } - - #[test] - fn constructor_throws() { - // WorkerLocation has no IDL constructor; `new WorkerLocation()` must throw. - assert_eq!( - eval( - "try { new WorkerLocation(); 'no-throw' } \ - catch(e) { (e instanceof TypeError) + ',' + e.message }" - ), - "true,Illegal constructor" - ); - } - - #[test] - fn to_string_tag_is_location() { - assert_eq!( - eval_with_url( - "https://example.com/", - "Object.prototype.toString.call(location)" - ), - "[object Location]" - ); - } - - #[test] - fn href_returns_serialized_url() { - assert_eq!( - eval_with_url("https://example.com/path?q=1#frag", "location.href"), - "https://example.com/path?q=1#frag" - ); - } - - #[test] - fn origin_returns_origin() { - assert_eq!( - eval_with_url("https://example.com:8443/path", "location.origin"), - "https://example.com:8443" - ); - } - - #[test] - fn protocol_returns_scheme_with_colon() { - assert_eq!( - eval_with_url("https://example.com/", "location.protocol"), - "https:" - ); - } - - #[test] - fn host_and_hostname() { - assert_eq!( - eval_with_url("https://example.com:8443/", "location.host"), - "example.com:8443" - ); - assert_eq!( - eval_with_url("https://example.com:8443/", "location.hostname"), - "example.com" - ); - } - - #[test] - fn port_includes_explicit_only() { - assert_eq!( - eval_with_url("https://example.com:8443/", "location.port"), - "8443" - ); - // Default ports are omitted by the URL parser. - assert_eq!(eval_with_url("https://example.com/", "location.port"), ""); - } - - #[test] - fn pathname_search_hash() { - assert_eq!( - eval_with_url("https://example.com/foo/bar?x=1#frag", "location.pathname"), - "/foo/bar" - ); - assert_eq!( - eval_with_url("https://example.com/foo/bar?x=1#frag", "location.search"), - "?x=1" - ); - assert_eq!( - eval_with_url("https://example.com/foo/bar?x=1#frag", "location.hash"), - "#frag" - ); - } - - #[test] - fn empty_search_and_hash_return_empty_string() { - assert_eq!(eval_with_url("https://example.com/", "location.search"), ""); - assert_eq!(eval_with_url("https://example.com/", "location.hash"), ""); - } - - #[test] - fn to_string_returns_href() { - assert_eq!( - eval_with_url( - "https://example.com/path?q=1", - "location.toString() === location.href" - ), - "true" - ); - } - - #[test] - fn unset_location_throws_typeerror() { - // With no init-location configured, every accessor throws. - assert_eq!( - eval( - "try { location.href; 'no-throw' } \ - catch(e) { (e instanceof TypeError) + ',' + e.message.includes('--init-location') }" - ), - "true,true" - ); - assert_eq!( - eval( - "try { location.toString(); 'no-throw' } \ - catch(e) { e instanceof TypeError }" - ), - "true" - ); - } -} diff --git a/crates/builtins/web-globals/tests/dom_events.rs b/crates/builtins/web-globals/tests/dom_events.rs index 8617d7a..3ec1ea0 100644 --- a/crates/builtins/web-globals/tests/dom_events.rs +++ b/crates/builtins/web-globals/tests/dom_events.rs @@ -4,14 +4,18 @@ //! //! These tests deliberately cover only behavior that the enabled, passing WPT //! suite (see `tests/wpt-harness/tests.json`) doesn't exercise. What remains is -//! coverage for behaviors whose only WPT home is a `.html` test (which this -//! headless harness cannot run), an idlharness test that is not enabled, or — -//! in the case of GC interaction — something WPT cannot express at all. +//! coverage for behaviors whose only WPT home is a `.html` test, which this +//! headless harness cannot run, and for GC interaction, which WPT cannot +//! express. // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] +use core_runtime::config::RuntimeConfig; +use core_runtime::runtime::{self, Runtime}; use core_runtime::test_util::eval_with_setup; +use js::conversion::FromJSVal; +use web_globals::events::EventTarget; fn setup() { core_runtime::runtime::register_global_initializer(|scope, global| { @@ -31,22 +35,6 @@ fn event_composed_default() { assert_eq!(eval("new Event('x').composed"), "false"); } -#[test] -fn event_phase_constants() { - // Only Event-constants.html asserts the numeric values; the enabled - // any.js tests reference Event.NONE without pinning it to 0. - assert_eq!(eval("Event.NONE"), "0"); - assert_eq!(eval("Event.CAPTURING_PHASE"), "1"); - assert_eq!(eval("Event.AT_TARGET"), "2"); - assert_eq!(eval("Event.BUBBLING_PHASE"), "3"); -} - -#[test] -fn event_instance_phase_constants() { - assert_eq!(eval("new Event('x').NONE"), "0"); - assert_eq!(eval("new Event('x').AT_TARGET"), "2"); -} - #[test] fn event_prevent_default_non_cancelable() { // preventDefault on a non-cancelable event has no effect. The passive WPT @@ -58,59 +46,33 @@ fn event_prevent_default_non_cancelable() { } #[test] -fn event_stop_propagation() { +fn event_stop_propagation_and_cancel_bubble() { // stopPropagation()/cancelBubble live only in Event-cancelBubble.html. assert_eq!( eval("var e = new Event('x'); e.stopPropagation(); e.cancelBubble"), "true" ); -} - -#[test] -fn event_cancel_bubble_setter() { assert_eq!( eval("var e = new Event('x'); e.cancelBubble = true; e.cancelBubble"), "true" ); } -#[test] -fn event_to_string_tag() { - // No idlharness for dom/events is enabled, so the brand check lives here. - assert_eq!( - eval("Object.prototype.toString.call(new Event('x'))"), - "[object Event]" - ); -} - // ── Event.initEvent (only Event-initEvent.html otherwise) ── #[test] fn event_init_event() { assert_eq!( - eval("var e = new Event(''); e.initEvent('click', true, true); e.type"), - "click" - ); -} - -#[test] -fn event_init_event_bubbles() { - assert_eq!( - eval("var e = new Event(''); e.initEvent('click', true, false); e.bubbles"), - "true" + eval( + "var e = new Event(''); e.initEvent('click', true, false); \ + e.type + ',' + e.bubbles + ',' + e.cancelable" + ), + "click,true,false" ); } // ── EventTarget ── -#[test] -fn event_target_to_string_tag() { - assert_eq!( - eval("Object.prototype.toString.call(new EventTarget())"), - "[object EventTarget]" - ); -} - #[test] fn dispatch_event_at_target_phase() { // eventPhase during dispatch is only asserted by Event-dispatch-*.html. @@ -224,49 +186,56 @@ fn dispatch_redispatch_used_event() { ); } +/// A `once` listener leaves the event listener list when it runs. JS cannot observe the +/// list's length, so this reads it directly. +#[test] +fn dispatch_removes_once_listeners_from_the_list() { + runtime::register_global_initializer(web_globals::add_to_global); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + let value = js::compile::evaluate_with_filename( + &scope, + "const t = new EventTarget(); + t.addEventListener('x', () => {}); + for (let i = 0; i < 3; i++) { + t.addEventListener('x', () => {}, { once: true }); + t.dispatchEvent(new Event('x')); + } + t", + "test.js", + 1, + ) + .expect("script should evaluate"); + let target = EventTarget::from_jsval(&scope, value, ()).expect("an EventTarget"); + assert_eq!(target.listener_count(), 1); +} + // ── CustomEvent ── // // The only enabled WPT touching CustomEvent is one assertion in // Event-constructors.any.js (`detail: 54`) plus a subclass dispatch in // EventTarget-constructible.any.js. CustomEvent.html is not runnable here, so -// detail typing, defaults, the brand, and initCustomEvent are covered below. - -#[test] -fn custom_event_construct() { - assert_eq!(eval("new CustomEvent('x') instanceof CustomEvent"), "true"); -} +// detail typing, defaults, and initCustomEvent are covered below. #[test] fn custom_event_extends_event() { - assert_eq!(eval("new CustomEvent('x') instanceof Event"), "true"); -} - -#[test] -fn custom_event_detail_default_is_null() { - assert_eq!(eval("new CustomEvent('x').detail"), "null"); -} - -#[test] -fn custom_event_detail_string() { - assert_eq!( - eval("new CustomEvent('x', { detail: 'hello' }).detail"), - "hello" - ); -} - -#[test] -fn custom_event_detail_object() { assert_eq!( - eval("JSON.stringify(new CustomEvent('x', { detail: { a: 1 } }).detail)"), - r#"{"a":1}"# + eval( + "var e = new CustomEvent('x'); (e instanceof CustomEvent) + ',' + (e instanceof Event)" + ), + "true,true" ); } #[test] -fn custom_event_to_string_tag() { +fn custom_event_detail() { assert_eq!( - eval("Object.prototype.toString.call(new CustomEvent('x'))"), - "[object CustomEvent]" + eval( + "JSON.stringify([new CustomEvent('x').detail, \ + new CustomEvent('x', { detail: 'hello' }).detail, \ + new CustomEvent('x', { detail: { a: 1 } }).detail])" + ), + r#"[null,"hello",{"a":1}]"# ); } @@ -284,18 +253,8 @@ fn custom_event_init_custom_event() { ); } -/// Listener identity must survive a compacting GC. -/// -/// Listeners are compared by the live, barrier-updated pointer of their stored -/// `Heap` rather than a cached raw pointer. A full GC promotes the -/// callback out of the nursery and may compact it, changing its address — so a -/// cached pointer captured at registration time would go stale. This drives a -/// real collection *between* listener operations and checks that both -/// deduplication and removal still match the relocated callback. (With a cached -/// raw pointer this would observe a count of 4: dedup fails, both listeners -/// fire twice, and removal matches neither.) -/// -/// WPT cannot express this: it has no control over garbage collection. +/// Listener identity survives a compacting GC, which moves the callback between the listener +/// operations: deduplication and removal still match the relocated callback. #[test] fn listener_identity_survives_compacting_gc() { use core_runtime::config::RuntimeConfig; @@ -329,14 +288,14 @@ fn listener_identity_survives_compacting_gc() { // After relocation: a duplicate add must still be recognised (dedup), a // dispatch must fire the single listener once, and removal must match the - // relocated callback so the final dispatch fires nothing. + // relocated callback so the final dispatch doesn't call a listener. let val = evaluate_with_filename( &scope, r#" globalThis.t.addEventListener('e', globalThis.handler); // dedup globalThis.t.dispatchEvent(new Event('e')); // fires once globalThis.t.removeEventListener('e', globalThis.handler); - globalThis.t.dispatchEvent(new Event('e')); // fires nothing + globalThis.t.dispatchEvent(new Event('e')); // calls no listener String(globalThis.count) "#, "test.js", @@ -488,7 +447,7 @@ fn add_event_listener_accepts_handle_event_objects() { ); } -/// A non-object callback is a TypeError, while `null` adds nothing. +/// A non-object callback is a TypeError, while `null` doesn't add a listener. #[test] fn add_event_listener_rejects_non_object_callbacks() { assert_eq!( diff --git a/crates/builtins/web-globals/tests/dom_signals.rs b/crates/builtins/web-globals/tests/dom_signals.rs index 371fae1..a8f6792 100644 --- a/crates/builtins/web-globals/tests/dom_signals.rs +++ b/crates/builtins/web-globals/tests/dom_signals.rs @@ -9,7 +9,6 @@ //! `tests/wpt-harness/tests.json`), so that ground is not retread here. What //! remains is coverage WPT does not give us in this harness: //! -//! - `Symbol.toStringTag` brands (no idlharness is enabled). //! - `onabort` getter identity and the set-to-null path, which `event.any.js` //! does not assert. @@ -51,59 +50,21 @@ fn eval(code: &str) -> String { eval_with_setup(setup, code) } -// ── Brands / interface shape ── - -#[test] -fn abort_controller_to_string_tag() { - assert_eq!( - eval("Object.prototype.toString.call(new AbortController())"), - "[object AbortController]" - ); -} - -#[test] -fn abort_controller_has_signal() { - assert_eq!( - eval("new AbortController().signal instanceof AbortSignal"), - "true" - ); -} - -#[test] -fn signal_inherits_event_target() { - assert_eq!( - eval("new AbortController().signal instanceof EventTarget"), - "true" - ); -} - -#[test] -fn signal_to_string_tag() { - assert_eq!( - eval("Object.prototype.toString.call(new AbortController().signal)"), - "[object AbortSignal]" - ); -} - // ── onabort event handler ── -#[test] -fn onabort_initially_null() { - assert_eq!(eval("new AbortController().signal.onabort"), "null"); -} - #[test] fn onabort_getter_returns_handler() { assert_eq!( eval( r#" var ac = new AbortController(); + var initial = ac.signal.onabort; var fn = function() {}; ac.signal.onabort = fn; - ac.signal.onabort === fn + (initial === null) + "," + (ac.signal.onabort === fn) "# ), - "true" + "true,true" ); } @@ -124,6 +85,58 @@ fn onabort_set_null_removes_handler() { ); } +/// `onabort` keeps any object, callable or not, and is null for any other value. A handler that +/// isn't callable does nothing when the event is dispatched. +#[test] +fn onabort_keeps_objects_and_nulls_other_values() { + assert_eq!( + eval( + r#" + var ac = new AbortController(); + var object = {}; + var results = []; + ac.signal.onabort = object; + results.push(ac.signal.onabort === object); + ac.signal.onabort = 5; + results.push(ac.signal.onabort); + ac.signal.onabort = object; + var called = false; + ac.signal.addEventListener("abort", function() { called = true; }); + ac.abort(); + results.push(called); + results.join() + "# + ), + "true,,true" + ); +} + +/// Replacing `onabort` keeps its listener's place among the event listeners, and the handler's +/// listener is separate from a listener of the same function added with `addEventListener`. +#[test] +fn onabort_listener_keeps_its_place_and_identity() { + assert_eq!( + eval( + r#" + var ac = new AbortController(); + var order = []; + var shared = function() { order.push("shared"); }; + ac.signal.onabort = function() { order.push("a"); }; + ac.signal.addEventListener("abort", function() { order.push("b"); }); + ac.signal.onabort = function() { order.push("c"); }; + ac.signal.addEventListener("abort", shared); + ac.signal.removeEventListener("abort", shared); + ac.signal.addEventListener("abort", shared); + ac.signal.onabort = shared; + ac.signal.removeEventListener("abort", shared); + ac.abort(); + order.join() + "# + ), + "shared,b" + ); +} + // ── Abort algorithms #[test] @@ -170,11 +183,8 @@ fn throwing_abort_algorithm_still_aborts_dependents() { #[test] fn any_with_huge_array_like_length_throws_not_crashes() { - // Regression: a hostile `length` must not be used to pre-size an allocation. - // `AbortSignal.any({length: 1e20})` previously fed `u32::MAX` into - // `Vec::with_capacity`, attempting a ~34 GB allocation that aborted the - // process. Per spec it must throw a `TypeError` (the first element is not an - // `AbortSignal`), like every other browser/runtime. + // A hostile `length` isn't used to pre-size an allocation. The first element isn't an + // `AbortSignal`, so this throws a `TypeError`. assert_eq!( eval( "try { AbortSignal.any({ length: 1e20 }); 'no-throw' } \ diff --git a/crates/builtins/web-globals/tests/encoding.rs b/crates/builtins/web-globals/tests/encoding.rs index 74ba59d..d99ce8d 100644 --- a/crates/builtins/web-globals/tests/encoding.rs +++ b/crates/builtins/web-globals/tests/encoding.rs @@ -1,11 +1,11 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception -//! Integration tests for the Encoding API (TextEncoder, TextDecoder). +//! A `TextDecoder` check that the enabled WPT `encoding/` tests don't make. // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] -use core_runtime::test_util::{eval_with_setup, throws_with_setup}; +use core_runtime::test_util::eval_with_setup; fn setup() { core_runtime::runtime::register_global_initializer(|scope, global| { @@ -17,347 +17,6 @@ fn eval(code: &str) -> String { eval_with_setup(setup, code) } -fn throws(code: &str) -> bool { - throws_with_setup(setup, code) -} - -// ── TextEncoder existence ── - -#[test] -fn text_encoder_exists() { - assert_eq!(eval("typeof TextEncoder"), "function"); -} - -#[test] -fn text_encoder_construct() { - assert_eq!(eval("new TextEncoder() instanceof TextEncoder"), "true"); -} - -// ── TextEncoder.encoding ── - -#[test] -fn text_encoder_encoding() { - assert_eq!(eval("new TextEncoder().encoding"), "utf-8"); -} - -// ── TextEncoder.encode() ── - -#[test] -fn text_encoder_encode_empty() { - assert_eq!(eval("new TextEncoder().encode('').length"), "0"); -} - -#[test] -fn text_encoder_encode_ascii() { - assert_eq!( - eval("Array.from(new TextEncoder().encode('abc')).join(',')"), - "97,98,99" - ); -} - -#[test] -fn text_encoder_encode_returns_uint8array() { - assert_eq!( - eval("new TextEncoder().encode('a') instanceof Uint8Array"), - "true" - ); -} - -#[test] -fn text_encoder_encode_no_args() { - // encode() with no arguments encodes empty string - assert_eq!(eval("new TextEncoder().encode().length"), "0"); -} - -#[test] -fn text_encoder_encode_multibyte() { - // "€" is U+20AC, encoded as [0xE2, 0x82, 0xAC] in UTF-8 - assert_eq!( - eval(r#"Array.from(new TextEncoder().encode('€')).join(',')"#), - "226,130,172" - ); -} - -#[test] -fn text_encoder_encode_emoji() { - // "😀" is U+1F600, encoded as [0xF0, 0x9F, 0x98, 0x80] - assert_eq!( - eval(r#"Array.from(new TextEncoder().encode('😀')).join(',')"#), - "240,159,152,128" - ); -} - -// ── TextEncoder.encodeInto() ── - -#[test] -fn text_encoder_encode_into_basic() { - assert_eq!( - eval( - r#" - const enc = new TextEncoder(); - const buf = new Uint8Array(10); - const result = enc.encodeInto('abc', buf); - result.read + ',' + result.written - "# - ), - "3,3" - ); -} - -#[test] -fn text_encoder_encode_into_writes_bytes() { - assert_eq!( - eval( - r#" - const enc = new TextEncoder(); - const buf = new Uint8Array(10); - enc.encodeInto('abc', buf); - buf[0] + ',' + buf[1] + ',' + buf[2] - "# - ), - "97,98,99" - ); -} - -#[test] -fn text_encoder_encode_into_truncation() { - // Buffer too small to fit all characters - assert_eq!( - eval( - r#" - const enc = new TextEncoder(); - const buf = new Uint8Array(2); - const result = enc.encodeInto('abcdef', buf); - result.read + ',' + result.written - "# - ), - "2,2" - ); -} - -#[test] -fn text_encoder_encode_into_multibyte_truncation() { - // "€" needs 3 bytes; buffer of 2 can't fit it - assert_eq!( - eval( - r#" - const enc = new TextEncoder(); - const buf = new Uint8Array(2); - const result = enc.encodeInto('€', buf); - result.read + ',' + result.written - "# - ), - "0,0" - ); -} - -#[test] -fn text_encoder_encode_into_surrogate_pair_count() { - // "😀" is U+1F600, takes 4 UTF-8 bytes and 2 UTF-16 code units - assert_eq!( - eval( - r#" - const enc = new TextEncoder(); - const buf = new Uint8Array(10); - const result = enc.encodeInto('😀', buf); - result.read + ',' + result.written - "# - ), - "2,4" - ); -} - -// ── TextDecoder existence ── - -#[test] -fn text_decoder_exists() { - assert_eq!(eval("typeof TextDecoder"), "function"); -} - -#[test] -fn text_decoder_construct() { - assert_eq!(eval("new TextDecoder() instanceof TextDecoder"), "true"); -} - -// ── TextDecoder.encoding ── - -#[test] -fn text_decoder_encoding_default() { - assert_eq!(eval("new TextDecoder().encoding"), "utf-8"); -} - -#[test] -fn text_decoder_encoding_explicit_utf8() { - assert_eq!(eval("new TextDecoder('utf-8').encoding"), "utf-8"); -} - -#[test] -fn text_decoder_encoding_label_normalization() { - // "UTF-8" should be normalized to "utf-8" - assert_eq!(eval("new TextDecoder('UTF-8').encoding"), "utf-8"); -} - -#[test] -fn text_decoder_encoding_windows_1252() { - assert_eq!( - eval("new TextDecoder('windows-1252').encoding"), - "windows-1252" - ); -} - -// ── TextDecoder.fatal ── - -#[test] -fn text_decoder_fatal_default() { - assert_eq!(eval("new TextDecoder().fatal"), "false"); -} - -#[test] -fn text_decoder_fatal_true() { - assert_eq!( - eval("new TextDecoder('utf-8', { fatal: true }).fatal"), - "true" - ); -} - -// ── TextDecoder.ignoreBOM ── - -#[test] -fn text_decoder_ignore_bom_default() { - assert_eq!(eval("new TextDecoder().ignoreBOM"), "false"); -} - -#[test] -fn text_decoder_ignore_bom_true() { - assert_eq!( - eval("new TextDecoder('utf-8', { ignoreBOM: true }).ignoreBOM"), - "true" - ); -} - -// ── TextDecoder.decode() ── - -#[test] -fn text_decoder_decode_utf8() { - assert_eq!( - eval(r#"new TextDecoder().decode(new Uint8Array([97, 98, 99]))"#), - "abc" - ); -} - -#[test] -fn text_decoder_decode_no_args() { - // decode() with no arguments returns empty string - assert_eq!(eval("new TextDecoder().decode()"), ""); -} - -#[test] -fn text_decoder_decode_empty_array() { - assert_eq!(eval("new TextDecoder().decode(new Uint8Array([]))"), ""); -} - -#[test] -fn text_decoder_decode_multibyte_utf8() { - // "€" = [0xE2, 0x82, 0xAC] - assert_eq!( - eval("new TextDecoder().decode(new Uint8Array([0xE2, 0x82, 0xAC]))"), - "€" - ); -} - -#[test] -fn text_decoder_decode_emoji() { - // "😀" = [0xF0, 0x9F, 0x98, 0x80] - assert_eq!( - eval("new TextDecoder().decode(new Uint8Array([0xF0, 0x9F, 0x98, 0x80]))"), - "😀" - ); -} - -// ── TextDecoder error handling ── - -#[test] -fn text_decoder_invalid_label_throws() { - assert!(throws("new TextDecoder('invalid-encoding')")); -} - -#[test] -fn text_decoder_replacement_encoding_throws() { - // "replacement" is not a valid label for TextDecoder (excluded by spec) - assert!(throws("new TextDecoder('replacement')")); -} - -#[test] -fn text_decoder_fatal_invalid_bytes() { - // Invalid UTF-8 sequence with fatal: true should throw - assert!(throws( - "new TextDecoder('utf-8', { fatal: true }).decode(new Uint8Array([0xFF, 0xFE]))" - )); -} - -#[test] -fn text_decoder_replacement_mode_invalid_bytes() { - // Invalid UTF-8 in replacement mode should produce U+FFFD - assert_eq!( - eval( - r#" - const decoder = new TextDecoder('utf-8'); - const result = decoder.decode(new Uint8Array([0xFF])); - result === '\uFFFD' - "# - ), - "true" - ); -} - -// ── TextDecoder streaming ── - -#[test] -fn text_decoder_streaming() { - // Multi-byte character split across two decode() calls - // "€" = [0xE2, 0x82, 0xAC] - assert_eq!( - eval( - r#" - const decoder = new TextDecoder(); - let result = decoder.decode(new Uint8Array([0xE2, 0x82]), { stream: true }); - result += decoder.decode(new Uint8Array([0xAC])); - result - "# - ), - "€" - ); -} - -// ── TextDecoder with non-UTF-8 encodings ── - -#[test] -fn text_decoder_windows_1252() { - // 0xE9 in windows-1252 is "é" - assert_eq!( - eval(r#"new TextDecoder('windows-1252').decode(new Uint8Array([0xE9]))"#), - "é" - ); -} - -// ── WebIDL interface compliance ── - -#[test] -fn text_encoder_to_string_tag() { - assert_eq!( - eval("Object.prototype.toString.call(new TextEncoder())"), - "[object TextEncoder]" - ); -} - -#[test] -fn text_decoder_to_string_tag() { - assert_eq!( - eval("Object.prototype.toString.call(new TextDecoder())"), - "[object TextDecoder]" - ); -} - /// windows-1252 input without bytes in 0x80..=0x9F decodes to the same code points, and input /// with them maps those bytes through the encoding's table, including when a streaming decode /// alternates between the two. diff --git a/crates/builtins/web-globals/tests/globals.rs b/crates/builtins/web-globals/tests/globals.rs new file mode 100644 index 0000000..aeebeef --- /dev/null +++ b/crates/builtins/web-globals/tests/globals.rs @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception + +//! Integration tests for the globals that have no test file of their own: `location`, +//! `DOMException` and the errors builtins throw, and `performance`. + +// This file contains nothing platform-specific, so skip it on wasm32. +#![cfg(not(target_arch = "wasm32"))] + +mod worker_location { + use core_runtime::{runtime, test_util::eval_with_setup}; + use web_globals::worker_location::{add_to_global, clear_init_location, set_init_location}; + + fn eval(code: &str) -> String { + eval_with_setup( + || { + clear_init_location(); + runtime::register_global_initializer(add_to_global); + }, + code, + ) + } + + fn eval_with_url(location_url: &str, code: &str) -> String { + eval_with_setup( + || { + set_init_location(url::Url::parse(location_url).unwrap()); + runtime::register_global_initializer(add_to_global); + }, + code, + ) + } + + #[test] + fn accessors_serialize_the_url() { + assert_eq!( + eval_with_url( + "https://example.com:8443/foo/bar?x=1#frag", + "[location.href, location.origin, location.protocol, location.host, + location.hostname, location.port, location.pathname, location.search, + location.hash, location.toString() === location.href, + Object.prototype.toString.call(location)].join(' ')" + ), + "https://example.com:8443/foo/bar?x=1#frag https://example.com:8443 https: \ + example.com:8443 example.com 8443 /foo/bar ?x=1 #frag true [object Location]" + ); + } + + #[test] + fn absent_port_search_and_hash_are_empty_strings() { + assert_eq!( + eval_with_url( + "https://example.com/", + "[location.port, location.search, location.hash].join(',')" + ), + ",," + ); + } + + #[test] + fn constructor_throws() { + // WorkerLocation has no IDL constructor; `new WorkerLocation()` must throw. + assert_eq!( + eval( + "try { new WorkerLocation(); 'no-throw' } \ + catch(e) { (e instanceof TypeError) + ',' + e.message }" + ), + "true,Illegal constructor" + ); + } + + #[test] + fn unset_location_throws_typeerror() { + // With no init-location configured, every accessor throws. + assert_eq!( + eval( + "try { location.href; 'no-throw' } \ + catch(e) { (e instanceof TypeError) + ',' + e.message.includes('--init-location') }" + ), + "true,true" + ); + assert_eq!( + eval( + "try { location.toString(); 'no-throw' } \ + catch(e) { e instanceof TypeError }" + ), + "true" + ); + } +} + +mod dom_exception { + use core_runtime::{ + config::RuntimeConfig, + runtime::{self, Runtime}, + }; + use web_globals::dom_exception::DOMException; + + #[test] + fn rust_new() { + runtime::register_global_initializer(DOMException::add_to_global); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + let e = DOMException::new(&scope, String::new(), "Error".into()).unwrap(); + assert_eq!(&*e.name(), "Error"); + assert_eq!(&*e.message(), ""); + assert_eq!(e.code(), 0); + } +} + +/// Tests for typed error dispatch via `ThrowException` trait. +/// +/// These tests use `#[jsglobals]` to register functions that return +/// `Result`, `Result`, `Result`, +/// `Result`, and `Result`. +mod throw_exception { + use core_runtime::{jsglobals, test_util::eval_with_setup}; + use js::error::{RangeError, SyntaxError, TypeError}; + use web_globals::dom_exception::DOMExceptionError; + + #[jsglobals] + mod test_error_globals { + use super::*; + + /// Throws a TypeError. + pub fn throw_type_error() -> Result<(), TypeError> { + Err(TypeError("test type error".into())) + } + + /// Throws a RangeError. + pub fn throw_range_error() -> Result<(), RangeError> { + Err(RangeError("test range error".into())) + } + + /// Throws a SyntaxError. + pub fn throw_syntax_error() -> Result<(), SyntaxError> { + Err(SyntaxError("test syntax error".into())) + } + + /// Throws a TypeError from a String error. + pub fn throw_string_error() -> Result<(), String> { + Err("test string error".into()) + } + + /// Throws a DOMExceptionError. + pub fn throw_dom_exception_error() -> Result<(), DOMExceptionError> { + Err(DOMExceptionError::new( + "NotFoundError", + "test dom exception", + )) + } + } + + fn eval(code: &str) -> String { + eval_with_setup( + || { + core_runtime::runtime::register_global_initializer( + web_globals::dom_exception::DOMException::add_to_global, + ); + core_runtime::runtime::register_global_initializer( + test_error_globals::add_to_global, + ); + }, + code, + ) + } + + /// Capturing the stack for a new `DOMException` is best-effort: a throwing + /// `Error.prototype.stack` accessor leaves the construction intact and no + /// exception pending. + #[test] + fn construction_survives_a_throwing_stack_accessor() { + assert_eq!( + eval( + "Object.defineProperty(Error.prototype, 'stack', { get() { throw 1; }, configurable: true }); + const e = new DOMException('m', 'NotFoundError'); + e.name + ',' + e.message" + ), + "NotFoundError,m" + ); + } + + #[test] + fn type_error_thrown() { + assert_eq!( + eval( + "try { throwTypeError(); 'no' } catch(e) { (e instanceof TypeError) + ',' + e.message }" + ), + "true,test type error" + ); + } + + #[test] + fn range_error_thrown() { + assert_eq!( + eval( + "try { throwRangeError(); 'no' } catch(e) { (e instanceof RangeError) + ',' + e.message }" + ), + "true,test range error" + ); + } + + #[test] + fn syntax_error_thrown() { + assert_eq!( + eval( + "try { throwSyntaxError(); 'no' } catch(e) { (e instanceof SyntaxError) + ',' + e.message }" + ), + "true,test syntax error" + ); + } + + #[test] + fn string_error_throws_type_error() { + // String errors throw TypeError. + assert_eq!( + eval( + "try { throwStringError(); 'no' } catch(e) { (e instanceof TypeError) + ',' + e.message }" + ), + "true,test string error" + ); + } + + #[test] + fn dom_exception_error_thrown() { + assert_eq!( + eval( + "try { throwDomExceptionError(); 'no' } catch(e) { \ + (e instanceof DOMException) + ',' + e.name + ',' + e.message }" + ), + "true,NotFoundError,test dom exception" + ); + } +} + +mod performance { + use core_runtime::{runtime, test_util::eval_with_setup}; + + /// Whether 20 readings each of `performance.now()` and `new Event('x').timeStamp` are all + /// multiples of 100 microseconds, as `"{now},{timeStamp}"`, with the web security + /// restrictions set to `restricted`. + fn readings_are_coarsened(restricted: bool) -> String { + eval_with_setup( + || match restricted { + true => runtime::register_global_initializer(|scope, global| { + core_runtime::config::set_web_security_restrictions(true); + web_globals::add_to_global(scope, global); + }), + false => runtime::register_global_initializer(|scope, global| { + core_runtime::config::set_web_security_restrictions(false); + web_globals::add_to_global(scope, global); + }), + }, + "(() => { + const coarsened = (read) => { + const times = []; + for (let i = 0; i < 20; i++) { + times.push(read()); + } + return times.every((t) => Math.abs(t * 10 - Math.round(t * 10)) < 1e-6); + }; + return [ + coarsened(() => performance.now()), + coarsened(() => new Event('x').timeStamp), + ].join(','); + })()", + ) + } + + #[test] + fn clocks_are_coarsened_under_web_security_restrictions() { + assert_eq!(readings_are_coarsened(true), "true,true"); + } + + /// Only `performance.now()` is checked: the coarse clock behind `Event.timeStamp` can lag the + /// time origin, and a reading that lags it is zero, which is a multiple of 100 microseconds. + #[test] + fn performance_now_is_precise_without_web_security_restrictions() { + assert!(readings_are_coarsened(false).starts_with("false,")); + } +} diff --git a/crates/builtins/web-globals/tests/structured_clone.rs b/crates/builtins/web-globals/tests/structured_clone.rs index 1698260..406db36 100644 --- a/crates/builtins/web-globals/tests/structured_clone.rs +++ b/crates/builtins/web-globals/tests/structured_clone.rs @@ -19,253 +19,128 @@ fn throws(code: &str) -> bool { throws_with_setup(setup, code) } -// ── structuredClone exists ── - -#[test] -fn structured_clone_exists() { - assert_eq!(eval("typeof structuredClone"), "function"); -} - -#[test] -fn structured_clone_length_excludes_optional() { - // WebIDL `.length` counts required arguments only: the optional `options` - // dictionary must not be counted. - assert_eq!(eval("structuredClone.length"), "1"); -} - -// ── Primitives ── - -#[test] -fn clone_undefined() { - assert_eq!(eval("structuredClone(undefined)"), "undefined"); -} - -#[test] -fn clone_null() { - assert_eq!(eval("structuredClone(null)"), "null"); -} - -#[test] -fn clone_boolean_true() { - assert_eq!(eval("structuredClone(true)"), "true"); -} - #[test] -fn clone_boolean_false() { - assert_eq!(eval("structuredClone(false)"), "false"); -} - -#[test] -fn clone_number() { - assert_eq!(eval("structuredClone(42)"), "42"); -} - -#[test] -fn clone_negative_zero() { - assert_eq!(eval("Object.is(structuredClone(-0), -0)"), "true"); -} - -#[test] -fn clone_nan() { - assert_eq!(eval("Number.isNaN(structuredClone(NaN))"), "true"); -} - -#[test] -fn clone_infinity() { - assert_eq!(eval("structuredClone(Infinity)"), "Infinity"); -} - -#[test] -fn clone_string() { - assert_eq!(eval("structuredClone('hello')"), "hello"); -} - -#[test] -fn clone_empty_string() { - assert_eq!(eval("structuredClone('')"), ""); -} - -#[test] -fn clone_bigint() { - assert_eq!(eval("structuredClone(42n).toString()"), "42"); -} - -// ── Objects ── - -#[test] -fn clone_plain_object() { +fn structured_clone_is_a_function_of_one_argument() { + // WebIDL `.length` counts required arguments only, so the optional `options` dictionary isn't + // counted. assert_eq!( - eval("JSON.stringify(structuredClone({a: 1, b: 'hello'}))"), - r#"{"a":1,"b":"hello"}"# + eval("typeof structuredClone + ',' + structuredClone.length"), + "function,1" ); } #[test] -fn clone_is_deep_copy() { - assert_eq!( - eval("let o = {a: {b: 1}}; let c = structuredClone(o); o.a.b = 2; c.a.b"), - "1" - ); -} - -#[test] -fn clone_preserves_identity() { - // The clone should be a different object. - assert_eq!(eval("let o = {}; structuredClone(o) === o"), "false"); -} - -// ── Arrays ── - -#[test] -fn clone_array() { +fn clone_primitives() { assert_eq!( - eval("JSON.stringify(structuredClone([1, 2, 3]))"), - "[1,2,3]" - ); -} - -#[test] -fn clone_nested_array() { - assert_eq!( - eval("JSON.stringify(structuredClone([[1], [2, [3]]]))"), - "[[1],[2,[3]]]" - ); -} - -// ── Date ── - -#[test] -fn clone_date() { - assert_eq!( - eval("structuredClone(new Date('2024-01-01T00:00:00Z')).toISOString()"), - "2024-01-01T00:00:00.000Z" - ); -} - -// ── RegExp ── - -#[test] -fn clone_regexp() { - assert_eq!( - eval("let r = structuredClone(/abc/gi); r.source + ',' + r.flags"), - "abc,gi" + eval( + "[undefined, null, true, false, 42, Infinity, 'hello', '', 42n] + .map((v) => String(structuredClone(v))) + .concat(Object.is(structuredClone(-0), -0), Number.isNaN(structuredClone(NaN))) + .join()" + ), + "undefined,null,true,false,42,Infinity,hello,,42,true,true" ); } -// ── Map ── - #[test] -fn clone_map() { +fn clone_wrapped_primitives() { assert_eq!( eval( - "let m = new Map([['a', 1], ['b', 2]]); \ - let c = structuredClone(m); \ - c.get('a') + ',' + c.get('b') + ',' + (c !== m)" + "[Object(true), Object(42), Object('hello')] + .map((v) => { const c = structuredClone(v); return typeof c + ':' + (c == v.valueOf()); }) + .join()" ), - "1,2,true" + "object:true,object:true,object:true" ); } -// ── Set ── - #[test] -fn clone_set() { +fn clone_objects_and_arrays_deeply() { assert_eq!( eval( - "let s = new Set([1, 2, 3]); \ - let c = structuredClone(s); \ - c.has(1) + ',' + c.has(2) + ',' + c.has(3) + ',' + (c !== s)" + "let o = {a: {b: 1}, s: 'hello', arr: [[1], [2, [3]]]}; \ + let c = structuredClone(o); \ + o.a.b = 2; \ + [c !== o, c.a.b, c.s, JSON.stringify(c.arr)].join(' ')" ), - "true,true,true,true" + "true 1 hello [[1],[2,[3]]]" ); } -// ── ArrayBuffer ── - #[test] -fn clone_arraybuffer() { +fn clone_circular_reference() { assert_eq!( eval( - "let buf = new ArrayBuffer(4); \ - new Uint8Array(buf).set([1, 2, 3, 4]); \ - let clone = structuredClone(buf); \ - let arr = new Uint8Array(clone); \ - arr[0] + ',' + arr[1] + ',' + arr[2] + ',' + arr[3] + ',' + (clone !== buf)" + "let o = {a: 1}; o.self = o; \ + let c = structuredClone(o); \ + c.a + ',' + (c.self === c)" ), - "1,2,3,4,true" + "1,true" ); } -// ── TypedArray ── - #[test] -fn clone_uint8array() { +fn clone_date_and_regexp() { assert_eq!( eval( - "let a = new Uint8Array([10, 20, 30]); \ - let c = structuredClone(a); \ - c[0] + ',' + c[1] + ',' + c[2] + ',' + (c !== a)" + "let r = structuredClone(/abc/gi); \ + structuredClone(new Date('2024-01-01T00:00:00Z')).toISOString() + ',' + \ + r.source + ',' + r.flags" ), - "10,20,30,true" + "2024-01-01T00:00:00.000Z,abc,gi" ); } -// ── Circular references ── - #[test] -fn clone_circular_reference() { +fn clone_map_and_set() { assert_eq!( eval( - "let o = {a: 1}; o.self = o; \ - let c = structuredClone(o); \ - c.a + ',' + (c.self === c)" + "let m = new Map([['a', 1], ['b', 2]]); \ + let s = new Set([1, 2, 3]); \ + let cm = structuredClone(m); \ + let cs = structuredClone(s); \ + [cm.get('a'), cm.get('b'), cm !== m, cs.has(1), cs.has(2), cs.has(3), cs !== s].join()" ), - "1,true" + "1,2,true,true,true,true,true" ); } -// ── Error ── - #[test] -fn clone_error() { +fn clone_arraybuffer_and_typed_array() { assert_eq!( eval( - "let e = new Error('test'); \ - let c = structuredClone(e); \ - (c instanceof Error) + ',' + c.message" + "let buf = new ArrayBuffer(4); \ + new Uint8Array(buf).set([1, 2, 3, 4]); \ + let clone = structuredClone(buf); \ + let a = new Uint8Array([10, 20, 30]); \ + let c = structuredClone(a); \ + [...new Uint8Array(clone), clone !== buf, ...c, c !== a].join()" ), - "true,test" + "1,2,3,4,true,10,20,30,true" ); } #[test] -fn clone_type_error() { +fn clone_errors() { assert_eq!( eval( - "let e = new TypeError('bad'); \ - let c = structuredClone(e); \ - (c instanceof TypeError) + ',' + c.message" + "[new Error('test'), new TypeError('bad')] \ + .map((e) => { const c = structuredClone(e); \ + return (c instanceof e.constructor) + ':' + c.message; }) \ + .join()" ), - "true,bad" + "true:test,true:bad" ); } -// ── Non-cloneable types ── - #[test] -fn clone_function_throws() { +fn clone_functions_and_symbols_throws() { assert!(throws("structuredClone(function() {})")); -} - -#[test] -fn clone_symbol_throws() { assert!(throws("structuredClone(Symbol('test'))")); } -// ── Transfer ── - #[test] -fn transfer_arraybuffer() { +fn transfer_arraybuffer_detaches_the_source() { assert_eq!( eval( "let buf = new ArrayBuffer(4); \ @@ -275,10 +150,6 @@ fn transfer_arraybuffer() { ), "1,0" ); -} - -#[test] -fn transfer_detaches_source() { assert!(throws( "let buf = new ArrayBuffer(4); \ structuredClone(buf, { transfer: [buf] }); \ @@ -286,59 +157,13 @@ fn transfer_detaches_source() { )); } -// ── Options edge cases ── - -#[test] -fn options_null_is_ok() { - assert_eq!(eval("structuredClone(42, null)"), "42"); -} - -#[test] -fn options_undefined_is_ok() { - assert_eq!(eval("structuredClone(42, undefined)"), "42"); -} - -#[test] -fn options_empty_object_is_ok() { - assert_eq!(eval("structuredClone(42, {})"), "42"); -} - -#[test] -fn options_empty_transfer_is_ok() { - assert_eq!(eval("structuredClone(42, { transfer: [] })"), "42"); -} - -// ── Wrapped primitives ── - -#[test] -fn clone_boolean_object() { - assert_eq!( - eval( - "let b = structuredClone(Object(true)); \ - (typeof b) + ',' + (b == true)" - ), - "object,true" - ); -} - -#[test] -fn clone_number_object() { - assert_eq!( - eval( - "let n = structuredClone(Object(42)); \ - (typeof n) + ',' + (n == 42)" - ), - "object,true" - ); -} - #[test] -fn clone_string_object() { +fn options_can_be_null_undefined_or_empty() { assert_eq!( eval( - "let s = structuredClone(Object('hello')); \ - (typeof s) + ',' + (s == 'hello')" + "[structuredClone(42, null), structuredClone(42, undefined), structuredClone(42, {}), \ + structuredClone(42, { transfer: [] })].join()" ), - "object,true" + "42,42,42,42" ); } diff --git a/crates/builtins/web-streams/Cargo.toml b/crates/builtins/web-streams/Cargo.toml index e0aa2df..1daa1e6 100644 --- a/crates/builtins/web-streams/Cargo.toml +++ b/crates/builtins/web-streams/Cargo.toml @@ -7,7 +7,7 @@ authors.workspace = true edition.workspace = true [lib] -# No unit tests; skip building the (SpiderMonkey-sized) lib test harness binary. +# No unit tests. This skips building the (SpiderMonkey-sized) lib test harness binary. test = false [dependencies] diff --git a/crates/builtins/web-streams/src/lib.rs b/crates/builtins/web-streams/src/lib.rs index 9db6e55..702e65e 100644 --- a/crates/builtins/web-streams/src/lib.rs +++ b/crates/builtins/web-streams/src/lib.rs @@ -31,13 +31,9 @@ pub fn add_to_global(scope: &Scope<'_>, global: Object<'_>) { queuing::ByteLengthQueuingStrategy::add_to_global(scope, global); queuing::CountQueuingStrategy::add_to_global(scope, global); - // The tee/pipe/from-iterable algorithms keep their cross-callback state in - // internal `#[jsclass]` objects (so the state is GC-traced and reachable - // through each callback's payload). They are created via `create_instance_with`, - // which needs the prototype registered per-global — but they are not - // web-exposed interfaces, so the named constructor that `add_to_global` - // installs is deleted from the global afterward (the prototype registry, keyed - // by TypeId, is unaffected). + // Internal classes holding the state of tee, pipe, from-iterable and read-all-bytes + // operations. Creating instances needs their prototypes registered, and `hidden` keeps + // their constructors off the global. readable::algorithms::TeeState::add_to_global(scope, global); readable::algorithms::ByteTeeState::add_to_global(scope, global); readable::algorithms::PipeState::add_to_global(scope, global); @@ -71,7 +67,7 @@ pub fn add_to_global(scope: &Scope<'_>, global: Object<'_>) { // (auto-allocation and `ReadableStreamBYOBRequest.view`). Resolve those standard // constructors now, while no stream state is live, so their lazy // first-construction never runs mid-operation with byte-stream objects - // reachable — which otherwise trips a GC tracer assertion under moving GC. + // reachable. That trips a GC tracer assertion under moving GC. for key in [ js::class_spec::JSProtoKey::JSProto_ArrayBuffer, js::class_spec::JSProtoKey::JSProto_Uint8Array, diff --git a/crates/builtins/web-streams/src/queuing/queue_with_sizes.rs b/crates/builtins/web-streams/src/queuing/queue_with_sizes.rs index 2ef7c92..8837fd0 100644 --- a/crates/builtins/web-streams/src/queuing/queue_with_sizes.rs +++ b/crates/builtins/web-streams/src/queuing/queue_with_sizes.rs @@ -4,13 +4,6 @@ //! controllers. //! //! -//! -//! The spec models these containers as having `[[queue]]` (a list of -//! "value-with-size" records) and `[[queueTotalSize]]` (a number) internal -//! slots, and defines the queue operations (`EnqueueValueWithSize`, -//! `DequeueValue`, `PeekQueueValue`, `ResetQueue`) generically over any such -//! container. The [`QueueWithSizes`] trait captures that shared shape so the -//! operations in [`crate::algorithms`] can run against either controller. use std::collections::VecDeque; diff --git a/crates/builtins/web-streams/src/readable/algorithms.rs b/crates/builtins/web-streams/src/readable/algorithms.rs index b0b7466..b229ee0 100644 --- a/crates/builtins/web-streams/src/readable/algorithms.rs +++ b/crates/builtins/web-streams/src/readable/algorithms.rs @@ -160,7 +160,7 @@ impl GenericReader for BYOBReader<'_> { /// Construct a view over `buffer` and return it as a rooted value, panicking on /// failure. Used where the spec performs an infallible `! Construct(...)` (e.g. /// the empty view handed to a read-into request's close steps). -fn construct_view_or_throw<'r>( +fn construct_view_or_panic<'r>( scope: &'r Scope<'_>, kind: js::typedarray::ViewKind, buffer: js::ArrayBuffer<'_>, @@ -190,9 +190,7 @@ fn error_controller_with_pending( ) } -/// A native fulfillment reaction that ignores its argument and returns -/// undefined, used to implement the spec's "react to `p` with a fulfillment -/// step that returns undefined". +/// A native reaction that ignores its argument and returns undefined. fn return_undefined( _scope: &Scope<'_>, _args: CallbackArgs<'_>, @@ -438,9 +436,6 @@ fn tee_closed_rejected( Ok(value::undefined()) } -/// Tee branches are created with the default (constant-1) size algorithm, so -/// their controllers' `enqueue` can never run the size algorithm and therefore -/// never throws — matching the spec's `! ReadableStreamDefaultControllerEnqueue`. const TEE_ENQUEUE_INFALLIBLE: &str = "tee branch enqueue is infallible: branches use the default constant-1 size algorithm"; @@ -1064,13 +1059,9 @@ pub(crate) fn readable_stream_pipe_to<'r>( if let Some(host_source) = source.native_source(scope) { let transform = dest.data().identity_transform.get(scope); if let Some(transform) = transform { - // Only when this pipe will close the destination behind it. Handing the native source - // on says "the whole of what comes out of this transform is that one body", and a sink - // that takes it up bypasses the transform entirely — so anything written into the - // transform afterwards would go to a readable end nobody reads, stalling its writer and - // never reaching the wire. This pipe holds the writer lock until it is done and then - // closes the destination, so nothing can follow it; `preventClose` is precisely the - // author saying they intend to write more, and disqualifies the claim. + // A sink that takes up the native source bypasses the transform, so chunks written + // into the transform after this pipe would never be read. Without `preventClose`, + // this pipe holds the writer lock until it closes the destination. if !prevent_close { transform .data() @@ -1184,9 +1175,7 @@ pub(crate) fn readable_stream_pipe_to<'r>( // --------------------------------------------------------------------------- // `ReadableStreamPipeTo` propagation reactions, shutdown machinery, and the -// read-write loop. See `readable_stream_pipe_to` above; the shared state is a -// `PipeState` (its fields read/written through `state.data()`/`data_mut()`), and -// every reaction receives the state object as its payload. +// read-write loop. // --------------------------------------------------------------------------- /// The action a pipe shutdown performs once pending writes finish. @@ -1336,7 +1325,7 @@ fn pipe_step(scope: &Scope<'_>, state: PipeState<'_>) { // Backpressure must be enforced: wait until the writer is ready before // reading. The reactions are the per-pipe callbacks created in - // `PipeState::new`; attaching them directly allocates nothing per loop turn. + // `PipeState::new`. Attaching them directly doesn't allocate per loop turn. let writer = pipe_writer(scope, state); let ready = writer.data().ready_promise.get(scope); let fulfilled = state.data().ready_fulfilled_fn.get(scope); @@ -1369,30 +1358,14 @@ fn pipe_ready_fulfilled( Ok(value::undefined()) } -/// The writer's ready promise rejected (the destination errored). The backward -/// error-propagation reaction drives shutdown; nothing to do here, but consuming -/// the rejection keeps it from surfacing as unhandled. -fn pipe_ready_rejected( - _scope: &Scope<'_>, - _args: CallbackArgs<'_>, - _payload: HandleValue<'_>, -) -> Result { - Ok(value::undefined()) -} - /// The pipe read request's chunk steps: park the chunk, count its pending /// write, and schedule the deferred write that will write it to the destination /// and continue the loop. /// -/// The write and the next read are deliberately deferred by one microtask rather -/// than run synchronously here. The spec's pipe step 15 is informal about timing -/// (whatwg/streams#1243), but every browser defers: Gecko's `OnReadFulfilled` -/// does `Promise.resolve().then(() => { write(chunk); readNext(); })`. Deferring -/// matters because a chunk can be delivered to these chunk steps synchronously -/// (e.g. when `enqueue()` fulfils a pending read in the same turn); running the -/// sink's `write()` synchronously from `enqueue()` is observable and forbidden -/// (`streams/piping/general-addition.any.js`: "enqueue() must not synchronously -/// call write algorithm"). +/// The write and the next read are deferred by one microtask. These chunk steps +/// can run synchronously from `enqueue()`, and running the sink's `write()` +/// synchronously from `enqueue()` is observable (WPT +/// `streams/piping/general-addition.any.js`). pub(crate) fn pipe_read_request_chunk_steps( scope: &Scope<'_>, state: PipeState<'_>, @@ -1405,9 +1378,8 @@ pub(crate) fn pipe_read_request_chunk_steps( } /// Runs one microtask after a chunk reaches the pipe read request: writes the -/// parked chunk to the destination and continues the loop. Deferring to here is -/// what keeps `enqueue()` from synchronously invoking the sink's write algorithm -/// (see `pipe_read_request_chunk_steps`). +/// parked chunk to the destination and continues the loop (see +/// `pipe_read_request_chunk_steps`). fn pipe_deferred_write( scope: &Scope<'_>, _args: CallbackArgs<'_>, @@ -1739,10 +1711,8 @@ fn pipe_finalize(scope: &Scope<'_>, state: PipeState<'_>, error: Option, } -/// Steps 4-18 of `ReadableStreamDefaultTee` are algorithm. #[jsmethods] impl TeeState<'_> { fn new( @@ -1825,7 +1794,6 @@ impl TeeState<'_> { self.data_mut().stream.set(stream); self.data_mut().reader.set(reader); - // The chunk-steps microtask callback, reused for every chunk. let microtask = Function::new_callback(scope, c"", 0, tee_chunk_microtask, self)?; self.data_mut().chunk_microtask_fn.set(microtask); @@ -1879,11 +1847,8 @@ pub(crate) struct PipeState { /// `pipe_read_request_chunk_steps`). pending_chunk: Heap, /// The number of chunks that have been read but whose writes have not yet - /// settled. Counted eagerly at the read request's chunk steps; decremented - /// by each write's settle reaction (`pipe_write_settled`). Shutdown's "wait - /// until every chunk that has been read has been written" is implemented as - /// waiting for this count to drain. The count is pipe-internal, so the - /// difference from the spec's promise chaining is unobservable. + /// settled. Incremented by the read request's chunk steps and decremented + /// by each write's settle reaction (`pipe_write_settled`). pending_writes: u32, /// The loop's reaction callbacks, allocated once in the constructor, so the /// steady-state loop allocates no callback objects per chunk. @@ -1924,11 +1889,12 @@ impl PipeState<'_> { self.data_mut().write_settled_fn.set(settled); let ready_ok = Function::new_callback(scope, c"", 1, pipe_ready_fulfilled, self)?; self.data_mut().ready_fulfilled_fn.set(ready_ok); - // The ready-rejection swallower uses no per-pipe state; share one per global. + // A rejected ready promise means the destination errored, which the backward + // error-propagation reaction handles. The rejection only needs consuming. let ready_err = js::class::get_or_init_shared_function( scope, - pipe_ready_rejected as *const () as usize, - |scope| Function::new_callback(scope, c"", 1, pipe_ready_rejected, ()), + return_undefined as *const () as usize, + |scope| Function::new_callback(scope, c"", 1, return_undefined, ()), )?; self.data_mut().ready_rejected_fn.set(ready_err); self.data_mut().promise.set(Promise::new_pending(scope)?); @@ -2141,10 +2107,9 @@ pub(crate) fn readable_byte_stream_tee<'r>( Ok((branch1, branch2)) } -/// The shared state backing `ReadableByteStreamTee`. Not exposed to JS -/// (`hidden`); created internally and reached through each callback's payload. -/// The `reader` holds whichever reader type (default or BYOB) is currently in -/// use; it is swapped by `pullWithDefaultReader` / `pullWithBYOBReader`. +/// The shared state backing `ReadableByteStreamTee`, passed as each callback's +/// payload. The `reader` holds whichever reader type (default or BYOB) is +/// currently in use. `pullWithDefaultReader` and `pullWithBYOBReader` swap it. #[jsclass(hidden)] pub(crate) struct ByteTeeState { stream: Heap, @@ -2164,7 +2129,7 @@ pub(crate) struct ByteTeeState { pending_chunk: Heap, pending_for_branch2: bool, /// The two chunk-steps microtask callbacks, allocated once in the constructor - /// and queued directly on the job queue per chunk, so delivery allocates nothing. + /// and queued directly on the job queue per chunk, so delivery doesn't allocate. default_microtask_fn: Heap, byob_microtask_fn: Heap, } @@ -2184,7 +2149,6 @@ impl ByteTeeState<'_> { .cancel_promise .set(Promise::new_pending(scope)?); - // The chunk-steps microtask callbacks, reused for every chunk. let default_mt = Function::new_callback(scope, c"", 0, byte_tee_default_microtask, self)?; self.data_mut().default_microtask_fn.set(default_mt); let byob_mt = Function::new_callback(scope, c"", 0, byte_tee_byob_microtask, self)?; @@ -2210,10 +2174,7 @@ impl ByteTeeState<'_> { } // --------------------------------------------------------------------------- -// `ReadableByteStreamTee` reader switching and callbacks. The shared state is a -// `ByteTeeState` (above), passed as every callback's payload. Its `reader` holds -// whichever reader type (default or BYOB) is currently in use; it is swapped by -// `pullWithDefaultReader` / `pullWithBYOBReader`. +// `ReadableByteStreamTee` reader switching and callbacks. // --------------------------------------------------------------------------- fn byte_tee_stream<'r>(scope: &'r Scope<'_>, state: ByteTeeState<'_>) -> ReadableStream<'r> { @@ -2493,24 +2454,15 @@ pub(crate) fn byte_tee_default_chunk_steps( js::jobs::queue_microtask(scope, µtask) } -/// The chunk enqueued into a byte-tee branch is always a fresh, non-detached, -/// transferable view (the original read result or a `CloneAsUint8Array` clone), -/// so `ReadableByteStreamControllerEnqueue` never throws — matching the spec's -/// `! ReadableByteStreamControllerEnqueue`. const BYTE_TEE_ENQUEUE_INFALLIBLE: &str = "byte tee branch enqueue is infallible: the chunk is a fresh, transferable view"; -/// `ReadableByteStreamTee` closes each branch and responds to its pending -/// pull-into using the branch's own created descriptor and an aligned -/// (zero-length) view, so these never throw — matching the spec's -/// `! ReadableByteStreamControllerClose` / `! …Respond` / `! …RespondWithNewView`. const BYTE_TEE_SETTLE_INFALLIBLE: &str = "byte tee branch close/respond is infallible: created descriptor, aligned view (spec `!`)"; /// Error both branches with `error`, cancel the underlying source, and resolve -/// the tee's cancel promise — the byte-tee read-request steps' shared failure -/// path. It backs the chunk steps' chunk-clone failure, and the close steps' -/// fallible branch close/respond (see [`byte_tee_default_close_steps`]). +/// the tee's cancel promise. Called when a chunk clone fails and when a branch +/// close/respond fails (see [`byte_tee_default_close_steps`]). fn byte_tee_error_both_branches_and_cancel( scope: &Scope<'_>, state: ByteTeeState<'_>, @@ -2730,12 +2682,7 @@ pub(crate) fn byte_tee_byob_close_steps( } else { state.data().canceled2 }; - // The branch close/respond steps are infallible in the spec, but a - // branch whose pending BYOB pull-into is partially filled below its element - // size (reachable when a consumer does a multi-byte-element BYOB read and the - // source delivers a non-aligned remainder) makes `…ControllerClose` throw its - // "insufficient bytes" `TypeError`. Run them as a fallible unit; on failure, - // error both branches and cancel the source rather than aborting. + // The branch close/respond steps can throw, as in `byte_tee_default_close_steps`. let settle = (|| -> Result<(), ExnThrown> { // If _byobCanceled_ is false, perform ! `ReadableByteStreamControllerClose`(byobBranch). if !byob_canceled { @@ -3245,14 +3192,8 @@ pub(crate) fn readable_stream_byob_reader_read( min: usize, read_into_request: ReadIntoRequest, ) { - // The read-into request carries a `Heap` — the `read()` promise, or the - // byte-tee state — that nothing else traces while this synchronous path - // runs. The steps below transfer the view's buffer and construct views, - // any of which can move that referent under a compacting GC; an untraced - // `Heap` would then be left pointing at freed memory and its settling - // (`*_steps` → `Heap::take`) would dereference it. Root it for the duration - // so the GC keeps its pointer current, and hand it to each consumer via - // `take` (leaving the box empty, which traces to nothing). + // The steps below allocate and can move the request's referent, so the request + // stays rooted until a consumer `take`s it. let mut request = RootedTraceableBox::new(Some(read_into_request)); // Step 1: Let _stream_ be _reader_.`[[stream]]`. // Step 2: Assert: _stream_ is not undefined. @@ -3332,12 +3273,8 @@ pub(crate) fn readable_stream_default_reader_read( reader: DefaultReader<'_>, read_request: ReadRequest, ) { - // The read request carries an untraced `Heap` (the `read()` promise, or the - // tee/pipe/async-iterator state). The steps below — `stream`/controller - // lookups, `[[PullSteps]]`, queue fills — all allocate and can compact, - // which would stale that `Heap`. Root it for the duration and hand it to - // each consumer via `take` (see `readable_stream_byob_reader_read` for the - // same pattern on the BYOB side). + // The steps below allocate and can move the request's referent, so the request + // stays rooted until a consumer `take`s it. let mut read_request = RootedTraceableBox::new(Some(read_request)); // Step 1: Let _stream_ be _reader_.`[[stream]]`. // Step 2: Assert: _stream_ is not undefined. @@ -4312,9 +4249,6 @@ fn copy_data_block_bytes( src_start: usize, count: usize, ) { - if count == 0 { - return; - } dest.copy_region_from(dest_start, src, src_start, count); } @@ -4764,7 +4698,7 @@ pub(crate) fn readable_byte_stream_controller_pull_into( // constructed and the close steps run (both can GC). let buffer = descriptor_buffer(scope, pull_into_descriptor.as_ref().unwrap()); let byte_offset = pull_into_descriptor.as_ref().unwrap().byte_offset; - let empty_view = construct_view_or_throw(scope, view_kind, buffer, byte_offset, 0); + let empty_view = construct_view_or_panic(scope, view_kind, buffer, byte_offset, 0); read_into_request .take() .unwrap() @@ -4914,19 +4848,11 @@ pub(crate) fn readable_byte_stream_controller_respond_in_closed_state( let stream = controller.stream(scope); if readable_stream_has_byob_reader(scope, &stream) { // The spec collects all the descriptors into `filledPullIntos` and then - // commits them. A descriptor shifted out of `[[pendingPullIntos]]` is no - // longer traced by the controller, and `CommitPullIntoDescriptor` runs - // steps that can GC (`TransferArrayBuffer`, constructing the view). A - // shifted descriptor must therefore stay rooted for the whole commit — - // both so its `buffer` survives until it is transferred, and so its - // `Heap`'s drop write barrier sees a live pointer — so each is held in a - // `RootedTraceableBox` while committed. Capture the count up front, then - // shift and commit one descriptor at a time so the remaining descriptors - // stay traced in `[[pendingPullIntos]]`. This is not observable: the count - // is only reduced by these commits (which fulfill read-into requests via - // queued reactions, never synchronous author code), so committing each - // descriptor as it is shifted is equivalent to shifting all then - // committing all. + // commits them. A shifted descriptor is no longer traced by the controller + // and committing it can GC, so we shift and commit one descriptor at a time, + // each in a `RootedTraceableBox`. Only these commits reduce the count, and + // they fulfill read-into requests through queued reactions, so the order is + // not observable. let num_read_into_requests = readable_stream_get_num_read_into_requests(scope, &stream); for _ in 0..num_read_into_requests { let descriptor = RootedTraceableBox::new( diff --git a/crates/builtins/web-streams/src/readable/async_iterator.rs b/crates/builtins/web-streams/src/readable/async_iterator.rs index 8065b24..117d45d 100644 --- a/crates/builtins/web-streams/src/readable/async_iterator.rs +++ b/crates/builtins/web-streams/src/readable/async_iterator.rs @@ -26,9 +26,6 @@ use js::prelude::{CallbackArgs, HandleValue}; use js::{Function, Object, Promise}; /// -/// -/// The prototype's `[[Prototype]]` is set to `%AsyncIteratorPrototype%` at -/// registration (see `add_to_global`), which supplies `[Symbol.asyncIterator]`. // WebIDL §3.7.10: the class string of an asynchronous iterator prototype object // is the interface identifier followed by " AsyncIterator" (with a space). #[webidl_interface(hidden, to_string_tag = "ReadableStream AsyncIterator")] @@ -220,8 +217,8 @@ fn run_next_steps<'r>( // Step 7: Let _rejectSteps_ be the following steps, given _reason_: (implemented in // `next_rejected`) // Step 8: Let _onRejected_ be CreateBuiltinFunction(_rejectSteps_, 1, "", « »). - // The unique sentinel the close steps resolve the next-iteration promise - // with (recognized by identity in `next_fulfilled`) and the reaction + // The unique sentinel with which the close steps resolve the next-iteration + // promise (recognized by identity in `next_fulfilled`) and the reaction // callbacks are per-iterator; create them on the first `next()` and reuse // them for every subsequent chunk. if iter.data().next_fulfilled_fn.is_none() { @@ -397,16 +394,6 @@ fn iter_reader<'r>( // --- ReadRequest::AsyncIter step bodies (called from `read_request.rs`) ------- -/// Chunk steps: resolve `promise` with the raw chunk. -/// Wrapping in `{ value, done }` happens in `next()`'s fulfill steps. -pub(crate) fn async_iter_chunk_steps( - scope: &Scope<'_>, - promise: Promise<'_>, - chunk: HandleValue<'_>, -) -> Result<(), ExnThrown> { - promise.resolve(scope, chunk) -} - /// Close steps: release the reader, then resolve `promise` with the end-of-iteration /// sentinel (recognised by `next()`'s fulfill steps). pub(crate) fn async_iter_close_steps( diff --git a/crates/builtins/web-streams/src/readable/byte_stream_controller.rs b/crates/builtins/web-streams/src/readable/byte_stream_controller.rs index ef90fa4..bfabd87 100644 --- a/crates/builtins/web-streams/src/readable/byte_stream_controller.rs +++ b/crates/builtins/web-streams/src/readable/byte_stream_controller.rs @@ -22,11 +22,6 @@ use js::prelude::HandleValue; /// `ArrayBuffer`. /// /// -/// -/// `must_root`: holds a `Heap`, so crown rejects holding it by value across an -/// allocation. It is traced inside the controller's `[[queue]]`; once -/// `pop_front`'d it must be consumed (extract its buffer, then drop it) before -/// anything allocates, or kept in a `RootedTraceableBox`. #[js::must_root] #[derive(Traceable, Default, js::ScopeRoot)] pub(crate) struct ByteQueueEntry { @@ -68,11 +63,6 @@ pub(crate) enum ReaderType { /// a view of the requested type. /// /// -/// -/// `must_root`: holds a `Heap` (the buffer), so crown rejects holding it by -/// value across an allocation. It is traced inside the controller's -/// `[[pendingPullIntos]]`; once moved out it must be consumed immediately or -/// kept in a `RootedTraceableBox`. #[js::must_root] #[derive(Traceable)] pub(crate) struct PullIntoDescriptor { diff --git a/crates/builtins/web-streams/src/readable/mod.rs b/crates/builtins/web-streams/src/readable/mod.rs index c04ded5..e463389 100644 --- a/crates/builtins/web-streams/src/readable/mod.rs +++ b/crates/builtins/web-streams/src/readable/mod.rs @@ -19,7 +19,6 @@ pub mod readable_stream; pub mod underlying_source; pub(crate) mod unions; -pub use async_iterator::ReadableStreamAsyncIterator as AsyncIterator; pub use byob_reader::BYOBReader; pub use byob_request::ReadableStreamBYOBRequest; pub use byte_stream_controller::ReadableByteStreamController; diff --git a/crates/builtins/web-streams/src/readable/read_all_bytes.rs b/crates/builtins/web-streams/src/readable/read_all_bytes.rs index 57c514d..ad33049 100644 --- a/crates/builtins/web-streams/src/readable/read_all_bytes.rs +++ b/crates/builtins/web-streams/src/readable/read_all_bytes.rs @@ -4,19 +4,9 @@ //! //! //! -//! Drains a default reader to a single byte sequence using *internal* read -//! requests, then hands the bytes to a caller-supplied success callback. Fetch's -//! `fully read a body` is the sole consumer: it acquires an internal reader for a -//! body's stream and reads all the bytes, converting them to the requested value -//! (text, JSON, an `ArrayBuffer`, …) in the success callback. -//! -//! Reading via internal read requests — rather than the author-facing -//! `reader.read()` — is what makes this immune to a hijacked `Object.prototype.then`: -//! the chunk steps receive the chunk directly and accumulate its bytes, never -//! building an `{ value, done }` iterator-result object nor resolving a promise -//! with one, so the engine never performs thenable assimilation that would look up -//! and invoke an author-installed `then`. (The same property is why `pipeTo` -//! survives that attack.) +//! Drains a default reader to a single byte sequence using internal read +//! requests (see [`native_read`](super::native_read) for why), then hands the +//! bytes to a caller-supplied success callback. use super::algorithms::{ acquire_readable_stream_default_reader, readable_stream_default_reader_read, @@ -27,7 +17,7 @@ use super::readable_stream::ReadableStream; use crate::readable::DefaultReader; use core_runtime::{jsclass, jsmethods}; use js::conversion::FromJSVal; -use js::error::ExnThrown; +use js::error::{ExnThrown, OutOfMemory, ThrowException}; use js::function::cast_payload; use js::gc::handle::Heap; use js::gc::scope::Scope; @@ -38,9 +28,8 @@ use js::{Function, Promise, Uint8Array}; /// The completion of a read-all-bytes drain (the caller's `successSteps`): /// receives the payload value given to [`read_all_bytes`] and the assembled -/// bytes **by value** — no intermediate JS `Uint8Array` round-trip — and -/// returns the value the result promise resolves with (or throws to reject -/// it). +/// bytes, and returns the value the result promise resolves with (or throws to +/// reject it). pub type ReadAllBytesComplete = for<'r> fn(&'r Scope<'r>, HandleValue<'_>, Vec) -> Result, ExnThrown>; @@ -62,11 +51,10 @@ pub(crate) struct ReadAllBytesState { /// [`read_all_bytes`] right after the state is created (`Option` only /// because the callback's payload is the state object, which must exist /// first) and queued directly on the job queue per chunk, so the read - /// loop allocates nothing per chunk. + /// loop doesn't allocate per chunk. read_next_fn: Option>, } -// Needed to fully initialize the class. Intentionally left blank. #[jsmethods] impl ReadAllBytesState { fn new( @@ -97,20 +85,9 @@ pub fn read_all_bytes<'r>( payload: HandleValue<'_>, ) -> Result, ExnThrown> { let promise = Promise::new_pending(scope)?; - // Acquire an *internal* default reader (not the author-facing `getReader()`), - // so its read requests deliver chunks to native steps rather than to author - // code via promise resolution. - // - // The reader is deliberately never released (neither close nor error steps do - // so): the sole caller is the Fetch "fully read a body" path, which consumes a - // body exactly once and must leave the stream locked-and-disturbed so it - // cannot be re-read (mirroring `body::readable_stream_consumed`). A future - // non-fetch caller that wants the stream reusable afterward must release it - // itself. + // The reader is never released, so the stream stays locked afterwards. let reader = acquire_readable_stream_default_reader(scope, &stream)?; let state = ReadAllBytesState::new(scope, reader, promise, on_complete, payload)?; - // The deferred-read callback, reused for every chunk. Created here (not in - // the closure above) because its payload is the state object. let read_next_fn = Function::new_callback(scope, c"", 0, read_next_microtask, state)?; state.data_mut().read_next_fn.set(read_next_fn); read_next(scope, state); @@ -142,8 +119,7 @@ fn read_next(scope: &Scope<'_>, state: ReadAllBytesState<'_>) { /// synchronously (`readable_stream_default_reader_read`'s pull steps run the /// chunk steps on the same stack), so issuing the next read directly from here /// recurses once per queued chunk and overflows the stack on a body with many -/// queued chunks. One flat microtask per chunk keeps the loop iterative, the -/// same way the tee and pipe read requests defer their per-chunk work. +/// queued chunks. One flat microtask per chunk keeps the loop iterative. pub(crate) fn read_all_bytes_chunk_steps( scope: &Scope<'_>, state: ReadAllBytesState<'_>, @@ -159,8 +135,19 @@ pub(crate) fn read_all_bytes_chunk_steps( // Step 2: Append the bytes represented by chunk to bytes. let mut data = state.data_mut(); let bytes = &mut data.bytes; - array.with_slice(|chunk_bytes| bytes.extend_from_slice(chunk_bytes)); + let appended = array.with_slice(|chunk| { + let reserved = bytes.try_reserve(chunk.len()).is_ok(); + if reserved { + bytes.extend_from_slice(chunk); + } + reserved + }); drop(data); + if !appended { + OutOfMemory.throw(scope); + let promise = state.data().promise.get(scope); + return promise.reject_with_pending(scope); + } // Step 3: Read-loop — deferred one microtask (see above): queue the // state's deferred-read callback directly on the job queue. let microtask = state @@ -183,9 +170,9 @@ fn read_next_microtask( Ok(value::undefined()) } -/// The `Consume` read request's close steps: hand the accumulated bytes (by -/// value — no `Uint8Array` round-trip) to the native success callback, and -/// resolve the result promise with its return value (or reject if it throws). +/// The `Consume` read request's close steps: hand the accumulated bytes to the +/// native success callback, and resolve the result promise with its return +/// value (or reject if it throws). pub(crate) fn read_all_bytes_close_steps( scope: &Scope<'_>, state: ReadAllBytesState<'_>, diff --git a/crates/builtins/web-streams/src/readable/read_request.rs b/crates/builtins/web-streams/src/readable/read_request.rs index 87cce38..e06d662 100644 --- a/crates/builtins/web-streams/src/readable/read_request.rs +++ b/crates/builtins/web-streams/src/readable/read_request.rs @@ -1,18 +1,9 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception -//! Read requests. +//! Read requests and read-into requests, one enum variant per consumer. //! //! -//! -//! A read request is a spec struct bundling three native step-algorithms — -//! `chunk steps` (given a chunk), `close steps`, and `error steps` (given an -//! error). Each consumer of a `DefaultReader` (a `read()` call, -//! `pipeTo`, `tee`, the async iterator) supplies its own. They are modelled as -//! an enum — one variant per consumer — rather than as JS values, because the -//! steps are native and the `[[readRequests]]` list must trace whatever rooted -//! state they hold. -use js::error::ExnThrown; use js::gc::handle::Heap; use js::gc::scope::Scope; use js::iteration::create_iter_result; @@ -30,14 +21,9 @@ use crate::readable::read_all_bytes::{ read_all_bytes_chunk_steps, read_all_bytes_close_steps, read_all_bytes_error_steps, ReadAllBytesStateImpl, }; +use crate::support::report_failure; /// An entry in a reader's `[[readRequests]]` list. -/// -/// Marked `#[js::must_root]` because each variant holds a `Heap` (the read -/// promise, or tee/pipe/iterator state): crown then rejects holding a -/// `ReadRequest` by value in a plain local across an allocation — the exact -/// untraced-`Heap`-across-GC hazard — forcing callers to keep it traced -/// (`RootedTraceableBox`) until they settle it. #[js::must_root] #[derive(core_runtime::Traceable, js::ScopeRoot)] pub(crate) enum ReadRequest { @@ -100,10 +86,6 @@ pub(crate) enum ReadRequest { /// its error steps reject the result promise. The shared state is a /// `ReadAllBytesState` (see `crate::readable::read_all_bytes`). /// - /// Unlike a `Read` request, the chunk never becomes an `{ value, done }` - /// object resolved into a promise, so a hijacked `Object.prototype.then` - /// cannot intercept it. - /// /// Consume { /// The read-all-bytes state object. @@ -111,9 +93,7 @@ pub(crate) enum ReadRequest { }, /// An out-of-crate native consumer's read request (see /// `crate::readable::native_read`): the steps are plain fn pointers - /// receiving the rooted `payload`. Like `Consume`, delivery never builds an - /// `{ value, done }` object or resolves a promise, so author code cannot - /// observe it. + /// receiving the rooted `payload`. Native { /// The consumer's step functions. #[no_trace] @@ -131,16 +111,15 @@ impl<'s> StackReadRequest<'s> { .and_then(|result| promise.resolve(scope, result)), StackReadRequest::Tee { state } => tee_read_request_chunk_steps(scope, state, chunk), StackReadRequest::Pipe { state } => pipe_read_request_chunk_steps(scope, state, chunk), - StackReadRequest::AsyncIter { promise, .. } => { - super::async_iterator::async_iter_chunk_steps(scope, promise, chunk) - } + // `next()`'s fulfill steps wrap the raw chunk in `{ value, done }`. + StackReadRequest::AsyncIter { promise, .. } => promise.resolve(scope, chunk), StackReadRequest::ByteTeeDefault { state } => { byte_tee_default_chunk_steps(scope, state, chunk) } StackReadRequest::Consume { state } => read_all_bytes_chunk_steps(scope, state, chunk), StackReadRequest::Native { steps, payload } => (steps.chunk)(scope, payload, chunk), }; - report_failed_steps(scope, result, "read request chunk steps"); + report_failure(scope, result, "read request chunk steps"); } /// Perform the read request's `close steps`. @@ -170,7 +149,7 @@ impl<'s> StackReadRequest<'s> { StackReadRequest::Consume { state } => read_all_bytes_close_steps(scope, state), StackReadRequest::Native { steps, payload } => (steps.close)(scope, payload), }; - report_failed_steps(scope, result, "read request close steps"); + report_failure(scope, result, "read request close steps"); } /// Perform the read request's `error steps` given `e`. @@ -194,7 +173,7 @@ impl<'s> StackReadRequest<'s> { StackReadRequest::Consume { state } => read_all_bytes_error_steps(scope, state, e), StackReadRequest::Native { steps, payload } => (steps.error)(scope, payload, e), }; - report_failed_steps(scope, result, "read request error steps"); + report_failure(scope, result, "read request error steps"); } } @@ -205,9 +184,6 @@ impl<'s> StackReadRequest<'s> { /// Like [`ReadRequest`] but for `BYOBReader.read(view)`. Its /// `chunk`/`close` steps both receive a view (the filled or empty destination), /// unlike a default read request whose close step produces `undefined`. -/// -/// Marked `#[js::must_root]` for the same reason as [`ReadRequest`]: its -/// variants hold a `Heap`, so crown forbids holding one untraced across a GC. #[js::must_root] #[derive(core_runtime::Traceable, js::ScopeRoot)] pub(crate) enum ReadIntoRequest { @@ -244,7 +220,7 @@ impl<'s> StackReadIntoRequest<'s> { byte_tee_byob_chunk_steps(scope, state, for_branch2, chunk) } }; - report_failed_steps(scope, result, "read-into request chunk steps"); + report_failure(scope, result, "read-into request chunk steps"); } /// Perform the read-into request's `close steps` given `chunk`. @@ -256,7 +232,7 @@ impl<'s> StackReadIntoRequest<'s> { byte_tee_byob_close_steps(scope, state, for_branch2, chunk) } }; - report_failed_steps(scope, result, "read-into request close steps"); + report_failure(scope, result, "read-into request close steps"); } /// Perform the read-into request's `error steps` given `e`. @@ -271,15 +247,6 @@ impl<'s> StackReadIntoRequest<'s> { Ok(()) } }; - report_failed_steps(scope, result, "read-into request error steps"); - } -} - -/// Read request steps have no caller to throw to, so an exception one of them -/// leaves (from a consumer's native step, or running out of memory) is reported -/// and cleared. -fn report_failed_steps(scope: &Scope<'_>, result: Result<(), ExnThrown>, context: &str) { - if result.is_err() { - js::exception::report_and_clear(scope, context); + report_failure(scope, result, "read-into request error steps"); } } diff --git a/crates/builtins/web-streams/src/readable/readable_stream.rs b/crates/builtins/web-streams/src/readable/readable_stream.rs index afe2485..792e85e 100644 --- a/crates/builtins/web-streams/src/readable/readable_stream.rs +++ b/crates/builtins/web-streams/src/readable/readable_stream.rs @@ -484,20 +484,16 @@ impl ReadableStream { self.data().disturbed } - /// Whether the stream is still readable — neither closed nor errored, so - /// closing or erroring it would still have an observable effect. + /// Whether the stream is still readable: neither closed nor errored. pub fn is_readable(&self) -> bool { self.data().state == ReadableStreamState::Readable } /// /// Cancel the stream with `reason`, running its underlying source's cancel - /// steps. This is `ReadableStreamCancel`, the internal algorithm, so — unlike - /// the author-facing `cancel()` method — it applies to a locked stream too. - /// Callers that hold a reader should prefer + /// steps. Unlike the author-facing [`cancel`](Self::cancel) method, this + /// applies to a locked stream too. Callers that hold a reader should prefer /// [`native_read::native_reader_cancel`](crate::readable::native_read::native_reader_cancel). - /// Distinct from the author-facing [`cancel`](Self::cancel) method above, - /// which rejects when the stream is locked. pub fn cancel_internal<'r>( &self, scope: &'r Scope<'_>, @@ -568,18 +564,11 @@ impl ReadableStream { /// `None` once that demand has arrived, or if this stream is not piped to such /// a transform. /// - /// This allows native underlying stream sources to hold off on performing an - /// actual read operation until a `pull` happens on the pipeline's readable end, - /// without deviating from the spec. Instead of immediately performing the - /// operation, it can subscribe to this promise and get notified once a `pull` - /// happens, or on the next tick if one is already pending. - /// - /// Why is this needed at all? The fact that a `TransformStream`'s writable side - /// has a high-water mark of `1` means that a `pull` is enqueued upon the TS's - /// creation. When a `ReadableStream` is later piped to the TS, that `pull` is - /// applied to it. Without this kind of mechanism, the source would have no - /// choice but to immediately perform the operation, only for the result to be - /// parked in the `TransformStream`. + /// A native source can wait on this promise instead of reading at once, so it + /// reads only when a `pull` happens on the pipeline's readable end. A + /// `TransformStream`'s writable side has a high-water mark of `1`, so piping + /// into it pulls from the source before anything reads the transform's + /// readable end. pub fn deferred_demand<'r>(&self, scope: &'r Scope<'_>) -> Option> { let transform = self.data().piped_to_identity_transform.get(scope)?; // Backpressure is set while the readable end holds no demand, and diff --git a/crates/builtins/web-streams/src/support.rs b/crates/builtins/web-streams/src/support.rs index 4df25e9..52db141 100644 --- a/crates/builtins/web-streams/src/support.rs +++ b/crates/builtins/web-streams/src/support.rs @@ -172,18 +172,15 @@ pub(crate) fn invoke_algorithm<'r>( } } -/// Attach native fulfillment/rejection reactions to `promise`, each carrying a +/// Attach native fulfillment/rejection reactions to `promise`, each called with a /// single payload value (typically the controller or reader the reaction -/// operates on). Mirrors the spec's "upon fulfillment / upon rejection of -/// promise" and "react to promise" phrasing. +/// operates on). Implements the spec's "upon fulfillment / upon rejection of +/// promise" and "react to promise". /// -/// These are internal reactions: the dependent promise is discarded and never -/// surfaced to author code, so it must not participate in unhandled-rejection -/// tracking. A fulfillment-only reaction on a promise that later rejects (e.g. -/// pipeTo's forward-close reaction on `reader.[[closedPromise]]`) would -/// otherwise produce a spurious unhandled rejection. Attaching the reaction also -/// marks `promise` itself handled, which is correct — the stream is consuming -/// it. +/// The dependent promise is never exposed to author code, so it is excluded from +/// unhandled-rejection tracking. Otherwise a fulfillment-only reaction on a +/// promise that later rejects would report a spurious unhandled rejection. +/// Attaching the reactions also marks `promise` handled. pub(crate) fn react<'r, P: ToJSVal<'r>>( scope: &'r Scope<'_>, promise: &Promise<'_>, diff --git a/crates/builtins/web-streams/src/transform/mod.rs b/crates/builtins/web-streams/src/transform/mod.rs index 5f71dfb..4fd32db 100644 --- a/crates/builtins/web-streams/src/transform/mod.rs +++ b/crates/builtins/web-streams/src/transform/mod.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception -//! +//! pub(crate) mod algorithms; @@ -11,5 +11,5 @@ pub mod transformer; pub use readable_writable_pair::ReadableWritablePair; pub use transform_stream::TransformStream; -pub use transform_stream_default_controller::TransformStreamDefaultController as ReadableStreamDefaultController; +pub use transform_stream_default_controller::TransformStreamDefaultController; pub use transformer::Transformer; diff --git a/crates/builtins/web-streams/src/transform/transform_stream.rs b/crates/builtins/web-streams/src/transform/transform_stream.rs index c3ec18d..8bedbaa 100644 --- a/crates/builtins/web-streams/src/transform/transform_stream.rs +++ b/crates/builtins/web-streams/src/transform/transform_stream.rs @@ -141,13 +141,10 @@ impl TransformStream { // Step 13: Otherwise, `resolve` _startPromise_ with undefined. start_promise.resolve(scope, HandleValue::undefined())?; } - // Not a spec step: mark an identity transform by linking its writable to its readable, + // Not a spec step: mark an identity transform by linking its writable to the transform, // so a native byte source piped into the writable can be propagated to the readable - // directly. Only a transform that runs no content code per chunk qualifies, so the - // shortcut has no content-visible effect: a transformer with any callback - // (`transform`/`start`/`flush`/`cancel` — which could enqueue extra chunks, or expect a - // cancel hook), or either strategy carrying a `size` function (invoked once per chunk on - // the non-shortcut path), disqualifies it. + // directly. The shortcut has no content-visible effect only if no content code runs per + // chunk, so any transformer callback or strategy `size` function disqualifies it. let no_strategy_size = readable_strategy .as_ref() .and_then(|s| s.size.as_ref()) diff --git a/crates/builtins/web-streams/src/writable/writable_stream.rs b/crates/builtins/web-streams/src/writable/writable_stream.rs index 8264a89..dcb5191 100644 --- a/crates/builtins/web-streams/src/writable/writable_stream.rs +++ b/crates/builtins/web-streams/src/writable/writable_stream.rs @@ -22,13 +22,6 @@ use js::Promise; use web_globals::events::algorithms::ScriptStackState; /// A traced slot holding a `Promise`. -/// -/// `must_root`: the slot wraps a `Heap`, so crown rejects holding a -/// `PromiseSlot` by value in a plain local across an allocation — the -/// untraced-`Heap`-across-GC hazard. While it lives inside a traced container (a -/// stream's write-request queue or an in-flight/close slot) its `Heap` is traced -/// normally; once moved out via `take`/`pop_front` it must be consumed -/// immediately (`into_promise`) or kept in a `RootedTraceableBox`. #[js::must_root] #[derive(core_runtime::Traceable, Default, js::ScopeRoot)] pub(crate) struct PromiseSlot(Heap); @@ -50,10 +43,6 @@ impl<'s> StackPromiseSlot<'s> { /// A pending abort request. /// /// -/// -/// `must_root`: holds `Heap` fields, so crown rejects holding it by value across -/// an allocation. It lives in the stream's traced `[[pendingAbortRequest]]` -/// slot; once moved out it must be consumed immediately or kept rooted. #[js::must_root] #[derive(core_runtime::Traceable, Default, js::ScopeRoot)] pub(crate) struct PendingAbortRequest { diff --git a/crates/builtins/web-streams/tests/async_iterator.rs b/crates/builtins/web-streams/tests/async_iterator.rs deleted file mode 100644 index 37da9ce..0000000 --- a/crates/builtins/web-streams/tests/async_iterator.rs +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception - -//! Keystone integration tests for `ReadableStream` async iteration -//! (`values()` / `[Symbol.asyncIterator]`): `for await` drains a stream, an -//! early `break` cancels the source (unless `preventCancel`), and the iterator's -//! prototype chains to `%AsyncIteratorPrototype%`. Covered far more thoroughly by -//! `streams/readable-streams/async-iterator.any.js`; this is the fast, -//! design-validating (and GC-rooting-validating) smoke test. - -// This file contains nothing platform-specific, so skip it on wasm32. -#![cfg(not(target_arch = "wasm32"))] - -use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; -use core_runtime::test_util::eval_out_with_setup; - -fn run(code: &str) -> String { - eval_out_with_setup( - || { - clear_global_initializers(); - register_global_initializer(web_streams::add_to_global); - }, - code, - ) -} - -/// `for await` drains all of the stream's chunks. -#[test] -fn for_await_drains_stream() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ start(c) { c.enqueue("a"); c.enqueue("b"); c.enqueue("c"); c.close(); } }); - (async () => { - let acc = ""; - for await (const chunk of rs) acc += chunk; - globalThis.__out = acc; - })(); - "#); - assert_eq!(out, "abc"); -} - -/// A promise chunk is adopted, not yielded as-is: the spec's "get the next -/// iteration result" resolves its promise with the raw chunk, so a thenable chunk -/// is unwrapped before the `{ value, done }` result is built. `for await` of a -/// stream that enqueues `Promise.resolve(5)` yields `5`, not the promise object. -#[test] -fn for_await_adopts_thenable_chunk() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ start(c) { c.enqueue(Promise.resolve(5)); c.close(); } }); - (async () => { - const vals = []; - for await (const x of rs) vals.push(x); - globalThis.__out = vals.join(",") + "|" + typeof vals[0]; - })(); - "#); - assert_eq!(out, "5|number"); -} - -/// A rejected-thenable chunk rejects `next()` and finishes the iterator (the -/// adopted rejection propagates), rather than yielding the rejected promise. -#[test] -fn for_await_rejected_thenable_chunk_throws() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ start(c) { c.enqueue(Promise.reject(new TypeError("boom"))); c.close(); } }); - (async () => { - try { - for await (const x of rs) { globalThis.__out = "yielded:" + x; return; } - globalThis.__out = "done-no-throw"; - } catch (e) { globalThis.__out = "threw:" + e.constructor.name + ":" + e.message; } - })(); - "#); - assert_eq!(out, "threw:TypeError:boom"); -} - -/// Breaking out of `for await` cancels the source (the stream becomes unlocked). -#[test] -fn break_cancels_source() { - let out = run(r#" - globalThis.__out = "pending"; - let cancelled = false; - const rs = new ReadableStream({ - start(c) { c.enqueue(1); c.enqueue(2); c.enqueue(3); }, - cancel() { cancelled = true; }, - }); - (async () => { - for await (const chunk of rs) { if (chunk === 2) break; } - globalThis.__out = "cancelled:" + cancelled + ",locked:" + rs.locked; - })(); - "#); - assert_eq!(out, "cancelled:true,locked:false"); -} - -/// `values({ preventCancel: true })` leaves the source uncancelled after break. -#[test] -fn prevent_cancel_leaves_source_open() { - let out = run(r#" - globalThis.__out = "pending"; - let cancelled = false; - const rs = new ReadableStream({ - start(c) { c.enqueue(1); c.enqueue(2); }, - cancel() { cancelled = true; }, - }); - (async () => { - for await (const chunk of rs.values({ preventCancel: true })) { break; } - globalThis.__out = "cancelled:" + cancelled; - })(); - "#); - assert_eq!(out, "cancelled:false"); -} - -/// The default async iterator is not a WebIDL interface, so it has no interface -/// object: `ReadableStreamAsyncIterator` must not leak onto the global, and the -/// prototype's class string is `"ReadableStream AsyncIterator"` (with a space), -/// per WebIDL §3.7.10. -#[test] -fn async_iterator_has_no_global_and_spaced_string_tag() { - let out = run(r#" - const leaked = "ReadableStreamAsyncIterator" in globalThis; - const tag = Object.prototype.toString.call(new ReadableStream().values()); - globalThis.__out = `leaked=${leaked},tag=${tag}`; - "#); - assert_eq!( - out, - "leaked=false,tag=[object ReadableStream AsyncIterator]" - ); -} - -/// The iterator's prototype chains to `%AsyncIteratorPrototype%` and exposes only -/// `next`/`return`. -#[test] -fn iterator_prototype_shape() { - let out = run(r#" - const it = new ReadableStream().values(); - const proto = Object.getPrototypeOf(it); - const AsyncIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf(async function* () {}).prototype); - const chained = Object.getPrototypeOf(proto) === AsyncIteratorPrototype; - const names = Object.getOwnPropertyNames(proto).sort().join(","); - const aliased = ReadableStream.prototype[Symbol.asyncIterator] === ReadableStream.prototype.values; - globalThis.__out = `${chained},${names},${aliased}`; - "#); - assert_eq!(out, "true,next,return,true"); -} - -/// `next()` clears the ongoing promise when the previous call settles, so a call -/// made after that runs the next steps directly instead of chaining behind the -/// settled promise. Both calls then settle after the same number of ticks. -#[test] -fn settled_next_does_not_delay_the_following_call() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ start(c) { c.enqueue(1); c.enqueue(2); } }); - const it = rs.values(); - function ticksUntil(p) { - let done = false; - p.then(() => { done = true; }); - return new Promise(resolve => { - let n = 0; - (function spin() { - if (done) { resolve(n); } else { n++; Promise.resolve().then(spin); } - })(); - }); - } - (async () => { - const first = await ticksUntil(it.next()); - const second = await ticksUntil(it.next()); - globalThis.__out = `${first},${second}`; - })(); - "#); - let (first, second) = out.split_once(',').expect("two tick counts"); - assert_eq!(first, second, "ticks: {out}"); -} diff --git a/crates/builtins/web-streams/tests/body.rs b/crates/builtins/web-streams/tests/body.rs deleted file mode 100644 index 4352e5b..0000000 --- a/crates/builtins/web-streams/tests/body.rs +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception - -//! Keystone test for the bytes → `ReadableStream` bridge (`web_streams::body`). -//! -//! Fetch (and the File API) create response/request body streams from Rust byte -//! buffers via `readable_stream_from_bytes`, then hand them to JS as `.body`. -//! This validates that such a Rust-created stream is a fully functional JS -//! `ReadableStream`: a reader drains it chunk-by-chunk and observes exactly the -//! original bytes, then `done`. Runs JS, drains microtasks, reads back a result -//! stashed on `globalThis`. - -// This file contains nothing platform-specific, so skip it on wasm32. -#![cfg(not(target_arch = "wasm32"))] - -use core_runtime::config::RuntimeConfig; -use core_runtime::runtime::{clear_global_initializers, register_global_initializer, Runtime}; -use core_runtime::test_util::eval_and_read_out; -use web_streams::readable::ReadableStream; - -/// Create a stream from `bytes`, expose it as `globalThis.__stream`, then evaluate -/// `code`, drain microtasks, and return `String(globalThis.__out)`. -fn run_with_stream(bytes: &[u8], code: &str) -> String { - clear_global_initializers(); - register_global_initializer(web_streams::add_to_global); - let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); - let scope = rt.default_global(); - let global = scope.global(); - let stream = ReadableStream::from_bytes(&scope, bytes).expect("create stream from bytes"); - global - .set_property(&scope, c"__stream", stream) - .expect("expose __stream"); - eval_and_read_out(&scope, code) -} - -/// A non-empty Rust-created stream drains to exactly its original bytes, then -/// closes. Chunks are `Uint8Array`s (as observed for `Response.body`). -#[test] -fn bytes_round_trip_through_js_reader() { - let out = run_with_stream( - b"hello world", - r#" - globalThis.__out = "pending"; - (async () => { - const reader = globalThis.__stream.getReader(); - const bytes = []; - let chunkCount = 0; - for (;;) { - const { value, done } = await reader.read(); - if (done) break; - chunkCount++; - if (!(value instanceof Uint8Array)) { - globalThis.__out = "chunk not Uint8Array: " + value; - return; - } - bytes.push(...value); - } - globalThis.__out = chunkCount + ":" + bytes.join(","); - })(); - "#, - ); - // "hello world" is 11 bytes, delivered as a single Uint8Array chunk. - assert_eq!(out, "1:104,101,108,108,111,32,119,111,114,108,100"); -} - -/// An empty Rust-created stream is immediately closed: the first `read()` -/// resolves to `{ done: true }` with no chunk. -#[test] -fn empty_stream_closes_immediately() { - let out = run_with_stream( - b"", - r#" - globalThis.__out = "pending"; - (async () => { - const reader = globalThis.__stream.getReader(); - const { value, done } = await reader.read(); - globalThis.__out = `done=${done},value=${value}`; - })(); - "#, - ); - assert_eq!(out, "done=true,value=undefined"); -} diff --git a/crates/builtins/web-streams/tests/pipe_to.rs b/crates/builtins/web-streams/tests/pipe_to.rs deleted file mode 100644 index c7707a0..0000000 --- a/crates/builtins/web-streams/tests/pipe_to.rs +++ /dev/null @@ -1,190 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception - -//! Keystone integration tests for `ReadableStream.prototype.pipeTo` and -//! `pipeThrough`: chunks flow from a source to a destination, the result -//! promise settles, `preventClose` leaves the destination open, source errors -//! propagate forward (aborting the destination and rejecting the promise), and -//! `pipeThrough` returns the transform's readable side. Covered far more -//! thoroughly by `streams/piping/*.any.js`; this is the fast, -//! design-validating (and GC-rooting-validating) smoke test. - -// This file contains nothing platform-specific, so skip it on wasm32. -#![cfg(not(target_arch = "wasm32"))] - -use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; -use core_runtime::test_util::eval_out_with_setup; - -fn run(code: &str) -> String { - eval_out_with_setup( - || { - clear_global_initializers(); - register_global_initializer(web_globals::add_to_global); - register_global_initializer(web_streams::add_to_global); - }, - code, - ) -} - -/// A WritableStream that records written chunks and whether it was closed. -const RECORDING: &str = r#" - function recordingWritable() { - const written = []; - const state = { closed: false, aborted: false }; - const ws = new WritableStream({ - write(chunk) { written.push(chunk); }, - close() { state.closed = true; }, - abort(reason) { state.aborted = true; state.abortReason = reason; }, - }); - return { ws, written, state }; - } -"#; - -/// Every chunk flows from the source to the destination, the destination is -/// closed, and the pipe promise fulfills. -#[test] -fn pipes_all_chunks_and_closes_dest() { - let out = run(&format!( - r#"{RECORDING} - globalThis.__out = "pending"; - const {{ ws, written, state }} = recordingWritable(); - const rs = new ReadableStream({{ start(c) {{ c.enqueue("a"); c.enqueue("b"); c.enqueue("c"); c.close(); }} }}); - rs.pipeTo(ws).then(() => {{ - globalThis.__out = written.join("") + "|closed:" + state.closed; - }}, e => {{ globalThis.__out = "rejected:" + e; }}); - "# - )); - assert_eq!(out, "abc|closed:true"); -} - -/// With `preventClose: true`, the destination is left open after the source -/// closes (the pipe promise still fulfills). -#[test] -fn prevent_close_leaves_dest_open() { - let out = run(&format!( - r#"{RECORDING} - globalThis.__out = "pending"; - const {{ ws, written, state }} = recordingWritable(); - const rs = new ReadableStream({{ start(c) {{ c.enqueue("x"); c.close(); }} }}); - rs.pipeTo(ws, {{ preventClose: true }}).then(() => {{ - globalThis.__out = written.join("") + "|closed:" + state.closed; - }}, e => {{ globalThis.__out = "rejected:" + e; }}); - "# - )); - assert_eq!(out, "x|closed:false"); -} - -/// A source error propagates forward: the destination is aborted and the pipe -/// promise rejects with the source's error. -#[test] -fn source_error_aborts_dest_and_rejects() { - let out = run(&format!( - r#"{RECORDING} - globalThis.__out = "pending"; - const {{ ws, written, state }} = recordingWritable(); - let ctrl; - const rs = new ReadableStream({{ start(c) {{ ctrl = c; }} }}); - const p = rs.pipeTo(ws); - ctrl.error(new Error("boom")); - p.then(() => {{ globalThis.__out = "fulfilled"; }}, - e => {{ globalThis.__out = "rejected:" + e.message + "|aborted:" + state.aborted; }}); - "# - )); - assert_eq!(out, "rejected:boom|aborted:true"); -} - -/// `pipeThrough` returns the transform's readable side, and chunks piped into -/// the writable side flow through (an identity transform) to it. -#[test] -fn pipe_through_identity_transform() { - let out = run(&format!( - r#"{RECORDING} - globalThis.__out = "pending"; - async function drain(stream) {{ - const reader = stream.getReader(); - const acc = []; - for (;;) {{ - const {{ value, done }} = await reader.read(); - if (done) break; - acc.push(value); - }} - return acc.join(""); - }} - const rs = new ReadableStream({{ start(c) {{ c.enqueue("p"); c.enqueue("q"); c.close(); }} }}); - const ts = new TransformStream(); - const readable = rs.pipeThrough(ts); - const isReadable = readable instanceof ReadableStream; - drain(readable).then(s => {{ globalThis.__out = s + "|" + isReadable; }}); - "# - )); - assert_eq!(out, "pq|true"); -} - -/// Aborting the pipe's signal mid-flight rejects the pipe promise with the abort -/// reason, cancels the source, and aborts the destination. This exercises the -/// abort algorithm, the wait-for-all composite action, and the deferred-shutdown -/// state slots — the most allocation-heavy path, validated under compacting GC. -#[test] -fn abort_signal_rejects_and_cancels_both_sides() { - let out = run(&format!( - r#"{RECORDING} - globalThis.__out = "pending"; - const {{ ws, written, state }} = recordingWritable(); - const ac = new AbortController(); - let cancelled = false; - const rs = new ReadableStream({{ - start(c) {{ c.enqueue("a"); }}, - cancel() {{ cancelled = true; }}, - }}); - const p = rs.pipeTo(ws, {{ signal: ac.signal }}); - ac.abort(new Error("stop")); - p.then(() => {{ globalThis.__out = "fulfilled"; }}, - e => {{ globalThis.__out = "rejected:" + e.message + "|cancelled:" + cancelled + "|aborted:" + state.aborted; }}); - "# - )); - assert_eq!(out, "rejected:stop|cancelled:true|aborted:true"); -} - -/// pipeTo's abort handling is a DOM abort algorithm, not an `abort` event -/// listener: an author `abort` listener registered earlier that calls -/// `stopImmediatePropagation()` must not suppress the pipe's shutdown. The pipe -/// still rejects with the abort reason. -#[test] -fn abort_signal_not_suppressed_by_stop_immediate_propagation() { - let out = run(&format!( - r#"{RECORDING} - globalThis.__out = "pending"; - const {{ ws, state }} = recordingWritable(); - const ac = new AbortController(); - ac.signal.addEventListener("abort", (e) => {{ e.stopImmediatePropagation(); }}); - const rs = new ReadableStream({{ start(c) {{ c.enqueue("a"); }} }}); - const p = rs.pipeTo(ws, {{ signal: ac.signal }}); - ac.abort(new Error("stop")); - p.then(() => {{ globalThis.__out = "fulfilled"; }}, - e => {{ globalThis.__out = "rejected:" + e.message + "|aborted:" + state.aborted; }}); - "# - )); - assert_eq!(out, "rejected:stop|aborted:true"); -} - -/// Aborting a pipe runs author code synchronously: the destination controller's -/// `signal` listeners. A listener that enqueues into the source re-enters the -/// pipe's pending read request, which must not find the pipe state borrowed. -#[test] -fn abort_listener_enqueueing_into_source_does_not_panic() { - let out = run(r#" - globalThis.__out = "pending"; - let sourceController; - const rs = new ReadableStream({ start(c) { sourceController = c; } }); - const ws = new WritableStream({ - start(c) { - c.signal.addEventListener("abort", () => sourceController.enqueue("late")); - }, - }); - const ac = new AbortController(); - const p = rs.pipeTo(ws, { signal: ac.signal }); - Promise.resolve().then(() => ac.abort(new Error("stop"))); - p.then(() => { globalThis.__out = "fulfilled"; }, - e => { globalThis.__out = "rejected:" + e.message; }); - "#); - assert_eq!(out, "rejected:stop"); -} diff --git a/crates/builtins/web-streams/tests/readable_byob.rs b/crates/builtins/web-streams/tests/readable_byob.rs deleted file mode 100644 index 14debeb..0000000 --- a/crates/builtins/web-streams/tests/readable_byob.rs +++ /dev/null @@ -1,346 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception - -//! Keystone integration tests for the BYOB reader / read-into path: pull-into -//! descriptors, `byobRequest.respond` / `respondWithNewView`, and -//! `autoAllocateChunkSize`. -//! -//! These exercise the parts the M2 design rests on: a `read(view)` transfers the -//! caller's buffer into a pull-into descriptor, the source fills it through -//! `byobRequest.view` and commits with `respond`, and the filled region comes -//! back as a view of the requested type. Auto-allocation routes a default -//! `read()` through the same machinery. They run JS, drain microtasks, and read -//! back a result stashed on `globalThis`. -//! -//! WPT `streams/readable-byte-streams/*` covers this far more thoroughly; this is -//! the fast, design-validating smoke test (and the GC-rooting keystone under -//! zeal, since pull-into transfers buffers and creates views). - -// This file contains nothing platform-specific, so skip it on wasm32. -#![cfg(not(target_arch = "wasm32"))] - -use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; -use core_runtime::test_util::eval_out_with_setup; - -/// Evaluate `code`, drain microtasks, and return `String(globalThis.__out)`. -fn run(code: &str) -> String { - eval_out_with_setup( - || { - clear_global_initializers(); - register_global_initializer(web_streams::add_to_global); - }, - code, - ) -} - -/// A BYOB `read(view)`: the source fills the provided view through -/// `byobRequest.view` and commits with `respond`, and the filled region is -/// returned as a `Uint8Array`. Exercises the pull-into descriptor, buffer -/// transfer, and `ReadableByteStreamControllerRespond`. -#[test] -fn byob_read_respond() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ - type: "bytes", - pull(c) { - const v = c.byobRequest.view; - v[0] = 11; v[1] = 22; v[2] = 33; - c.byobRequest.respond(3); - }, - }); - rs.getReader({ mode: "byob" }).read(new Uint8Array(3)).then(r => { - globalThis.__out = `${r.value.constructor.name},${Array.from(r.value).join("-")},${r.done}`; - }); - "#); - assert_eq!(out, "Uint8Array,11-22-33,false"); -} - -/// `respondWithNewView`: the source writes into a fresh view over the request's -/// buffer region and commits it. Exercises -/// `ReadableByteStreamControllerRespondWithNewView`. -#[test] -fn byob_respond_with_new_view() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ - type: "bytes", - pull(c) { - const req = c.byobRequest; - const nv = new Uint8Array(req.view.buffer, req.view.byteOffset, 2); - nv[0] = 99; nv[1] = 88; - req.respondWithNewView(nv); - }, - }); - rs.getReader({ mode: "byob" }).read(new Uint8Array(4)).then(r => { - globalThis.__out = `${Array.from(r.value).join("-")},len=${r.value.length}`; - }); - "#); - assert_eq!(out, "99-88,len=2"); -} - -/// `autoAllocateChunkSize`: a default `read()` on a byte stream routes through a -/// pull-into descriptor with an auto-allocated buffer; the source fills it via -/// `byobRequest` and the bytes come back as a `Uint8Array`. Exercises PullSteps -/// step 5 and the read-into-to-default-reader commit. -#[test] -fn auto_allocate_default_read() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ - type: "bytes", - autoAllocateChunkSize: 16, - pull(c) { - const v = c.byobRequest.view; - v[0] = 7; v[1] = 8; - c.byobRequest.respond(2); - }, - }); - rs.getReader().read().then(r => { - globalThis.__out = `${r.value.constructor.name},len=${r.value.length},${r.value[0]}-${r.value[1]}`; - }); - "#); - assert_eq!(out, "Uint8Array,len=2,7-8"); -} - -/// A typed-array view with `min` honours the element alignment: a `read(view, { -/// min })` on a `Uint16Array` is fulfilled only once at least `min` elements are -/// available. Here the source supplies exactly the requested bytes. -#[test] -fn byob_read_typed_array_min() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ - type: "bytes", - pull(c) { - const v = c.byobRequest.view; - for (let i = 0; i < v.byteLength; i++) v[i] = i + 1; - c.byobRequest.respond(v.byteLength); - }, - }); - rs.getReader({ mode: "byob" }).read(new Uint16Array(2), { min: 2 }).then(r => { - globalThis.__out = `${r.value.constructor.name},len=${r.value.length},${r.value[0]}`; - }); - "#); - // Bytes 1,2,3,4 little-endian -> u16 [0x0201, 0x0403] = [513, 1027]. - assert_eq!(out, "Uint16Array,len=2,513"); -} - -/// A BYOB `read()` with no view argument rejects (it does not throw -/// synchronously): a promise-returning WebIDL operation surfaces an -/// argument-conversion failure as a rejected promise. This drives the macro's -/// `ResultPromise` rejection branch (`new_rejected_with_pending_error` with a -/// live pending exception), so under GC zeal it exercises that path directly. -#[test] -fn byob_read_without_view_rejects() { - let out = run(r#" - globalThis.__out = "pending"; - const reader = new ReadableStream({ type: "bytes" }).getReader({ mode: "byob" }); - let threw = false; - let p; - try { p = reader.read(); } catch (e) { threw = true; } - Promise.resolve(p).then( - () => { globalThis.__out = `threw=${threw},resolved`; }, - e => { globalThis.__out = `threw=${threw},rejected:${e.constructor.name}`; }, - ); - "#); - assert_eq!(out, "threw=false,rejected:TypeError"); -} - -/// Teeing a byte stream and reading both branches: `ReadableByteStreamTee` -/// acquires a default reader, clones each chunk's buffer for the second branch, -/// and pushes into both branches' byte controllers. This is the -/// allocation-heavy native-callback + GC-traced-state-object path; the buffer -/// clone and the dual enqueue make it the byte-tee GC-rooting keystone. -#[test] -fn byte_tee_reads_both_branches() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ - type: "bytes", - start(c) { c.enqueue(new Uint8Array([5, 6, 7])); }, - }); - const [a, b] = rs.tee(); - Promise.all([a.getReader().read(), b.getReader().read()]).then(([ra, rb]) => { - globalThis.__out = - `${Array.from(ra.value).join("-")}|${Array.from(rb.value).join("-")}|` + - `${ra.value.buffer !== rb.value.buffer}`; - }); - "#); - // Both branches see the same bytes, but over distinct (cloned) buffers. - assert_eq!(out, "5-6-7|5-6-7|true"); -} - -/// Three un-awaited BYOB reads leave three pending pull-into descriptors; a -/// single `enqueue` fills the head while the other two are still queued. The -/// fill loop calls `ReadableByteStreamControllerFillHeadPullIntoDescriptor` -/// after popping the head into a stack local, so the descriptor deque still -/// holds the two trailing descriptors at that point — a regression test that -/// fill_head does not assume at most one pending pull-into remains. -#[test] -fn byob_multiple_pending_reads_then_enqueue() { - let out = run(r#" - globalThis.__out = "pending"; - let controller; - const rs = new ReadableStream({ - type: "bytes", - start(c) { controller = c; }, - }); - const reader = rs.getReader({ mode: "byob" }); - const p1 = reader.read(new Uint8Array(10)); - reader.read(new Uint8Array(10)); - reader.read(new Uint8Array(10)); - controller.enqueue(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); - p1.then(r => { - globalThis.__out = `${r.value.constructor.name},len=${r.value.length},${r.value[0]}-${r.value[9]}`; - }); - "#); - // The first read is fulfilled with all 10 bytes; the other two stay pending. - assert_eq!(out, "Uint8Array,len=10,1-10"); -} - -/// A `Float16Array` BYOB view round-trips with its element kind intact: the -/// committed chunk comes back as a `Float16Array` (element size 2), not a -/// `DataView`, and the `min` option is validated against the view's element -/// length rather than its byte length. -#[test] -fn byob_read_float16_view_kind() { - let out = run(r#" - globalThis.__out = "pending"; - if (typeof Float16Array === "undefined") { - globalThis.__out = "no-float16"; - } else { - const rs = new ReadableStream({ - type: "bytes", - pull(c) { - const v = c.byobRequest.view; - c.byobRequest.respond(v.byteLength); - }, - }); - // `min` of 3 exceeds the 2-element view; the spec validates against the - // element length, so this rejects with a RangeError before any pull. - rs.getReader({ mode: "byob" }).read(new Float16Array(2), { min: 3 }).then( - r => { globalThis.__out = `resolved:${r.value.constructor.name}`; }, - e => { globalThis.__out = `rejected:${e.constructor.name}`; }, - ); - } - "#); - assert_eq!(out, "rejected:RangeError"); -} - -/// Teeing a byte stream and doing a multi-byte-element BYOB read on a branch can -/// leave that branch's pending pull-into partially filled (e.g. 3 of 4 bytes for -/// an `Int32Array`). If the source then closes, the branch's -/// `ReadableByteStreamControllerClose` hits its "insufficient bytes" `TypeError`. -/// The spec marks that close `!` (infallible), but the partial fill makes it -/// reachable, so the branch read must reject with a `TypeError` rather than -/// aborting the runtime. -#[test] -fn byte_tee_branch_partial_fill_close_rejects_not_aborts() { - let out = run(r#" - globalThis.__out = "pending"; - let step = 0; - const rs = new ReadableStream({ - type: "bytes", - pull(c) { - if (step === 0) { - step = 1; - const v = c.byobRequest.view; - v[0] = 1; v[1] = 2; v[2] = 3; - c.byobRequest.respond(3); // 3 of 4 bytes -> branch partial fill - } else { - c.close(); - c.byobRequest.respond(0); - } - }, - }); - const [b1, b2] = rs.tee(); - // Draining branch2 to completion discriminates the clean error path from - // the (dev-mode-masked) abort: branch2 receives the 3-byte clone, then its - // next read must reject (the source closed and the misaligned branch1 close - // errored the tee). Aborting at branch1's close instead leaves branch2 - // unclosed, so that second read would hang forever. - async function drainB2() { - const r = b2.getReader(); - let n = 0; - try { - for (;;) { - const { done } = await r.read(); - if (done) return "b2:done@" + n; - n++; - } - } catch (e) { return "b2:" + e.constructor.name + "@" + n; } - } - const r1 = b1.getReader({ mode: "byob" }).read(new Int32Array(1)).then( - () => "b1:resolved", e => "b1:" + e.constructor.name); - Promise.all([r1, drainB2()]).then(([a, b]) => { globalThis.__out = a + "," + b; }); - "#); - assert_eq!(out, "b1:TypeError,b2:TypeError@1"); -} - -/// Reading a byte stream's BYOB reader after `close()` resolves with an empty -/// view and `done: true`. -#[test] -fn byob_read_after_close_is_done() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ - type: "bytes", - start(c) { c.close(); }, - }); - rs.getReader({ mode: "byob" }).read(new Uint8Array(8)).then(r => { - globalThis.__out = `${r.value.constructor.name},len=${r.value.length},${r.done}`; - }); - "#); - assert_eq!(out, "Uint8Array,len=0,true"); -} - -/// `respond(bytesWritten)` is `[EnforceRange] unsigned long long`: a negative -/// argument throws a `TypeError` at argument conversion, not a `RangeError` from -/// the length checks (which is what a wrapping conversion produces). -#[test] -fn respond_enforce_range_rejects_negative() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ - type: "bytes", - pull(c) { - try { c.byobRequest.respond(-1); globalThis.__out = "no-throw"; } - catch (e) { globalThis.__out = "respond:" + e.constructor.name; } - }, - }); - rs.getReader({ mode: "byob" }).read(new Uint8Array(4)); - "#); - assert_eq!(out, "respond:TypeError"); -} - -/// WebIDL converts arguments left to right: `read(view, options)` must coerce -/// `view` to an `ArrayBufferView` (rejecting a non-view with a `TypeError`) before -/// the `options` dictionary is converted, so an author getter on `options.min` is -/// never invoked when the view is invalid. -#[test] -fn byob_read_validates_view_before_options() { - let out = run(r#" - globalThis.__out = "pending"; - let getterRan = false; - const reader = new ReadableStream({ type: "bytes" }).getReader({ mode: "byob" }); - reader.read(42, { get min() { getterRan = true; throw new Error("boom"); } }).then( - () => { globalThis.__out = "resolved"; }, - e => { globalThis.__out = `rejected:${e.constructor.name}|getterRan:${getterRan}`; }, - ); - "#); - assert_eq!(out, "rejected:TypeError|getterRan:false"); -} - -/// `autoAllocateChunkSize` is `[EnforceRange] unsigned long long`: a negative -/// value throws a `TypeError` at construction, rather than wrapping to a huge -/// positive value and constructing successfully. -#[test] -fn auto_allocate_chunk_size_enforce_range() { - let out = run(r#" - let r; - try { new ReadableStream({ type: "bytes", autoAllocateChunkSize: -1 }); r = "no-throw"; } - catch (e) { r = e.constructor.name; } - globalThis.__out = r; - "#); - assert_eq!(out, "TypeError"); -} diff --git a/crates/builtins/web-streams/tests/readable_byte.rs b/crates/builtins/web-streams/tests/readable_byte.rs deleted file mode 100644 index a73fb70..0000000 --- a/crates/builtins/web-streams/tests/readable_byte.rs +++ /dev/null @@ -1,180 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception - -//! Keystone integration tests for the readable byte stream path (default -//! reader, no auto-allocation). -//! -//! These validate the parts the byte controller's design rests on, on a -//! minimal end-to-end slice: the queue of `ArrayBuffer`-backed entries, buffer -//! transfer on `enqueue`, constructing a `Uint8Array` view over a transferred -//! region, the byte controller's own start/pull reactions, and the -//! default-reader `[[PullSteps]]` dispatch. They run JS, drain the microtask -//! queue, and read back a result stashed on `globalThis`. -//! -//! Behaviour here is covered far more thoroughly by the -//! `streams/readable-byte-streams` WPT suites; this is the fast, -//! design-validating smoke test (and the GC-rooting keystone under zeal). - -// This file contains nothing platform-specific, so skip it on wasm32. -#![cfg(not(target_arch = "wasm32"))] - -use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; -use core_runtime::test_util::eval_out_with_setup; - -/// Evaluate `code`, drain microtasks, and return `String(globalThis.__out)`. -fn run(code: &str) -> String { - eval_out_with_setup( - || { - clear_global_initializers(); - register_global_initializer(web_streams::add_to_global); - }, - code, - ) -} - -/// A byte chunk enqueued during `start` is queued, then delivered to a `read()` -/// as a fresh `Uint8Array` over the transferred buffer. Exercises the queue, -/// `TransferArrayBuffer`, the `Uint8Array`-over-region construction, and -/// `FillReadRequestFromQueue`. -#[test] -fn enqueue_in_start_then_read() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ - type: "bytes", - start(c) { c.enqueue(new Uint8Array([1, 2, 3])); }, - }); - rs.getReader().read().then(r => { - globalThis.__out = `${r.value.constructor.name},${Array.from(r.value).join("-")},${r.done}`; - }); - "#); - assert_eq!(out, "Uint8Array,1-2-3,false"); -} - -/// A `pull`-driven byte source: the read request is queued first, then -/// satisfied once `start` settles and `CallPullIfNeeded` invokes `pull` (which -/// enqueues directly into the waiting read request). Exercises the byte pull -/// reaction and `ProcessReadRequestsUsingQueue`/fulfill-via-`Construct`. -#[test] -fn pull_driven_byte_read() { - let out = run(r#" - globalThis.__out = "pending"; - let pulls = 0; - const rs = new ReadableStream({ - type: "bytes", - pull(c) { pulls++; c.enqueue(new Uint8Array([42])); }, - }); - rs.getReader().read().then(r => { - globalThis.__out = `${Array.from(r.value).join("-")},pulls=${pulls}`; - }); - "#); - assert_eq!(out, "42,pulls=1"); -} - -/// Closing after enqueuing drains the queue before delivering `done`. The first -/// read gets the chunk; the second sees the close. -#[test] -fn enqueue_then_close_drains_then_done() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ - type: "bytes", - start(c) { c.enqueue(new Uint8Array([9])); c.close(); }, - }); - const reader = rs.getReader(); - reader.read().then(r1 => { - const first = `${Array.from(r1.value).join("-")},${r1.done}`; - reader.read().then(r2 => { - globalThis.__out = `${first}|${r2.value},${r2.done}`; - }); - }); - "#); - assert_eq!(out, "9,false|undefined,true"); -} - -/// `cancel()` runs the underlying byte source's cancel algorithm with the -/// reason, exercising the byte controller's `[[CancelSteps]]`. -#[test] -fn cancel_runs_byte_cancel_steps() { - let out = run(r#" - globalThis.__out = "pending"; - let reason = null; - const rs = new ReadableStream({ type: "bytes", cancel(r) { reason = r; } }); - rs.cancel("stop").then(() => { globalThis.__out = `cancelled:${reason}`; }); - "#); - assert_eq!(out, "cancelled:stop"); -} - -/// `desiredSize` reflects the strategy high-water mark minus the queued bytes, -/// and `byobRequest` is null with no pending pull-into. Exercises the two -/// getters and `GetDesiredSize`. -#[test] -fn desired_size_and_byob_request_getters() { - let out = run(r#" - let ds0, dsAfter, br; - const rs = new ReadableStream({ - type: "bytes", - start(c) { - ds0 = c.desiredSize; - c.enqueue(new Uint8Array([1, 2, 3, 4])); - dsAfter = c.desiredSize; - br = c.byobRequest; - }, - }, { highWaterMark: 10 }); - globalThis.__out = `${ds0},${dsAfter},${br}`; - "#); - assert_eq!(out, "10,6,null"); -} - -/// Enqueuing a zero-length view or a non-`ArrayBufferView` throws a -/// `TypeError`; a byte source with a positive `autoAllocateChunkSize` -/// constructs fine, while `autoAllocateChunkSize: 0` throws a `TypeError`. -#[test] -fn enqueue_validation_and_auto_allocate() { - let out = run(r#" - let zeroThrew = false, nonViewThrew = false, auto16Ok = false, auto0Threw = false; - const rs = new ReadableStream({ - type: "bytes", - start(c) { - try { c.enqueue(new Uint8Array(0)); } catch (e) { zeroThrew = e instanceof TypeError; } - try { c.enqueue([1]); } catch (e) { nonViewThrew = e instanceof TypeError; } - }, - }); - try { new ReadableStream({ type: "bytes", autoAllocateChunkSize: 16 }); auto16Ok = true; } - catch (e) {} - try { new ReadableStream({ type: "bytes", autoAllocateChunkSize: 0 }); } - catch (e) { auto0Threw = e instanceof TypeError; } - globalThis.__out = `${zeroThrew},${nonViewThrew},${auto16Ok},${auto0Threw}`; - "#); - assert_eq!(out, "true,true,true,true"); -} - -/// For a byte stream, a *callable* `size` is rejected with a `RangeError` (a -/// byte stream's strategy must not have a size function), but a *non-callable* -/// `size` is rejected earlier with a `TypeError` — the `QueuingStrategySize` -/// callback conversion fails before the byte-stream RangeError. Not covered by -/// the enabled WPT (`general.any.js` only exercises callable size functions). -#[test] -fn byte_stream_size_function_error_ordering() { - let out = run(r#" - let callableErr = "none", nonCallableErr = "none"; - try { new ReadableStream({ type: "bytes" }, { size() { return 1; } }); } - catch (e) { callableErr = e.constructor.name; } - try { new ReadableStream({ type: "bytes" }, { size: {} }); } - catch (e) { nonCallableErr = e.constructor.name; } - globalThis.__out = `${callableErr},${nonCallableErr}`; - "#); - assert_eq!(out, "RangeError,TypeError"); -} - -/// A non-callable underlying-source callback throws a `TypeError` at dictionary -/// conversion, before the byte-stream + size-function `RangeError`. -#[test] -fn non_callable_callback_throws_type_error_before_byte_size_check() { - let out = run(r#" - let err = "none"; - try { new ReadableStream({ pull: {}, type: "bytes" }, { size: () => 1 }); err = "no-throw"; } - catch (e) { err = e.constructor.name; } - globalThis.__out = err; - "#); - assert_eq!(out, "TypeError"); -} diff --git a/crates/builtins/web-streams/tests/readable_default.rs b/crates/builtins/web-streams/tests/readable_default.rs deleted file mode 100644 index 8b5e4d3..0000000 --- a/crates/builtins/web-streams/tests/readable_default.rs +++ /dev/null @@ -1,182 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception - -//! Keystone integration tests for the readable default path. -//! -//! These validate the three things the implementation's design rests on, on a -//! minimal end-to-end slice: native promise reactions created via -//! `Function::new_callback` (the start/pull reactions), the `this`-binding of -//! the underlying source's start/pull callbacks, and read-request promise -//! resolution. They run JS, drain the microtask queue, and read back a result -//! stashed on `globalThis`. -//! -//! Behaviour here is covered far more thoroughly by the `streams/readable-streams` -//! WPT suites; this is the fast, design-validating smoke test. - -// This file contains nothing platform-specific, so skip it on wasm32. -#![cfg(not(target_arch = "wasm32"))] - -use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; -use core_runtime::test_util::eval_out_with_setup; - -/// Evaluate `code`, drain microtasks, and return `String(globalThis.__out)`. -fn run(code: &str) -> String { - eval_out_with_setup( - || { - clear_global_initializers(); - register_global_initializer(web_streams::add_to_global); - }, - code, - ) -} - -/// Bound functions are accepted as WebIDL callback-type members: a bound `pull` -/// and a bound queuing-strategy `size` are both `IsCallable` even though a bound -/// function is not a `JSFunction`. The dictionary conversion must not reject -/// them, and they must be invoked with their bound `this`. -#[test] -fn bound_function_callbacks() { - let out = run(r#" - globalThis.__out = "pending"; - let sizeCalledWith = null; - // `start` enqueues before any reader exists, so the chunk goes through the - // queue and the strategy `size` is actually invoked (an enqueue that - // satisfies a pending read bypasses `size`). - const src = { first: 7, start(c) { c.enqueue(this.first); } }; - const strat = { - factor: 3, - size(chunk) { sizeCalledWith = chunk; return chunk * this.factor; }, - }; - const rs = new ReadableStream( - { start: src.start.bind(src) }, - { size: strat.size.bind(strat), highWaterMark: 100 }, - ); - rs.getReader().read().then(r => { - globalThis.__out = `${r.value},${r.done},size=${sizeCalledWith}`; - }); - "#); - // Bound `start` enqueues this.first (7); bound `size` is invoked with that chunk. - assert_eq!(out, "7,false,size=7"); -} - -/// A promise-returning operation invoked with a wrong-`this` (the prototype, or -/// `undefined`) must *reject* with a `TypeError`, not throw synchronously — -/// WebIDL §3.7.7. This drives the macro's `ResultPromise` brand-check reject -/// branch (a rejected promise created from the pending brand-check exception), a -/// distinct path from the argument-error reject branch; under GC zeal it -/// exercises that branch directly. -#[test] -fn cancel_with_wrong_this_rejects() { - let out = run(r#" - globalThis.__out = "pending"; - let threw = false; - let p; - try { p = ReadableStream.prototype.cancel.call(undefined); } - catch (e) { threw = true; } - Promise.resolve(p).then( - () => { globalThis.__out = `threw=${threw},resolved`; }, - e => { globalThis.__out = `threw=${threw},rejected:${e.constructor.name}`; }, - ); - "#); - assert_eq!(out, "threw=false,rejected:TypeError"); -} - -/// A promise-typed attribute getter (`closed`) invoked with a wrong-`this` -/// rejects with a `TypeError`, not throws — WebIDL §3.7.7 ("Attributes"). This -/// drives the accessor trampoline's promise-getter reject branch, distinct from -/// the operation path; under GC zeal it exercises that branch directly. -#[test] -fn closed_getter_with_wrong_this_rejects() { - let out = run(r#" - globalThis.__out = "pending"; - const getter = Object.getOwnPropertyDescriptor( - ReadableStreamDefaultReader.prototype, "closed").get; - let threw = false; - let p; - try { p = getter.call(undefined); } catch (e) { threw = true; } - Promise.resolve(p).then( - () => { globalThis.__out = `threw=${threw},resolved`; }, - e => { globalThis.__out = `threw=${threw},rejected:${e.constructor.name}`; }, - ); - "#); - assert_eq!(out, "threw=false,rejected:TypeError"); -} - -/// A chunk enqueued during `start` is delivered to a `read()`, exercising the -/// start reaction (native `new_callback`), the `start` callback's `this` -/// binding, and read-request resolution. -#[test] -fn start_enqueue_then_read() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ start(c) { c.enqueue("hello"); } }); - const reader = rs.getReader(); - reader.read().then(r => { globalThis.__out = `${r.value},${r.done}`; }); - "#); - assert_eq!(out, "hello,false"); -} - -/// A `pull`-driven source: the read request is queued first, then satisfied -/// once `start` settles and `CallPullIfNeeded` invokes `pull` (which enqueues -/// and closes). Exercises the pull reaction and the `pull` callback's `this`. -#[test] -fn pull_then_read_and_close() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ - pull(c) { c.enqueue("p"); c.close(); }, - }); - const reader = rs.getReader(); - reader.read().then(r => { globalThis.__out = `${r.value},${r.done}`; }); - "#); - assert_eq!(out, "p,false"); -} - -/// Reading a closed stream resolves with `{ value: undefined, done: true }`. -#[test] -fn read_after_close_is_done() { - let out = run(r#" - globalThis.__out = "pending"; - const rs = new ReadableStream({ start(c) { c.close(); } }); - rs.getReader().read().then(r => { globalThis.__out = `${r.value},${r.done}`; }); - "#); - assert_eq!(out, "undefined,true"); -} - -/// `locked` reflects reader acquisition, and a second `getReader` throws. -#[test] -fn locked_and_double_get_reader() { - let out = run(r#" - const rs = new ReadableStream(); - const before = rs.locked; - rs.getReader(); - const after = rs.locked; - let threw = false; - try { rs.getReader(); } catch (e) { threw = e instanceof TypeError; } - globalThis.__out = `${before},${after},${threw}`; - "#); - assert_eq!(out, "false,true,true"); -} - -/// A `read()` result is an ordinary object with *own* `value`/`done` data -/// properties (the spec's `CreateIterResultObject`), so a hostile accessor -/// installed on `Object.prototype` cannot intercept or replace them. Building the -/// result with `[[Set]]` would walk the prototype chain and let the accessor -/// observe the chunk and prevent the own property from being created. -#[test] -fn read_result_has_own_properties_immune_to_proto_accessor() { - let out = run(r#" - globalThis.__out = "pending"; - let leaked = "none"; - Object.defineProperty(Object.prototype, "value", { - configurable: true, - get() { return "HIJACKED"; }, - set(v) { leaked = v; }, - }); - const rs = new ReadableStream({ start(c) { c.enqueue(42); } }); - rs.getReader().read().then(r => { - globalThis.__out = - `own=${Object.hasOwn(r, "value")},value=${r.value},leaked=${leaked}`; - }); - "#); - assert_eq!(out, "own=true,value=42,leaked=none"); -} diff --git a/crates/builtins/web-streams/tests/stream_from.rs b/crates/builtins/web-streams/tests/stream_from.rs deleted file mode 100644 index 123cb5b..0000000 --- a/crates/builtins/web-streams/tests/stream_from.rs +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception - -//! Keystone integration tests for `ReadableStream.from`: a sync iterable (array) -//! and an async iterable (async generator) both stream their values, and the -//! returned stream's cancel calls the iterator's `return`. Covered far more -//! thoroughly by `streams/readable-streams/from.any.js`; this is the fast, -//! design-validating (and GC-rooting-validating) smoke test for the native -//! iterator-record callbacks. - -// This file contains nothing platform-specific, so skip it on wasm32. -#![cfg(not(target_arch = "wasm32"))] - -use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; -use core_runtime::test_util::eval_out_with_setup; - -fn run(code: &str) -> String { - eval_out_with_setup( - || { - clear_global_initializers(); - register_global_initializer(web_streams::add_to_global); - }, - code, - ) -} - -const DRAIN: &str = r#" - async function drain(stream) { - const reader = stream.getReader(); - const out = []; - for (;;) { - const { value, done } = await reader.read(); - if (done) break; - out.push(value); - } - return out.join(""); - } -"#; - -/// A sync iterable (array) streams its values via the async-from-sync wrapper. -#[test] -fn from_sync_array_streams_values() { - let out = run(&format!( - r#"{DRAIN} - globalThis.__out = "pending"; - const rs = ReadableStream.from(["a", "b", "c"]); - drain(rs).then(s => {{ globalThis.__out = s; }}); - "# - )); - assert_eq!(out, "abc"); -} - -/// An async iterable (async generator) streams its values. -#[test] -fn from_async_generator_streams_values() { - let out = run(&format!( - r#"{DRAIN} - globalThis.__out = "pending"; - async function* gen() {{ yield "x"; yield "y"; yield "z"; }} - const rs = ReadableStream.from(gen()); - drain(rs).then(s => {{ globalThis.__out = s; }}); - "# - )); - assert_eq!(out, "xyz"); -} - -/// A sync iterable that yields a rejected promise: the async-from-sync wrapper -/// adopts the rejection and closes the sync iterator (running its `return` / -/// `finally`) before the stream errors, per AsyncFromSyncIteratorContinuation -/// with closeOnRejection = true. -#[test] -fn from_sync_rejected_value_closes_iterator() { - let out = run(r#" - globalThis.__out = "pending"; - let cleaned = false; - const it = (function* () { - try { yield Promise.reject(new TypeError("boom")); } - finally { cleaned = true; } - })(); - ReadableStream.from(it).getReader().read().then( - () => { globalThis.__out = "resolved"; }, - e => { globalThis.__out = `rejected:${e.constructor.name}:${e.message}|cleaned:${cleaned}`; }, - ); - "#); - assert_eq!(out, "rejected:TypeError:boom|cleaned:true"); -} - -/// Cancelling the stream invokes the iterator's `return` method. -#[test] -fn from_cancel_calls_return() { - let out = run(&format!( - r#"{DRAIN} - globalThis.__out = "pending"; - let returned = false; - const iterable = {{ - [Symbol.asyncIterator]() {{ - let i = 0; - return {{ - next() {{ return Promise.resolve({{ value: i++, done: false }}); }}, - return(v) {{ returned = true; return Promise.resolve({{ value: v, done: true }}); }}, - }}; - }} - }}; - const rs = ReadableStream.from(iterable); - const reader = rs.getReader(); - reader.read().then(() => reader.cancel("stop")).then(() => {{ - globalThis.__out = "returned:" + returned; - }}); - "# - )); - assert_eq!(out, "returned:true"); -} diff --git a/crates/builtins/web-streams/tests/streams.rs b/crates/builtins/web-streams/tests/streams.rs new file mode 100644 index 0000000..d75630d --- /dev/null +++ b/crates/builtins/web-streams/tests/streams.rs @@ -0,0 +1,550 @@ +// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception + +//! Stream behavior that the WPT `streams` suites don't check: the Rust-side +//! `ReadableStream::from_bytes` API, WebIDL conversion order, chunk adoption by +//! async iteration, isolation of internal state from `Object.prototype`, and +//! regressions for specific bugs. + +// This file contains nothing platform-specific, so skip it on wasm32. +#![cfg(not(target_arch = "wasm32"))] + +use core_runtime::config::RuntimeConfig; +use core_runtime::runtime::{clear_global_initializers, register_global_initializer, Runtime}; +use core_runtime::test_util::{eval_and_read_out, eval_out_with_setup}; +use web_streams::readable::ReadableStream; + +fn add_globals() { + clear_global_initializers(); + register_global_initializer(web_globals::add_to_global); + register_global_initializer(web_streams::add_to_global); +} + +/// Evaluate `code`, drain microtasks, and return `String(globalThis.__out)`. +fn run(code: &str) -> String { + eval_out_with_setup(add_globals, code) +} + +/// Create a stream from `bytes`, expose it as `globalThis.__stream`, then evaluate +/// `code`, drain microtasks, and return `String(globalThis.__out)`. +fn run_with_stream(bytes: &[u8], code: &str) -> String { + add_globals(); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + let global = scope.global(); + let stream = ReadableStream::from_bytes(&scope, bytes).expect("create stream from bytes"); + global + .set_property(&scope, c"__stream", stream) + .expect("expose __stream"); + eval_and_read_out(&scope, code) +} + +/// A stream from `ReadableStream::from_bytes` delivers the bytes as a single +/// `Uint8Array` chunk, then closes. +#[test] +fn from_bytes_round_trips_through_js_reader() { + let out = run_with_stream( + b"hello world", + r#" + globalThis.__out = "pending"; + (async () => { + const reader = globalThis.__stream.getReader(); + const bytes = []; + let chunkCount = 0; + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + chunkCount++; + if (!(value instanceof Uint8Array)) { + globalThis.__out = "chunk not Uint8Array: " + value; + return; + } + bytes.push(...value); + } + globalThis.__out = chunkCount + ":" + bytes.join(","); + })(); + "#, + ); + assert_eq!(out, "1:104,101,108,108,111,32,119,111,114,108,100"); +} + +/// A stream from `ReadableStream::from_bytes` with no bytes is closed without a chunk. +#[test] +fn from_bytes_empty_stream_closes_immediately() { + let out = run_with_stream( + b"", + r#" + globalThis.__out = "pending"; + (async () => { + const reader = globalThis.__stream.getReader(); + const { value, done } = await reader.read(); + globalThis.__out = `done=${done},value=${value}`; + })(); + "#, + ); + assert_eq!(out, "done=true,value=undefined"); +} + +/// A non-callable callback member throws a `TypeError` at dictionary conversion, +/// before each constructor's own `RangeError` checks. +#[test] +fn non_callable_callback_throws_before_constructor_checks() { + let out = run(r#" + function errorName(f) { + try { f(); return "no-throw"; } catch (e) { return e.constructor.name; } + } + globalThis.__out = [ + errorName(() => new ReadableStream({ pull: {}, type: "bytes" }, { size: () => 1 })), + errorName(() => new WritableStream({ write: {}, type: "bytes" })), + errorName(() => new TransformStream({ transform: {}, readableType: "bytes" })), + ].join(","); + "#); + assert_eq!(out, "TypeError,TypeError,TypeError"); +} + +/// For a byte stream, a callable `size` is rejected with a `RangeError`, but a +/// non-callable `size` fails the `QueuingStrategySize` conversion first with a +/// `TypeError`. +#[test] +fn byte_stream_size_function_error_ordering() { + let out = run(r#" + let callableErr = "none", nonCallableErr = "none"; + try { new ReadableStream({ type: "bytes" }, { size() { return 1; } }); } + catch (e) { callableErr = e.constructor.name; } + try { new ReadableStream({ type: "bytes" }, { size: {} }); } + catch (e) { nonCallableErr = e.constructor.name; } + globalThis.__out = `${callableErr},${nonCallableErr}`; + "#); + assert_eq!(out, "RangeError,TypeError"); +} + +/// `autoAllocateChunkSize` is `[EnforceRange] unsigned long long`: a negative +/// value throws a `TypeError` rather than wrapping to a huge positive value. +#[test] +fn auto_allocate_chunk_size_enforce_range() { + let out = run(r#" + let r; + try { new ReadableStream({ type: "bytes", autoAllocateChunkSize: -1 }); r = "no-throw"; } + catch (e) { r = e.constructor.name; } + globalThis.__out = r; + "#); + assert_eq!(out, "TypeError"); +} + +/// A byte controller's `enqueue` rejects a chunk that is not an `ArrayBufferView`. +#[test] +fn byte_enqueue_of_non_view_throws_type_error() { + let out = run(r#" + let threw = false; + new ReadableStream({ + type: "bytes", + start(c) { + try { c.enqueue([1]); } catch (e) { threw = e instanceof TypeError; } + }, + }); + globalThis.__out = String(threw); + "#); + assert_eq!(out, "true"); +} + +/// Bound functions are accepted as WebIDL callback-type members, and are invoked +/// with their bound `this`. +#[test] +fn bound_function_callbacks() { + let out = run(r#" + globalThis.__out = "pending"; + let sizeCalledWith = null; + // `start` enqueues before any reader exists, so the chunk goes through the + // queue and the strategy `size` is actually invoked (an enqueue that + // satisfies a pending read bypasses `size`). + const src = { first: 7, start(c) { c.enqueue(this.first); } }; + const strat = { + factor: 3, + size(chunk) { sizeCalledWith = chunk; return chunk * this.factor; }, + }; + const rs = new ReadableStream( + { start: src.start.bind(src) }, + { size: strat.size.bind(strat), highWaterMark: 100 }, + ); + rs.getReader().read().then(r => { + globalThis.__out = `${r.value},${r.done},size=${sizeCalledWith}`; + }); + "#); + assert_eq!(out, "7,false,size=7"); +} + +/// A `read()` result has own `value`/`done` data properties, so an accessor on +/// `Object.prototype` can neither observe nor replace them. +#[test] +fn read_result_has_own_properties_immune_to_proto_accessor() { + let out = run(r#" + globalThis.__out = "pending"; + let leaked = "none"; + Object.defineProperty(Object.prototype, "value", { + configurable: true, + get() { return "HIJACKED"; }, + set(v) { leaked = v; }, + }); + const rs = new ReadableStream({ start(c) { c.enqueue(42); } }); + rs.getReader().read().then(r => { + globalThis.__out = + `own=${Object.hasOwn(r, "value")},value=${r.value},leaked=${leaked}`; + }); + "#); + assert_eq!(out, "own=true,value=42,leaked=none"); +} + +/// Numeric accessors on `Object.prototype` don't reach `tee()`'s internal state. +#[test] +fn tee_internal_state_immune_to_proto_numeric_accessor() { + // `drain` concatenates into a string rather than an array: the polluted + // numeric accessors below would also intercept ordinary `array[0] = …` writes + // in author code. + let out = run(r#" + async function drain(stream) { + const reader = stream.getReader(); + let s = ""; + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + s += value; + } + return s; + } + globalThis.__out = "pending"; + for (const k of ["0", "1"]) { + Object.defineProperty(Object.prototype, k, { + configurable: true, + get() { return "HIJACKED"; }, + set(_v) {}, + }); + } + const rs = new ReadableStream({ start(c) { c.enqueue("a"); c.enqueue("b"); c.close(); } }); + const [b1, b2] = rs.tee(); + Promise.all([drain(b1), drain(b2)]).then( + ([s1, s2]) => { globalThis.__out = s1 + "|" + s2; }, + e => { globalThis.__out = "rejected:" + e; }, + ); + "#); + assert_eq!(out, "ab|ab"); +} + +/// A sync iterable that yields a rejected promise: the async-from-sync wrapper +/// closes the sync iterator (running its `finally`) before the stream errors. +#[test] +fn from_sync_rejected_value_closes_iterator() { + let out = run(r#" + globalThis.__out = "pending"; + let cleaned = false; + const it = (function* () { + try { yield Promise.reject(new TypeError("boom")); } + finally { cleaned = true; } + })(); + ReadableStream.from(it).getReader().read().then( + () => { globalThis.__out = "resolved"; }, + e => { globalThis.__out = `rejected:${e.constructor.name}:${e.message}|cleaned:${cleaned}`; }, + ); + "#); + assert_eq!(out, "rejected:TypeError:boom|cleaned:true"); +} + +/// Async iteration adopts a promise chunk: `for await` yields `5`, not the +/// promise. +#[test] +fn for_await_adopts_thenable_chunk() { + let out = run(r#" + globalThis.__out = "pending"; + const rs = new ReadableStream({ start(c) { c.enqueue(Promise.resolve(5)); c.close(); } }); + (async () => { + const vals = []; + for await (const x of rs) vals.push(x); + globalThis.__out = vals.join(",") + "|" + typeof vals[0]; + })(); + "#); + assert_eq!(out, "5|number"); +} + +/// A rejected promise chunk rejects `next()` instead of being yielded. +#[test] +fn for_await_rejected_thenable_chunk_throws() { + let out = run(r#" + globalThis.__out = "pending"; + const rs = new ReadableStream({ start(c) { c.enqueue(Promise.reject(new TypeError("boom"))); c.close(); } }); + (async () => { + try { + for await (const x of rs) { globalThis.__out = "yielded:" + x; return; } + globalThis.__out = "done-no-throw"; + } catch (e) { globalThis.__out = "threw:" + e.constructor.name + ":" + e.message; } + })(); + "#); + assert_eq!(out, "threw:TypeError:boom"); +} + +/// The default async iterator has no interface object on the global, and its +/// class string is `"ReadableStream AsyncIterator"` (with a space), per WebIDL +/// §3.7.10. +#[test] +fn async_iterator_has_no_global_and_spaced_string_tag() { + let out = run(r#" + const leaked = "ReadableStreamAsyncIterator" in globalThis; + const tag = Object.prototype.toString.call(new ReadableStream().values()); + globalThis.__out = `leaked=${leaked},tag=${tag}`; + "#); + assert_eq!( + out, + "leaked=false,tag=[object ReadableStream AsyncIterator]" + ); +} + +/// `ReadableStream.prototype[Symbol.asyncIterator]` is the `values` function. +#[test] +fn async_iterator_method_is_values() { + let out = run(r#" + globalThis.__out = String( + ReadableStream.prototype[Symbol.asyncIterator] === ReadableStream.prototype.values); + "#); + assert_eq!(out, "true"); +} + +/// `next()` clears the ongoing promise when the previous call settles, so a call +/// made after that runs the next steps directly instead of chaining behind the +/// settled promise. Both calls then settle after the same number of ticks. +#[test] +fn settled_next_does_not_delay_the_following_call() { + let out = run(r#" + globalThis.__out = "pending"; + const rs = new ReadableStream({ start(c) { c.enqueue(1); c.enqueue(2); } }); + const it = rs.values(); + function ticksUntil(p) { + let done = false; + p.then(() => { done = true; }); + return new Promise(resolve => { + let n = 0; + (function spin() { + if (done) { resolve(n); } else { n++; Promise.resolve().then(spin); } + })(); + }); + } + (async () => { + const first = await ticksUntil(it.next()); + const second = await ticksUntil(it.next()); + globalThis.__out = `${first},${second}`; + })(); + "#); + let (first, second) = out.split_once(',').expect("two tick counts"); + assert_eq!(first, second, "ticks: {out}"); +} + +/// pipeTo's abort handling is a DOM abort algorithm, not an `abort` event +/// listener: an earlier listener that calls `stopImmediatePropagation()` doesn't +/// suppress the pipe's shutdown. +#[test] +fn abort_signal_not_suppressed_by_stop_immediate_propagation() { + let out = run(r#" + globalThis.__out = "pending"; + let aborted = false; + const ws = new WritableStream({ abort() { aborted = true; } }); + const ac = new AbortController(); + ac.signal.addEventListener("abort", (e) => { e.stopImmediatePropagation(); }); + const rs = new ReadableStream({ start(c) { c.enqueue("a"); } }); + const p = rs.pipeTo(ws, { signal: ac.signal }); + ac.abort(new Error("stop")); + p.then(() => { globalThis.__out = "fulfilled"; }, + e => { globalThis.__out = "rejected:" + e.message + "|aborted:" + aborted; }); + "#); + assert_eq!(out, "rejected:stop|aborted:true"); +} + +/// Aborting a pipe runs the destination controller's `signal` listeners +/// synchronously. A listener that enqueues into the source re-enters the pipe's +/// pending read request, which must not find the pipe state borrowed. +#[test] +fn abort_listener_enqueueing_into_source_does_not_panic() { + let out = run(r#" + globalThis.__out = "pending"; + let sourceController; + const rs = new ReadableStream({ start(c) { sourceController = c; } }); + const ws = new WritableStream({ + start(c) { + c.signal.addEventListener("abort", () => sourceController.enqueue("late")); + }, + }); + const ac = new AbortController(); + const p = rs.pipeTo(ws, { signal: ac.signal }); + Promise.resolve().then(() => ac.abort(new Error("stop"))); + p.then(() => { globalThis.__out = "fulfilled"; }, + e => { globalThis.__out = "rejected:" + e.message; }); + "#); + assert_eq!(out, "rejected:stop"); +} + +/// Three pending BYOB reads, then one `enqueue` that fills the first: filling the +/// head descriptor works while two more descriptors are still queued. +#[test] +fn byob_multiple_pending_reads_then_enqueue() { + let out = run(r#" + globalThis.__out = "pending"; + let controller; + const rs = new ReadableStream({ + type: "bytes", + start(c) { controller = c; }, + }); + const reader = rs.getReader({ mode: "byob" }); + const p1 = reader.read(new Uint8Array(10)); + reader.read(new Uint8Array(10)); + reader.read(new Uint8Array(10)); + controller.enqueue(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); + p1.then(r => { + globalThis.__out = `${r.value.constructor.name},len=${r.value.length},${r.value[0]}-${r.value[9]}`; + }); + "#); + assert_eq!(out, "Uint8Array,len=10,1-10"); +} + +/// The `min` option of a BYOB read into a `Float16Array` is checked against the +/// view's element length, so `min: 3` on a 2-element view rejects. +#[test] +fn byob_read_float16_view_kind() { + let out = run(r#" + globalThis.__out = "pending"; + if (typeof Float16Array === "undefined") { + globalThis.__out = "no-float16"; + } else { + const rs = new ReadableStream({ + type: "bytes", + pull(c) { + const v = c.byobRequest.view; + c.byobRequest.respond(v.byteLength); + }, + }); + rs.getReader({ mode: "byob" }).read(new Float16Array(2), { min: 3 }).then( + r => { globalThis.__out = `resolved:${r.value.constructor.name}`; }, + e => { globalThis.__out = `rejected:${e.constructor.name}`; }, + ); + } + "#); + assert_eq!(out, "rejected:RangeError"); +} + +/// A BYOB `read(view, { min })` on a `Uint16Array` is fulfilled once `min` elements are +/// available. Here the source supplies exactly the requested bytes. +#[test] +fn byob_read_uint16_view_with_min() { + let out = run(r#" + globalThis.__out = "pending"; + const rs = new ReadableStream({ + type: "bytes", + pull(c) { + const v = c.byobRequest.view; + for (let i = 0; i < v.byteLength; i++) v[i] = i + 1; + c.byobRequest.respond(v.byteLength); + }, + }); + rs.getReader({ mode: "byob" }).read(new Uint16Array(2), { min: 2 }).then(r => { + globalThis.__out = `${r.value.constructor.name},len=${r.value.length},${r.value[0]}`; + }); + "#); + // Bytes 1,2,3,4 little-endian -> u16 [0x0201, 0x0403] = [513, 1027]. + assert_eq!(out, "Uint16Array,len=2,513"); +} + +/// A teed byte stream whose branch has a partially filled `Int32Array` read when +/// the source closes: the branch's close throws "insufficient bytes", which the +/// spec marks infallible. Both branches then reject with a `TypeError` instead of +/// the runtime aborting. +#[test] +fn byte_tee_branch_partial_fill_close_rejects_not_aborts() { + let out = run(r#" + globalThis.__out = "pending"; + let step = 0; + const rs = new ReadableStream({ + type: "bytes", + pull(c) { + if (step === 0) { + step = 1; + const v = c.byobRequest.view; + v[0] = 1; v[1] = 2; v[2] = 3; + c.byobRequest.respond(3); // 3 of 4 bytes -> branch partial fill + } else { + c.close(); + c.byobRequest.respond(0); + } + }, + }); + const [b1, b2] = rs.tee(); + // Branch 2 receives the 3-byte clone, then its next read rejects. If the + // runtime aborted at branch 1's close, branch 2 would never close and this + // read would never settle. + async function drainB2() { + const r = b2.getReader(); + let n = 0; + try { + for (;;) { + const { done } = await r.read(); + if (done) return "b2:done@" + n; + n++; + } + } catch (e) { return "b2:" + e.constructor.name + "@" + n; } + } + const r1 = b1.getReader({ mode: "byob" }).read(new Int32Array(1)).then( + () => "b1:resolved", e => "b1:" + e.constructor.name); + Promise.all([r1, drainB2()]).then(([a, b]) => { globalThis.__out = a + "," + b; }); + "#); + assert_eq!(out, "b1:TypeError,b2:TypeError@1"); +} + +/// `respond(bytesWritten)` is `[EnforceRange] unsigned long long`: a negative +/// argument throws a `TypeError` at conversion, not a `RangeError` from the +/// length checks. +#[test] +fn respond_enforce_range_rejects_negative() { + let out = run(r#" + globalThis.__out = "pending"; + const rs = new ReadableStream({ + type: "bytes", + pull(c) { + try { c.byobRequest.respond(-1); globalThis.__out = "no-throw"; } + catch (e) { globalThis.__out = "respond:" + e.constructor.name; } + }, + }); + rs.getReader({ mode: "byob" }).read(new Uint8Array(4)); + "#); + assert_eq!(out, "respond:TypeError"); +} + +/// `read(view, options)` converts `view` before `options`, so an invalid view +/// rejects without running a getter on `options.min`. +#[test] +fn byob_read_validates_view_before_options() { + let out = run(r#" + globalThis.__out = "pending"; + let getterRan = false; + const reader = new ReadableStream({ type: "bytes" }).getReader({ mode: "byob" }); + reader.read(42, { get min() { getterRan = true; throw new Error("boom"); } }).then( + () => { globalThis.__out = "resolved"; }, + e => { globalThis.__out = `rejected:${e.constructor.name}|getterRan:${getterRan}`; }, + ); + "#); + assert_eq!(out, "rejected:TypeError|getterRan:false"); +} + +/// A promise-returning operation (`cancel`) and a promise-typed getter (`closed`) called with a +/// wrong `this` return a promise rejected with a `TypeError` instead of throwing, per WebIDL +/// §3.7.7. These are the macros' two brand-check rejection branches. +#[test] +fn promise_members_reject_a_wrong_this() { + let out = run(r#" + globalThis.__out = "pending"; + const closed = Object.getOwnPropertyDescriptor( + ReadableStreamDefaultReader.prototype, "closed").get; + const outcome = (call) => { + let p; + try { p = call(); } catch (e) { return Promise.resolve("threw"); } + return p.then(() => "resolved", e => `rejected:${e.constructor.name}`); + }; + Promise.all([ + outcome(() => ReadableStream.prototype.cancel.call(undefined)), + outcome(() => closed.call(undefined)), + ]).then(results => { globalThis.__out = results.join("|"); }); + "#); + assert_eq!(out, "rejected:TypeError|rejected:TypeError"); +} diff --git a/crates/builtins/web-streams/tests/tee_default.rs b/crates/builtins/web-streams/tests/tee_default.rs deleted file mode 100644 index 027c749..0000000 --- a/crates/builtins/web-streams/tests/tee_default.rs +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception - -//! Keystone integration tests for `ReadableStream.tee()`: both branches receive -//! the source's chunks, branch cancellation is independent, and the source's -//! error propagates to both branches. Covered far more thoroughly by -//! `streams/readable-streams/tee.any.js`; this is the fast, design-validating -//! (and GC-rooting-validating) smoke test. - -// This file contains nothing platform-specific, so skip it on wasm32. -#![cfg(not(target_arch = "wasm32"))] - -use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; -use core_runtime::test_util::eval_out_with_setup; - -fn run(code: &str) -> String { - eval_out_with_setup( - || { - clear_global_initializers(); - register_global_initializer(web_streams::add_to_global); - }, - code, - ) -} - -const DRAIN: &str = r#" - async function drain(stream) { - const reader = stream.getReader(); - const out = []; - for (;;) { - const { value, done } = await reader.read(); - if (done) break; - out.push(value); - } - return out.join(""); - } -"#; - -/// Both branches receive all of the source's chunks. -#[test] -fn both_branches_receive_chunks() { - let out = run(&format!( - r#"{DRAIN} - globalThis.__out = "pending"; - const rs = new ReadableStream({{ start(c) {{ c.enqueue("a"); c.enqueue("b"); c.close(); }} }}); - const [b1, b2] = rs.tee(); - Promise.all([drain(b1), drain(b2)]).then(([s1, s2]) => {{ globalThis.__out = s1 + "|" + s2; }}); - "# - )); - assert_eq!(out, "ab|ab"); -} - -/// `tee()` locks the source and returns two readable branches. -#[test] -fn tee_locks_and_returns_two_branches() { - let out = run(r#" - const rs = new ReadableStream(); - const branches = rs.tee(); - const locked = rs.locked; - const two = branches.length === 2; - const both = branches[0] instanceof ReadableStream && branches[1] instanceof ReadableStream; - globalThis.__out = `${locked},${two},${both}`; - "#); - assert_eq!(out, "true,true,true"); -} - -/// Cancelling one branch does not stop the other from draining. -#[test] -fn cancel_one_branch_other_still_drains() { - let out = run(&format!( - r#"{DRAIN} - globalThis.__out = "pending"; - const rs = new ReadableStream({{ start(c) {{ c.enqueue("x"); c.enqueue("y"); c.close(); }} }}); - const [b1, b2] = rs.tee(); - b1.cancel(); - drain(b2).then(s => {{ globalThis.__out = s; }}); - "# - )); - assert_eq!(out, "xy"); -} - -/// `tee()` packs its internal per-chunk reaction state into an object that author -/// code cannot reach or tamper with. A hostile numeric accessor on -/// `Object.prototype` must not be able to intercept or corrupt that packed state: -/// if the internal payload were an ordinary `Object.prototype`-inheriting object -/// written with `[[Set]]`/read with `[[Get]]`, the accessor would hijack indices -/// `0`/`1` and the tee microtask would mis-recover its state. -#[test] -fn tee_internal_state_immune_to_proto_numeric_accessor() { - // `drain` concatenates into a string rather than an array: the polluted - // numeric accessors below would also intercept ordinary `array[0] = …` writes - // in author code, which is correct JS behaviour and not what we are testing. - let out = run(r#" - async function drain(stream) { - const reader = stream.getReader(); - let s = ""; - for (;;) { - const { value, done } = await reader.read(); - if (done) break; - s += value; - } - return s; - } - globalThis.__out = "pending"; - for (const k of ["0", "1"]) { - Object.defineProperty(Object.prototype, k, { - configurable: true, - get() { return "HIJACKED"; }, - set(_v) {}, - }); - } - const rs = new ReadableStream({ start(c) { c.enqueue("a"); c.enqueue("b"); c.close(); } }); - const [b1, b2] = rs.tee(); - Promise.all([drain(b1), drain(b2)]).then( - ([s1, s2]) => { globalThis.__out = s1 + "|" + s2; }, - e => { globalThis.__out = "rejected:" + e; }, - ); - "#); - assert_eq!(out, "ab|ab"); -} - -/// An error in the source propagates to both branches' readers. -#[test] -fn source_error_propagates_to_both() { - let out = run(&format!( - r#"{DRAIN} - globalThis.__out = "pending"; - let ctrl; - const rs = new ReadableStream({{ start(c) {{ ctrl = c; }} }}); - const [b1, b2] = rs.tee(); - ctrl.error(new Error("boom")); - const r1 = b1.getReader(); - const r2 = b2.getReader(); - Promise.all([ - r1.read().then(() => "ok", e => "e1:" + e.message), - r2.read().then(() => "ok", e => "e2:" + e.message), - ]).then(([a, b]) => {{ globalThis.__out = a + "," + b; }}); - "# - )); - assert_eq!(out, "e1:boom,e2:boom"); -} diff --git a/crates/builtins/web-streams/tests/transform_default.rs b/crates/builtins/web-streams/tests/transform_default.rs deleted file mode 100644 index fd68656..0000000 --- a/crates/builtins/web-streams/tests/transform_default.rs +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception - -//! Keystone integration tests for the transform default path: the transform -//! stream wires its writable side (sink → transform) to its readable side -//! (source) through the controller, on top of the readable/writable defaults. -//! `web-globals` is registered because the underlying writable controller -//! creates an `AbortController`. -//! -//! Behaviour is covered far more thoroughly by the `streams/transform-streams` -//! WPT suites; this is the fast, design-validating smoke test. - -// This file contains nothing platform-specific, so skip it on wasm32. -#![cfg(not(target_arch = "wasm32"))] - -use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; -use core_runtime::test_util::eval_out_with_setup; - -fn run(code: &str) -> String { - eval_out_with_setup( - || { - clear_global_initializers(); - register_global_initializer(|scope, global| { - web_globals::add_to_global(scope, global); - web_streams::add_to_global(scope, global); - }); - }, - code, - ) -} - -/// A transformer that uppercases each chunk: written chunks come out transformed -/// on the readable side. -#[test] -fn transform_uppercases_chunks() { - let out = run(r#" - globalThis.__out = "pending"; - const ts = new TransformStream({ - transform(chunk, controller) { controller.enqueue(chunk.toUpperCase()); }, - }); - const writer = ts.writable.getWriter(); - const reader = ts.readable.getReader(); - writer.write("a"); - reader.read().then(r => { globalThis.__out = r.value; }); - "#); - assert_eq!(out, "A"); -} - -/// The default (identity) transform passes chunks through unchanged. -#[test] -fn identity_transform_passes_through() { - let out = run(r#" - globalThis.__out = "pending"; - const ts = new TransformStream(); - const writer = ts.writable.getWriter(); - const reader = ts.readable.getReader(); - writer.write("hello"); - reader.read().then(r => { globalThis.__out = r.value; }); - "#); - assert_eq!(out, "hello"); -} - -/// `flush` runs on close and can enqueue a final chunk; closing the writable -/// closes the readable. -#[test] -fn flush_enqueues_and_close_propagates() { - let out = run(r#" - globalThis.__out = "pending"; - const ts = new TransformStream({ - transform(chunk, c) { c.enqueue(chunk); }, - flush(c) { c.enqueue("end"); }, - }); - const writer = ts.writable.getWriter(); - const reader = ts.readable.getReader(); - writer.write("x"); - writer.close(); - const seen = []; - function pump() { - return reader.read().then(r => { - if (r.done) { globalThis.__out = seen.join(","); return; } - seen.push(r.value); - return pump(); - }); - } - pump(); - "#); - assert_eq!(out, "x,end"); -} - -/// `readable` and `writable` are the two sides and round-trip object identity. -#[test] -fn readable_writable_shape() { - let out = run(r#" - const ts = new TransformStream(); - const a = ts.readable instanceof ReadableStream; - const b = ts.writable instanceof WritableStream; - const c = ts.readable === ts.readable; - globalThis.__out = `${a},${b},${c}`; - "#); - assert_eq!(out, "true,true,true"); -} - -/// A non-callable transformer callback throws a `TypeError` at dictionary -/// conversion, before the step-3 `readableType` `RangeError`. -#[test] -fn non_callable_callback_throws_type_error_before_readable_type_check() { - let out = run(r#" - let err = "none"; - try { new TransformStream({ transform: {}, readableType: "bytes" }); err = "no-throw"; } - catch (e) { err = e.constructor.name; } - globalThis.__out = err; - "#); - assert_eq!(out, "TypeError"); -} diff --git a/crates/builtins/web-streams/tests/writable_default.rs b/crates/builtins/web-streams/tests/writable_default.rs deleted file mode 100644 index c2e1963..0000000 --- a/crates/builtins/web-streams/tests/writable_default.rs +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception - -//! Keystone integration tests for the writable default path: the write/close/ -//! abort state machine, the writer, and the default controller. `web-globals` -//! is registered alongside `web-streams` because the writable controller -//! creates an `AbortController` during setup. -//! -//! These exercise behaviour covered far more thoroughly by the -//! `streams/writable-streams` WPT suites; this is the fast, design-validating -//! smoke test. - -// This file contains nothing platform-specific, so skip it on wasm32. -#![cfg(not(target_arch = "wasm32"))] - -use core_runtime::runtime::{clear_global_initializers, register_global_initializer}; -use core_runtime::test_util::eval_out_with_setup; - -/// Evaluate `code`, drain microtasks, and return `String(globalThis.__out)`. -fn run(code: &str) -> String { - eval_out_with_setup( - || { - clear_global_initializers(); - register_global_initializer(|scope, global| { - web_globals::add_to_global(scope, global); - web_streams::add_to_global(scope, global); - }); - }, - code, - ) -} - -/// Chunks written through a writer reach the sink's `write` algorithm in order, -/// and the per-write promises settle. -#[test] -fn write_chunks_reach_sink() { - let out = run(r#" - globalThis.__out = "pending"; - const written = []; - const ws = new WritableStream({ write(chunk) { written.push(chunk); } }); - const w = ws.getWriter(); - w.write("a").then(() => w.write("b")).then(() => { - globalThis.__out = written.join(","); - }); - "#); - assert_eq!(out, "a,b"); -} - -/// `close()` runs the sink's `close` algorithm and resolves. -#[test] -fn close_runs_sink_close() { - let out = run(r#" - globalThis.__out = "pending"; - let closed = false; - const ws = new WritableStream({ close() { closed = true; } }); - const w = ws.getWriter(); - w.write("x"); - w.close().then(() => { globalThis.__out = "closed:" + closed; }); - "#); - assert_eq!(out, "closed:true"); -} - -/// `abort(reason)` runs the sink's `abort` algorithm with the reason and resolves. -#[test] -fn abort_runs_sink_abort() { - let out = run(r#" - globalThis.__out = "pending"; - let abortReason; - const ws = new WritableStream({ abort(r) { abortReason = r; } }); - const w = ws.getWriter(); - w.abort("boom").then(() => { globalThis.__out = "aborted:" + abortReason; }); - "#); - assert_eq!(out, "aborted:boom"); -} - -/// `locked` reflects writer acquisition, and a second `getWriter` throws. -#[test] -fn locked_and_double_get_writer() { - let out = run(r#" - const ws = new WritableStream(); - const before = ws.locked; - ws.getWriter(); - const after = ws.locked; - let threw = false; - try { ws.getWriter(); } catch (e) { threw = e instanceof TypeError; } - globalThis.__out = `${before},${after},${threw}`; - "#); - assert_eq!(out, "false,true,true"); -} - -/// A sink whose `write` rejects errors the stream: the write promise rejects and -/// the writer's `closed` promise rejects. -#[test] -fn write_rejection_errors_stream() { - let out = run(r#" - globalThis.__out = "pending"; - const ws = new WritableStream({ write() { return Promise.reject(new Error("nope")); } }); - const w = ws.getWriter(); - w.write("a").then( - () => { globalThis.__out = "unexpected-fulfill"; }, - (e) => { globalThis.__out = "rejected:" + e.message; }, - ); - "#); - assert_eq!(out, "rejected:nope"); -} - -/// A non-callable callback dictionary member throws a `TypeError` at dictionary -/// conversion, before the step-3 `type` `RangeError` — WebIDL converts the -/// `UnderlyingSink` dictionary (validating `write` is callable) before the -/// constructor checks `type`. -#[test] -fn non_callable_callback_throws_type_error_before_type_check() { - let out = run(r#" - let err = "none"; - try { new WritableStream({ write: {}, type: "bytes" }); err = "no-throw"; } - catch (e) { err = e.constructor.name; } - globalThis.__out = err; - "#); - assert_eq!(out, "TypeError"); -} diff --git a/crates/builtins/web-url/Cargo.toml b/crates/builtins/web-url/Cargo.toml index a2800e4..601df60 100644 --- a/crates/builtins/web-url/Cargo.toml +++ b/crates/builtins/web-url/Cargo.toml @@ -7,7 +7,7 @@ authors.workspace = true edition.workspace = true [lib] -# No unit tests; skip building the (SpiderMonkey-sized) lib test harness binary. +# No unit tests. This skips building the (SpiderMonkey-sized) lib test harness binary. test = false [dependencies] diff --git a/crates/builtins/web-url/src/lib.rs b/crates/builtins/web-url/src/lib.rs index 390a80c..c68e88e 100644 --- a/crates/builtins/web-url/src/lib.rs +++ b/crates/builtins/web-url/src/lib.rs @@ -12,7 +12,12 @@ pub fn add_to_global(scope: &Scope<'_>, global: Object<'_>) { url::URL::add_to_global(scope, global); url_search_params::URLSearchParams::add_to_global(scope, global); url_search_params_iterator::URLSearchParamsIterator::add_to_global(scope, global); - url_search_params::install_symbol_iterator(scope); + // `URLSearchParams.prototype[Symbol.iterator]` is `entries`. + js::class::add_symbol_alias::( + scope, + c"entries", + js::native::SymbolCode::iterator, + ); js::class::inherit_from_iterator_prototype::< url_search_params_iterator::URLSearchParamsIteratorImpl, >(scope) diff --git a/crates/builtins/web-url/src/url_search_params.rs b/crates/builtins/web-url/src/url_search_params.rs index c681765..9ca160e 100644 --- a/crates/builtins/web-url/src/url_search_params.rs +++ b/crates/builtins/web-url/src/url_search_params.rs @@ -273,15 +273,6 @@ impl URLSearchParams<'_> { } } -/// Define `Symbol.iterator` on URLSearchParams.prototype. -pub fn install_symbol_iterator(scope: &Scope<'_>) { - js::class::add_symbol_alias::( - scope, - c"entries", - js::native::SymbolCode::iterator, - ); -} - impl URLSearchParams<'_> { /// pub(crate) fn update(&self, scope: &Scope<'_>) -> Result<(), ExnThrown> { diff --git a/crates/builtins/web-url/tests/url.rs b/crates/builtins/web-url/tests/url.rs index 9332196..6ae9e94 100644 --- a/crates/builtins/web-url/tests/url.rs +++ b/crates/builtins/web-url/tests/url.rs @@ -3,7 +3,7 @@ // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] -use core_runtime::test_util::{eval_with_setup, throws_with_setup}; +use core_runtime::test_util::eval_with_setup; fn setup() { core_runtime::runtime::register_global_initializer(|scope, global| { @@ -15,218 +15,22 @@ fn eval(code: &str) -> String { eval_with_setup(setup, code) } -fn throws(code: &str) -> bool { - throws_with_setup(setup, code) -} - -// ── URL constructor ── - -#[test] -fn url_constructor_basic() { - assert_eq!( - eval("new URL('https://example.com/').href"), - "https://example.com/" - ); -} - -#[test] -fn url_constructor_with_base() { - assert_eq!( - eval("new URL('/path', 'https://example.com').href"), - "https://example.com/path" - ); -} - -#[test] -fn url_constructor_invalid_throws() { - assert!(throws("new URL('not a url')")); -} - -#[test] -fn url_constructor_invalid_base_throws() { - assert!(throws("new URL('/path', 'not a url')")); -} - -// ── URL properties ── - -#[test] -fn url_origin() { - assert_eq!( - eval("new URL('https://example.com:8080/p').origin"), - "https://example.com:8080" - ); -} - -#[test] -fn url_protocol() { - assert_eq!(eval("new URL('https://example.com/').protocol"), "https:"); -} - -#[test] -fn url_username() { - assert_eq!( - eval("new URL('https://user@example.com/').username"), - "user" - ); -} - -#[test] -fn url_password() { - assert_eq!( - eval("new URL('https://user:pass@example.com/').password"), - "pass" - ); -} - -#[test] -fn url_host_with_port() { - assert_eq!( - eval("new URL('https://example.com:8080/').host"), - "example.com:8080" - ); -} - -#[test] -fn url_host_default_port() { - assert_eq!(eval("new URL('https://example.com/').host"), "example.com"); -} - -#[test] -fn url_hostname() { - assert_eq!( - eval("new URL('https://example.com:8080/').hostname"), - "example.com" - ); -} - -#[test] -fn url_port() { - assert_eq!(eval("new URL('https://example.com:8080/').port"), "8080"); -} - -#[test] -fn url_port_default_empty() { - assert_eq!(eval("new URL('https://example.com/').port"), ""); -} - -#[test] -fn url_pathname() { - assert_eq!(eval("new URL('https://example.com/a/b').pathname"), "/a/b"); -} - -#[test] -fn url_search() { - assert_eq!(eval("new URL('https://example.com/?q=1').search"), "?q=1"); -} - -#[test] -fn url_search_empty() { - assert_eq!(eval("new URL('https://example.com/').search"), ""); -} - -#[test] -fn url_hash() { - assert_eq!(eval("new URL('https://example.com/#frag').hash"), "#frag"); -} - -#[test] -fn url_hash_empty() { - assert_eq!(eval("new URL('https://example.com/').hash"), ""); -} - -// ── URL setters ── - -#[test] -fn url_set_href() { - assert_eq!( - eval("let u = new URL('https://a.com/'); u.href = 'https://b.com/path'; u.href"), - "https://b.com/path" - ); -} - -#[test] -fn url_set_href_invalid_throws() { - assert!(throws( - "let u = new URL('https://a.com/'); u.href = 'invalid'" - )); -} - -#[test] -fn url_set_protocol() { - assert_eq!( - eval("let u = new URL('https://a.com/'); u.protocol = 'http:'; u.protocol"), - "http:" - ); -} - -#[test] -fn url_set_username() { - assert_eq!( - eval("let u = new URL('https://a.com/'); u.username = 'bob'; u.username"), - "bob" - ); -} - -#[test] -fn url_set_password() { - assert_eq!( - eval("let u = new URL('https://a.com/'); u.password = 'secret'; u.password"), - "secret" - ); -} - -#[test] -fn url_set_host() { - assert_eq!( - eval("let u = new URL('https://a.com/'); u.host = 'b.com:9090'; u.host"), - "b.com:9090" - ); -} - -#[test] -fn url_set_hostname() { - assert_eq!( - eval("let u = new URL('https://a.com/'); u.hostname = 'b.com'; u.hostname"), - "b.com" - ); -} - -#[test] -fn url_set_port() { - assert_eq!( - eval("let u = new URL('https://a.com/'); u.port = '3000'; u.port"), - "3000" - ); -} +// The WPT `url/` tests cover the rest of `URL` and `URLSearchParams`. #[test] -fn url_set_pathname() { +fn url_search_params_iterator_is_not_a_global() { assert_eq!( - eval("let u = new URL('https://a.com/old'); u.pathname = '/new'; u.pathname"), - "/new" + eval("typeof globalThis['URLSearchParams Iterator']"), + "undefined" ); -} - -#[test] -fn url_set_search() { assert_eq!( - eval("let u = new URL('https://a.com/'); u.search = '?x=1'; u.search"), - "?x=1" - ); -} - -#[test] -fn url_set_hash() { - assert_eq!( - eval("let u = new URL('https://a.com/'); u.hash = '#sec'; u.hash"), - "#sec" + eval("Object.prototype.toString.call(new URLSearchParams('a=1').entries())"), + "[object URLSearchParams Iterator]" ); } -// ── URL methods ── - #[test] -fn url_to_string() { +fn url_to_string_returns_the_href() { assert_eq!( eval("new URL('https://example.com/path?q=1#h').toString()"), "https://example.com/path?q=1#h" @@ -234,209 +38,15 @@ fn url_to_string() { } #[test] -fn url_to_json() { - assert_eq!( - eval("new URL('https://example.com/').toJSON()"), - "https://example.com/" - ); -} - -// ── URL.parse ── - -#[test] -fn url_parse_valid() { +fn url_constructor_rejects_an_invalid_base() { assert_eq!( - eval("URL.parse('https://example.com/').href"), - "https://example.com/" + eval("try { new URL('/path', 'not a url'); 'no-throw' } catch (e) { e.constructor.name }"), + "TypeError" ); } #[test] -fn url_parse_invalid_returns_null() { - assert_eq!(eval("URL.parse('not a url')"), "null"); -} - -#[test] -fn url_parse_with_base() { - assert_eq!( - eval("URL.parse('/path', 'https://example.com').href"), - "https://example.com/path" - ); -} - -// ── URL.canParse ── - -#[test] -fn url_can_parse_valid() { - assert_eq!(eval("URL.canParse('https://example.com/')"), "true"); -} - -#[test] -fn url_can_parse_invalid() { - assert_eq!(eval("URL.canParse('not a url')"), "false"); -} - -// ── URLSearchParams constructor ── - -#[test] -fn usp_construct_empty() { - assert_eq!(eval("new URLSearchParams().toString()"), ""); -} - -#[test] -fn usp_construct_from_string() { - assert_eq!(eval("new URLSearchParams('a=1&b=2').toString()"), "a=1&b=2"); -} - -#[test] -fn usp_construct_from_string_strips_question_mark() { - assert_eq!(eval("new URLSearchParams('?a=1').toString()"), "a=1"); -} - -#[test] -fn usp_construct_from_sequence() { - assert_eq!( - eval("new URLSearchParams([['a', '1'], ['b', '2']]).toString()"), - "a=1&b=2" - ); -} - -#[test] -fn usp_construct_from_record() { - // Order may vary for records; check both entries exist. - assert_eq!( - eval("new URLSearchParams({a: '1', b: '2'}).toString()"), - "a=1&b=2" - ); -} - -#[test] -fn usp_construct_invalid_pair_throws() { - assert!(throws("new URLSearchParams([['only_one']])")); -} - -#[test] -fn usp_construct_from_custom_iterable() { - assert_eq!( - eval( - "let init = { [Symbol.iterator]: function* () { yield ['a', 'b']; yield ['c', 'd']; } };\ - new URLSearchParams(init).get('a')" - ), - "b" - ); -} - -#[test] -fn usp_construct_record_preserves_null_code_point() { - assert_eq!( - eval("new URLSearchParams({['a\\0b']: 42}).toString()"), - "a%00b=42" - ); -} - -// ── URLSearchParams methods ── - -#[test] -fn usp_get() { - assert_eq!(eval("new URLSearchParams('a=1&b=2').get('a')"), "1"); -} - -#[test] -fn usp_get_missing_returns_null() { - assert_eq!(eval("new URLSearchParams('a=1').get('x')"), "null"); -} - -#[test] -fn usp_get_all() { - assert_eq!( - eval("JSON.stringify(new URLSearchParams('a=1&a=2&b=3').getAll('a'))"), - "[\"1\",\"2\"]" - ); -} - -#[test] -fn usp_has() { - assert_eq!(eval("new URLSearchParams('a=1').has('a')"), "true"); -} - -#[test] -fn usp_has_missing() { - assert_eq!(eval("new URLSearchParams('a=1').has('b')"), "false"); -} - -#[test] -fn usp_has_with_value() { - assert_eq!(eval("new URLSearchParams('a=1&a=2').has('a', '2')"), "true"); -} - -#[test] -fn usp_append() { - assert_eq!( - eval("let p = new URLSearchParams('a=1'); p.append('b', '2'); p.toString()"), - "a=1&b=2" - ); -} - -#[test] -fn usp_delete() { - assert_eq!( - eval("let p = new URLSearchParams('a=1&b=2&a=3'); p.delete('a'); p.toString()"), - "b=2" - ); -} - -#[test] -fn usp_delete_with_value() { - assert_eq!( - eval("let p = new URLSearchParams('a=1&a=2&a=3'); p.delete('a', '2'); p.toString()"), - "a=1&a=3" - ); -} - -#[test] -fn usp_set() { - assert_eq!( - eval("let p = new URLSearchParams('a=1&a=2'); p.set('a', '3'); p.toString()"), - "a=3" - ); -} - -#[test] -fn usp_set_new_key() { - assert_eq!( - eval("let p = new URLSearchParams(); p.set('x', '1'); p.toString()"), - "x=1" - ); -} - -#[test] -fn usp_sort() { - assert_eq!( - eval("let p = new URLSearchParams('c=3&a=1&b=2'); p.sort(); p.toString()"), - "a=1&b=2&c=3" - ); -} - -#[test] -fn usp_size() { - assert_eq!(eval("new URLSearchParams('a=1&b=2&c=3').size"), "3"); -} - -#[test] -fn usp_entries_iterator_method() { - assert_eq!( - eval( - "let p = new URLSearchParams('a=1&b=2'); \ - let it = p.entries(); \ - let first = it.next().value; \ - first[0] + '=' + first[1]" - ), - "a=1" - ); -} - -#[test] -fn usp_keys_and_values_methods() { +fn url_search_params_keys_and_values() { assert_eq!( eval( "let p = new URLSearchParams('a=1&b=2'); \ @@ -445,124 +55,3 @@ fn usp_keys_and_values_methods() { "[[\"a\",\"b\"],[\"1\",\"2\"]]" ); } - -// ── URL + URLSearchParams integration ── - -#[test] -fn url_search_params_reflects_url_query() { - assert_eq!( - eval("new URL('https://example.com/?a=1&b=2').searchParams.get('a')"), - "1" - ); -} - -#[test] -fn url_search_params_updates_url() { - assert_eq!( - eval("let u = new URL('https://example.com/'); u.searchParams.append('x', '1'); u.search"), - "?x=1" - ); -} - -#[test] -fn url_set_search_updates_params() { - assert_eq!( - eval( - "let u = new URL('https://example.com/?old=1'); u.search = '?new=2'; u.searchParams.get('new')" - ), - "2" - ); -} - -#[test] -fn url_set_href_updates_search_params() { - assert_eq!( - eval( - "let u = new URL('https://a.com/?x=1'); u.href = 'https://b.com/?y=2'; u.searchParams.get('y')" - ), - "2" - ); -} - -#[test] -fn usp_for_of_sees_live_updates() { - assert_eq!( - eval( - "let a = new URL('http://a.b/c?a=1&b=2&c=3&d=4');\ - let seen = [];\ - for (const i of a.searchParams) {\ - a.search = 'x=1&y=2&z=3';\ - seen.push(i[0] + '=' + i[1]);\ - }\ - seen.join(',')" - ), - "a=1,y=2,z=3" - ); -} - -#[test] -fn usp_construct_record_normalizes_keys_last_wins() { - // WebIDL `record` conversion normalizes keys to USV - // and is map-shaped: when two JS keys normalize to the same USVString, the - // later entry overwrites the earlier one. Mirrors the WPT test - // "Construct with 2 unpaired surrogates (no trailing)". - assert_eq!( - eval( - "let p = new URLSearchParams({'\\uD835x': '1', 'xx': '2', '\\uD83Dx': '3'});\ - JSON.stringify([...p])" - ), - "[[\"\u{fffd}x\",\"3\"],[\"xx\",\"2\"]]" - ); -} - -#[test] -fn usp_delete_on_opaque_path_preserves_trailing_space_encoding() { - assert_eq!( - eval( - "const url = new URL('data:space ?test');\ - url.searchParams.delete('test');\ - url.pathname" - ), - "space %20" - ); - assert_eq!( - eval( - "const url = new URL('data:space ?test');\ - url.searchParams.delete('test');\ - url.href" - ), - "data:space %20" - ); -} - -#[test] -fn usp_delete_on_opaque_path_with_fragment_preserves_trailing_space_encoding() { - assert_eq!( - eval( - "const url = new URL('data:space ?test#test');\ - url.searchParams.delete('test');\ - url.pathname" - ), - "space %20" - ); - assert_eq!( - eval( - "const url = new URL('data:space ?test#test');\ - url.searchParams.delete('test');\ - url.href" - ), - "data:space %20#test" - ); -} - -#[test] -fn url_search_params_iterator_is_not_a_global() { - assert_eq!( - eval("typeof globalThis['URLSearchParams Iterator']"), - "undefined" - ); - assert_eq!( - eval("Object.prototype.toString.call(new URLSearchParams('a=1').entries())"), - "[object URLSearchParams Iterator]" - ); -} diff --git a/crates/js/src/error.rs b/crates/js/src/error.rs index 590ebb8..f4eebdc 100644 --- a/crates/js/src/error.rs +++ b/crates/js/src/error.rs @@ -445,6 +445,17 @@ pub trait ThrowException { fn throw(self, scope: &Scope<'_>) -> ExnThrown; } +/// A failed allocation. Throwing it reports out of memory, which no script can catch. +#[derive(Debug, PartialEq)] +pub struct OutOfMemory; + +impl ThrowException for OutOfMemory { + fn throw(self, scope: &Scope<'_>) -> ExnThrown { + crate::exception::report_out_of_memory(scope); + ExnThrown + } +} + impl ThrowException for String { /// Throw a `TypeError` with this string as the message. fn throw(self, scope: &Scope<'_>) -> ExnThrown { diff --git a/crates/js/src/promise.rs b/crates/js/src/promise.rs index 2844998..b10694a 100644 --- a/crates/js/src/promise.rs +++ b/crates/js/src/promise.rs @@ -19,7 +19,7 @@ use mozjs::jsapi::{JSObject, PromiseState}; use mozjs::rust::wrappers2; use mozjs::rust::{HandleObject, HandleValue}; -use super::error::ExnThrown; +use super::error::{ExnThrown, OutOfMemory, ThrowException}; use crate::Object; /// Marker type for JavaScript `Promise` objects. @@ -335,8 +335,7 @@ impl<'s> Stack<'s, Promise> { let vector = mozjs::rust::RootedObjectVectorWrapper::new(scope.cx_mut()); for promise in promises { if !vector.append(promise.get()) { - crate::exception::report_out_of_memory(scope); - return Err(ExnThrown); + return Err(OutOfMemory.throw(scope)); } } // SAFETY: `vector` is a live, valid ObjectVector for the duration of the call; diff --git a/crates/js/src/typedarray.rs b/crates/js/src/typedarray.rs index c487854..063bbd3 100644 --- a/crates/js/src/typedarray.rs +++ b/crates/js/src/typedarray.rs @@ -62,7 +62,7 @@ use mozjs::typedarray::{ Uint16, Uint32, Uint8, }; -use super::error::ExnThrown; +use super::error::{ExnThrown, OutOfMemory, ThrowException}; // --------------------------------------------------------------------------- // ArrayBuffer @@ -1014,10 +1014,7 @@ unsafe fn slice_from_parts_mut<'a, T>((ptr, len): (*mut T, usize)) -> &'a mut [T /// Returns `copy`, or, if it is `None`, an out-of-memory error reported on `scope`. fn copy_or_report(scope: &Scope<'_>, copy: Option>) -> Result, ExnThrown> { - copy.ok_or_else(|| { - crate::exception::report_out_of_memory(scope); - ExnThrown - }) + copy.ok_or_else(|| OutOfMemory.throw(scope)) } /// Get the data pointer of a freshly created (and therefore non-detached, From 5644ef56630e984e4bfe8447ccaa226a0b004569 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Thu, 22 Oct 2026 15:32:51 +0200 Subject: [PATCH 05/11] Simplify the platform, serve and CLI crates, and speed up the serve tests The native server prints the address it listens on, so `--serve 0` works, and `serve_with_shutdown` takes a listener its caller bound. A second `pre_initialize` call no longer leaves startup stuck in `Driving`, which made every later request wait forever. Native fetch errors include their underlying causes, `send` lives in the native and wasm backends, and wpt-support reports a failed eval on stderr. `serve-test-support` gains an `eventually` helper, port-0 servers, per-test work directories and a cached `wasmtime --version`, and the native serve tests listen on port 0. The wire-response unit tests, which never ran, move to an integration test. Redundant serve, CLI and WPT-support tests are merged or removed, and the examples that `showcase` covers no longer run as tests. --- Cargo.lock | 3 + crates/builtins/web-fetch/src/body_mixin.rs | 7 +- .../builtins/web-fetch/src/incoming_body.rs | 285 +- crates/builtins/web-fetch/src/lib.rs | 2 - .../builtins/web-fetch/src/outgoing_body.rs | 353 +-- .../builtins/web-fetch/tests/fetch_network.rs | 81 +- crates/builtins/web-streams/Cargo.toml | 4 + crates/builtins/web-streams/src/lib.rs | 8 +- .../web-streams/src/readable/algorithms.rs | 89 +- .../src/readable/byte_stream_controller.rs | 55 +- .../builtins/web-streams/src/readable/mod.rs | 2 + .../src/readable/native_byte_source.rs | 410 +++ .../web-streams/src/readable/native_pump.rs | 503 ++++ .../builtins/web-streams/tests/native_pump.rs | 230 ++ crates/builtins/web-streams/tests/streams.rs | 51 +- crates/libstarling/Cargo.toml | 5 +- crates/libstarling/src/lib.rs | 15 +- crates/libstarling/src/serve_common.rs | 200 +- crates/libstarling/src/serve_native.rs | 121 +- crates/libstarling/src/serve_wasm.rs | 50 +- crates/libstarling/tests/body_path.rs | 184 +- crates/libstarling/tests/common/mod.rs | 94 +- crates/libstarling/tests/serve.rs | 1757 +++++------- crates/libstarling/tests/serve_isolation.rs | 98 - crates/libstarling/tests/wire_response.rs | 208 ++ crates/platform/Cargo.toml | 4 +- crates/platform/src/clock.rs | 9 +- crates/platform/src/http.rs | 82 +- crates/platform/src/http_native.rs | 81 +- crates/platform/src/http_wasm.rs | 173 +- crates/platform/src/lib.rs | 7 +- crates/platform/tests/clock.rs | 4 +- crates/serve-test-support/Cargo.toml | 2 +- crates/serve-test-support/src/lib.rs | 258 +- crates/serve-test-support/src/wasm_serve.rs | 197 +- .../tests/serve_wasm_e2e.rs | 2426 ++++++++--------- crates/wpt-support/Cargo.toml | 2 +- crates/wpt-support/src/lib.rs | 22 +- crates/wpt-support/tests/wpt_integration.rs | 38 +- starling/Cargo.toml | 13 +- starling/examples/class_methods.rs | 9 +- starling/examples/globals.rs | 9 +- starling/examples/native_module.rs | 9 +- starling/src/main.rs | 14 - starling/tests/console_output.rs | 62 +- starling/tests/exit_status.rs | 138 +- 46 files changed, 4245 insertions(+), 4129 deletions(-) create mode 100644 crates/builtins/web-streams/src/readable/native_byte_source.rs create mode 100644 crates/builtins/web-streams/src/readable/native_pump.rs create mode 100644 crates/builtins/web-streams/tests/native_pump.rs delete mode 100644 crates/libstarling/tests/serve_isolation.rs create mode 100644 crates/libstarling/tests/wire_response.rs diff --git a/Cargo.lock b/Cargo.lock index 9690d47..2ada9d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1513,6 +1513,7 @@ dependencies = [ "js", "platform", "serve-test-support", + "tempfile", "tokio", "url", "wasip3", @@ -3085,7 +3086,9 @@ dependencies = [ "core-runtime", "js", "libstarling", + "platform", "smallvec", + "tokio", "web-globals", ] diff --git a/crates/builtins/web-fetch/src/body_mixin.rs b/crates/builtins/web-fetch/src/body_mixin.rs index f2a4dec..fd3439b 100644 --- a/crates/builtins/web-fetch/src/body_mixin.rs +++ b/crates/builtins/web-fetch/src/body_mixin.rs @@ -5,9 +5,7 @@ //! The `Body` interface mixin, which both `Request` and `Response` include. use crate::algorithms::{self, Body, BodySource, ConsumeType}; -use crate::incoming_body::{ - consume_host_body, materialize_host_body, HostBackedBodyOwner, HostBodySourceImpl, -}; +use crate::incoming_body::{consume_host_body, materialize_host_body, HostBackedBodyOwner}; use core_runtime::webidl_union; use js::error::{throw_type_error, ExnThrown}; use js::gc::handle::Heap; @@ -16,6 +14,7 @@ use js::prelude::HandleValue; use js::{ArrayBuffer, ArrayBufferView, Promise}; use platform::http::IncomingBody; use std::ffi::CStr; +use web_streams::readable::native_byte_source::NativeByteSourceImpl; use web_streams::readable::readable_stream::ReadableStream; use web_streams::readable::readable_stream::ReadableStreamImpl; use web_url::url_search_params::URLSearchParams; @@ -35,7 +34,7 @@ pub(crate) struct BodyStorage { pub(crate) host: Option, /// The `.body` stream's native host source, kept so an abort can cancel an in-flight host /// read. Set when `.body` materializes a host-backed stream. - pub(crate) source: Option>, + pub(crate) source: Option>, /// The record's byte source, attributed to the owning object for the GC. Kept in sync by /// [`sync_accounting`](Self::sync_accounting). #[no_trace] diff --git a/crates/builtins/web-fetch/src/incoming_body.rs b/crates/builtins/web-fetch/src/incoming_body.rs index a2b6360..85bbf6c 100644 --- a/crates/builtins/web-fetch/src/incoming_body.rs +++ b/crates/builtins/web-fetch/src/incoming_body.rs @@ -2,29 +2,20 @@ //! Streaming an incoming body straight from the host. //! -//! Used as the underlying source for a `body` [`ReadableStream`], or directly in +//! Read by the [`NativeByteSource`] of a `body` [`ReadableStream`], or directly in //! [`consume_host_body`] for `text`/`json`/`arrayBuffer`/`bytes`. use crate::algorithms::{convert_owned_bytes_to_js_value, Body, BodySource, ConsumeType}; use crate::body_mixin::BodyStorage; -use core_runtime::jsclass; -use core_runtime::jsmethods; -use js::conversion::FromJSVal; -use js::error::{ExnThrown, TypeError}; -use js::function::cast_payload; -use js::function::CallbackArgs; -use js::gc::handle::{Heap, OptionHeapExt, RootedHeap}; +use js::error::ExnThrown; +use js::gc::handle::{Heap, OptionHeapExt}; use js::gc::scope::Scope; -use js::native::Value; -use js::prelude::HandleValue; use js::promise::{PromiseFuture, PromiseOutcome}; -use js::{value, Function, Promise}; +use js::Promise; use platform::http::IncomingBody; -use web_streams::readable::default_controller::ReadableStreamDefaultControllerImpl; use web_streams::readable::default_reader::DefaultReader; -use web_streams::readable::readable_stream::{ReadableStream, ReadableStreamImpl}; -use web_streams::readable::ReadableStreamDefaultController; -use web_streams::AlgorithmArg; +use web_streams::readable::native_byte_source::NativeByteSource; +use web_streams::readable::readable_stream::ReadableStream; /// Back a new external `ArrayBuffer` with `bytes`, zero-copy when the chunk is /// uniquely owned. A shared chunk (another `Bytes` clone alive, e.g. a body @@ -69,104 +60,13 @@ pub(crate) fn consume_host_body<'r>( // it directly (no copy when aligned). convert_owned_bytes_to_js_value(scope, bytes, conversion) })), - Err(error) => PromiseOutcome::Reject(format!("Failed to read response body: {error}")), + Err(error) => PromiseOutcome::Reject(format!("the body could not be read: {error}")), } }; promise.spawn(PromiseFuture::new(future)); Ok(promise) } -/// Native byte source for a host-backed body's `.body` stream: holds the host -/// body and hands out one chunk per pull. `current_pull` is the promise of the -/// in-flight chunk read, kept so an abort can cancel that read (dropping it -/// closes the host connection). -/// -/// A pull can be deferred: if this body feeds an identity transform that has not -/// been read yet, the host body is left whole. If the TS's readable end ends up being -/// used as an outgoing body (or otherwise as a stream passed to the host), this -/// body's stream can be used directly. See [`host_pull`]. -#[jsclass(hidden)] -pub struct HostBodySource { - #[no_trace] - host_body: Option, - current_pull: Option>, - #[no_trace] - pulled: bool, - /// The `.body` stream this source backs, for asking whether anything - /// downstream wants bytes yet. - stream: Option>, - /// A deferred pull's promise, resolved once its deferred read has been - /// delivered — or when the body goes elsewhere and it never will be. - deferred_pull: Option>, - /// The controller a deferred pull will deliver to. - deferred_controller: Option>, -} - -#[jsmethods] -impl HostBodySource { - fn new(host_body: IncomingBody) -> Self { - HostBodySourceImpl { - host_body: Some(host_body), - current_pull: None, - pulled: false, - stream: None, - deferred_pull: None, - deferred_controller: None, - } - } -} - -impl HostBodySource<'_> { - /// Take the host body if it has not started being read. - /// - /// Returns `None` once a read operation has happened or the body has already been taken. - pub(crate) fn take_host_body(&self, scope: &Scope<'_>) -> Option { - if self.data().pulled || self.data().current_pull.is_some() { - return None; - } - let host_body = self.data_mut().host_body.take()?; - // The body will now be used by a sink directly, obviating the deferred pull. - self.settle_deferred_pull(scope, CloseStream::Yes); - Some(host_body) - } - - /// Abort the body: cancel the in-flight chunk read (if any) — which drops the - /// host body and closes the connection — and drop any idle host body. - pub(crate) fn abort(&self, scope: &Scope<'_>) { - let pull = self.data_mut().current_pull.take_rooted(scope); - if let Some(pull) = pull { - js::promise::cancel_pending_future(pull); - } - // A deferred pull has no host read to cancel, but its promise must still be - // settled, or the stream is left believing a pull is still running. The - // stream is not closed here: the caller errors it instead. - self.settle_deferred_pull(scope, CloseStream::No); - self.data_mut().host_body = None; - } - - /// Settle a deferred pull, if there is one, so the stream stops waiting on a - /// read that is no longer coming. - fn settle_deferred_pull(&self, scope: &Scope<'_>, close: CloseStream) { - let Some(pull) = self.data_mut().deferred_pull.take_rooted(scope) else { - return; - }; - let controller = self.data_mut().deferred_controller.take_rooted(scope); - if let (CloseStream::Yes, Some(controller)) = (close, controller) { - let _ = controller.close(scope); - } - let _ = pull.resolve(scope, HandleValue::undefined()); - } -} - -/// Whether settling a deferred pull should also close the stream it was pulling -/// for: it should when the body went elsewhere, but not when the stream is -/// already being torn down by a cancel or abort. -#[derive(Clone, Copy, PartialEq, Eq)] -enum CloseStream { - Yes, - No, -} - /// A `Request` or `Response` carrying a body still unread on the host. /// /// Both keep a [`BodyStorage`], so the operations over it ([`materialize_host_body`], @@ -290,173 +190,12 @@ pub(crate) fn materialize_host_body( Ok(()) } -/// Build the `.body` `ReadableStream` for a host-backed body: its underlying -/// source reads the host body one chunk per pull and enqueues it, closing at end -/// of body, so streaming or unbounded bodies are delivered progressively. +/// Build the `.body` `ReadableStream` for a host-backed body: a byte stream that reads the host +/// body when a reader asks for data, so streaming or unbounded bodies are delivered +/// progressively. pub(crate) fn host_body_stream<'r>( scope: &'r Scope<'_>, host_body: IncomingBody, -) -> Result<(ReadableStream<'r>, HostBodySource<'r>), ExnThrown> { - let state = HostBodySource::new(scope, host_body)?; - let pull = AlgorithmArg::native(scope, host_pull, state)?; - let cancel = AlgorithmArg::native(scope, host_cancel, state)?; - // Record the source so the stream can be recognized as host-backed via - // `ReadableStream::native_source`. - let stream = ReadableStream::new_native(scope, state, pull, cancel)?; - // Backlink the stream, so a pull can ask what it is feeding. - state.data_mut().stream = Some(Heap::from(stream)); - Ok((stream, state)) -} - -/// Underlying-source `pull`: read the next host chunk and enqueue it (or close at -/// end of body / error on a read failure). Returns a promise resolved once the -/// chunk has been handled. -/// -/// Makes use of the deferral machinery described in [`ReadableStream::deferred_demand`] -/// to avoid needlessly reading from a host stream. If, later on, the body used as an -/// outgoing stream, this allows it to be forwarded wholesale, skipping all handling -/// inside StarlingMonkey itself entirely. -fn host_pull( - scope: &Scope<'_>, - args: CallbackArgs<'_>, - payload: HandleValue<'_>, -) -> Result { - let state = cast_payload::(scope, payload); - let promise = Promise::new_pending(scope)?; - // Only an active event loop drives the host read. A rejected pull errors the stream. - if core_runtime::event_loop::reject_without_event_loop(scope, &promise, "reading a body")? { - return Ok(promise.as_value()); - } - let controller = ReadableStreamDefaultController::from_jsval_throwing(scope, args.get(0), ())?; - let stream = state.data().stream.get(scope); - if let Some(demand) = stream.and_then(|stream| stream.deferred_demand(scope)) { - state.data_mut().deferred_pull = Some(Heap::from(promise)); - state.data_mut().deferred_controller = Some(Heap::from(controller)); - let resume = Function::new_callback(scope, c"", 0, host_pull_resume, state)?; - demand.add_reactions_ignoring_unhandled_rejection(scope, Some(*resume), None)?; - return Ok(promise.as_value()); - } - start_host_read(scope, state, controller, promise)?; - Ok(promise.as_value()) -} - -/// Demand arrived for a deferred pull: perform the host read it deferred. -fn host_pull_resume( - scope: &Scope<'_>, - _args: CallbackArgs<'_>, - payload: HandleValue<'_>, -) -> Result { - let state = cast_payload::(scope, payload); - let controller = state.data_mut().deferred_controller.take_rooted(scope); - let pull = state.data_mut().deferred_pull.take_rooted(scope); - // Already settled: the body was claimed for direct forwarding, cancelled, or aborted while - // this pull sat deferred. - let (Some(controller), Some(promise)) = (controller, pull) else { - return Ok(value::undefined()); - }; - // Demand can arrive where no event loop is active to drive the host read. - if core_runtime::event_loop::reject_without_event_loop(scope, &promise, "reading a body")? { - return Ok(value::undefined()); - } - start_host_read(scope, state, controller, promise)?; - Ok(value::undefined()) -} - -/// Read the next host chunk into `controller`, resolving `promise` once the chunk -/// has been handled. Shared by an immediate pull and a deferred one resuming. -fn start_host_read( - scope: &Scope<'_>, - state: HostBodySource<'_>, - controller: ReadableStreamDefaultController<'_>, - promise: Promise<'_>, -) -> Result<(), ExnThrown> { - state.data_mut().pulled = true; - let Some(host_body) = state.data_mut().host_body.take() else { - // The body is gone: cancelled, or claimed for direct forwarding. Close the stream so - // a reader does not hang waiting for a chunk that will never arrive. - controller.close(scope)?; - promise.resolve(scope, HandleValue::undefined())?; - return Ok(()); - }; - // Remember the in-flight read so an abort can cancel it. - state.data_mut().current_pull = Some(Heap::from(promise)); - // The controller and state are used after the host read, keep them rooted across it. - let controller = RootedHeap::new(controller); - let state = RootedHeap::new(state); - let future = async move { - let mut host_body = host_body; - let result = host_body.next_chunk().await; - PromiseOutcome::Resolve(Box::new(move |scope: &Scope<'_>| { - deliver_chunk( - scope, - controller.get(scope), - state.get(scope), - result, - host_body, - ) - .map(|_| HandleValue::undefined()) - })) - }; - promise.spawn(PromiseFuture::new(future)); - Ok(()) -} - -/// Enqueue `result`'s chunk into the controller (keeping the host body for the -/// next pull), close at end of body, or error the stream on a read failure. -fn deliver_chunk( - scope: &Scope<'_>, - controller: ReadableStreamDefaultController, - state: HostBodySource, - result: Result, platform::http::Error>, - host_body: IncomingBody, -) -> Result<(), ExnThrown> { - // This pull has completed and can no longer be canceled. - state.data_mut().current_pull = None; - match result { - Ok(Some(bytes)) => { - // More may follow: keep the host body for the next pull. - state.data_mut().host_body = Some(host_body); - // Hand the host bytes to the engine without copying when they are uniquely owned - // and aligned (a fresh host read buffer is both); shared or unaligned chunks are - // copied. - let len = bytes.len(); - let buffer = array_buffer_from_body_bytes(scope, bytes)?; - let chunk = js::typedarray::construct_view( - scope, - js::typedarray::ViewKind::Uint8, - buffer, - 0, - len, - )?; - let chunk_value = scope.root_value(chunk.as_value()); - // Deliver via the controller abstract operation, not the - // author-patchable `…prototype.enqueue` — a page must not be able to - // observe or corrupt the network body's bytes. - controller.enqueue(scope, chunk_value)?; - } - // End of body (host body dropped): close the stream. - Ok(None) => { - controller.close(scope)?; - } - // Read failure (host body dropped): error the stream. - Err(error) => { - let _ = TypeError(format!("Failed to read response body: {error}")).throw(scope); - let reason = js::exception::take_pending(scope).ok_or(ExnThrown)?; - controller.error(scope, Some(reason))?; - } - } - Ok(()) -} - -/// Underlying-source `cancel`: drop the host body, ending the host read. -fn host_cancel( - scope: &Scope<'_>, - _args: CallbackArgs<'_>, - payload: HandleValue<'_>, -) -> Result { - let state = cast_payload::(scope, payload); - state.data_mut().host_body = None; - // The stream is being torn down, so a deferred pull's demand will never arrive. - state.settle_deferred_pull(scope, CloseStream::No); - Ok(value::undefined()) +) -> Result<(ReadableStream<'r>, NativeByteSource<'r>), ExnThrown> { + NativeByteSource::create_stream(scope, host_body) } diff --git a/crates/builtins/web-fetch/src/lib.rs b/crates/builtins/web-fetch/src/lib.rs index 9f058ee..1a30605 100644 --- a/crates/builtins/web-fetch/src/lib.rs +++ b/crates/builtins/web-fetch/src/lib.rs @@ -29,7 +29,5 @@ pub fn add_to_global(scope: &Scope<'_>, global: Object<'_>) { // The internal classes below are registered so instances can be created, and are not // web-exposed. abort::AbortFetchState::add_to_global(scope, global); - incoming_body::HostBodySource::add_to_global(scope, global); - outgoing_body::OutgoingBodyPump::add_to_global(scope, global); body_proxy::BodyProxySource::add_to_global(scope, global); } diff --git a/crates/builtins/web-fetch/src/outgoing_body.rs b/crates/builtins/web-fetch/src/outgoing_body.rs index 5b7935f..80a8c6c 100644 --- a/crates/builtins/web-fetch/src/outgoing_body.rs +++ b/crates/builtins/web-fetch/src/outgoing_body.rs @@ -15,20 +15,21 @@ //! internal reader are sent through a channel which the platform transport reads //! as it sends the body. -use core_runtime::jsclass; -use core_runtime::jsmethods; +use std::future::Future; + use js::conversion::FromJSVal; use js::error::ExnThrown; -use js::gc::handle::{Heap, RootedHeap}; use js::gc::scope::Scope; use js::prelude::HandleValue; use js::promise::{PromiseFuture, PromiseOutcome}; -use js::{Object, Promise, Uint8Array}; -use platform::http::{BodySender, OutgoingBody}; -use web_streams::readable::default_reader::{DefaultReader, DefaultReaderImpl}; -use web_streams::readable::native_read::{ - acquire_native_reader, native_reader_read, NativeReadSteps, +use js::{Promise, Uint8Array}; +use platform::http::{BodySender, IncomingBody, OutgoingBody}; +use web_streams::readable::default_reader::DefaultReader; +use web_streams::readable::native_byte_source::take_untouched_reader; +use web_streams::readable::native_pump::{ + append_bytes, NativePump, PumpBatch, PumpEnd, PumpSink, PUMP_BATCH_BYTES, }; +use web_streams::readable::native_read::acquire_native_reader; use web_streams::readable::readable_stream::ReadableStream; use crate::incoming_body::HostBackedBodyOwner; @@ -128,49 +129,6 @@ fn outgoing_body_inner( } } -/// State for the body pump: the reader being drained, the channel's write end, and the bytes read -/// but not yet sent. The sender is taken out while a send waits for channel capacity, and dropped -/// (closing the body) when the stream ends or errors. -#[jsclass(hidden)] -pub struct OutgoingBodyPump { - reader: Heap, - #[no_trace] - sender: Option, - /// Bytes of the chunks read since the last send. - #[no_trace] - batch: Vec, - /// How the stream ended, once its close or error steps ran, or a chunk was not a - /// `Uint8Array`. - #[no_trace] - end: Option, - /// Whether a read request is waiting for a chunk. - #[no_trace] - reading: bool, - /// Whether [`run`] is on the stack. - #[no_trace] - running: bool, -} - -/// How a pumped stream ended. -enum StreamEnd { - Closed, - Errored(String), -} - -#[jsmethods] -impl OutgoingBodyPump { - fn new(reader: DefaultReader, sender: BodySender) -> Self { - OutgoingBodyPumpImpl { - reader: Heap::from(reader), - sender: Some(sender), - batch: Vec::new(), - end: None, - reading: false, - running: false, - } - } -} - /// Turn a body's materialized `ReadableStream` into a [`OutgoingBody`]. /// /// A host body that has never been read is handed straight through; anything @@ -191,21 +149,11 @@ pub(crate) fn outgoing_body_from_stream( // buffer the shortcut cannot see (a transform's queue), and bypassing the stream would // drop those bytes. The pump drains the stream in order instead. A pipe that has merely // _asked_ for a chunk does not count: that pull is deferred without touching the host body - // (see `incoming_body::host_pull`). - let host_body = stream - .native_source(scope) - .and_then(|source| source.cast::().ok()) - .and_then(|source| source.take_host_body(scope)); + // (see `NativeByteSource`). Taking the host body leaves the stream locked and disturbed, as + // the pump would: author code can no longer read it and `bodyUsed` reports true. + let host_body = take_untouched_reader::(scope, &stream); match host_body { - Some(host_body) => { - // The bytes are transmitted directly, bypassing the stream, so leave the donor - // stream in the same locked + disturbed state the pump path (acquiring a reader) - // would: author code can no longer read it and `bodyUsed` reports true. - stream - .lock_and_disturb(scope) - .expect("If this happens, there's a bug in the runtime"); - OutgoingBody::Host(host_body) - } + Some(host_body) => OutgoingBody::Host(host_body), // Cancelled rather than left alone: nothing will read this stream now, and cancelling is // what makes it release whatever it draws from — an upstream response feeding a transform, // say. It runs the handler's `cancel()`, not its `pull()`, so no content is produced. @@ -238,249 +186,92 @@ fn pump_body_from_stream( Some(reader) => reader, None => acquire_native_reader(scope, &stream)?, }; - let state = OutgoingBodyPump::new(scope, reader, sender)?; - run(scope, &state); + let pump = NativePump::create(scope, reader, BodySink(sender), BodyBatch::default())?; + pump.start(scope); Ok(body) } -/// The maximum number of bytes the pump reads into one batch before sending it. -const BATCH_LIMIT: usize = 64 * 1024; +/// The bytes of the chunks read since the last send. +#[derive(Default)] +struct BodyBatch(Vec); -/// The pump's native read-request steps. Each records what it received in the pump's state and -/// then calls [`run`], which does nothing when the step ran from inside it. -const PUMP_STEPS: NativeReadSteps = NativeReadSteps { - chunk: pump_chunk_step, - close: pump_close_step, - error: pump_error_step, -}; - -/// Advance the pump. Reads chunks into the batch for as long as reads complete synchronously and -/// the batch is below [`BATCH_LIMIT`], then sends the batch once a read has to wait, the batch is -/// full, or the stream has ended. A batch the channel accepts right away is followed by more -/// reads. Otherwise the pump waits until the channel accepts it (`spawn_send` resumes it), so it -/// never outruns the peer. At most one read or send is outstanding at a time. -fn run(scope: &Scope<'_>, state: &OutgoingBodyPump<'_>) { - if state.data().running { - return; - } - state.data_mut().running = true; - loop { - while can_read(state) { - state.data_mut().reading = true; - if read_next(scope, state).is_err() { - js::exception::report_and_clear(scope, "reading a request or response body"); - state.data_mut().reading = false; - // A failed body fails the send instead of completing it. - let sender = state.data_mut().sender.take(); - if let Some(sender) = sender { - sender.fail("the body stream could not be read".to_string()); - } - } - if state.data().reading { - break; - } - } - if !send_batch(scope, state) { - break; - } +impl PumpBatch for BodyBatch { + /// Append a `Uint8Array` chunk's bytes. Any other chunk ends the body with an error, since the + /// spec's transmit-body chunk steps treat a non-`Uint8Array` chunk as a fatal error. + fn append(&mut self, scope: &Scope<'_>, chunk: HandleValue<'_>) -> Result<(), String> { + let Ok(array) = Uint8Array::from_jsval(scope, chunk, ()) else { + return Err("Body stream chunks must be of type Uint8Array".to_string()); + }; + // The spec's "a copy of chunk": the chunk's bytes are copied once, into the batch. + array + .with_slice(|bytes| append_bytes(&mut self.0, bytes)) + .map_err(|_| "The body chunk could not be allocated".to_string()) } - state.data_mut().running = false; -} - -/// Whether the pump may issue another read: the channel is not waiting for a send, the stream has -/// not ended, no read is outstanding, and the batch has room. -fn can_read(state: &OutgoingBodyPump<'_>) -> bool { - let data = state.data(); - data.sender.is_some() && data.end.is_none() && !data.reading && data.batch.len() < BATCH_LIMIT -} -/// Send the batch, or end the body, if there is reason to. Returns whether [`run`] should continue -/// reading: true if the channel accepted the batch right away. -fn send_batch(scope: &Scope<'_>, state: &OutgoingBodyPump<'_>) -> bool { - let mut data = state.data_mut(); - if data.sender.is_none() { - // A send is waiting for capacity, or the body is done. - return false; + fn is_empty(&self) -> bool { + self.0.is_empty() } - let ready = data.reading || data.end.is_some() || data.batch.len() >= BATCH_LIMIT; - if !data.batch.is_empty() && ready { - let batch = std::mem::take(&mut data.batch); - let sent = data - .sender - .as_mut() - .expect("checked above") - .try_send_chunk(batch); - drop(data); - return match sent { - Ok(()) => true, - Err(batch) => { - spawn_send(scope, state, Send::Chunk(batch)); - false - } - }; + + fn is_full(&self) -> bool { + self.0.len() >= PUMP_BATCH_BYTES } - if data.batch.is_empty() { - match data.end.take() { - // End of body: dropping the sender closes the channel. - Some(StreamEnd::Closed) => data.sender = None, - Some(StreamEnd::Errored(message)) => { - drop(data); - spawn_send(scope, state, Send::Error(message)); - } - None => {} - } + + fn take(&mut self) -> Self { + BodyBatch(std::mem::take(&mut self.0)) } - false } -/// Issue the next internal read, delivering to the pump's steps. -fn read_next(scope: &Scope<'_>, state: &OutgoingBodyPump<'_>) -> Result<(), ExnThrown> { - let reader = state.data().reader.get(scope); - native_reader_read(scope, reader, PUMP_STEPS, state.as_object()) -} +/// The write end of the channel from which the platform transport reads the body. A batch that the +/// channel doesn't accept right away waits for capacity, so the pump never outruns the peer. +struct BodySink(BodySender); -/// A pending send to the body channel: a batch of bytes (after which the pump reads on) or a -/// terminal error (after which the pump stops). -enum Send { - Chunk(Vec), - Error(String), -} +impl PumpSink for BodySink { + type Batch = BodyBatch; -/// Chunk steps: append a `Uint8Array` chunk's bytes to the batch. Anything else ends the body with -/// an error, since the spec's transmit-body chunk steps treat a non-`Uint8Array` chunk as a fatal -/// error. -fn pump_chunk_step( - scope: &Scope<'_>, - payload: Object<'_>, - chunk: HandleValue<'_>, -) -> Result<(), ExnThrown> { - let state = payload - .cast::() - .expect("payload is an OutgoingBodyPump"); - let mut data = state.data_mut(); - data.reading = false; - match Uint8Array::from_jsval(scope, chunk, ()) { - // The spec's "a copy of chunk": the chunk's bytes are copied once, into the batch. - Ok(array) => { - let batch = &mut data.batch; - let appended = array.with_slice(|bytes| { - // Appending to a batch reserves room up to the limit, so the batch is copied once - // rather than through every doubling of its capacity. - if batch.capacity() - batch.len() < bytes.len() { - let wanted = if batch.is_empty() { - bytes.len() - } else { - BATCH_LIMIT.max(batch.len() + bytes.len()) - batch.len() - }; - batch.try_reserve_exact(wanted)?; - } - batch.extend_from_slice(bytes); - Ok::<(), std::collections::TryReserveError>(()) - }); - if appended.is_err() { - data.end = Some(StreamEnd::Errored( - "The body chunk could not be allocated".to_string(), - )); - } - } - Err(_) => { - data.end = Some(StreamEnd::Errored( - "Body stream chunks must be of type Uint8Array".to_string(), - )) + fn try_write(&mut self, batch: BodyBatch) -> Result<(), BodyBatch> { + self.0.try_send_chunk(batch.0).map_err(BodyBatch) + } + + fn write(mut self, batch: BodyBatch) -> impl Future> + 'static { + // The event loop is kept alive until the receiver accepts the batch. + let interest = + core_runtime::event_loop::with_active_event_loop(|el| el.acquire_interest_handle()); + async move { + let accepted = self.0.send_chunk(batch.0).await; + drop(interest); + accepted.then_some(self) } } - drop(data); - run(scope, &state); - Ok(()) -} -/// Close steps: end of body, after the batch is sent. -fn pump_close_step(scope: &Scope<'_>, payload: Object<'_>) -> Result<(), ExnThrown> { - let state = payload - .cast::() - .expect("payload is an OutgoingBodyPump"); - let mut data = state.data_mut(); - data.reading = false; - data.end = Some(StreamEnd::Closed); - drop(data); - run(scope, &state); - Ok(()) -} + fn describe_source_error(_scope: &Scope<'_>, _error: HandleValue<'_>) -> String { + "body stream errored".to_string() + } -/// Error steps: abort the body with the stream's error, after the batch is sent. -fn pump_error_step( - scope: &Scope<'_>, - payload: Object<'_>, - _error: HandleValue<'_>, -) -> Result<(), ExnThrown> { - let state = payload - .cast::() - .expect("payload is an OutgoingBodyPump"); - let mut data = state.data_mut(); - data.reading = false; - data.end = Some(StreamEnd::Errored("body stream errored".to_string())); - drop(data); - run(scope, &state); - Ok(()) + /// End the body. A failed pump aborts it with the failure's message, which fails the send. + /// Otherwise, dropping the sender closes the channel. + fn finish(self, scope: &Scope<'_>, end: &PumpEnd) { + if let PumpEnd::Failed(message) = end { + spawn_send_error(scope, self.0, message.clone()); + } + } } -/// Send `item` to the body channel (taking the sender out of the pump), awaiting -/// channel capacity for backpressure. After a batch is accepted, the pump runs again. -/// After an error, the pump stops and the sender is dropped (closing the channel after -/// the queued error). -fn spawn_send(scope: &Scope<'_>, state: &OutgoingBodyPump<'_>, item: Send) { - let Some(sender) = state.data_mut().sender.take() else { - return; - }; +/// Abort the body with `message`, waiting for channel capacity so the error is queued after the +/// chunks already sent. The sender is dropped afterwards, which closes the channel. +fn spawn_send_error(scope: &Scope<'_>, mut sender: BodySender, message: String) { let Ok(promise) = Promise::new_pending(scope) else { js::exception::report_and_clear(scope, "sending a request or response body"); - sender.fail("the body could not be sent".to_string()); + sender.fail(message); return; }; let _ = promise.set_any_is_handled(scope); - let state = RootedHeap::new(*state); - let is_chunk = matches!(item, Send::Chunk(_)); - // Ensure the event loop stays alive until the receiver accepts the chunk or error. + // The event loop is kept alive until the receiver accepts the error. let interest = core_runtime::event_loop::with_active_event_loop(|el| el.acquire_interest_handle()); let future = async move { - let mut sender = sender; - let accepted = match item { - Send::Chunk(bytes) => sender.send_chunk(bytes).await, - Send::Error(message) => { - sender.send_error(message).await; - false - } - }; - PromiseOutcome::Resolve(Box::new(move |scope: &Scope<'_>| { - drop(interest); - if is_chunk { - let state = state.get(scope); - if accepted { - state.data_mut().sender = Some(sender); - run(scope, &state); - } else { - // A refused chunk means the receiver is gone: the peer stopped reading the - // body, typically because the client disconnected. Cancel the stream, and - // transitively the underlying source, so it can stop producing chunks. - let reader = state.data().reader.get(scope); - match reader.cancel(scope, None) { - // Cancelling rejects if the underlying source's `cancel()` throws. - // There is no caller left to report that to: the body is already - // undeliverable. Mark it handled so it is not announced as an unhandled - // rejection on top. - Ok(promise) => { - let _ = promise.set_any_is_handled(scope); - } - Err(_) => js::exception::report_and_clear( - scope, - "cancelling an abandoned response body", - ), - } - } - } - Ok(HandleValue::undefined()) - })) + sender.send_error(message).await; + drop(interest); + PromiseOutcome::Resolve(Box::new(|_scope: &Scope<'_>| Ok(HandleValue::undefined()))) }; promise.spawn(PromiseFuture::new(future)); } diff --git a/crates/builtins/web-fetch/tests/fetch_network.rs b/crates/builtins/web-fetch/tests/fetch_network.rs index bb87410..295b550 100644 --- a/crates/builtins/web-fetch/tests/fetch_network.rs +++ b/crates/builtins/web-fetch/tests/fetch_network.rs @@ -295,6 +295,34 @@ fn fetch_pipes_a_response_body_through_an_identity_transform() { assert_eq!(out, "piped through identity transform"); } +#[test] +fn a_pipe_whose_body_is_taken_before_it_reads_still_finishes() { + // `fetch()` takes the host body before the pipe's first read, so the pipe's read finds the + // body gone and must close the stream rather than wait for demand that never comes. + let source = start_server(vec![ok("taken before the pipe reads")]); + let echo = start_echo_server(); + let out = run_and_get_out(&format!( + r#" + globalThis.__out = "pending"; + fetch("{source}") + .then(async (response) => {{ + const transform = new TransformStream(); + const piped = response.body.pipeTo(transform.writable); + const echoed = await fetch("{echo}", {{ + method: "POST", + body: transform.readable, + duplex: "half", + }}); + const text = await echoed.text(); + await piped; + globalThis.__out = text; + }}) + .catch(e => {{ globalThis.__out = "error:" + e; }}); + "# + )); + assert_eq!(out, "taken before the pipe reads"); +} + #[test] fn fetch_pumps_a_piped_multichunk_body_without_dropping_chunks() { // The request-body *pump* path (as opposed to the incoming→outgoing shortcut): a multi-chunk @@ -549,17 +577,64 @@ fn redirect_with_credentialed_location_is_a_network_error() { assert_eq!(out, "rejected:TypeError"); } +#[test] +fn a_transport_error_message_includes_its_causes() { + // Nothing listens on port 1, so connecting to it is refused. + let out = run_and_get_out( + r#" + globalThis.__out = "pending"; + fetch("http://127.0.0.1:1/") + .then(r => { globalThis.__out = "resolved:" + r.status; }) + .catch(e => { globalThis.__out = e.constructor.name + ":" + e.message; }); + "#, + ); + assert!( + out.starts_with("TypeError:Failed to fetch: ") && out.contains("refused"), + "{out}" + ); +} + +#[test] +fn a_network_body_is_a_byte_stream() { + // A body read into views of 4 bytes arrives in chunks larger than the view. A view of 64 bytes + // holds the whole body. + let url = start_server(vec![ok("seventeen bytes!!"), ok("seventeen bytes!!")]); + let out = run_and_get_out(&format!( + r#" + globalThis.__out = "pending"; + async function readInto(viewLength) {{ + const response = await fetch("{url}"); + const reader = response.body.getReader({{ mode: "byob" }}); + const dec = new TextDecoder(); + const reads = []; + for (;;) {{ + const {{ value, done }} = await reader.read(new Uint8Array(viewLength)); + if (done) break; + reads.push(dec.decode(value)); + }} + return reads.join("|"); + }} + (async () => {{ + const small = await readInto(4); + const large = await readInto(64); + globalThis.__out = small + " / " + large; + }})().catch(e => {{ globalThis.__out = "error:" + e; }}); + "# + )); + assert_eq!(out, "seve|ntee|n by|tes!|! / seventeen bytes!!"); +} + #[test] fn response_body_unaffected_by_patched_controller_enqueue() { // A network response body is delivered through the controller abstract - // operation, not the author-visible ReadableStreamDefaultController.prototype - // methods. Patching prototype.enqueue to drop chunks must not affect the - // body's bytes. + // operation, not the author-visible controller prototypes' methods. Patching + // their `enqueue` to drop chunks must not affect the body's bytes. let url = start_server(vec![ok("untampered body bytes")]); let out = run_and_get_out(&format!( r#" globalThis.__out = "pending"; ReadableStreamDefaultController.prototype.enqueue = function () {{ /* drop */ }}; + ReadableByteStreamController.prototype.enqueue = function () {{ /* drop */ }}; fetch("{url}") .then(async (response) => {{ const reader = response.body.getReader(); diff --git a/crates/builtins/web-streams/Cargo.toml b/crates/builtins/web-streams/Cargo.toml index 1daa1e6..4d8a4b8 100644 --- a/crates/builtins/web-streams/Cargo.toml +++ b/crates/builtins/web-streams/Cargo.toml @@ -14,10 +14,14 @@ test = false smallvec.workspace = true core-runtime.workspace = true js.workspace = true +platform.workspace = true web-globals.workspace = true [dev-dependencies] libstarling.workspace = true +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tokio.workspace = true + [lints] workspace = true diff --git a/crates/builtins/web-streams/src/lib.rs b/crates/builtins/web-streams/src/lib.rs index 702e65e..a0fb19e 100644 --- a/crates/builtins/web-streams/src/lib.rs +++ b/crates/builtins/web-streams/src/lib.rs @@ -31,14 +31,16 @@ pub fn add_to_global(scope: &Scope<'_>, global: Object<'_>) { queuing::ByteLengthQueuingStrategy::add_to_global(scope, global); queuing::CountQueuingStrategy::add_to_global(scope, global); - // Internal classes holding the state of tee, pipe, from-iterable and read-all-bytes - // operations. Creating instances needs their prototypes registered, and `hidden` keeps - // their constructors off the global. + // Internal classes holding the state of tee, pipe, from-iterable, read-all-bytes, native + // source and native pump operations. Creating instances needs their prototypes registered, + // and `hidden` keeps their constructors off the global. readable::algorithms::TeeState::add_to_global(scope, global); readable::algorithms::ByteTeeState::add_to_global(scope, global); readable::algorithms::PipeState::add_to_global(scope, global); readable::algorithms::FromIterableState::add_to_global(scope, global); readable::read_all_bytes::ReadAllBytesState::add_to_global(scope, global); + readable::native_byte_source::NativeByteSource::add_to_global(scope, global); + readable::native_pump::NativePump::add_to_global(scope, global); js::iteration::AsyncIteratorRecord::add_to_global(scope, global); // Chain the async iterator's prototype under `%AsyncIteratorPrototype%` so it diff --git a/crates/builtins/web-streams/src/readable/algorithms.rs b/crates/builtins/web-streams/src/readable/algorithms.rs index b229ee0..162362f 100644 --- a/crates/builtins/web-streams/src/readable/algorithms.rs +++ b/crates/builtins/web-streams/src/readable/algorithms.rs @@ -2505,14 +2505,22 @@ fn byte_tee_default_microtask( // If _canceled1_ is false, perform ! `ReadableByteStreamControllerEnqueue`(branch1, chunk1). if !canceled1 { let b1 = byte_tee_branch_controller(scope, state, false); - readable_byte_stream_controller_enqueue(scope, &b1, byte_tee_view(scope, chunk)) - .expect(BYTE_TEE_ENQUEUE_INFALLIBLE); + readable_byte_stream_controller_enqueue( + scope, + &b1, + EnqueueChunk::View(byte_tee_view(scope, chunk)), + ) + .expect(BYTE_TEE_ENQUEUE_INFALLIBLE); } // If _canceled2_ is false, perform ! `ReadableByteStreamControllerEnqueue`(branch2, chunk2). if !canceled2 { let b2 = byte_tee_branch_controller(scope, state, true); - readable_byte_stream_controller_enqueue(scope, &b2, byte_tee_view(scope, chunk2)) - .expect(BYTE_TEE_ENQUEUE_INFALLIBLE); + readable_byte_stream_controller_enqueue( + scope, + &b2, + EnqueueChunk::View(byte_tee_view(scope, chunk2)), + ) + .expect(BYTE_TEE_ENQUEUE_INFALLIBLE); } state.data_mut().reading = false; if state.data().read_again_for_branch1 { @@ -2631,7 +2639,7 @@ fn byte_tee_byob_microtask( readable_byte_stream_controller_enqueue( scope, &oc, - byte_tee_view(scope, cloned_chunk), + EnqueueChunk::View(byte_tee_view(scope, cloned_chunk)), ) .expect(BYTE_TEE_ENQUEUE_INFALLIBLE); } @@ -3960,12 +3968,40 @@ fn readable_byte_stream_controller_convert_pull_into_descriptor<'r>( ) } +/// The chunk passed to [`readable_byte_stream_controller_enqueue`]. +#[derive(Clone, Copy)] +pub(crate) enum EnqueueChunk<'v, 's> { + /// A view that JavaScript can hold. Its buffer is transferred. + View(js::ArrayBufferView<'v>), + /// A buffer created by [`ReadableByteStreamController::enqueue_bytes`], which no JavaScript + /// has seen. It is queued without being transferred. + Owned(js::ArrayBuffer<'s>), +} + +impl EnqueueChunk<'_, '_> { + /// The chunk's length in bytes. + pub(crate) fn byte_length(&self) -> usize { + match self { + EnqueueChunk::View(view) => view.byte_length(), + EnqueueChunk::Owned(buffer) => buffer.byte_length(), + } + } + + /// The length in bytes of the buffer holding the chunk. + pub(crate) fn buffer_byte_length(&self, scope: &Scope<'_>) -> Result { + match self { + EnqueueChunk::View(view) => Ok(view.viewed_buffer(scope)?.byte_length()), + EnqueueChunk::Owned(buffer) => Ok(buffer.byte_length()), + } + } +} + /// /// ByteStreamControllerEnqueue(controller, chunk) performs the following steps: -pub(crate) fn readable_byte_stream_controller_enqueue( - scope: &Scope<'_>, +pub(crate) fn readable_byte_stream_controller_enqueue<'s>( + scope: &'s Scope<'_>, controller: &ReadableByteStreamController<'_>, - chunk: js::ArrayBufferView<'_>, + chunk: EnqueueChunk<'_, 's>, ) -> Result<(), ExnThrown> { // Step 1: Let _stream_ be _controller_.`[[stream]]`. let stream = controller.stream(scope); @@ -3974,21 +4010,28 @@ pub(crate) fn readable_byte_stream_controller_enqueue( if controller.data().close_requested || stream.data().state != ReadableStreamState::Readable { return Ok(()); } - // Step 3: Let _buffer_ be _chunk_.[[ViewedArrayBuffer]]. - let buffer = chunk.viewed_buffer(scope)?; - // Step 4: Let _byteOffset_ be _chunk_.[[ByteOffset]]. - let byte_offset = chunk.byte_offset(); - // Step 5: Let _byteLength_ be _chunk_.[[ByteLength]]. - let byte_length = chunk.byte_length(); - // Step 6: If ! `IsDetachedBuffer`(_buffer_) is true, throw a ``TypeError`` exception. - if buffer.is_detached() { - return Err(js::error::throw_type_error( - scope, - c"enqueue() view's buffer is detached", - )); - } - // Step 7: Let _transferredBuffer_ be ? `TransferArrayBuffer`(_buffer_). - let transferred_buffer = buffer.transfer(scope)?; + let (transferred_buffer, byte_offset, byte_length) = match chunk { + EnqueueChunk::View(chunk) => { + // Step 3: Let _buffer_ be _chunk_.[[ViewedArrayBuffer]]. + let buffer = chunk.viewed_buffer(scope)?; + // Step 4: Let _byteOffset_ be _chunk_.[[ByteOffset]]. + let byte_offset = chunk.byte_offset(); + // Step 5: Let _byteLength_ be _chunk_.[[ByteLength]]. + let byte_length = chunk.byte_length(); + // Step 6: If ! `IsDetachedBuffer`(_buffer_) is true, throw a ``TypeError`` exception. + if buffer.is_detached() { + return Err(js::error::throw_type_error( + scope, + c"enqueue() view's buffer is detached", + )); + } + // Step 7: Let _transferredBuffer_ be ? `TransferArrayBuffer`(_buffer_). + (buffer.transfer(scope)?, byte_offset, byte_length) + } + // Step 7 transfers the buffer, which JavaScript holds, so that it can't change the queued + // bytes. No JavaScript holds an owned buffer, so it is queued whole without a transfer. + EnqueueChunk::Owned(buffer) => (buffer, 0, buffer.byte_length()), + }; // Step 8: If _controller_.`[[pendingPullIntos]]` is not `empty`, Let _firstPendingPullInto_ be // _controller_.`[[pendingPullIntos]]`[0]. If ! // `IsDetachedBuffer`(_firstPendingPullInto_’s `buffer`) is true, throw a diff --git a/crates/builtins/web-streams/src/readable/byte_stream_controller.rs b/crates/builtins/web-streams/src/readable/byte_stream_controller.rs index bfabd87..00be684 100644 --- a/crates/builtins/web-streams/src/readable/byte_stream_controller.rs +++ b/crates/builtins/web-streams/src/readable/byte_stream_controller.rs @@ -6,6 +6,7 @@ use std::collections::VecDeque; use crate::readable::ReadableStream; +use super::algorithms::EnqueueChunk; use super::byob_request::ReadableStreamBYOBRequest; use super::byob_request::ReadableStreamBYOBRequestImpl; use super::readable_stream::ReadableStreamImpl; @@ -211,9 +212,44 @@ impl ReadableByteStreamController { &self, scope: &Scope<'_>, view: js::ArrayBufferView<'_>, + ) -> Result<(), ExnThrown> { + self.enqueue_chunk(scope, EnqueueChunk::View(view)) + } + + /// + #[method] + pub fn error(&self, scope: &Scope<'_>, e: Option>) -> Result<(), ExnThrown> { + // Step 1: Perform ! `ByteStreamControllerError`(`this`, _e_). + let e = e.unwrap_or(HandleValue::undefined()); + super::algorithms::readable_byte_stream_controller_error(scope, self, e); + Ok(()) + } + + pub(crate) fn stream<'r>(&self, scope: &'r Scope<'_>) -> ReadableStream<'r> { + self.data().stream.get(scope) + } +} + +impl ReadableByteStreamController<'_> { + /// Enqueue `bytes` as a chunk, as [`enqueue`](Self::enqueue) does for a `Uint8Array` holding + /// them. The chunk's buffer is created from `bytes`, without copying where possible (see + /// [`js::ArrayBuffer::from_external`]). No JavaScript has seen it, so it is queued without + /// being transferred. Throws a `TypeError` in the cases in which `enqueue` throws one: for + /// empty `bytes`, after `close()`, and if the stream isn't readable. + pub fn enqueue_bytes(&self, scope: &Scope<'_>, bytes: Vec) -> Result<(), ExnThrown> { + let buffer = js::ArrayBuffer::from_external(scope, bytes)?; + self.enqueue_chunk(scope, EnqueueChunk::Owned(buffer)) + } + + /// The steps of [`enqueue`](Self::enqueue), for a chunk from JavaScript or from + /// [`enqueue_bytes`](Self::enqueue_bytes). + fn enqueue_chunk<'s>( + &self, + scope: &'s Scope<'_>, + chunk: EnqueueChunk<'_, 's>, ) -> Result<(), ExnThrown> { // Step 1: If _chunk_.[[ByteLength]] is 0, throw a ``TypeError`` exception. - if view.byte_length() == 0 { + if chunk.byte_length() == 0 { return Err(js::error::throw_type_error( scope, c"enqueue() view has a byte length of 0", @@ -221,7 +257,7 @@ impl ReadableByteStreamController { } // Step 2: If _chunk_.[[ViewedArrayBuffer]].[[ByteLength]] is 0, throw a ``TypeError`` // exception. - if view.viewed_buffer(scope)?.byte_length() == 0 { + if chunk.buffer_byte_length(scope)? == 0 { return Err(js::error::throw_type_error( scope, c"enqueue() view's buffer has a byte length of 0", @@ -244,19 +280,6 @@ impl ReadableByteStreamController { )); } // Step 5: Return ? `ByteStreamControllerEnqueue`(`this`, _chunk_). - super::algorithms::readable_byte_stream_controller_enqueue(scope, self, view) - } - - /// - #[method] - pub fn error(&self, scope: &Scope<'_>, e: Option>) -> Result<(), ExnThrown> { - // Step 1: Perform ! `ByteStreamControllerError`(`this`, _e_). - let e = e.unwrap_or(HandleValue::undefined()); - super::algorithms::readable_byte_stream_controller_error(scope, self, e); - Ok(()) - } - - pub(crate) fn stream<'r>(&self, scope: &'r Scope<'_>) -> ReadableStream<'r> { - self.data().stream.get(scope) + super::algorithms::readable_byte_stream_controller_enqueue(scope, self, chunk) } } diff --git a/crates/builtins/web-streams/src/readable/mod.rs b/crates/builtins/web-streams/src/readable/mod.rs index e463389..dcfd43c 100644 --- a/crates/builtins/web-streams/src/readable/mod.rs +++ b/crates/builtins/web-streams/src/readable/mod.rs @@ -11,6 +11,8 @@ pub mod byte_stream_controller; pub mod default_controller; pub mod default_reader; pub mod enums; +pub mod native_byte_source; +pub mod native_pump; pub mod native_read; pub mod options; pub mod read_all_bytes; diff --git a/crates/builtins/web-streams/src/readable/native_byte_source.rs b/crates/builtins/web-streams/src/readable/native_byte_source.rs new file mode 100644 index 0000000..e43c2b3 --- /dev/null +++ b/crates/builtins/web-streams/src/readable/native_byte_source.rs @@ -0,0 +1,410 @@ +// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception + +//! A readable byte stream whose bytes come from a native reader. +//! +//! [`NativeByteSource::create_stream`] creates a byte stream that reads from a [`ByteReader`] +//! when a reader of the stream asks for data. Each read requests at most 64 KiB, or the length of +//! a pending BYOB request's view. The bytes read are copied into that view if they fit. Otherwise, +//! they are enqueued as a chunk that uses the read's buffer if at least half of it is used, and a +//! copy if not. The next read with the same capacity reuses a buffer that wasn't enqueued. The end +//! of the reader closes the stream, and a failed read errors it with a `TypeError`. Cancelling the +//! stream drops the reader, along with a read in flight. +//! +//! While the stream is piped to an identity `TransformStream` whose readable end has no demand, a +//! pull waits for demand before reading (see [`ReadableStream::deferred_demand`]). Until a read +//! starts, [`take_untouched_reader`] can take the reader, so that a native consumer reads from it +//! directly. + +use std::any::Any; +use std::future::Future; +use std::pin::Pin; + +use core_runtime::{jsclass, jsmethods}; +use js::conversion::FromJSVal; +use js::error::{ExnThrown, ThrowException, TypeError}; +use js::function::{cast_payload, CallbackArgs}; +use js::gc::handle::{Heap, OptionHeapExt, RootedHeap}; +use js::gc::scope::Scope; +use js::native::Value; +use js::prelude::HandleValue; +use js::promise::{PromiseFuture, PromiseOutcome}; +use js::{value, Function, Promise}; + +use super::byte_stream_controller::{ + ReadableByteStreamController, ReadableByteStreamControllerImpl, +}; +use super::readable_stream::{ReadableStream, ReadableStreamImpl}; +use crate::AlgorithmArg; + +/// The maximum number of bytes that one read requests from a [`ByteReader`]. +const READ_BYTES: usize = 64 * 1024; + +/// The result of [`ByteReader::read`]. +pub enum ByteRead { + /// The reader, and the bytes read, which are never empty. + Bytes(R, Vec), + /// The reader has no more bytes. + End, + /// The read failed. The stream is errored with a `TypeError` with this message. + Failed(String), +} + +/// A native source of bytes for [`NativeByteSource`]. The source doesn't trace its reader, so a +/// reader must not hold GC things. +pub trait ByteReader: Sized + 'static { + /// Read the next bytes. `buf` is empty, and has capacity for the number of bytes wanted. The + /// bytes read can be in `buf` or in another allocation, and can be more than wanted. + fn read(self, buf: Vec) -> impl Future> + 'static; +} + +impl ByteReader for platform::http::IncomingBody { + async fn read(mut self, buf: Vec) -> ByteRead { + match self.read_into(buf).await { + Ok(Some(bytes)) => ByteRead::Bytes(self, bytes), + Ok(None) => ByteRead::End, + Err(error) => ByteRead::Failed(format!("the body could not be read: {error}")), + } + } +} + +/// A [`ByteReader`] with its type erased. +trait ErasedReader { + fn read(self: Box, buf: Vec) -> ErasedRead; + fn as_any(&self) -> &dyn Any; + fn into_any(self: Box) -> Box; +} + +/// A read from an [`ErasedReader`]. +type ErasedRead = Pin>>>>; + +impl ErasedReader for R { + fn read(self: Box, buf: Vec) -> ErasedRead { + let read = ByteReader::read(*self, buf); + Box::pin(async move { + match read.await { + ByteRead::Bytes(reader, bytes) => { + ByteRead::Bytes(Box::new(reader) as Box, bytes) + } + ByteRead::End => ByteRead::End, + ByteRead::Failed(message) => ByteRead::Failed(message), + } + }) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn into_any(self: Box) -> Box { + self + } +} + +/// The underlying source of a byte stream created by [`NativeByteSource::create_stream`]. +#[jsclass(hidden)] +pub struct NativeByteSource { + /// `None` while a read is in flight, and after the reader ended or failed, the stream was + /// cancelled, the source was aborted, or the reader was taken. + #[no_trace] + reader: Option>, + /// Whether a read has started. + #[no_trace] + pulled: bool, + /// The promise of the pull whose read is in flight, used to cancel the read. + current_pull: Option>, + /// Whether the stream was cancelled or the source aborted. The bytes of a read that completes + /// afterwards are not delivered. + #[no_trace] + cancelled: bool, + /// The buffer of a read whose bytes were copied into JavaScript. The next read with the same + /// capacity reuses it. + #[no_trace] + spare: Option>, + /// The stream this source reads for, used to look up its deferred demand. + stream: Option>, + /// A deferred pull's promise, resolved once its read was delivered, or once the reader was + /// taken or dropped. + deferred_pull: Option>, + /// The controller to which a deferred pull delivers. + deferred_controller: Option>, +} + +#[jsmethods] +impl NativeByteSource { + fn new(reader: Box) -> Self { + NativeByteSourceImpl { + reader: Some(reader), + ..Default::default() + } + } +} + +/// Whether settling a deferred pull also closes the stream. +#[derive(Clone, Copy, PartialEq, Eq)] +enum CloseStream { + /// The reader was taken, so the stream ends. + Yes, + /// The stream is being cancelled or errored by its caller. + No, +} + +impl NativeByteSource<'_> { + /// Create a byte stream that reads from `reader`, and the stream's source. + pub fn create_stream<'r, R: ByteReader>( + scope: &'r Scope<'_>, + reader: R, + ) -> Result<(ReadableStream<'r>, NativeByteSource<'r>), ExnThrown> { + let source = NativeByteSource::new(scope, Box::new(reader))?; + let pull = AlgorithmArg::native(scope, pull, source)?; + let cancel = AlgorithmArg::native(scope, cancel, source)?; + let stream = ReadableStream::new_native_bytes(scope, source, pull, cancel)?; + source.data_mut().stream = Some(Heap::from(stream)); + Ok((stream, source)) + } + + /// Stop reading: drop a read in flight and the reader, and settle a deferred pull without + /// closing the stream. The caller errors the stream. + pub fn abort(&self, scope: &Scope<'_>) { + self.data_mut().cancelled = true; + let pull = self.data_mut().current_pull.take_rooted(scope); + if let Some(pull) = pull { + // Dropping the read's future drops the reader it owns. + js::promise::cancel_pending_future(pull); + } + self.data_mut().reader = None; + self.data_mut().spare = None; + self.settle_deferred_pull(scope, CloseStream::No); + } + + /// Resolve a deferred pull, if there is one, and close the stream if `close` is + /// [`CloseStream::Yes`]. + fn settle_deferred_pull(&self, scope: &Scope<'_>, close: CloseStream) { + let Some(pull) = self.data_mut().deferred_pull.take_rooted(scope) else { + return; + }; + let controller = self.data_mut().deferred_controller.take_rooted(scope); + if let (CloseStream::Yes, Some(controller)) = (close, controller) { + let _ = close_stream(scope, controller); + } + let _ = pull.resolve(scope, HandleValue::undefined()); + } +} + +/// Take the reader of the [`NativeByteSource`] of `stream` if no read has started and the reader +/// is an `R`. +/// +/// `stream` is the source's own stream, or the readable end of an identity `TransformStream` to +/// which that stream is piped. The source's stream closes right away if a pull is waiting for +/// demand, and on its next pull otherwise. `stream` is locked and disturbed, since its bytes are +/// read from the reader directly from now on. +pub fn take_untouched_reader( + scope: &Scope<'_>, + stream: &ReadableStream<'_>, +) -> Option { + let source = stream + .native_source(scope)? + .cast::() + .ok()?; + if source.data().pulled { + return None; + } + let reader = source.data_mut().reader.take()?; + if !reader.as_any().is::() { + source.data_mut().reader = Some(reader); + return None; + } + let reader = *reader + .into_any() + .downcast::() + .expect("the reader was checked above"); + source.settle_deferred_pull(scope, CloseStream::Yes); + stream + .lock_and_disturb(scope) + .expect("an unlocked stream can be locked"); + Some(reader) +} + +/// Underlying-source `pull`: read the next bytes and deliver them, or close the stream once the +/// reader ended. A pull while the stream is piped to an identity `TransformStream` without demand +/// waits for demand. +fn pull( + scope: &Scope<'_>, + args: CallbackArgs<'_>, + payload: HandleValue<'_>, +) -> Result { + let source = cast_payload::(scope, payload); + let promise = Promise::new_pending(scope)?; + // The read requires an active event loop. A rejected pull errors the stream. + if core_runtime::event_loop::reject_without_event_loop(scope, &promise, "reading a stream")? { + return Ok(promise.as_value()); + } + let controller = ReadableByteStreamController::from_jsval_throwing(scope, args.get(0), ())?; + // A pull waits for demand only while there is a reader to read from. Without one, + // `start_read` closes the stream. + let stream = source.data().stream.get(scope); + let demand = if source.data().reader.is_some() { + stream.and_then(|stream| stream.deferred_demand(scope)) + } else { + None + }; + if let Some(demand) = demand { + source.data_mut().deferred_pull = Some(Heap::from(promise)); + source.data_mut().deferred_controller = Some(Heap::from(controller)); + let resume = Function::new_callback(scope, c"", 0, resume_pull, source)?; + demand.add_reactions_ignoring_unhandled_rejection(scope, Some(*resume), None)?; + return Ok(promise.as_value()); + } + start_read(scope, source, controller, promise)?; + Ok(promise.as_value()) +} + +/// Demand arrived for a deferred pull: start the read it deferred. +fn resume_pull( + scope: &Scope<'_>, + _args: CallbackArgs<'_>, + payload: HandleValue<'_>, +) -> Result { + let source = cast_payload::(scope, payload); + let controller = source.data_mut().deferred_controller.take_rooted(scope); + let pull = source.data_mut().deferred_pull.take_rooted(scope); + // The pull was settled while it waited: the reader was taken, or the stream was cancelled + // or aborted. + let (Some(controller), Some(promise)) = (controller, pull) else { + return Ok(value::undefined()); + }; + // Demand can arrive where no event loop is active to drive the read. + if core_runtime::event_loop::reject_without_event_loop(scope, &promise, "reading a stream")? { + return Ok(value::undefined()); + } + start_read(scope, source, controller, promise)?; + Ok(value::undefined()) +} + +/// Read the next bytes into `controller`'s stream, and resolve `promise` once they were +/// delivered. +fn start_read( + scope: &Scope<'_>, + source: NativeByteSource<'_>, + controller: ReadableByteStreamController<'_>, + promise: Promise<'_>, +) -> Result<(), ExnThrown> { + source.data_mut().pulled = true; + let reader = source.data_mut().reader.take(); + let Some(reader) = reader else { + // The reader ended, or was taken or dropped, so no more bytes will arrive. + close_stream(scope, controller)?; + promise.resolve(scope, HandleValue::undefined())?; + return Ok(()); + }; + source.data_mut().current_pull = Some(Heap::from(promise)); + // A pending BYOB read requests at most its view's length, so that the bytes fit into it. + let byob_length = controller + .byob_request(scope) + .and_then(|request| request.view(scope)) + .map(|view| view.byte_length()); + let capacity = byob_length.map_or(READ_BYTES, |length| length.clamp(1, READ_BYTES)); + let spare = source + .data_mut() + .spare + .take() + .filter(|spare| spare.capacity() == capacity); + let mut buf = spare.unwrap_or_else(|| Vec::with_capacity(capacity)); + buf.clear(); + let controller = RootedHeap::new(controller); + let source = RootedHeap::new(source); + let future = async move { + let read = reader.read(buf).await; + PromiseOutcome::Resolve(Box::new(move |scope: &Scope<'_>| { + deliver(scope, source.get(scope), controller.get(scope), read) + .map(|()| HandleValue::undefined()) + })) + }; + promise.spawn(PromiseFuture::new(future)); + Ok(()) +} + +/// Deliver the result of a read to `controller`'s stream. A source that was cancelled or aborted +/// during the read doesn't deliver the bytes, and drops the reader. +fn deliver( + scope: &Scope<'_>, + source: NativeByteSource<'_>, + controller: ReadableByteStreamController<'_>, + read: ByteRead>, +) -> Result<(), ExnThrown> { + source.data_mut().current_pull = None; + if source.data().cancelled { + return Ok(()); + } + match read { + ByteRead::Bytes(reader, bytes) => { + source.data_mut().reader = Some(reader); + let spare = deliver_bytes(scope, controller, bytes)?; + source.data_mut().spare = spare; + Ok(()) + } + ByteRead::End => { + source.data_mut().spare = None; + close_stream(scope, controller) + } + ByteRead::Failed(message) => { + source.data_mut().spare = None; + let _ = TypeError(message).throw(scope); + let reason = js::exception::take_pending(scope).ok_or(ExnThrown)?; + controller.error(scope, Some(reason)) + } + } +} + +/// Copy `bytes` into the view of a pending BYOB request of `controller`'s stream if they fit, and +/// enqueue them as a chunk otherwise. The chunk uses the allocation of `bytes`, unless less than +/// half of it is used. Returns `bytes` if they were copied, so that another read can reuse the +/// buffer. +fn deliver_bytes( + scope: &Scope<'_>, + controller: ReadableByteStreamController<'_>, + bytes: Vec, +) -> Result>, ExnThrown> { + if let Some(request) = controller.byob_request(scope) { + if let Some(view) = request.view(scope) { + let written = view.with_bytes_mut(|target| { + (target.len() >= bytes.len()).then(|| { + target[..bytes.len()].copy_from_slice(&bytes); + bytes.len() + }) + }); + if let Some(written) = written { + request.respond(scope, js::conversion::EnforceRange(written as u64))?; + return Ok(Some(bytes)); + } + } + } + if bytes.len() * 2 >= bytes.capacity() { + controller.enqueue_bytes(scope, bytes)?; + return Ok(None); + } + controller.enqueue_bytes(scope, bytes.to_vec())?; + Ok(Some(bytes)) +} + +/// Close `controller`'s stream. A pending BYOB request is answered with no bytes, which its +/// reader receives as the end of the stream. +fn close_stream( + scope: &Scope<'_>, + controller: ReadableByteStreamController<'_>, +) -> Result<(), ExnThrown> { + controller.close(scope)?; + match controller.byob_request(scope) { + Some(request) => request.respond(scope, js::conversion::EnforceRange(0)), + None => Ok(()), + } +} + +/// Underlying-source `cancel`: drop the reader and a read in flight. +fn cancel( + scope: &Scope<'_>, + _args: CallbackArgs<'_>, + payload: HandleValue<'_>, +) -> Result { + cast_payload::(scope, payload).abort(scope); + Ok(value::undefined()) +} diff --git a/crates/builtins/web-streams/src/readable/native_pump.rs b/crates/builtins/web-streams/src/readable/native_pump.rs new file mode 100644 index 0000000..49d4054 --- /dev/null +++ b/crates/builtins/web-streams/src/readable/native_pump.rs @@ -0,0 +1,503 @@ +// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception + +//! Pumping a `ReadableStream` into a native sink. +//! +//! A [`NativePump`] reads a stream through an internal reader (see +//! [`native_read`](super::native_read)) and appends each chunk to a [`PumpBatch`]. It writes the +//! batch to a [`PumpSink`] once a read has to wait, the batch is full, or the stream has ended. +//! At most one read and one write are outstanding at a time. No read starts while a write is in +//! flight, but a read that was waiting when the write started can complete during it, and its +//! chunk goes into the next batch. +//! +//! The pump finishes once the stream has ended and every batch is written, or when it is stopped. +//! It then drops the chunks not yet written and calls [`PumpSink::finish`] with the [`PumpEnd`]. +//! For [`PumpEnd::Failed`], it prints the failure to `stderr`. It cancels the stream unless it +//! closed, and calls the callback given to [`NativePump::on_finish`]. + +use std::any::Any; +use std::collections::TryReserveError; +use std::future::Future; +use std::pin::Pin; + +use core_runtime::{jsclass, jsmethods}; +use js::error::{ExnThrown, ThrowException, TypeError}; +use js::gc::handle::{Heap, RootedHeap}; +use js::gc::scope::Scope; +use js::prelude::HandleValue; +use js::promise::{PromiseFuture, PromiseOutcome}; +use js::{Object, Promise}; + +use super::default_reader::{DefaultReader, DefaultReaderImpl}; +use super::native_read::{native_reader_cancel, native_reader_read, NativeReadSteps}; + +/// The number of bytes at which a byte batch is full. +pub const PUMP_BATCH_BYTES: usize = 64 * 1024; + +/// The cancellation reason of a stream whose sink's receiver went away. +const RECEIVER_DROPPED: &str = "the stream's reader was dropped"; + +/// How a [`NativePump`] ended. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PumpEnd { + /// The stream closed. + Closed, + /// The stream errored, a chunk was rejected by [`PumpBatch::append`] or [`PumpBatch::check`], + /// or the stream couldn't be read. The message describes the failure. + Failed(String), + /// The pump stopped before the stream ended: the sink's receiver went away, or + /// [`NativePump::stop`] was called. The message gives the reason. + Stopped(String), +} + +/// The chunks a [`NativePump`] has read but not yet written. +pub trait PumpBatch: Sized + 'static { + /// Append `chunk`. An `Err` ends the pump with [`PumpEnd::Failed`] once the chunks appended + /// before it are written. + fn append(&mut self, scope: &Scope<'_>, chunk: HandleValue<'_>) -> Result<(), String>; + + /// Check the batch right before it is written. To reject a chunk, truncate the batch before + /// it and return an `Err`. The chunks before it are written, and the pump then ends with + /// [`PumpEnd::Failed`]. + fn check(&mut self, _scope: &Scope<'_>) -> Result<(), String> { + Ok(()) + } + + fn is_empty(&self) -> bool; + + /// Whether the batch is written before more chunks are read. + fn is_full(&self) -> bool; + + /// Take the batch's chunks, leaving it empty. + fn take(&mut self) -> Self; +} + +/// The destination to which a [`NativePump`] writes its batches. +pub trait PumpSink: Sized + 'static { + type Batch: PumpBatch; + + /// Whether the future returned by [`write`](Self::write) is polled once right away if an + /// event loop is active. Otherwise its first poll happens in the event loop. + const POLL_WRITE_AT_ONCE: bool = false; + + /// Write `batch` if that can complete without waiting. Otherwise returns `batch`, which the + /// pump then passes to [`write`](Self::write). + fn try_write(&mut self, batch: Self::Batch) -> Result<(), Self::Batch> { + Err(batch) + } + + /// Write `batch`. The future resolves to the sink once all of `batch` was written, or to + /// `None` if the receiver went away. The pump then ends with [`PumpEnd::Stopped`] without + /// calling [`finish`](Self::finish). + fn write(self, batch: Self::Batch) -> impl Future> + 'static; + + /// Returns the message of the [`PumpEnd::Failed`] for a stream that errored with `error`. + fn describe_source_error(scope: &Scope<'_>, error: HandleValue<'_>) -> String; + + /// End the sink's output. Called once when the pump finishes, after its last write. Not called + /// if a write's future resolved to `None`. The default implementation drops the sink. + fn finish(self, _scope: &Scope<'_>, _end: &PumpEnd) {} +} + +/// Append `bytes` to `batch`. Growing a non-empty `batch` reserves room up to +/// [`PUMP_BATCH_BYTES`], so a batch filled by small chunks is copied at most once. +pub fn append_bytes(batch: &mut Vec, bytes: &[u8]) -> Result<(), TryReserveError> { + if batch.capacity() - batch.len() < bytes.len() { + let wanted = if batch.is_empty() { + bytes.len() + } else { + PUMP_BATCH_BYTES.max(batch.len() + bytes.len()) - batch.len() + }; + batch.try_reserve_exact(wanted)?; + } + batch.extend_from_slice(bytes); + Ok(()) +} + +/// A write in flight. It resolves to the sink, boxed as `Any`, or to `None` if the receiver went +/// away. +type SinkWrite = Pin>>>>; + +/// A sink and its batch, with the sink's type erased. +trait ErasedSink { + fn append(&mut self, scope: &Scope<'_>, chunk: HandleValue<'_>) -> Result<(), String>; + fn check(&mut self, scope: &Scope<'_>) -> Result<(), String>; + fn is_empty(&self) -> bool; + fn is_full(&self) -> bool; + fn is_writing(&self) -> bool; + /// Write the batch with [`PumpSink::try_write`]. Returns whether it was written. + fn try_write(&mut self) -> bool; + /// Write the batch with [`PumpSink::write`]. + fn write(&mut self) -> SinkWrite; + fn poll_write_at_once(&self) -> bool; + /// Return the sink from a write that completed. + fn restore(&mut self, sink: Box); + fn describe_source_error(&self, scope: &Scope<'_>, error: HandleValue<'_>) -> String; + /// Drop the chunks not yet written, and call [`PumpSink::finish`] unless a write is in flight. + fn finish(&mut self, scope: &Scope<'_>, end: &PumpEnd); +} + +struct SinkSlot { + /// `None` while a write is in flight, and after the sink finished. + sink: Option, + batch: S::Batch, +} + +impl ErasedSink for SinkSlot { + fn append(&mut self, scope: &Scope<'_>, chunk: HandleValue<'_>) -> Result<(), String> { + self.batch.append(scope, chunk) + } + + fn check(&mut self, scope: &Scope<'_>) -> Result<(), String> { + self.batch.check(scope) + } + + fn is_empty(&self) -> bool { + self.batch.is_empty() + } + + fn is_full(&self) -> bool { + self.batch.is_full() + } + + fn is_writing(&self) -> bool { + self.sink.is_none() + } + + fn try_write(&mut self) -> bool { + let sink = self.sink.as_mut().expect("no write is in flight"); + match sink.try_write(self.batch.take()) { + Ok(()) => true, + Err(batch) => { + self.batch = batch; + false + } + } + } + + fn write(&mut self) -> SinkWrite { + let sink = self.sink.take().expect("no write is in flight"); + let write = sink.write(self.batch.take()); + Box::pin(async move { + let sink = write.await?; + Some(Box::new(sink) as Box) + }) + } + + fn poll_write_at_once(&self) -> bool { + S::POLL_WRITE_AT_ONCE + } + + fn restore(&mut self, sink: Box) { + let sink = sink.downcast::().expect("a write returns its own sink"); + self.sink = Some(*sink); + } + + fn describe_source_error(&self, scope: &Scope<'_>, error: HandleValue<'_>) -> String { + S::describe_source_error(scope, error) + } + + fn finish(&mut self, scope: &Scope<'_>, end: &PumpEnd) { + drop(self.batch.take()); + if let Some(sink) = self.sink.take() { + sink.finish(scope, end); + } + } +} + +/// The state of a pump from a `ReadableStream` to a [`PumpSink`]. +#[jsclass(hidden)] +pub struct NativePump { + reader: Heap, + /// The sink and its batch. `None` only while a sink method runs, so that no borrow of this + /// data is held across it. + #[no_trace] + sink: Option>, + /// How the pump ends, once the stream ended, a chunk was rejected, or the pump was stopped. + #[no_trace] + end: Option, + /// Whether a read request is waiting for a chunk. + #[no_trace] + reading: bool, + /// Whether [`run`] is on the stack. + #[no_trace] + running: bool, + #[no_trace] + finished: bool, + #[no_trace] + on_finish: Option>, +} + +#[jsmethods] +impl NativePump { + fn new(reader: DefaultReader, sink: Box) -> Self { + NativePumpImpl { + reader: Heap::from(reader), + sink: Some(sink), + end: None, + reading: false, + running: false, + finished: false, + on_finish: None, + } + } +} + +impl NativePump<'_> { + /// Create a pump that reads with `reader`, a reader from + /// [`acquire_native_reader`](super::native_read::acquire_native_reader), and writes to + /// `sink`, collecting chunks into `batch`, which must be empty. The pump starts reading once + /// [`start`](Self::start) is called. + pub fn create<'r, S: PumpSink>( + scope: &'r Scope<'_>, + reader: DefaultReader<'_>, + sink: S, + batch: S::Batch, + ) -> Result, ExnThrown> { + let slot = SinkSlot { + sink: Some(sink), + batch, + }; + NativePump::new(scope, reader, Box::new(slot)) + } + + /// Call `f` once the pump has finished. + pub fn on_finish(&self, f: impl FnOnce() + 'static) { + self.data_mut().on_finish = Some(Box::new(f)); + } + + /// Start reading the stream. + pub fn start(&self, scope: &Scope<'_>) { + run(scope, self); + } + + /// Finish the pump with [`PumpEnd::Stopped`], giving `reason`. The stream is cancelled with a + /// `TypeError` whose message is `reason`, and the chunks not yet written are dropped. A + /// pump that has finished is left as it is. + pub fn stop(&self, scope: &Scope<'_>, reason: String) { + if self.data().finished { + return; + } + self.data_mut().end = Some(PumpEnd::Stopped(reason)); + finish(scope, self); + } + + pub fn is_finished(&self) -> bool { + self.data().finished + } + + /// Run `f` on the sink, with the sink taken out of this pump's data while `f` runs. Returns + /// `None` if the sink is already taken out, by a call further up the stack. + fn with_sink(&self, f: impl FnOnce(&mut dyn ErasedSink) -> R) -> Option { + let mut sink = self.data_mut().sink.take()?; + let result = f(&mut *sink); + self.data_mut().sink = Some(sink); + Some(result) + } +} + +/// The pump's read-request steps. Each records what it received and then calls [`run`], which +/// does nothing when the step ran from inside it. +const PUMP_STEPS: NativeReadSteps = NativeReadSteps { + chunk: pump_chunk, + close: pump_close, + error: pump_error, +}; + +/// Advance the pump: read chunks into the batch for as long as reads complete synchronously and +/// the batch isn't full, then write the batch. A batch written synchronously is followed by +/// more reads. +fn run(scope: &Scope<'_>, pump: &NativePump<'_>) { + if pump.data().running || pump.data().finished { + return; + } + pump.data_mut().running = true; + loop { + while can_read(pump) { + pump.data_mut().reading = true; + let reader = pump.data().reader.get(scope); + if native_reader_read(scope, reader, PUMP_STEPS, pump.as_object()).is_err() { + let message = ExnThrown::capture(scope).to_string(); + let mut data = pump.data_mut(); + data.reading = false; + data.end = Some(PumpEnd::Failed(message)); + } + if pump.data().reading { + break; + } + } + if !flush(scope, pump) { + break; + } + } + pump.data_mut().running = false; +} + +/// Whether the pump may issue another read: it hasn't finished, the stream hasn't ended, no read +/// or write is outstanding, and the batch isn't full. +fn can_read(pump: &NativePump<'_>) -> bool { + let data = pump.data(); + !data.finished + && data.end.is_none() + && !data.reading + && data + .sink + .as_ref() + .is_some_and(|sink| !sink.is_writing() && !sink.is_full()) +} + +/// Write the batch if a read has to wait, the batch is full, or the stream has ended. Finish the +/// pump once the stream has ended and the batch is written. Returns whether [`run`] should read +/// on, which is the case if the batch was written synchronously. +fn flush(scope: &Scope<'_>, pump: &NativePump<'_>) -> bool { + if pump.data().finished { + return false; + } + let Some((writing, empty, full)) = + pump.with_sink(|sink| (sink.is_writing(), sink.is_empty(), sink.is_full())) + else { + return false; + }; + if writing { + return false; + } + let ready = full || pump.data().reading || pump.data().end.is_some(); + if !empty && ready { + if let Some(Err(message)) = pump.with_sink(|sink| sink.check(scope)) { + pump.data_mut().end = Some(PumpEnd::Failed(message)); + } + // `check` can empty the batch, which then isn't written. + if pump.with_sink(|sink| sink.is_empty()) == Some(false) { + if pump.with_sink(|sink| sink.try_write()) == Some(true) { + return true; + } + spawn_write(scope, pump); + return false; + } + } + if pump.data().end.is_some() && pump.with_sink(|sink| sink.is_empty()) == Some(true) { + finish(scope, pump); + } + false +} + +/// Write the batch with [`PumpSink::write`], and continue the pump once the write completes. +fn spawn_write(scope: &Scope<'_>, pump: &NativePump<'_>) { + let Ok(promise) = Promise::new_pending(scope) else { + let message = ExnThrown::capture(scope).to_string(); + pump.data_mut().end = Some(PumpEnd::Failed(message)); + finish(scope, pump); + return; + }; + let Some((write, poll_now)) = pump.with_sink(|sink| (sink.write(), sink.poll_write_at_once())) + else { + return; + }; + let pump = RootedHeap::new(*pump); + let future = async move { + let sink = write.await; + PromiseOutcome::Resolve(Box::new(move |scope: &Scope<'_>| { + write_done(scope, &pump.get(scope), sink); + Ok(HandleValue::undefined()) + })) + }; + let future = PromiseFuture::new(future); + if poll_now && core_runtime::event_loop::has_active_event_loop() { + promise.spawn_polled(future); + } else { + promise.spawn(future); + } +} + +/// Continue the pump after a write. `sink` is `None` if the receiver went away. +fn write_done(scope: &Scope<'_>, pump: &NativePump<'_>, sink: Option>) { + let Some(sink) = sink else { + if !pump.data().finished { + pump.data_mut().end = Some(PumpEnd::Stopped(RECEIVER_DROPPED.to_string())); + finish(scope, pump); + } + return; + }; + pump.with_sink(|slot| slot.restore(sink)); + if pump.data().finished { + // The pump was stopped during the write. + let end = pump.data().end.clone().expect("a finished pump has an end"); + pump.with_sink(|slot| slot.finish(scope, &end)); + return; + } + run(scope, pump); +} + +/// Finish the pump with its `end`: drop the chunks not yet written, finish the sink unless a write +/// is in flight, print a failure, cancel the stream unless it closed, and call the `on_finish` +/// callback. +fn finish(scope: &Scope<'_>, pump: &NativePump<'_>) { + let end = { + let mut data = pump.data_mut(); + data.finished = true; + data.end.clone().expect("a finishing pump has an end") + }; + pump.with_sink(|sink| sink.finish(scope, &end)); + if let PumpEnd::Failed(message) = &end { + eprintln!("a stream's source ended the stream early: {message}"); + } + if let PumpEnd::Failed(message) | PumpEnd::Stopped(message) = &end { + let _ = TypeError(message.clone()).throw(scope); + let reason = js::exception::take_pending_or_undefined(scope); + let reader = pump.data().reader.get(scope); + let cancelled = native_reader_cancel(scope, reader, reason); + // The cancellation rejects for a stream that errored, and nothing observes it. + let _ = cancelled.set_any_is_handled(scope); + } + let on_finish = pump.data_mut().on_finish.take(); + if let Some(on_finish) = on_finish { + on_finish(); + } +} + +fn pump_chunk( + scope: &Scope<'_>, + payload: Object<'_>, + chunk: HandleValue<'_>, +) -> Result<(), ExnThrown> { + let pump = payload + .cast::() + .expect("payload is a NativePump"); + pump.data_mut().reading = false; + if pump.data().finished { + return Ok(()); + } + // A chunk that arrives after the stream ended early isn't written. + if pump.data().end.is_none() { + if let Some(Err(message)) = pump.with_sink(|sink| sink.append(scope, chunk)) { + pump.data_mut().end = Some(PumpEnd::Failed(message)); + } + } + run(scope, &pump); + Ok(()) +} + +fn pump_close(scope: &Scope<'_>, payload: Object<'_>) -> Result<(), ExnThrown> { + let pump = payload + .cast::() + .expect("payload is a NativePump"); + pump.data_mut().reading = false; + pump.data_mut().end.get_or_insert(PumpEnd::Closed); + run(scope, &pump); + Ok(()) +} + +fn pump_error( + scope: &Scope<'_>, + payload: Object<'_>, + error: HandleValue<'_>, +) -> Result<(), ExnThrown> { + let pump = payload + .cast::() + .expect("payload is a NativePump"); + pump.data_mut().reading = false; + if let Some(message) = pump.with_sink(|sink| sink.describe_source_error(scope, error)) { + pump.data_mut().end.get_or_insert(PumpEnd::Failed(message)); + } + run(scope, &pump); + Ok(()) +} diff --git a/crates/builtins/web-streams/tests/native_pump.rs b/crates/builtins/web-streams/tests/native_pump.rs new file mode 100644 index 0000000..c36274d --- /dev/null +++ b/crates/builtins/web-streams/tests/native_pump.rs @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception + +//! `NativePump` behavior that its users' tests can't observe: the size of the batches it writes, +//! what happens to chunks that arrive after [`PumpBatch::check`] rejected one, and when unsent +//! chunks are released. + +#![cfg(not(target_arch = "wasm32"))] + +use std::cell::RefCell; +use std::rc::Rc; + +use core_runtime::config::RuntimeConfig; +use core_runtime::event_loop::{run_microtasks, run_to_completion, with_event_loop, EventLoop}; +use core_runtime::runtime::{clear_global_initializers, register_global_initializer, Runtime}; +use js::conversion::FromJSVal; +use js::gc::scope::Scope; +use js::prelude::HandleValue; +use web_streams::readable::native_pump::{ + append_bytes, NativePump, PumpBatch, PumpEnd, PumpSink, PUMP_BATCH_BYTES, +}; +use web_streams::readable::native_read::acquire_native_reader; +use web_streams::readable::ReadableStream; + +#[derive(Default)] +struct Log { + writes: Vec>, + ends: Vec, + /// The contents of each non-empty batch that was dropped instead of written. + dropped: Vec>, +} + +struct Bytes(Vec, Rc>); + +impl Drop for Bytes { + fn drop(&mut self) { + if !self.0.is_empty() { + self.1 + .borrow_mut() + .dropped + .push(std::mem::take(&mut self.0)); + } + } +} + +impl PumpBatch for Bytes { + fn append(&mut self, scope: &Scope<'_>, chunk: HandleValue<'_>) -> Result<(), String> { + let array = js::Uint8Array::from_jsval(scope, chunk, ()) + .map_err(|_| "not a Uint8Array".to_string())?; + array + .with_slice(|bytes| append_bytes(&mut self.0, bytes)) + .map_err(|_| "out of memory".to_string()) + } + + /// Reject the byte 255. + fn check(&mut self, _scope: &Scope<'_>) -> Result<(), String> { + match self.0.iter().position(|&byte| byte == 255) { + Some(rejected) => { + self.0.truncate(rejected); + Err("rejected 255".to_string()) + } + None => Ok(()), + } + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn is_full(&self) -> bool { + self.0.len() >= PUMP_BATCH_BYTES + } + + fn take(&mut self) -> Self { + Bytes(std::mem::take(&mut self.0), Rc::clone(&self.1)) + } +} + +/// A sink that records each batch it writes and the end it finishes with. If `gone` is set, its +/// receiver went away, and every write drops its batch. +struct TestSink { + log: Rc>, + gone: bool, +} + +impl PumpSink for TestSink { + type Batch = Bytes; + + async fn write(self, mut batch: Bytes) -> Option { + if self.gone { + return None; + } + self.log + .borrow_mut() + .writes + .push(std::mem::take(&mut batch.0)); + Some(self) + } + + fn describe_source_error(_scope: &Scope<'_>, _error: HandleValue<'_>) -> String { + "the source errored".to_string() + } + + fn finish(self, _scope: &Scope<'_>, end: &PumpEnd) { + self.log.borrow_mut().ends.push(end.clone()); + } +} + +/// Evaluate `source`, whose completion value is a `ReadableStream`, pump that stream into a +/// [`TestSink`] with `gone`, and run the event loop until it has no work left. A `cancel` function +/// that records its reason in `globalThis.__out` is in scope for `source`. The pump is kept alive +/// until the function returns, so that collecting it doesn't release its batch. Returns the sink's +/// log and `String(globalThis.__out)`. +fn pump(source: &str, gone: bool) -> (Log, String) { + clear_global_initializers(); + register_global_initializer(web_globals::add_to_global); + register_global_initializer(web_streams::add_to_global); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + let el = EventLoop::new(); + let log = Rc::new(RefCell::new(Log::default())); + let code = format!( + r#"globalThis.__out = "not cancelled"; + function cancel(reason) {{ + globalThis.__out = reason instanceof TypeError + ? "TypeError: " + reason.message + : String(reason); + }} + {source}"# + ); + with_event_loop(&el, |_| { + let value = js::compile::evaluate_with_filename(&scope, &code, "test.js", 1) + .expect("evaluating the source"); + let stream = ReadableStream::from_jsval(&scope, value, ()).expect("a ReadableStream"); + let reader = acquire_native_reader(&scope, &stream).expect("a reader"); + let sink = TestSink { + log: Rc::clone(&log), + gone, + }; + let batch = Bytes(Vec::new(), Rc::clone(&log)); + let pump = NativePump::create(&scope, reader, sink, batch).expect("a pump"); + scope + .global() + .set_property(&scope, c"__pump", pump.as_object()) + .expect("keeping the pump alive"); + pump.start(&scope); + run_microtasks(&scope); + }); + let tokio = tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .expect("a tokio runtime"); + tokio.block_on(async { + let raw_cx = unsafe { scope.raw_cx_no_gc() }; + unsafe { run_to_completion(raw_cx, &el, tokio::time::sleep).await } + }); + run_microtasks(&scope); + let out = js::compile::evaluate_with_filename(&scope, "String(globalThis.__out)", "out.js", 1) + .expect("reading __out"); + let out = String::from_jsval(&scope, out, ()).expect("a string"); + let log = std::mem::take(&mut *log.borrow_mut()); + (log, out) +} + +#[test] +fn a_batch_fills_up_and_the_rest_follows() { + let (log, out) = pump( + &format!( + r#"new ReadableStream({{ + start(c) {{ + for (let i = 0; i < 3; i++) c.enqueue(new Uint8Array({half})); + c.close(); + }}, + cancel, + }})"#, + half = PUMP_BATCH_BYTES / 2, + ), + false, + ); + let lengths: Vec = log.writes.iter().map(Vec::len).collect(); + assert_eq!(lengths, [PUMP_BATCH_BYTES, PUMP_BATCH_BYTES / 2]); + assert_eq!(log.ends, [PumpEnd::Closed]); + assert_eq!(out, "not cancelled"); +} + +#[test] +fn nothing_after_a_chunk_rejected_by_check_is_written() { + // The third read waits, so the batch is checked and written while it is pending. Its chunk + // arrives during the write. + let (log, out) = pump( + r#"let pulled = false; + new ReadableStream({ + start(c) { + c.enqueue(new Uint8Array([1])); + c.enqueue(new Uint8Array([255])); + }, + pull(c) { + if (pulled) return; + pulled = true; + return Promise.resolve().then(() => c.enqueue(new Uint8Array([2]))); + }, + cancel, + })"#, + false, + ); + assert_eq!(log.writes, [vec![1]]); + assert_eq!(log.ends, [PumpEnd::Failed("rejected 255".to_string())]); + assert_eq!(out, "TypeError: rejected 255"); +} + +#[test] +fn a_finished_pump_releases_unsent_chunks() { + // The second chunk arrives while the first is being written. The write then finds that the + // receiver went away, which finishes the pump with the second chunk unsent. + let (log, out) = pump( + r#"let pulled = false; + new ReadableStream({ + start(c) { c.enqueue(new Uint8Array([1])); }, + pull(c) { + if (pulled) return; + pulled = true; + return Promise.resolve().then(() => c.enqueue(new Uint8Array([2]))); + }, + cancel, + })"#, + true, + ); + assert!(log.writes.is_empty()); + assert_eq!(log.dropped, [vec![1], vec![2]]); + assert_eq!(out, "TypeError: the stream's reader was dropped"); +} diff --git a/crates/builtins/web-streams/tests/streams.rs b/crates/builtins/web-streams/tests/streams.rs index d75630d..bdbbfc9 100644 --- a/crates/builtins/web-streams/tests/streams.rs +++ b/crates/builtins/web-streams/tests/streams.rs @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception //! Stream behavior that the WPT `streams` suites don't check: the Rust-side -//! `ReadableStream::from_bytes` API, WebIDL conversion order, chunk adoption by -//! async iteration, isolation of internal state from `Object.prototype`, and -//! regressions for specific bugs. +//! `ReadableStream::from_bytes` and `ReadableByteStreamController::enqueue_bytes` +//! APIs, WebIDL conversion order, chunk adoption by async iteration, isolation +//! of internal state from `Object.prototype`, and regressions for specific bugs. // This file contains nothing platform-specific, so skip it on wasm32. #![cfg(not(target_arch = "wasm32"))] @@ -11,7 +11,8 @@ use core_runtime::config::RuntimeConfig; use core_runtime::runtime::{clear_global_initializers, register_global_initializer, Runtime}; use core_runtime::test_util::{eval_and_read_out, eval_out_with_setup}; -use web_streams::readable::ReadableStream; +use js::conversion::FromJSVal; +use web_streams::readable::{ReadableByteStreamController, ReadableStream}; fn add_globals() { clear_global_initializers(); @@ -548,3 +549,45 @@ fn promise_members_reject_a_wrong_this() { "#); assert_eq!(out, "rejected:TypeError|rejected:TypeError"); } + +/// `enqueue_bytes` hands its `Vec`'s allocation to the reader: the chunk read from the stream holds +/// the bytes at the address they had in the `Vec`. +#[test] +fn enqueue_bytes_hands_its_buffer_to_the_reader() { + add_globals(); + let rt = Runtime::init(&RuntimeConfig::default()).expect("runtime init"); + let scope = rt.default_global(); + let controller = js::compile::evaluate_with_filename( + &scope, + r#" + let controller; + globalThis.stream = new ReadableStream({ type: "bytes", start(c) { controller = c; } }); + controller + "#, + "test.js", + 1, + ) + .expect("creating the stream"); + let controller = + ReadableByteStreamController::from_jsval(&scope, controller, ()).expect("a controller"); + let bytes = vec![1u8, 2, 3, 4, 5, 6, 7, 8]; + let address = bytes.as_ptr() as usize; + controller + .enqueue_bytes(&scope, bytes) + .expect("enqueueing the bytes"); + let out = eval_and_read_out( + &scope, + r#" + globalThis.__out = "pending"; + stream.getReader().read().then(({ value }) => { + globalThis.chunk = value; + globalThis.__out = value.join(","); + }); + "#, + ); + assert_eq!(out, "1,2,3,4,5,6,7,8"); + let chunk = js::compile::evaluate_with_filename(&scope, "chunk", "chunk.js", 1) + .expect("reading the chunk"); + let chunk = js::Uint8Array::from_jsval(&scope, chunk, ()).expect("a Uint8Array"); + assert_eq!(chunk.with_slice(|bytes| bytes.as_ptr() as usize), address); +} diff --git a/crates/libstarling/Cargo.toml b/crates/libstarling/Cargo.toml index 1a73b79..ec30be8 100644 --- a/crates/libstarling/Cargo.toml +++ b/crates/libstarling/Cargo.toml @@ -7,7 +7,7 @@ authors.workspace = true edition.workspace = true [lib] -# No unit tests; skip building the (SpiderMonkey-sized) lib test harness binary. +# No unit tests. This skips building the (SpiderMonkey-sized) lib test harness binary. # The integration tests in `tests/` link the lib and are unaffected. test = false @@ -46,6 +46,7 @@ wasip3.workspace = true [dev-dependencies] futures-channel.workspace = true serve-test-support.workspace = true +tempfile = "3" [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/crates/libstarling/src/lib.rs b/crates/libstarling/src/lib.rs index 3115c7a..0ab6501 100644 --- a/crates/libstarling/src/lib.rs +++ b/crates/libstarling/src/lib.rs @@ -10,6 +10,10 @@ pub mod serve_native; /// The per-request dispatch core shared by the serve modes. mod serve_common; +/// Exported for `tests/wire_response.rs` only. +#[doc(hidden)] +pub use serve_common::{prepare_wire_response, WireResponse}; + /// Wasm HTTP serve mode: the body of the `wasi:http/handler` export. #[cfg(target_arch = "wasm32")] pub mod serve_wasm; @@ -39,9 +43,9 @@ pub fn register_builtins() { /// Apply CLI options that take effect before the runtime is initialized: the worker location URL /// from `--init-location`, and the WPT test globals from `--wpt-mode`. Shared between the native -/// and wasm32 entry points, and between the CLI and HTTP ones — a WPT run under `--serve` needs +/// and wasm32 entry points, and between the CLI and HTTP ones: a WPT run under `--serve` needs /// the same globals a command-mode run does. -fn apply_pre_init_config(config: &config::RuntimeConfig) -> Result<(), String> { +pub fn apply_pre_init_config(config: &config::RuntimeConfig) -> Result<(), String> { if let Some(location) = config.init_location.as_deref() { let url = url::Url::parse(location) .map_err(|e| format!("Invalid --init-location URL {location:?}: {e}"))?; @@ -113,7 +117,7 @@ pub async fn run(config: config::RuntimeConfig) -> Result<(), String> { let raw_cx = unsafe { scope.raw_cx_no_gc() }; let el = invocation.event_loop(); - // SAFETY: `raw_cx` is valid for the duration of the await — `scope` + // SAFETY: `raw_cx` is valid for the duration of the await. `scope` // keeps the `Runtime` alive and the realm entered. unsafe { evaluation @@ -158,9 +162,8 @@ fn drive_event_loop_native( let el = invocation.event_loop(); tokio_rt.block_on(async { - // SAFETY: the scope (and its Runtime) must outlive this future. - // Since the caller owns both and `block_on` runs synchronously on - // the same thread, this is guaranteed. + // SAFETY: the scope (and its Runtime) outlives this future. Since the caller owns both + // and `block_on` runs synchronously on the same thread, this is guaranteed. let raw_cx = unsafe { scope.raw_cx_no_gc() }; unsafe { evaluation diff --git a/crates/libstarling/src/serve_common.rs b/crates/libstarling/src/serve_common.rs index 79d24a4..00f3b39 100644 --- a/crates/libstarling/src/serve_common.rs +++ b/crates/libstarling/src/serve_common.rs @@ -102,7 +102,7 @@ impl RequestClock { } } -/// Run `work` under `limit`: `Some(output)` if it finished, `None` if `time_limit` was reached +/// Run `work` under `time_limit`: `Some(output)` if it finished, `None` if the limit was reached /// and `work` interrupted as a result. pub(crate) async fn with_timeout( sleep: &S, @@ -548,7 +548,7 @@ pub(crate) fn signal_body_outcome( } /// The `Create Fetch Event and Dispatch` step 17.4.20 abort a body send calls for, as -/// `(DOMException name, message)`, or `None` where the send needs no abort. +/// `(DOMException name, message)`, or `None` if the send needs no abort. /// /// The reason is logged here as well, since the response is already on its way and there is /// no status left to report a failed, stalled, or abandoned body with. @@ -588,11 +588,11 @@ fn abort_for_outcome(outcome: BodySendOutcome) -> Option<(&'static str, &'static /// A response reduced to what may go on the wire: the header fields that may be sent, the body to /// send, and the content length. -pub(crate) struct WireResponse { - pub(crate) status: u16, - pub(crate) headers: http::HeaderMap, - /// The content to send, empty where the status or the method rules content out. - pub(crate) body: platform::http::OutgoingBody, +pub struct WireResponse { + pub status: u16, + pub headers: http::HeaderMap, + /// The content to send, which is empty if the status or the method rules content out. + pub body: platform::http::OutgoingBody, /// The response's `Content-Length`. For non-streaming bodies, it's derived from the buffer. /// For forwarded incoming bodies, the incoming `Content-Length` header is used. For bodies /// backed by a JS `ReadableStream`, the guest-set `Content-Length` header is used, if any. @@ -605,7 +605,7 @@ pub(crate) struct WireResponse { /// connection is closed. /// /// Always `None` for body-less status responses. - pub(crate) declared_length: Option, + pub declared_length: Option, } /// Whether a status is framed by the status alone: no `Content-Length`, no `Transfer-Encoding`, no @@ -679,7 +679,7 @@ fn declared_content_length(headers: &[(String, String)]) -> Option { /// /// A `HEAD` still declares the length of the body a `GET` would have produced, where that is known /// without producing the content. RFC 9110 §9.3.2 allows omitting it otherwise. -pub(crate) fn prepare_wire_response( +pub fn prepare_wire_response( head_request: bool, status: u16, headers: Vec<(String, String)>, @@ -731,185 +731,3 @@ pub(crate) fn prepare_wire_response( declared_length, } } - -#[cfg(test)] -mod wire_response_tests { - use super::*; - use platform::http::OutgoingBody; - - fn headers(pairs: &[(&str, &str)]) -> Vec<(String, String)> { - pairs - .iter() - .map(|(n, v)| (n.to_string(), v.to_string())) - .collect() - } - - fn names(response: &WireResponse) -> Vec { - response - .headers - .iter() - .map(|(name, _)| name.as_str().to_string()) - .collect() - } - - #[test] - fn framing_headers_are_dropped_whoever_set_them() { - let response = prepare_wire_response( - false, - 200, - headers(&[ - ("Content-Length", "10000"), - ("transfer-encoding", "chunked"), - ("CONNECTION", "keep-alive"), - ("x-keep", "me"), - ]), - OutgoingBody::Bytes(bytes::Bytes::from_static(b"short")), - ); - assert_eq!(names(&response), ["x-keep"]); - } - - #[test] - fn headers_that_could_split_the_response_are_dropped() { - let response = prepare_wire_response( - false, - 200, - headers(&[ - ("x-cr", "a\rb"), - ("x-lf", "a\nb"), - ("x-nul", "a\0b"), - ("x-na\rme", "fine"), - ("x-keep", "me"), - ]), - OutgoingBody::Bytes(bytes::Bytes::new()), - ); - assert_eq!(names(&response), ["x-keep"]); - } - - /// The body a response sends, for a test that only ever builds in-memory ones. - fn content(response: &WireResponse) -> &[u8] { - match &response.body { - OutgoingBody::Bytes(bytes) => bytes, - _ => panic!("the test built an in-memory body"), - } - } - - #[test] - fn a_bodiless_status_has_no_content_even_for_a_get() { - for status in [204, 304] { - let response = prepare_wire_response( - false, - status, - headers(&[("x-keep", "me")]), - OutgoingBody::Bytes(bytes::Bytes::from_static(b"ignored")), - ); - assert!(content(&response).is_empty(), "{status}"); - assert_eq!(response.declared_length, None, "{status}"); - assert_eq!(names(&response), ["x-keep"], "{status}"); - } - } - - #[test] - fn a_head_request_declares_the_length_it_would_have_sent() { - let response = prepare_wire_response( - true, - 200, - Vec::new(), - OutgoingBody::Bytes(bytes::Bytes::from_static(b"BODYBYTES")), - ); - assert!(content(&response).is_empty()); - assert_eq!(response.declared_length, Some(9)); - } - - #[test] - fn a_head_request_declares_no_length_it_would_have_to_produce() { - // A streamed body's length is only knowable by generating the content. - let (_sender, streamed) = platform::http::body_channel(); - let response = prepare_wire_response(true, 200, Vec::new(), streamed); - assert!(content(&response).is_empty()); - assert_eq!(response.declared_length, None); - } - - /// A streamed body has no length until it is produced, so the handler's declaration is the - /// only thing that can frame it. The transport then enforces it against the body. - #[test] - fn a_streamed_body_takes_the_declared_length() { - let (_sender, streamed) = platform::http::body_channel(); - let response = - prepare_wire_response(false, 200, headers(&[("content-length", "42")]), streamed); - assert_eq!(response.declared_length, Some(42)); - assert_eq!(names(&response), Vec::::new()); - } - - /// Content already in memory is measured. A declaration that disagrees with it would frame the - /// response as something other than what it sends. - #[test] - fn an_in_memory_body_is_measured_not_declared() { - let response = prepare_wire_response( - false, - 200, - headers(&[("content-length", "10000")]), - OutgoingBody::Bytes(bytes::Bytes::from_static(b"short")), - ); - assert_eq!(response.declared_length, Some(5)); - assert_eq!(content(&response), b"short"); - } - - /// Only an unambiguous `1*DIGIT` frames a response. A `Headers` object joins repeated fields - /// into one comma-separated value, which lands here as a value that is not a number. - #[test] - fn a_declared_length_that_is_not_one_number_is_refused() { - for value in ["", "x", "10000, 10000", " 5", "+5", "5.0", "0x5", "-1"] { - let (_sender, streamed) = platform::http::body_channel(); - let response = - prepare_wire_response(false, 200, headers(&[("content-length", value)]), streamed); - assert_eq!(response.declared_length, None, "{value:?}"); - } - } - - /// RFC 9110 §9.3.2: a `HEAD` may report the length its `GET` would have had. - #[test] - fn a_head_declares_the_length_it_was_given() { - let response = prepare_wire_response( - true, - 200, - headers(&[("content-length", "12345")]), - OutgoingBody::Bytes(bytes::Bytes::new()), - ); - assert!(content(&response).is_empty()); - assert_eq!(response.declared_length, Some(12345)); - } - - /// The status still outranks the declaration, so nothing frames a response that has no - /// content. - #[test] - fn a_bodiless_status_ignores_a_declared_length() { - let (_sender, streamed) = platform::http::body_channel(); - let response = - prepare_wire_response(false, 204, headers(&[("content-length", "42")]), streamed); - assert_eq!(response.declared_length, None); - } - - #[test] - fn an_in_memory_body_declares_its_length() { - let response = prepare_wire_response( - false, - 200, - Vec::new(), - OutgoingBody::Bytes(bytes::Bytes::from_static(b"BODYBYTES")), - ); - assert_eq!(content(&response), b"BODYBYTES"); - assert_eq!(response.declared_length, Some(9)); - } - - #[test] - fn a_bodiless_status_outranks_the_method() { - let response = prepare_wire_response( - true, - 204, - Vec::new(), - OutgoingBody::Bytes(bytes::Bytes::from_static(b"ignored")), - ); - assert!(content(&response).is_empty()); - assert_eq!(response.declared_length, None); - } -} diff --git a/crates/libstarling/src/serve_native.rs b/crates/libstarling/src/serve_native.rs index 56c33da..68e0715 100644 --- a/crates/libstarling/src/serve_native.rs +++ b/crates/libstarling/src/serve_native.rs @@ -54,7 +54,7 @@ enum RequestHandlingMode { } /// Everything a connection needs that outlives the requests it serves: the runtime to dispatch in, -/// where each request's JS runs, and the time limits each request runs under. +/// where each request's JS runs, and the time limits under which each request runs. struct ServeContext { runtime: Rc, request_handling_mode: RequestHandlingMode, @@ -90,25 +90,28 @@ impl ServeContext { } } -/// Run the serve loop on the given TCP port: evaluate the script (registering handlers), then -/// accept and dispatch requests until the process is killed. +/// Bind `127.0.0.1:port`, or a port the system picks when `port` is `0`, and run the serve loop on +/// it until the process is killed. See [`serve_with_shutdown`]. pub fn serve(config: RuntimeConfig, port: u16) -> Result<(), String> { - serve_with_shutdown(config, port, std::future::pending()) + let listener = std::net::TcpListener::bind(("127.0.0.1", port)) + .map_err(|e| format!("Failed to bind 127.0.0.1:{port}: {e}"))?; + serve_with_shutdown(config, listener, std::future::pending()) } -/// Like [`serve`], but stops accepting and returns once `shutdown` resolves (any in-flight requests -/// are abandoned). Lets a caller (a test, or a future signal handler) shut the server down -/// gracefully so the runtime drops cleanly. +/// Run the serve loop on `listener`: print the listener's address on stderr, evaluate the script +/// (registering handlers), then accept and dispatch requests until `shutdown` resolves. Requests +/// still in flight at that point are abandoned. A caller that shuts the server down this way, such +/// as a test, lets the runtime drop cleanly. pub fn serve_with_shutdown( config: RuntimeConfig, - port: u16, + listener: std::net::TcpListener, shutdown: impl Future, ) -> Result<(), String> { config.validate_serve_timeouts()?; config.serve_limits.validate()?; // Isolated mode has no process-wide content script: the script belongs to each request's own // global, and every request evaluates it there. Evaluating it here as well would run it (and - // its side effects) one extra time, in a global that goes on to serve nothing, so that mode + // its side effects) one extra time, in a global that never serves a request, so that mode // only sets up the runtime itself. let (runtime, startup) = if config.serve_isolated { (Runtime::init(&config)?, None) @@ -120,7 +123,7 @@ pub fn serve_with_shutdown( .enable_all() .build() .map_err(|e| format!("Failed to create tokio runtime: {e}"))?; - tokio_rt.block_on(run_server(runtime, startup, config, port, shutdown)) + tokio_rt.block_on(run_server(runtime, startup, config, listener, shutdown)) } /// One step of the accept loop. @@ -136,7 +139,7 @@ async fn run_server( runtime: Rc, startup: Option<(InvocationState, core_runtime::ScriptEvaluation)>, config: RuntimeConfig, - port: u16, + listener: std::net::TcpListener, shutdown: impl Future, ) -> Result<(), String> { // An isolated request serves from a global of its own, not the default global, but the @@ -159,10 +162,14 @@ async fn run_server( RequestHandlingMode::Shared }; - let listener = TcpListener::bind(("127.0.0.1", port)) - .await - .map_err(|e| format!("Failed to bind 127.0.0.1:{port}: {e}"))?; - eprintln!("serving on http://127.0.0.1:{port}"); + let bound = listener + .local_addr() + .map_err(|e| format!("Failed to read the listener's bound address: {e}"))?; + let listener = listener + .set_nonblocking(true) + .and_then(|()| TcpListener::from_std(listener)) + .map_err(|e| format!("Failed to listen on {bound}: {e}"))?; + eprintln!("serving on http://{bound}"); // Let the script finish evaluating before serving, so a handler registered after a top-level // `await` is in place for the first request. Binding first leaves a client that connects @@ -192,9 +199,10 @@ async fn run_server( return Err(crate::serve_common::NO_FETCH_LISTENER.to_string()); } } - // Borrowed for the rest of the function. `background` is not touched again, only released and - // dropped on the way out. Isolated mode has none of this: with a per-request global entered - // for each request, there is no single realm a background loop could be stepped in. + // The background loop is borrowed for the rest of the function. `background` is not touched + // again, only released and dropped on the way out. Isolated mode has none of this: with a + // per-request global entered for each request, there is no single realm a background loop + // could be stepped in. let background_loop = background .as_mut() .map(|(invocation, _)| invocation.state().event_loop()); @@ -285,11 +293,17 @@ async fn run_server( // Keep the content script's own loop running alongside serving, so a // `setInterval` it left running actually fires. This arm never yields // a `Step`: it runs for as long as the server does, and its timers - // wake this task when they come due. + // wake this task when they come due. A `setInterval(…, 0)` cannot + // starve the accept loop, since the re-queued timer is clamped to a + // non-zero delay. // TODO: same as in wasm: should reconsider this and abort work not // added to waitUntil. if let Some(event_loop) = background_loop { - run_background_work(raw_cx, event_loop).await; + // SAFETY: `raw_cx` is valid, with the serve loop's realm entered, + // for the server's life. + unsafe { + run_to_completion(raw_cx, event_loop, tokio::time::sleep).await + }; } std::future::pending::().await }, @@ -324,20 +338,6 @@ async fn run_server( } } -/// Keep the content script's own event loop running for as long as the server runs: the -/// `setInterval`s it left behind, and whatever they start. -/// -/// Never returns while that loop still has work (with a repeating timer it never will), so this -/// is raced against the accept loop rather than awaited. -/// -/// Between firings it waits on the next deadline like any idle loop, leaving the thread free to -/// accept and serve. A `setInterval(…, 0)` cannot starve that: the re-queued timer is clamped to -/// a non-zero delay, so the loop reports idle rather than finding ready work every step. -async fn run_background_work(raw_cx: *mut js::native::RawJSContext, event_loop: &EventLoop) { - // SAFETY: `raw_cx` is valid, with the serve loop's realm entered, for the server's life. - unsafe { run_to_completion(raw_cx, event_loop, tokio::time::sleep).await }; -} - /// A request's event loop, released once all work related to the request is done or abandoned. /// /// [`EventLoop::cancel_pending_futures`] has to run while the JSContext is alive. The paths that @@ -419,59 +419,30 @@ async fn serve_connection<'a>( builder.header_read_timeout(silence); } - let serving = std::pin::pin!(builder.serve_connection( + let mut serving = std::pin::pin!(builder.serve_connection( hyper_util::rt::TokioIo::new(stream), hyper::service::service_fn(|request| serve_request(&request_context, request)), )); - if let Err(e) = drive_connection(serving, &mut closed).await { - // Ignore clients that disconnect mid-request. - if !e.is_incomplete_message() { - eprintln!("serve: connection error: {e}"); - } - } -} - -/// Drive hyper's connection future to the connection's close. -/// -/// An unfinished response body or a request body left unread results in the connection being -/// closed: it stops taking requests and ends once the one in flight is done. -async fn drive_connection( - mut serving: Pin<&mut C>, - closed: &mut futures_channel::mpsc::UnboundedReceiver<()>, -) -> hyper::Result<()> -where - C: Future> + GracefulShutdown, -{ + // An unfinished response body or a request body left unread results in the connection being + // closed: it stops taking requests and ends once the one in flight is done. let mut ending = false; - std::future::poll_fn(|cx| { + let served = std::future::poll_fn(|cx| { // Checked before hyper is polled, so a close signal sent by this connection's request // work takes effect before hyper can start on a request already sitting in its read buffer. if !ending { if let Poll::Ready(Some(())) = closed.poll_next_unpin(cx) { - serving.as_mut().shut_down_gracefully(); + serving.as_mut().graceful_shutdown(); ending = true; } } serving.as_mut().poll(cx) }) - .await -} - -/// Close a connection once the request in flight completes, rather than immediately. -trait GracefulShutdown { - fn shut_down_gracefully(self: Pin<&mut Self>); -} - -impl GracefulShutdown for hyper::server::conn::http1::Connection -where - S: hyper::service::HttpService, - S::Error: Into>, - I: hyper::rt::Read + hyper::rt::Write + Unpin, - S::ResBody: Body + 'static, - ::Error: Into>, -{ - fn shut_down_gracefully(self: Pin<&mut Self>) { - self.graceful_shutdown(); + .await; + if let Err(e) = served { + // Ignore clients that disconnect mid-request. + if !e.is_incomplete_message() { + eprintln!("serve: connection error: {e}"); + } } } @@ -993,7 +964,7 @@ type BodyAbort = Rc>>; /// The response body hyper writes. enum WireBody { - /// A body already in memory. Empty where the status or the method leaves no content to send. + /// A body already in memory. Empty if the status or the method leaves no content to send. Complete(bytes::Bytes), /// Chunks the connection's background work feeds in as the handler's body produces them. Streamed { diff --git a/crates/libstarling/src/serve_wasm.rs b/crates/libstarling/src/serve_wasm.rs index fb8d087..d1a1c41 100644 --- a/crates/libstarling/src/serve_wasm.rs +++ b/crates/libstarling/src/serve_wasm.rs @@ -9,8 +9,8 @@ //! Requests therefore share one global, and are separated by their event loops rather than their //! realms. //! -//! Shares [`serve_native`](crate::serve_native)'s dispatch core and the request/response bridging -//! in `platform::http`. +//! Shares the dispatch core in `serve_common` with `serve_native`, and the request/response +//! bridging in `platform::http`. #![cfg(target_arch = "wasm32")] @@ -46,8 +46,8 @@ js::instance_local! { static STARTUP: RefCell = const { RefCell::new(Startup::Done) }; /// Signalled on every [`STARTUP`] transition a waiting request cares about: driven to - /// completion, or handed back by a driver that was cancelled. Every write of `Startup` other - /// than `Driving` must notify, or a waiter in [`ensure_started`] sleeps through it. + /// completion, or handed back by a driver that was cancelled. Every transition out of + /// `Driving` must notify, or a waiter in [`ensure_started`] sleeps through it. static STARTUP_CHANGED: event_listener::Event = const { event_listener::Event::new() }; /// Set while a Wizer snapshot is being taken, so it is set in the snapshot and the resumed @@ -70,8 +70,8 @@ enum Startup { /// The held reference is never released, so the runtime is never dropped. Nothing here tears an /// instance down except thread-local destruction, and `Runtime::drop` reaches thread-locals in the /// `js` crate (the scope handle pool among them) that the same destruction may already have run, -/// which traps. Dropping it would reclaim nothing either, since the instance's memory goes away -/// whole. `serve_native` runs the real `Drop`. +/// which traps. Dropping it wouldn't reclaim any memory either, since the instance's memory goes +/// away whole. `serve_native` runs the real `Drop`. fn install_runtime(pair: (Rc, *mut js::native::RawJSContext, ServeTimeouts)) { std::mem::forget(Rc::clone(&pair.0)); RUNTIME.with(|cell| *cell.borrow_mut() = Some(pair)); @@ -94,8 +94,8 @@ fn runtime() -> Result<(Rc, *mut js::native::RawJSContext, ServeTimeout }) } -/// [`runtime`], creating a runtime configured for a Wizer snapshot if `pre_initialize` is set and -/// none exists yet. +/// Like [`runtime`], but creates a runtime configured for a Wizer snapshot if `pre_initialize` is +/// set and no runtime exists yet. fn runtime_for( pre_initialize: bool, ) -> Result<(Rc, *mut js::native::RawJSContext, ServeTimeouts), String> { @@ -183,9 +183,17 @@ pub fn prepare_for_snapshot() { /// behind once its top level has finished. pub async fn pre_initialize() -> Result<(), String> { let (_runtime, raw_cx, _) = runtime_for(true)?; - let Startup::Pending(invocation, evaluation) = - STARTUP.with(|cell| std::mem::replace(&mut *cell.borrow_mut(), Startup::Driving)) - else { + let pending = STARTUP.with(|cell| { + let mut state = cell.borrow_mut(); + match std::mem::replace(&mut *state, Startup::Driving) { + Startup::Pending(invocation, evaluation) => Some((invocation, evaluation)), + other => { + *state = other; + None + } + } + }); + let Some((invocation, evaluation)) = pending else { // Nothing to evaluate: `runtime` was already stood up, so this is a second call. prepare_for_snapshot(); return Ok(()); @@ -214,7 +222,7 @@ pub async fn pre_initialize() -> Result<(), String> { /// Whether the content script has finished evaluating without registering a `fetch` listener, in /// which case every request it could ever receive is a 500 (see /// [`NO_FETCH_LISTENER`](crate::serve_common::NO_FETCH_LISTENER)). A script still evaluating (a -/// top-level `await` that never settles) has registered nothing yet and cannot be judged. +/// top-level `await` that never settles) may still register a listener and cannot be judged. fn evaluated_without_listener( scope: &js::gc::scope::Scope<'_>, evaluation: &core_runtime::ScriptEvaluation, @@ -224,7 +232,7 @@ fn evaluated_without_listener( /// Some state, such as process time origins, needs fixing up after snapshot resumption. /// -/// The monotonic clock comes first: its offset puts the timestamps the snapshot holds in the +/// The monotonic clock comes first: its offset puts the timestamps held by the snapshot in the /// resumed instance's past, and both the engine's timing and every fixup below read the clock /// through it. fn fix_up_after_resume() { @@ -346,7 +354,7 @@ async fn drive_startup( } /// Report, once evaluation completes, that the content script registered no `fetch` listener, so -/// the 500s every request will get have a stated reason. We can't decline to serve +/// the 500 response sent to every request has a stated reason. We can't decline to serve /// at all, since the host owns the instance's lifecycle. Only applies to non-snapshot configs, /// since [`pre_initialize`] refuses to snapshot a script without a listener. fn report_missing_fetch_listener( @@ -523,10 +531,10 @@ async fn dispatch_request( outcome, ); } - // Drained regardless of how the send ended. The writer's `TimedOut` and the limit above - // expiring are the same deadline enforced in two places, so on an ordinary body - // timeout they expire together and either may be reported first. The request's - // `waitUntil` window must not depend on which. + // Lifetime work is drained regardless of how the send ended. The writer's `TimedOut` + // and the limit above expiring are the same deadline enforced in two places, so on an + // ordinary body timeout they expire together and either may be reported first. The + // request's `waitUntil` window must not depend on which. crate::serve_common::drain_lifetime_work( raw_cx, invocation.state().event_loop(), @@ -546,7 +554,7 @@ async fn dispatch_request( /// `response_body` time limit expires first. /// /// If the loop runs out of work before the body has been fully sent out, that means it's -/// effectively abandoned and can't be completed anymore. In that case, [`abandon`] is called, +/// effectively abandoned and can't be completed anymore. In that case, `abandon` is called, /// and must close the transport. /// /// The timeout here is a backstop: the same limit is applied to `spawn_body_writer`, which reports @@ -609,9 +617,7 @@ fn body_framing(method: &str, headers: &http::HeaderMap) -> (bool, Option) } /// A minimal text error response. Its body is written out by a task of its own like any other -/// response's, and also bounded by a timeout like any other: otherwise a client that doesn't -/// read the response could stall indefinitely. (At least I think it might: chances are the -/// host runtime takes care of this.) +/// response's, and bounded by a timeout like any other. // TODO: unify with `status_response` in `serve_native`. fn error_response( status: u16, diff --git a/crates/libstarling/tests/body_path.rs b/crates/libstarling/tests/body_path.rs index d5a3628..4c8fe5f 100644 --- a/crates/libstarling/tests/body_path.rs +++ b/crates/libstarling/tests/body_path.rs @@ -10,7 +10,7 @@ //! These run in a test binary of their own because they read process-wide counters //! (`web_fetch::outgoing_body::paths_taken`), which any other request in flight would perturb. The //! counters belong to the process, and this file holds a single test, so the counts always belong -//! to the request that just ran. The ports below are this binary's own for the same reason. +//! to the request that just ran. #![cfg(not(target_arch = "wasm32"))] @@ -20,8 +20,9 @@ use common::{dechunk, request, start_chunked_upstream, start_echo_upstream, star use web_fetch::outgoing_body::paths_taken; /// Serve one request against `handler` and report `(response body, shortcuts, pumps)`. -fn serve_once(handler: &str, port: u16, path: &str) -> (String, usize, usize) { - let handle = start_serve(handler, port); +fn serve_once(handler: &str, path: &str) -> (String, usize, usize) { + let handle = start_serve(handler); + let port = handle.port(); paths_taken::reset(); let body = request(port, "GET", path, ""); let (shortcut, pumped) = paths_taken::counts(); @@ -32,7 +33,6 @@ fn serve_once(handler: &str, port: u16, path: &str) -> (String, usize, usize) { #[test] fn outgoing_bodies_take_the_route_they_should() { single_bodies_take_the_route_they_should(); - many_bodies_through_one_transform_keep_every_byte(); piping_many_bodies_into_one_transform_keeps_every_byte(); a_queued_chunk_survives_a_pipe_that_is_still_running(); a_pipe_that_keeps_the_transform_open_forfeits_the_shortcut(); @@ -49,10 +49,11 @@ fn outgoing_bodies_take_the_route_they_should() { /// shows. fn a_byte_response_body_leaves_its_owner_holding_nothing() { let handler = "addEventListener('fetch', (e) => e.respondWith(new Response('payload')))"; - let handle = start_serve(handler, 18531); + let handle = start_serve(handler); + let port = handle.port(); paths_taken::reset(); - let body = request(18531, "GET", "/", ""); + let body = request(port, "GET", "/", ""); let (sole, shared) = paths_taken::byte_counts(); handle.stop(); @@ -77,10 +78,11 @@ fn a_byte_request_body_leaves_no_copy_behind_on_the_request_it_came_from() { }}))); }})"# ); - let handle = start_serve(&handler, 18533); + let handle = start_serve(&handler); + let port = handle.port(); paths_taken::reset(); - let body = request(18533, "GET", "/", ""); + let body = request(port, "GET", "/", ""); let (sole, shared) = paths_taken::byte_counts(); handle.stop(); @@ -121,13 +123,14 @@ fn a_head_request_cancels_the_body_it_will_not_send() { ? source.pipeThrough(new TransformStream(undefined, { highWaterMark: 0 })) : source)); })"#; - let handle = start_serve(handler, 18521); + let handle = start_serve(handler); + let port = handle.port(); paths_taken::reset(); - let direct = request(18521, "HEAD", "/direct", ""); - let piped = request(18521, "HEAD", "/piped", ""); + let direct = request(port, "HEAD", "/direct", ""); + let piped = request(port, "HEAD", "/piped", ""); let (shortcut, pumped) = paths_taken::counts(); - let seen = request(18521, "GET", "/seen", ""); + let seen = request(port, "GET", "/seen", ""); handle.stop(); assert_eq!( @@ -156,10 +159,11 @@ fn a_head_request_still_claims_a_proxied_body() { let handler = format!( "addEventListener('fetch', (e) => e.respondWith(fetch('http://127.0.0.1:{upstream}/')))" ); - let handle = start_serve(&handler, 18519); + let handle = start_serve(&handler); + let port = handle.port(); paths_taken::reset(); - let response = request(18519, "HEAD", "/", ""); + let response = request(port, "HEAD", "/", ""); let (shortcut, pumped) = paths_taken::counts(); handle.stop(); @@ -182,10 +186,11 @@ fn a_head_request_still_claims_a_proxied_body() { }})()); }})"# ); - let handle = start_serve(&handler, 18523); + let handle = start_serve(&handler); + let port = handle.port(); paths_taken::reset(); - let response = request(18523, "HEAD", "/", ""); + let response = request(port, "HEAD", "/", ""); let (shortcut, pumped) = paths_taken::counts(); handle.stop(); @@ -200,9 +205,9 @@ fn a_head_request_still_claims_a_proxied_body() { /// A pipe that leaves the transform open (`preventClose`) must not let the body be claimed whole. /// /// Claiming it hands the wire that one body and bypasses the transform, so whatever is written in -/// afterwards goes to a readable end nobody reads: the second pipe stalls forever and its bytes are -/// dropped without a word. Worse, it made the choice of route observable — the same handler -/// delivered `AAA` when the body was claimed and `AAABBB` when it was pumped. +/// afterwards goes to a readable end that nothing reads: the second pipe stalls forever and its +/// bytes are dropped without a word. Worse, it made the choice of route observable: the same +/// handler delivered `AAA` when the body was claimed and `AAABBB` when it was pumped. fn a_pipe_that_keeps_the_transform_open_forfeits_the_shortcut() { let first = start_chunked_upstream(&["AAA"]); let second = start_chunked_upstream(&["BBB"]); @@ -227,10 +232,11 @@ fn a_pipe_that_keeps_the_transform_open_forfeits_the_shortcut() { }})()); }})"# ); - let handle = start_serve(&handler, 18517); + let handle = start_serve(&handler); + let port = handle.port(); paths_taken::reset(); - let body = request(18517, "GET", "/", ""); + let body = request(port, "GET", "/", ""); let (shortcut, pumped) = paths_taken::counts(); assert_eq!( @@ -280,7 +286,6 @@ fn a_queued_chunk_survives_a_pipe_that_is_still_running() { }})()); }})"# ), - 18515, "/", ); @@ -306,7 +311,6 @@ fn single_bodies_take_the_route_they_should() { &format!( "addEventListener('fetch', (e) => e.respondWith(fetch('http://127.0.0.1:{upstream}/')))" ), - 18501, "/", ); assert_eq!(dechunk(&body), "one two", "got: {body}"); @@ -328,7 +332,6 @@ fn single_bodies_take_the_route_they_should() { }})()); }})"# ), - 18503, "/", ); assert_eq!(dechunk(&body), "one two", "got: {body}"); @@ -350,7 +353,6 @@ fn single_bodies_take_the_route_they_should() { }})()); }})"# ), - 18505, "/", ); assert_eq!(dechunk(&body), "one two", "got: {body}"); @@ -372,7 +374,6 @@ fn single_bodies_take_the_route_they_should() { start(c) { c.enqueue(new TextEncoder().encode('made up')); c.close(); } }))); })"#, - 18507, "/", ); assert_eq!(dechunk(&body), "made up", "got: {body}"); @@ -405,7 +406,6 @@ fn single_bodies_take_the_route_they_should() { }})()); }})"# ), - 18509, "/", ); assert_eq!(dechunk(&body), "abcd", "got: {body}"); @@ -416,75 +416,20 @@ fn single_bodies_take_the_route_they_should() { ); } -/// Several incoming bodies funnelled through one `TransformStream`, with the handler's own chunks -/// interleaved between them. +/// Several upstream bodies fed into one `TransformStream`, with the handler's own chunks written +/// between them. The response mixes host bytes from two upstreams with bytes JS made up, so it +/// cannot be handed to the wire whole, and every byte has to arrive in order. Piping into an +/// identity transform moves a native source to its readable side, so two of them arriving at the +/// same transform would overwrite each other. /// -/// The response is a mixture — host bytes from two upstreams and bytes JS made up — so it cannot be -/// handed to the wire whole, and every byte has to survive the trip in order. This is the shape that -/// would break silently if a native source were carried across the transform and then claimed: the -/// claim would forward one upstream's host body and drop everything queued alongside it. -fn many_bodies_through_one_transform_keep_every_byte() { - let upstream = start_chunked_upstream(&["AB", "CD"]); - let handler = format!( - r#"addEventListener('fetch', (event) => {{ - event.respondWith((async () => {{ - const encoder = new TextEncoder(); - const transform = new TransformStream(); - const writer = transform.writable.getWriter(); - const response = new Response(transform.readable); - - // Filled after the response is committed: a JS chunk, an upstream body, another JS - // chunk, a second upstream body, a last JS chunk. - (async () => {{ - await writer.write(encoder.encode('<')); - for (const round of [1, 2]) {{ - const upstream = await fetch('http://127.0.0.1:{upstream}/?round=' + round); - const reader = upstream.body.getReader(); - while (true) {{ - const {{ done, value }} = await reader.read(); - if (done) break; - await writer.write(value); - }} - await writer.write(encoder.encode('|')); - }} - await writer.write(encoder.encode('>')); - await writer.close(); - }})(); - - return response; - }})()); - }})"# - ); - let handle = start_serve(&handler, 18511); - paths_taken::reset(); - - let body = request(18511, "GET", "/", ""); - let (shortcut, pumped) = paths_taken::counts(); - - assert_eq!( - dechunk(&body), - "", - "every byte from both upstreams and the handler's own chunks must arrive in order: {body}" - ); - assert_eq!( - (shortcut, pumped), - (0, 1), - "a response mixing several bodies with JS chunks cannot be handed over whole" - ); - - handle.stop(); -} - -/// The same funnel, but the upstream bodies are *piped* in rather than read chunk by chunk — the -/// route on which an identity transform does carry a native source across, and on which two of them -/// arriving at the same transform would overwrite each other. -/// -/// Piping into a writable locks it, so the bodies go in one after another with the transform held -/// open in between, and the handler's own chunks are written around them. +/// `/piped` pipes each body in. Piping into a writable locks it, so the bodies go in one after +/// another with the transform held open in between, and the handler's own chunks are written around +/// them. `/read` reads each body with a reader and writes its chunks with a writer. fn piping_many_bodies_into_one_transform_keeps_every_byte() { let upstream = start_chunked_upstream(&["AB", "CD"]); let handler = format!( r#"addEventListener('fetch', (event) => {{ + const route = new URL(event.request.url).pathname; event.respondWith((async () => {{ const encoder = new TextEncoder(); const transform = new TransformStream(); @@ -493,18 +438,26 @@ fn piping_many_bodies_into_one_transform_keeps_every_byte() { (async () => {{ let writer = transform.writable.getWriter(); await writer.write(encoder.encode('<')); - writer.releaseLock(); for (const round of [1, 2]) {{ const upstream = await fetch('http://127.0.0.1:{upstream}/?round=' + round); - // The pipe owns the writable until it finishes; keep it open for the next. - await upstream.body.pipeTo(transform.writable, {{ preventClose: true }}); - writer = transform.writable.getWriter(); + if (route === '/piped') {{ + writer.releaseLock(); + // The pipe owns the writable until it finishes. Keep it open for the + // next. + await upstream.body.pipeTo(transform.writable, {{ preventClose: true }}); + writer = transform.writable.getWriter(); + }} else {{ + const reader = upstream.body.getReader(); + while (true) {{ + const {{ done, value }} = await reader.read(); + if (done) break; + await writer.write(value); + }} + }} await writer.write(encoder.encode('|')); - writer.releaseLock(); }} - writer = transform.writable.getWriter(); await writer.write(encoder.encode('>')); await writer.close(); }})(); @@ -513,23 +466,26 @@ fn piping_many_bodies_into_one_transform_keeps_every_byte() { }})()); }})"# ); - let handle = start_serve(&handler, 18513); - paths_taken::reset(); - - let body = request(18513, "GET", "/", ""); - let (shortcut, pumped) = paths_taken::counts(); - - assert_eq!( - dechunk(&body), - "", - "piping two bodies into one transform must not lose either, nor the chunks around them: \ - {body}" - ); - assert_eq!( - (shortcut, pumped), - (0, 1), - "a transform fed by more than one body cannot be handed to the wire whole" - ); + let handle = start_serve(&handler); + let port = handle.port(); + + for route in ["/piped", "/read"] { + paths_taken::reset(); + let body = request(port, "GET", route, ""); + let (shortcut, pumped) = paths_taken::counts(); + + assert_eq!( + dechunk(&body), + "", + "{route}: feeding two bodies into one transform must not lose either, nor the chunks \ + around them: {body}" + ); + assert_eq!( + (shortcut, pumped), + (0, 1), + "{route}: a transform fed by more than one body cannot be handed to the wire whole" + ); + } handle.stop(); } diff --git a/crates/libstarling/tests/common/mod.rs b/crates/libstarling/tests/common/mod.rs index a04e165..49cc7b9 100644 --- a/crates/libstarling/tests/common/mod.rs +++ b/crates/libstarling/tests/common/mod.rs @@ -8,7 +8,7 @@ #![allow(dead_code)] // The transport helpers and upstream servers are shared with the wasm end-to-end suite through -// `serve-test-support`. Only the serve starters, which link the engine, remain here. +// `serve-test-support`. Only the serve starters and their config and script helpers remain here. pub use serve_test_support::*; // `serve-test-support` duplicates `IDLE_TRACE` rather than depend on `core-runtime`. This fails @@ -25,62 +25,96 @@ const _: () = { }; use libstarling::config::RuntimeConfig; -use std::net::TcpStream; -use std::time::Duration; +use std::net::TcpListener; /// A running serve loop; [`ServeHandle::stop`] shuts it down and joins the thread so the runtime /// drops cleanly (a leaked runtime aborts at process teardown with outstanding engine handles). pub struct ServeHandle { + port: u16, shutdown: futures_channel::oneshot::Sender<()>, thread: std::thread::JoinHandle<()>, } impl ServeHandle { + /// The port the server listens on. + pub fn port(&self) -> u16 { + self.port + } + pub fn stop(self) { let _ = self.shutdown.send(()); let _ = self.thread.join(); } } -/// Start a serve loop with `handler` on `port` in a background thread, and wait until it accepts -/// connections. -pub fn start_serve(handler: &str, port: u16) -> ServeHandle { - start_serve_with(handler, port, false) +/// Start a serve loop with `handler` on a port the system picks, in a background thread. +pub fn start_serve(handler: &str) -> ServeHandle { + start_serve_with(handler, false) } /// Like [`start_serve`], but chooses whether each request gets its own global. -pub fn start_serve_with(handler: &str, port: u16, isolated: bool) -> ServeHandle { - start_serve_config( - RuntimeConfig { - eval_script: Some(handler.to_string()), - legacy_script: true, - serve: Some(port), - serve_isolated: isolated, - ..Default::default() - }, - port, - ) +pub fn start_serve_with(handler: &str, isolated: bool) -> ServeHandle { + start_serve_config(RuntimeConfig { + serve_isolated: isolated, + ..legacy_script(handler) + }) +} + +/// A config that runs `handler` as an inline classic script. +pub fn legacy_script(handler: &str) -> RuntimeConfig { + RuntimeConfig { + eval_script: Some(handler.to_string()), + legacy_script: true, + ..Default::default() + } +} + +/// Start a serve loop from `config` on a port the system picks, for the cases that need more than +/// an inline script, such as a module entry point on disk. +pub fn start_serve_config(config: RuntimeConfig) -> ServeHandle { + start_serve_on(bind_local().0, config) } -/// Start a serve loop from a fully-specified config — for the cases that need more than an inline -/// script, such as a module entry point on disk. -pub fn start_serve_config(config: RuntimeConfig, port: u16) -> ServeHandle { +/// Start a serve loop from `config` on `listener`, for a handler that needs to know its own port +/// before the server starts. +/// +/// The listener is bound before this returns, so a request made right away waits in its backlog +/// until the script has finished evaluating. +pub fn start_serve_on(listener: TcpListener, config: RuntimeConfig) -> ServeHandle { + let port = listener.local_addr().unwrap().port(); let (shutdown_tx, shutdown_rx) = futures_channel::oneshot::channel(); let thread = std::thread::spawn(move || { // `run` does this before serving; the test calls `serve` directly, so register here. + libstarling::apply_pre_init_config(&config).expect("valid config"); libstarling::register_builtins(); - let _ = libstarling::serve_native::serve_with_shutdown(config, port, async { + let _ = libstarling::serve_native::serve_with_shutdown(config, listener, async { let _ = shutdown_rx.await; }); }); - for _ in 0..200 { - if TcpStream::connect(("127.0.0.1", port)).is_ok() { - return ServeHandle { - shutdown: shutdown_tx, - thread, - }; + ServeHandle { + port, + shutdown: shutdown_tx, + thread, + } +} + +/// A temporary directory of content-script files, removed on drop. +pub struct ScriptDir(tempfile::TempDir); + +impl ScriptDir { + /// Create a directory holding `files`, given as `(relative path, contents)`. + pub fn new(files: &[(&str, &str)]) -> Self { + let dir = libstarling::test_util::test_tempdir(); + for (path, contents) in files { + let path = dir.path().join(path); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(path, contents).unwrap(); } - std::thread::sleep(Duration::from_millis(25)); + ScriptDir(dir) + } + + /// The path of `file` in this directory. + pub fn path(&self, file: &str) -> String { + self.0.path().join(file).to_string_lossy().into_owned() } - panic!("serve did not start on port {port}"); } diff --git a/crates/libstarling/tests/serve.rs b/crates/libstarling/tests/serve.rs index cbf6a1f..42b5166 100644 --- a/crates/libstarling/tests/serve.rs +++ b/crates/libstarling/tests/serve.rs @@ -2,44 +2,27 @@ //! End-to-end test of native serve mode: a script registers a `fetch` handler, //! the runtime serves HTTP requests, and each is dispatched as a `fetch` event -//! whose `respondWith` reply is sent back. Covers the request body, an async -//! (promise) response, and concurrent requests on isolated per-request loops. +//! whose `respondWith` reply is sent back. #![cfg(not(target_arch = "wasm32"))] mod common; use common::{ - dechunk, raw_request, read_one_response, read_until_eof, request, request_within, - start_chunked_upstream, start_echo_upstream, start_endless_upstream, start_serve, - start_serve_config, start_serve_with, start_silent_upstream, start_upstream, PATIENCE, + bind_local, dechunk, legacy_script, raw_request, read_one_response, read_until_eof, request, + request_within, start_chunked_upstream, start_echo_upstream, start_endless_upstream, + start_serve, start_serve_config, start_serve_on, start_serve_with, start_silent_upstream, + start_upstream, ScriptDir, PATIENCE, }; use libstarling::config::RuntimeConfig; use std::io::{Read, Write}; use std::net::TcpStream; use std::time::Duration; -/// How long [`eventually`] keeps asking. Long enough for a loaded machine to reach the response, and -/// finite so that work which never lands fails its test. -const SETTLE_PATIENCE: Duration = Duration::from_secs(20); - -/// Ask `path` until its response satisfies `settled`, and return the last one. -/// -/// Work a handler leaves behind runs while the request's loop is drained after the response: a -/// `waitUntil` promise, a signal's reactions, a stream's cancel. Asking until the outcome is there -/// takes as long as the work does, rather than a fixed span chosen for the slowest machine. -/// -/// Only for assertions that wait for something to happen. A request drives the loop it is served -/// on, so an assertion that something does not happen waits a fixed span instead. +/// Ask `path` until its response satisfies `settled`, and return the last one. See +/// [`common::eventually`]. fn eventually(port: u16, path: &str, settled: impl Fn(&str) -> bool) -> String { - let deadline = std::time::Instant::now() + SETTLE_PATIENCE; - loop { - let answer = request(port, "GET", path, ""); - if settled(&answer) || std::time::Instant::now() >= deadline { - return answer; - } - std::thread::sleep(Duration::from_millis(20)); - } + common::eventually(|| request(port, "GET", path, ""), settled) } #[test] @@ -55,22 +38,23 @@ fn serve_dispatches_fetch_events() { new Response(event.request.method + ' ' + url.pathname + ' body=' + body + ' aborted=' + event.request.signal.aborted))); })"#; - let handle = start_serve(handler, 18137); + let handle = start_serve(handler); + let port = handle.port(); // A GET (no body) and a POST (with a body) — exercises Request::from_incoming + request.text(). assert_eq!( - request(18137, "GET", "/foo", ""), + request(port, "GET", "/foo", ""), "GET /foo body= aborted=false" ); assert_eq!( - request(18137, "POST", "/submit", "hello world"), + request(port, "POST", "/submit", "hello world"), "POST /submit body=hello world aborted=false" ); // Two concurrent requests: each runs on its own event loop and must get exactly its own // response (no cross-request contamination). - let a = std::thread::spawn(|| request(18137, "GET", "/a", "")); - let b = std::thread::spawn(|| request(18137, "GET", "/b", "")); + let a = std::thread::spawn(move || request(port, "GET", "/a", "")); + let b = std::thread::spawn(move || request(port, "GET", "/b", "")); assert_eq!(a.join().unwrap(), "GET /a body= aborted=false"); assert_eq!(b.join().unwrap(), "GET /b body= aborted=false"); @@ -105,40 +89,19 @@ fn serve_forwards_an_incoming_body_upstream_while_reading_it() { }})()); }})"# ); - let handle = start_serve(&handler, 18209); + let handle = start_serve(&handler); + let port = handle.port(); assert_eq!( - request(18209, "POST", "/", "payload bytes"), + request(port, "POST", "/", "payload bytes"), "upstream=payload bytes local=payload bytes" ); handle.stop(); } -/// Forwarding an upstream response's body as this response's body — `new Response(upstream.body, -/// upstream)` — rather than handing back the upstream `Response` whole. The body travels as a -/// stream the handler has taken apart and rebuilt, and every chunk still has to arrive, in order. -#[test] -fn serve_forwards_an_upstream_body_as_a_stream() { - let upstream = start_chunked_upstream(&["one ", "two ", "three"]); - let handler = format!( - r#"addEventListener('fetch', (event) => {{ - event.respondWith((async () => {{ - const upstream = await fetch('http://127.0.0.1:{upstream}/'); - return new Response(upstream.body, upstream); - }})()); - }})"# - ); - let handle = start_serve(&handler, 18197); - - let body = request(18197, "GET", "/", ""); - assert_eq!(dechunk(&body), "one two three", "got: {body}"); - - handle.stop(); -} - -/// A handler may respond with a stream it goes on to fill once `respondWith` has been called. That -/// is the shape every incremental response takes, where the work producing the body outlives the +/// A handler may respond with a stream it goes on to fill once `respondWith` has been called. Every +/// incremental response takes that shape, where the work producing the body outlives the /// call that promised it. The chunks written afterwards still have to reach the client. #[test] fn serve_streams_a_body_written_after_respond_with() { @@ -154,9 +117,10 @@ fn serve_streams_a_body_written_after_respond_with() { await writer.write('world'); await writer.close(); })"#; - let handle = start_serve(handler, 18199); + let handle = start_serve(handler); + let port = handle.port(); - let body = request(18199, "GET", "/", ""); + let body = request(port, "GET", "/", ""); assert_eq!(dechunk(&body), "hello world", "got: {body}"); handle.stop(); @@ -192,9 +156,10 @@ fn serve_concatenates_several_upstream_bodies() { }})()); }})"# ); - let handle = start_serve(&handler, 18201); + let handle = start_serve(&handler); + let port = handle.port(); - let body = request(18201, "GET", "/", ""); + let body = request(port, "GET", "/", ""); assert_eq!(dechunk(&body), "abcdabcdabcd", "got: {body}"); handle.stop(); @@ -202,58 +167,37 @@ fn serve_concatenates_several_upstream_bodies() { #[test] fn serve_proxies_via_fetch() { - // The handler proxies each request to an upstream via `fetch` and replies with the result. This - // exercises the future-attribution fix: respondWith resolves from a `fetch` future, whose - // reaction must release the request loop's interest *on that loop*. Two concurrent proxies must - // each get the upstream body, with no cross-request contamination. + // The handler proxies each request to an upstream via `fetch` and replies with the result. + // respondWith resolves from a `fetch` future, whose reaction must release the request loop's + // interest on that loop. Two concurrent proxies must each get the upstream body, with no + // cross-request contamination. let upstream = start_upstream("UPSTREAM-BODY"); let handler = format!( "addEventListener('fetch', (e) => e.respondWith(fetch('http://127.0.0.1:{upstream}/')))" ); - let handle = start_serve(&handler, 18139); + let handle = start_serve(&handler); + let port = handle.port(); - assert!(request(18139, "GET", "/", "").contains("UPSTREAM-BODY")); + assert!(request(port, "GET", "/", "").contains("UPSTREAM-BODY")); - let a = std::thread::spawn(|| request(18139, "GET", "/a", "")); - let b = std::thread::spawn(|| request(18139, "GET", "/b", "")); + let a = std::thread::spawn(move || request(port, "GET", "/a", "")); + let b = std::thread::spawn(move || request(port, "GET", "/b", "")); assert!(a.join().unwrap().contains("UPSTREAM-BODY")); assert!(b.join().unwrap().contains("UPSTREAM-BODY")); handle.stop(); } -#[test] -fn serve_streams_a_readable_stream_response_body() { - // respondWith(new Response(stream)) with a chunk already queued: taking the send body must - // run on the request's loop with its reaction jobs drained (previously this tripped the - // pending-microtask assert under debugmozjs, and hung after the headers in release). - let handler = r#"addEventListener('fetch', (event) => { - const stream = new ReadableStream({ - start(c) { - c.enqueue(new TextEncoder().encode('streamed ')); - c.enqueue(new TextEncoder().encode('body')); - c.close(); - } - }); - event.respondWith(new Response(stream)); - })"#; - let handle = start_serve(handler, 18143); - let body = request(18143, "GET", "/", ""); - // The body arrives chunk-framed; de-chunk and assert the exact payload, so a - // chunk-framing corruption that merely preserved the substrings would fail. - assert_eq!(dechunk(&body), "streamed body", "got: {body}"); - handle.stop(); -} - #[test] fn serve_enforces_request_limits() { let handler = r#"addEventListener('fetch', (e) => e.respondWith(new Response('ok')))"#; - let handle = start_serve(handler, 18141); + let handle = start_serve(handler); + let port = handle.port(); - // A Content-Length beyond the body cap is rejected up front — the server must not - // allocate it (previously `vec![0; content_length]` was attacker-sized). + // A Content-Length beyond the body cap is rejected up front, before anything is allocated + // for it. let response = raw_request( - 18141, + port, b"POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 99999999999\r\nConnection: close\r\n\r\n", ); assert!(response.starts_with("HTTP/1.1 413"), "got: {response}"); @@ -264,18 +208,18 @@ fn serve_enforces_request_limits() { let mut huge = Vec::from(&b"GET / HTTP/1.1\r\nHost: x\r\nX-Big: "[..]); huge.extend(std::iter::repeat_n(b'a', 1024 * 1024)); huge.extend_from_slice(b"\r\nConnection: close\r\n\r\n"); - let response = raw_request(18141, &huge); + let response = raw_request(port, &huge); assert!(response.starts_with("HTTP/1.1 431"), "got: {response}"); // A malformed Content-Length is a 400, not silently treated as zero. let response = raw_request( - 18141, + port, b"POST / HTTP/1.1\r\nHost: x\r\nContent-Length: banana\r\nConnection: close\r\n\r\n", ); assert!(response.starts_with("HTTP/1.1 400"), "got: {response}"); // The server survives all of the above and still serves. - assert_eq!(request(18141, "GET", "/", ""), "ok"); + assert_eq!(request(port, "GET", "/", ""), "ok"); handle.stop(); } @@ -286,12 +230,13 @@ fn serve_enforces_request_limits() { #[test] fn serve_rejects_ambiguous_body_framing() { let handler = r#"addEventListener('fetch', (e) => e.respondWith(new Response('ok')))"#; - let handle = start_serve(handler, 18179); + let handle = start_serve(handler); + let port = handle.port(); // Two Content-Length headers: a front-end framing by the first and this server framing by the // second disagree about where the request ends, so the remainder is read as a second request. let response = raw_request( - 18179, + port, b"POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\nContent-Length: 41\r\n\ Connection: close\r\n\r\n", ); @@ -299,33 +244,32 @@ fn serve_rejects_ambiguous_body_framing() { // The same ambiguity inside one field. let response = raw_request( - 18179, + port, b"POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 0, 41\r\nConnection: close\r\n\r\n", ); assert!(response.starts_with("HTTP/1.1 400"), "got: {response}"); // Transfer-Encoding with Content-Length: the classic pair, each party picking a different one. - // The message is framed by the Transfer-Encoding and the Content-Length is dropped, so what - // the other party would have read as a second request is never served (RFC 9112 §6.3). - let mut stream = TcpStream::connect(("127.0.0.1", 18179)).unwrap(); - stream - .write_all( - b"POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 44\r\nTransfer-Encoding: chunked\r\n\ - \r\n0\r\n\r\nGET /smuggled HTTP/1.1\r\nHost: x\r\nFoo: ", - ) - .unwrap(); - let mut carry = Vec::new(); - let first = read_one_response(&mut stream, &mut carry, PATIENCE).expect("the framed request"); - assert!(first.starts_with("HTTP/1.1 200"), "got: {first}"); - assert!( - read_one_response(&mut stream, &mut carry, Duration::from_secs(2)).is_none(), - "the smuggled request must not be served" + // The message is framed by the Transfer-Encoding and the connection is closed after the + // response, so the request a Transfer-Encoding framing finds after the chunked body is never + // served (RFC 9112 §6.3). A server that kept the connection open would answer it, or leave + // `raw_request` waiting for an EOF that never comes. + let response = raw_request( + port, + b"POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 40\r\nTransfer-Encoding: chunked\r\n\ + \r\n0\r\n\r\nGET /smuggled HTTP/1.1\r\nHost: x\r\n\r\n", + ); + assert!(response.starts_with("HTTP/1.1 200"), "got: {response}"); + assert_eq!( + response.matches("HTTP/1.1 ").count(), + 1, + "the smuggled request must not be served: {response}" ); - // A Transfer-Encoding whose final coding is not chunked delimits nothing at all, so there is no - // framing to fall back on. + // A Transfer-Encoding whose final coding is not chunked doesn't delimit the body, so there is + // no framing to fall back on. let response = raw_request( - 18179, + port, b"POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked, gzip\r\nConnection: close\r\n\r\n", ); assert!(response.starts_with("HTTP/1.1 400"), "got: {response}"); @@ -333,13 +277,13 @@ fn serve_rejects_ambiguous_body_framing() { // Repeated Transfer-Encoding fields are one list: `gzip` then `chunked` still ends in chunked, // so this one *is* framed and must be served rather than caught by the check above. let response = raw_request( - 18179, + port, b"POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: gzip\r\nTransfer-Encoding: chunked\r\n\ Connection: close\r\n\r\n0\r\n\r\n", ); assert!(response.starts_with("HTTP/1.1 200"), "got: {response}"); - assert_eq!(request(18179, "GET", "/", ""), "ok"); + assert_eq!(request(port, "GET", "/", ""), "ok"); handle.stop(); } @@ -350,10 +294,11 @@ fn serve_maps_invalid_status_to_500() { // status 0 — must not serialize the protocol-illegal status line // `HTTP/1.1 0`; the status is surfaced as a 500. let handler = r#"addEventListener('fetch', (e) => e.respondWith(Response.error()))"#; - let handle = start_serve(handler, 18151); + let handle = start_serve(handler); + let port = handle.port(); let response = raw_request( - 18151, + port, b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", ); assert!(response.starts_with("HTTP/1.1 500"), "got: {response}"); @@ -368,11 +313,12 @@ fn serve_decodes_a_chunked_request_body() { let handler = r#"addEventListener('fetch', (event) => { event.respondWith(event.request.text().then((body) => new Response('got:' + body))); })"#; - let handle = start_serve(handler, 18153); + let handle = start_serve(handler); + let port = handle.port(); // "hello world" as two chunks: "hello " (0x6) then "world" (0x5). let chunked = b"POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n6\r\nhello \r\n5\r\nworld\r\n0\r\n\r\n"; - let response = raw_request(18153, chunked); + let response = raw_request(port, chunked); assert!(response.contains("got:hello world"), "got: {response}"); // Transfer-Encoding together with Content-Length is a smuggling vector: the message is framed @@ -380,7 +326,7 @@ fn serve_decodes_a_chunked_request_body() { // Content-Length claims. `serve_rejects_ambiguous_body_framing` covers what that leaves on the // connection. let smuggle = b"POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\nConnection: close\r\n\r\n0\r\n\r\n"; - let response = raw_request(18153, smuggle); + let response = raw_request(port, smuggle); assert!(response.contains("got:"), "got: {response}"); assert!(!response.contains("got:hello"), "got: {response}"); @@ -405,11 +351,12 @@ fn client_disconnect_stops_a_streamed_body_pump() { } }))); })"#; - let handle = start_serve(handler, 18145); + let handle = start_serve(handler); + let port = handle.port(); // Open the endless stream, read a little, and drop the connection. { - let mut stream = TcpStream::connect(("127.0.0.1", 18145)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); stream .write_all(b"GET /stream HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") .unwrap(); @@ -426,11 +373,21 @@ fn client_disconnect_stops_a_streamed_body_pump() { } // The pump must come to rest: the pull counter settles instead of growing - // while nobody consumes the body. - std::thread::sleep(Duration::from_millis(400)); - let first: u64 = request(18145, "GET", "/pulls", "").parse().unwrap(); - std::thread::sleep(Duration::from_millis(400)); - let second: u64 = request(18145, "GET", "/pulls", "").parse().unwrap(); + // while nobody consumes the body. Wait for two equal readings in a row, then check that the + // count stays put. + let pulls = || -> u64 { request(port, "GET", "/pulls", "").parse().unwrap() }; + let deadline = std::time::Instant::now() + common::SETTLE_PATIENCE; + let mut first = pulls(); + loop { + std::thread::sleep(Duration::from_millis(50)); + let next = pulls(); + if next == first || std::time::Instant::now() >= deadline { + break; + } + first = next; + } + std::thread::sleep(Duration::from_millis(250)); + let second = pulls(); assert_eq!( first, second, "the body pump must stop after the client disconnects" @@ -439,15 +396,15 @@ fn client_disconnect_stops_a_streamed_body_pump() { handle.stop(); } -/// Losing the client aborts the request's signal, and nothing more. The abort tells a handler that -/// the fetch it was serving is over, so it can drop work it was doing *for that client* — an -/// upstream it passed the signal to, say. It must not touch the event's lifetime work: -/// `waitUntil` exists for exactly the things that outlive the response, and telemetry a client -/// never waited for is still telemetry that has to be sent. +/// Losing the client aborts the request's signal with an `AbortError` (`Create Fetch Event and +/// Dispatch` step 17.4.20, whose "terminated" here is the response write failing), and nothing +/// more. The abort tells a handler that the fetch it was serving is over, so it can drop work it +/// was doing for that client, such as an upstream fetch it passed the signal to. It must not touch +/// the event's lifetime work: `waitUntil` exists for exactly the things that outlive the response, +/// and telemetry a client never waited for is still telemetry that has to be sent. /// -/// The lifetime promise here deliberately ignores the signal, which is what separates this from -/// the abort test further down: that one watches the signal *from* a `waitUntil`, so it would -/// still pass if the only work that survived a disconnect were work waiting on the abort itself. +/// The lifetime promise here deliberately ignores the signal, so the test would fail if the only +/// work that survived a disconnect were work waiting on the abort itself. #[test] fn a_client_disconnect_does_not_cancel_wait_until_work() { let handler = r#"globalThis.state = {}; @@ -457,7 +414,7 @@ fn a_client_disconnect_does_not_cancel_wait_until_work() { return; } event.request.signal.addEventListener('abort', () => { - globalThis.state.aborted = true; + globalThis.state.aborted = event.request.signal.reason.name; }); // Stands in for telemetry: nothing to do with the client, and deliberately not wired to // the request's signal. @@ -475,12 +432,13 @@ fn a_client_disconnect_does_not_cancel_wait_until_work() { } }))); })"#; - let handle = start_serve(handler, 18215); + let handle = start_serve(handler); + let port = handle.port(); // Take the head and the first of the body, then vanish: the next write fails and the request's // signal is aborted. { - let mut stream = TcpStream::connect(("127.0.0.1", 18215)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); stream .write_all(b"GET /stream HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n") .unwrap(); @@ -490,11 +448,11 @@ fn a_client_disconnect_does_not_cancel_wait_until_work() { } // The 300ms lifetime promise settles after the disconnect, and the abort lands before it. - let state = eventually(18215, "/check", |state| { + let state = eventually(port, "/check", |state| { state.contains("\"telemetry\":\"sent\"") }); assert!( - state.contains("\"aborted\":true"), + state.contains("\"aborted\":\"AbortError\""), "losing the client must abort the request's signal: {state}" ); assert!( @@ -522,10 +480,11 @@ fn wait_until_does_not_delay_the_response() { resolve(); }, 800))); })"#; - let handle = start_serve(handler, 18147); + let handle = start_serve(handler); + let port = handle.port(); let started = std::time::Instant::now(); - assert_eq!(request(18147, "GET", "/", ""), "hi"); + assert_eq!(request(port, "GET", "/", ""), "hi"); let elapsed = started.elapsed(); assert!( elapsed < Duration::from_millis(700), @@ -533,7 +492,7 @@ fn wait_until_does_not_delay_the_response() { ); // The waitUntil work still runs to completion after the response. - assert_eq!(eventually(18147, "/check", |seen| seen == "true"), "true"); + assert_eq!(eventually(port, "/check", |seen| seen == "true"), "true"); handle.stop(); } @@ -558,9 +517,10 @@ fn serve_streams_an_incoming_request_body() { } }))); })"#; - let handle = start_serve(handler, 18155); + let handle = start_serve(handler); + let port = handle.port(); - let mut stream = TcpStream::connect(("127.0.0.1", 18155)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); stream .set_read_timeout(Some(Duration::from_secs(10))) .unwrap(); @@ -611,9 +571,10 @@ fn serve_rejects_a_truncated_request_body() { () => new Response('truncated'), )); })"#; - let handle = start_serve(handler, 18157); + let handle = start_serve(handler); + let port = handle.port(); - let mut stream = TcpStream::connect(("127.0.0.1", 18157)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); // Declare ten bytes, send four, then close the sending side. stream .write_all( @@ -640,7 +601,8 @@ fn serve_rejects_an_over_budget_trailer_section() { () => new Response('rejected'), )); })"#; - let handle = start_serve(handler, 18291); + let handle = start_serve(handler); + let port = handle.port(); let mut request = Vec::from( &b"POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n\ @@ -650,12 +612,12 @@ fn serve_rejects_an_over_budget_trailer_section() { request.extend_from_slice(format!("X-Trailer-{i}: v\r\n").as_bytes()); } request.extend_from_slice(b"\r\n"); - let response = raw_request(18291, &request); + let response = raw_request(port, &request); assert!(response.contains("rejected"), "got: {response}"); // A trailer section within the budget still ends the body cleanly. let response = raw_request( - 18291, + port, b"POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n\ 5\r\nhello\r\n0\r\nX-Trailer: v\r\n\r\n", ); @@ -678,10 +640,11 @@ fn a_response_edited_after_it_was_answered_with_goes_out_as_it_was() { }); e.respondWith(response); })"#; - let handle = start_serve(handler, 18295); + let handle = start_serve(handler); + let port = handle.port(); let response = raw_request( - 18295, + port, b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", ); let head = response.to_lowercase(); @@ -697,17 +660,18 @@ fn a_response_edited_after_it_was_answered_with_goes_out_as_it_was() { #[test] fn serve_rejects_a_second_host_header() { let handler = r#"addEventListener('fetch', (e) => e.respondWith(new Response(new URL(e.request.url).host)))"#; - let handle = start_serve(handler, 18293); + let handle = start_serve(handler); + let port = handle.port(); let response = raw_request( - 18293, + port, b"GET / HTTP/1.1\r\nHost: first.example\r\nHost: second.example\r\nConnection: close\r\n\r\n", ); assert!(response.starts_with("HTTP/1.1 400"), "got: {response}"); // One Host still routes as before. let response = raw_request( - 18293, + port, b"GET / HTTP/1.1\r\nHost: only.example\r\nConnection: close\r\n\r\n", ); assert!(response.contains("only.example"), "got: {response}"); @@ -723,9 +687,10 @@ fn serve_pipes_an_incoming_request_body_to_the_response() { let handler = r#"addEventListener('fetch', (event) => { event.respondWith(new Response(event.request.body)); })"#; - let handle = start_serve(handler, 18159); + let handle = start_serve(handler); + let port = handle.port(); - let mut stream = TcpStream::connect(("127.0.0.1", 18159)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); stream .write_all(b"POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 11\r\nConnection: close\r\n\r\nhello world") .unwrap(); @@ -753,17 +718,18 @@ fn serve_caps_a_chunked_request_body_mid_stream() { )); })"#; const CAP: usize = 64 * 1024; - let handle = serve_with_limits(handler, 18161, "--max-request-body-bytes 64KiB"); + let handle = serve_with_limits(handler, "--max-request-body-bytes 64KiB"); + let port = handle.port(); // Whole, well-formed chunked bodies, terminating chunk included, so the upload never stalls // and only the cap can reject one. A stalled upload rejects the handler's read too, and would // pass an assertion on the rejection alone. - let over = upload_chunked(18161, CAP + 1); + let over = upload_chunked(port, CAP + 1); assert!(over.contains("too large"), "got: {over}"); // One byte less is delivered whole, so the cap rejected the body above rather than its // framing. - let under = upload_chunked(18161, CAP); + let under = upload_chunked(port, CAP); assert!(under.contains(&format!("complete:{CAP}")), "got: {under}"); handle.stop(); @@ -800,9 +766,10 @@ fn serve_caps_a_chunked_framing_line() { () => new Response('bad framing'), )); })"#; - let handle = start_serve(handler, 18165); + let handle = start_serve(handler); + let port = handle.port(); - let mut stream = TcpStream::connect(("127.0.0.1", 18165)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); stream .write_all(b"POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n") .unwrap(); @@ -827,10 +794,11 @@ fn serve_clones_an_incoming_request_body() { event.respondWith(Promise.all([event.request.text(), copy.text()]) .then(([a, b]) => new Response(a + '|' + b))); })"#; - let handle = start_serve(handler, 18163); + let handle = start_serve(handler); + let port = handle.port(); assert_eq!( - request(18163, "POST", "/", "payload"), + request(port, "POST", "/", "payload"), "payload|payload", "both halves of the tee must see the body" ); @@ -843,37 +811,27 @@ fn serve_clones_an_incoming_request_body() { /// them waits, not just the first. #[test] fn an_isolated_request_waits_for_the_scripts_top_level_await() { - let dir = std::env::temp_dir().join(format!( - "starling-serve-isolated-tla-{}", - std::process::id() - )); - std::fs::create_dir_all(&dir).unwrap(); - std::fs::write( - dir.join("entry.mjs"), + let dir = ScriptDir::new(&[( + "entry.mjs", r#"await new Promise((resolve) => setTimeout(resolve, 50)); addEventListener('fetch', (event) => { const url = new URL(event.request.url); event.respondWith(new Response('async-handler' + url.pathname)); }); "#, - ) - .unwrap(); + )]); - let handle = start_serve_config( - RuntimeConfig { - script_path: dir.join("entry.mjs").to_string_lossy().into_owned(), - serve: Some(18167), - serve_isolated: true, - ..Default::default() - }, - 18167, - ); + let handle = start_serve_config(RuntimeConfig { + script_path: dir.path("entry.mjs"), + serve_isolated: true, + ..Default::default() + }); + let port = handle.port(); - assert_eq!(request(18167, "GET", "/a", ""), "async-handler/a"); - assert_eq!(request(18167, "GET", "/b", ""), "async-handler/b"); + assert_eq!(request(port, "GET", "/a", ""), "async-handler/a"); + assert_eq!(request(port, "GET", "/b", ""), "async-handler/b"); handle.stop(); - let _ = std::fs::remove_dir_all(&dir); } /// `FetchEvent.handled` is one promise for the event's lifetime — a WebIDL promise attribute @@ -903,17 +861,18 @@ fn fetch_event_handled_reports_the_outcome() { } // /silent falls through without calling respondWith at all. })"#; - let handle = start_serve(handler, 18181); + let handle = start_serve(handler); + let port = handle.port(); // `handled` settles as the request's outcome becomes final, and its reactions run while the // loop is drained afterwards, so ask until they have recorded something. - let recorded = || eventually(18181, "/check", |state| state != "never settled"); + let recorded = || eventually(port, "/check", |state| state != "never settled"); let settled = |path: &str| { - request(18181, "GET", path, ""); + request(port, "GET", path, ""); recorded() }; - assert_eq!(request(18181, "GET", "/responds", ""), "ok"); + assert_eq!(request(port, "GET", "/responds", ""), "ok"); assert_eq!(recorded(), "resolved,same=true"); // A respondWith whose promise rejects is a network error, and `handled` reports it as one. @@ -962,12 +921,13 @@ fn wait_until_throws_once_the_event_is_over() { } }))); })"#; - let handle = start_serve(handler, 18183); + let handle = start_serve(handler); + let port = handle.port(); - assert_eq!(dechunk(&request(18183, "GET", "/", "")), "during=allowed"); + assert_eq!(dechunk(&request(port, "GET", "/", "")), "during=allowed"); // The timer runs while the request's loop is drained after its response. assert_eq!( - eventually(18183, "/check", |late| late != "never ran"), + eventually(port, "/check", |late| late != "never ran"), "InvalidStateError" ); @@ -1001,11 +961,12 @@ fn wait_until_is_allowed_while_an_earlier_one_is_pending() { releaseFirst(); }, 20); })"#; - let handle = start_serve(handler, 18185); + let handle = start_serve(handler); + let port = handle.port(); - assert_eq!(request(18185, "GET", "/", ""), "ok"); + assert_eq!(request(port, "GET", "/", ""), "ok"); assert_eq!( - eventually(18185, "/check", |second| second != "never ran"), + eventually(port, "/check", |second| second != "never ran"), "allowed" ); @@ -1018,10 +979,8 @@ fn wait_until_is_allowed_while_an_earlier_one_is_pending() { /// the rest of the module still has to run once it does settle. #[test] fn a_top_level_await_settled_by_a_request_resumes_the_script() { - let dir = std::env::temp_dir().join(format!("starling-serve-tla-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - std::fs::write( - dir.join("entry.mjs"), + let dir = ScriptDir::new(&[( + "entry.mjs", r#"let release; const gate = new Promise((resolve) => { release = resolve; }); addEventListener('fetch', (event) => { @@ -1035,28 +994,23 @@ fn a_top_level_await_settled_by_a_request_resumes_the_script() { await gate; globalThis.resumed = true; "#, - ) - .unwrap(); + )]); - let handle = start_serve_config( - RuntimeConfig { - script_path: dir.join("entry.mjs").to_string_lossy().into_owned(), - serve: Some(18207), - ..Default::default() - }, - 18207, - ); + let handle = start_serve_config(RuntimeConfig { + script_path: dir.path("entry.mjs"), + ..Default::default() + }); + let port = handle.port(); // The server accepts even though the module's evaluation is still suspended. - assert_eq!(request(18207, "GET", "/", ""), "released"); + assert_eq!(request(port, "GET", "/", ""), "released"); // And once the request settles it, the script picks up where it left off. assert_eq!( - eventually(18207, "/check", |state| state == "resumed"), + eventually(port, "/check", |state| state == "resumed"), "resumed" ); handle.stop(); - let _ = std::fs::remove_dir_all(&dir); } /// What the wire's headers look like from JS: a field that arrived in mixed case is found by any @@ -1086,10 +1040,11 @@ fn serve_exposes_incoming_headers_and_keeps_them_immutable() { response.headers.append('another', 'B'); event.respondWith(response); })"#; - let handle = start_serve(handler, 18203); + let handle = start_serve(handler); + let port = handle.port(); let response = raw_request( - 18203, + port, b"GET / HTTP/1.1\r\nHost: x\r\neXample-hEader: Header Value\r\nUser-Agent: test-agent\r\n\ Connection: close\r\n\r\n", ); @@ -1137,16 +1092,17 @@ fn a_throwing_handler_is_answered_with_500() { event.respondWith(new Response('recovered')); } })"#; - let handle = start_serve(handler, 18205); + let handle = start_serve(handler); + let port = handle.port(); let response = raw_request( - 18205, + port, b"GET /boom HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", ); assert!(response.starts_with("HTTP/1.1 500"), "got: {response}"); // The throw does not take the dispatch down: a listener after it still gets the event. - assert_eq!(request(18205, "GET", "/ok", ""), "recovered"); + assert_eq!(request(port, "GET", "/ok", ""), "recovered"); handle.stop(); } @@ -1168,12 +1124,13 @@ fn an_abandoned_response_body_does_not_hang_the_connection() { start(_c) { /* never enqueues, never closes */ } }))); })"#; - let handle = start_serve(handler, 18193); + let handle = start_serve(handler); + let port = handle.port(); // The connection has to close on its own; `request` reads to EOF, so it would hang otherwise. let started = std::time::Instant::now(); let response = raw_request( - 18193, + port, b"GET /abandoned HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", ); let elapsed = started.elapsed(); @@ -1188,7 +1145,7 @@ fn an_abandoned_response_body_does_not_hang_the_connection() { ); // And the slot is back: the server still serves. - assert_eq!(request(18193, "GET", "/ok", ""), "still serving"); + assert_eq!(request(port, "GET", "/ok", ""), "still serving"); handle.stop(); } @@ -1205,13 +1162,14 @@ fn an_abandoned_response_body_does_not_wedge_an_isolated_server() { } event.respondWith(new Response(new ReadableStream({ start(_c) {} }))); })"#; - let handle = start_serve_with(handler, 18195, true); + let handle = start_serve_with(handler, true); + let port = handle.port(); raw_request( - 18195, + port, b"GET /abandoned HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", ); - assert_eq!(request(18195, "GET", "/ok", ""), "still serving"); + assert_eq!(request(port, "GET", "/ok", ""), "still serving"); handle.stop(); } @@ -1232,9 +1190,10 @@ fn a_streamed_response_sends_its_head_before_the_first_chunk() { }); event.respondWith(new Response(stream, { headers: { 'x-marker': 'early' } })); })"#; - let handle = start_serve(handler, 18187); + let handle = start_serve(handler); + let port = handle.port(); - let mut stream = TcpStream::connect(("127.0.0.1", 18187)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); stream .write_all(b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n") .unwrap(); @@ -1256,26 +1215,10 @@ fn a_streamed_response_sends_its_head_before_the_first_chunk() { handle.stop(); } -/// A script that leaves a repeating timer behind — a background refresh, the ordinary reason to -/// call `setInterval` at top level — still has to reach the accept loop. Waiting for its startup -/// loop to *complete* never returns, so the server would bind and then serve nothing. -#[test] -fn a_repeating_timer_does_not_prevent_serving() { - let handler = r#"setInterval(() => { globalThis.ticks = (globalThis.ticks ?? 0) + 1; }, 20); - addEventListener('fetch', (event) => { - event.respondWith(new Response('served')); - });"#; - // `start_serve` fails the test outright if the server never accepts. - let handle = start_serve(handler, 18171); - - assert_eq!(request(18171, "GET", "/", ""), "served"); - - handle.stop(); -} - -/// A `setInterval` in a shared-global script is background work the script expects to keep running -/// — a cache refresh, a metrics flush — so the server has to keep driving its loop between -/// requests, not just get past it at startup. +/// A `setInterval` in a shared-global script is background work that has to keep running, such as a +/// cache refresh or a metrics flush. The server has to reach the accept loop despite it, and keep +/// driving the script's loop between requests. Waiting for the startup loop to complete never +/// returns, so the server would bind and then serve nothing. #[test] fn a_repeating_timer_keeps_running_between_requests() { let handler = r#"globalThis.ticks = 0; @@ -1283,20 +1226,15 @@ fn a_repeating_timer_keeps_running_between_requests() { addEventListener('fetch', (event) => { event.respondWith(new Response('ticks=' + globalThis.ticks)); });"#; - let handle = start_serve(handler, 18189); + let handle = start_serve(handler); + let port = handle.port(); // Nothing has driven the interval yet beyond startup. - let first: u32 = request(18189, "GET", "/", "") - .strip_prefix("ticks=") - .and_then(|n| n.parse().ok()) - .expect("a tick count"); + let first = ticks_at(port, "/"); // Idle time with no requests at all: the interval has to fire on its own. std::thread::sleep(Duration::from_millis(500)); - let second: u32 = request(18189, "GET", "/", "") - .strip_prefix("ticks=") - .and_then(|n| n.parse().ok()) - .expect("a tick count"); + let second = ticks_at(port, "/"); assert!( second > first, @@ -1306,6 +1244,35 @@ fn a_repeating_timer_keeps_running_between_requests() { handle.stop(); } +/// The number after `ticks=` in the response to a request for `path` on `port`. +fn ticks_at(port: u16, path: &str) -> u32 { + request(port, "GET", path, "") + .strip_prefix("ticks=") + .and_then(|n| n.parse().ok()) + .expect("a tick count") +} + +/// A request handler that clears a `setInterval` the script's top level started stops it, although +/// the interval runs on the script's event loop rather than the request's. +#[test] +fn a_request_clears_a_top_level_interval() { + let handler = r#"globalThis.ticks = 0; + const interval = setInterval(() => { globalThis.ticks += 1; }, 20); + addEventListener('fetch', (event) => { + if (event.request.url.endsWith('/clear')) clearInterval(interval); + event.respondWith(new Response('ticks=' + globalThis.ticks)); + });"#; + let handle = start_serve(handler); + let port = handle.port(); + + let cleared = ticks_at(port, "/clear"); + std::thread::sleep(Duration::from_millis(300)); + let later = ticks_at(port, "/"); + assert_eq!(cleared, later, "the cleared interval kept firing"); + + handle.stop(); +} + /// A zero-delay `setInterval` is always ready to run again, so driving the script's loop to /// completion would pin the thread and the server would never accept anything. It has to keep /// serving regardless. @@ -1316,11 +1283,12 @@ fn a_zero_delay_repeating_timer_does_not_starve_the_server() { addEventListener('fetch', (event) => { event.respondWith(new Response('ticks=' + globalThis.ticks)); });"#; - let handle = start_serve(handler, 18191); + let handle = start_serve(handler); + let port = handle.port(); // Three requests in a row: each has to be served while the timer keeps re-arming. for _ in 0..3 { - let body = request(18191, "GET", "/", ""); + let body = request(port, "GET", "/", ""); assert!(body.starts_with("ticks="), "got: {body}"); } @@ -1335,10 +1303,11 @@ fn a_repeating_timer_does_not_prevent_serving_isolated_requests() { addEventListener('fetch', (event) => { event.respondWith(new Response('served')); });"#; - let handle = start_serve_with(handler, 18173, true); + let handle = start_serve_with(handler, true); + let port = handle.port(); - assert_eq!(request(18173, "GET", "/", ""), "served"); - assert_eq!(request(18173, "GET", "/", ""), "served"); + assert_eq!(request(port, "GET", "/", ""), "served"); + assert_eq!(request(port, "GET", "/", ""), "served"); handle.stop(); } @@ -1360,10 +1329,11 @@ fn wait_until_does_not_delay_a_streamed_response() { event.respondWith(new Response(stream)); event.waitUntil(new Promise((resolve) => setTimeout(resolve, 1500))); })"#; - let handle = start_serve(handler, 18175); + let handle = start_serve(handler); + let port = handle.port(); let started = std::time::Instant::now(); - let body = request(18175, "GET", "/", ""); + let body = request(port, "GET", "/", ""); let elapsed = started.elapsed(); assert_eq!(dechunk(&body), "streamed body", "got: {body}"); assert!( @@ -1381,37 +1351,32 @@ fn wait_until_does_not_delay_a_streamed_response() { /// 500), and the state they hold outlives the request that made it. #[test] fn isolated_requests_re_evaluate_imported_modules() { - let dir = std::env::temp_dir().join(format!("starling-serve-modules-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - std::fs::write(dir.join("entry.mjs"), "import './handler.mjs';\n").unwrap(); - std::fs::write( - dir.join("handler.mjs"), - r#"let count = 0; + let dir = ScriptDir::new(&[ + ("entry.mjs", "import './handler.mjs';\n"), + ( + "handler.mjs", + r#"let count = 0; addEventListener('fetch', (event) => { count += 1; event.respondWith(new Response('count=' + count)); }); "#, - ) - .unwrap(); + ), + ]); - let handle = start_serve_config( - RuntimeConfig { - script_path: dir.join("entry.mjs").to_string_lossy().into_owned(), - serve: Some(18177), - serve_isolated: true, - ..Default::default() - }, - 18177, - ); + let handle = start_serve_config(RuntimeConfig { + script_path: dir.path("entry.mjs"), + serve_isolated: true, + ..Default::default() + }); + let port = handle.port(); // `count=1` twice: the handler is registered afresh for the second request (a cached module // would leave it unregistered), and the module's own state starts over with it. - assert_eq!(request(18177, "GET", "/", ""), "count=1"); - assert_eq!(request(18177, "GET", "/", ""), "count=1"); + assert_eq!(request(port, "GET", "/", ""), "count=1"); + assert_eq!(request(port, "GET", "/", ""), "count=1"); handle.stop(); - let _ = std::fs::remove_dir_all(&dir); } /// Each isolated request gets a global of its own: what one request leaves on `globalThis` is not @@ -1424,128 +1389,38 @@ fn isolated_requests_do_not_share_global_state() { globalThis.leaked = 'from-a-previous-request'; event.respondWith(new Response(perRequest + ',' + seen)); });"#; - let handle = start_serve_with(handler, 18169, true); + let handle = start_serve_with(handler, true); + let port = handle.port(); // A shared global would serve `fresh,from-a-previous-request` the second time, and would not // reach the second request at all, since re-evaluating `const perRequest` would throw. - assert_eq!(request(18169, "GET", "/", ""), "fresh,none"); - assert_eq!(request(18169, "GET", "/", ""), "fresh,none"); - - handle.stop(); -} - -/// The request's `AbortSignal` fires when the connection is lost mid-response: `Create Fetch Event -/// and Dispatch` step 17.4.20, whose "terminated" here is the response write failing. -/// -/// The handler streams forever, so the client can hang up while the write is still going; a second -/// request reads back what the first one's `waitUntil` observed. -#[test] -fn a_lost_connection_aborts_the_request_signal() { - let handler = r#"let observed = 'no-request-yet'; - addEventListener('fetch', (event) => { - const path = new URL(event.request.url).pathname; - if (path === '/observed') { - event.respondWith(new Response(observed)); - return; - } - observed = 'not-aborted'; - // Poll rather than listen, and give up well inside the lifetime timeout, so a signal that - // never fires fails the assertion instead of hanging the test for two minutes. - event.waitUntil(new Promise((resolve) => { - let tries = 0; - const check = () => { - if (event.request.signal.aborted) { - observed = 'aborted:' + event.request.signal.reason.name; - resolve(); - } else if (++tries > 300) { - resolve(); - } else { - setTimeout(check, 10); - } - }; - setTimeout(check, 10); - })); - // A body with no end: whatever the client reads, there is always another write to fail. - const chunk = new Uint8Array(64 * 1024); - event.respondWith(new Response(new ReadableStream({ - pull(controller) { controller.enqueue(chunk); }, - }))); - });"#; - let handle = start_serve(handler, 18211); - - { - let mut stream = TcpStream::connect(("127.0.0.1", 18211)).unwrap(); - stream - .write_all(b"GET /stream HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") - .unwrap(); - // Read enough to know the response is flowing, then hang up on it. - let mut buf = [0u8; 1024]; - assert!( - stream.read(&mut buf).unwrap() > 0, - "the response should start flowing" - ); - } - - // The abort lands while the first request's lifetime work is still being drained, so give that - // a moment before asking. - std::thread::sleep(Duration::from_millis(500)); - assert_eq!(request(18211, "GET", "/observed", ""), "aborted:AbortError"); + assert_eq!(request(port, "GET", "/", ""), "fresh,none"); + assert_eq!(request(port, "GET", "/", ""), "fresh,none"); handle.stop(); } -/// Startup waits for the content script to finish evaluating, and for nothing else. A timer the -/// script leaves behind is not part of that, however long it runs for — waiting on one would keep -/// the server from responding for exactly as long as the script felt like sleeping. -#[test] -fn a_timer_left_at_top_level_does_not_delay_serving() { - let handler = r#"setTimeout(() => {}, 60000); - addEventListener('fetch', (event) => event.respondWith(new Response('ready')));"#; - let handle = start_serve(handler, 18241); - - let started = std::time::Instant::now(); - assert_eq!( - request_within(18241, "/", Duration::from_secs(10)).as_deref(), - Some("ready"), - "the server should serve without waiting the timer out" - ); - assert!( - started.elapsed() < Duration::from_secs(5), - "took {:?}", - started.elapsed() - ); - - handle.stop(); -} - -/// The other half: the script's *own* completion is waited for, including a top-level `await` that -/// is itself waiting on a timer. Nothing is registered until that await resolves, so a server that -/// dispatched before it would response the first request with a 500. +/// Startup waits for the script's own completion, including a top-level `await` that is itself +/// waiting on a timer. Nothing is registered until that await resolves, so a server that +/// dispatched before it would answer the first request with a 500. #[test] fn startup_waits_for_a_top_level_await_on_a_timer() { - let dir = std::env::temp_dir().join(format!("starling-serve-await-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - std::fs::write( - dir.join("entry.mjs"), + let dir = ScriptDir::new(&[( + "entry.mjs", r#"await new Promise((resolve) => setTimeout(resolve, 400)); addEventListener('fetch', (event) => event.respondWith(new Response('after-await'))); "#, - ) - .unwrap(); + )]); - let handle = start_serve_config( - RuntimeConfig { - script_path: dir.join("entry.mjs").to_string_lossy().into_owned(), - serve: Some(18243), - ..Default::default() - }, - 18243, - ); + let handle = start_serve_config(RuntimeConfig { + script_path: dir.path("entry.mjs"), + ..Default::default() + }); + let port = handle.port(); - assert_eq!(request(18243, "GET", "/", ""), "after-await"); + assert_eq!(request(port, "GET", "/", ""), "after-await"); handle.stop(); - let _ = std::fs::remove_dir_all(&dir); } /// Only `waitUntil` extends a fetch event's lifetime. A timer the handler leaves behind is not @@ -1559,15 +1434,16 @@ fn a_bare_timer_does_not_extend_the_event_lifetime() { event.respondWith(new Response(globalThis.ran ?? 'never ran')); return; } - setTimeout(() => { globalThis.ran = 'ran'; }, 300); + setTimeout(() => { globalThis.ran = 'ran'; }, 100); event.respondWith(new Response('answered')); })"#; - let handle = start_serve(handler, 18237); + let handle = start_serve(handler); + let port = handle.port(); - assert_eq!(request(18237, "GET", "/", ""), "answered"); + assert_eq!(request(port, "GET", "/", ""), "answered"); // Well past the timer's delay: it went with the request's loop rather than firing. - std::thread::sleep(Duration::from_millis(900)); - assert_eq!(request(18237, "GET", "/check", ""), "never ran"); + std::thread::sleep(Duration::from_millis(400)); + assert_eq!(request(port, "GET", "/check", ""), "never ran"); handle.stop(); } @@ -1584,13 +1460,14 @@ fn an_unawaited_fetch_does_not_extend_the_event_lifetime() { event.respondWith(new Response('answered')); }})"# ); - let handle = start_serve_with(&handler, 18239, true); + let handle = start_serve_with(&handler, true); + let port = handle.port(); - assert_eq!(request(18239, "GET", "/", ""), "answered"); + assert_eq!(request(port, "GET", "/", ""), "answered"); // The upstream never responds, so a request that waited for it would hold isolated mode's // single slot and leave this one queued behind it for good. assert_eq!( - request_within(18239, "/", Duration::from_secs(5)).as_deref(), + request_within(port, "/", Duration::from_secs(5)).as_deref(), Some("answered"), "the second request should not queue behind the first's abandoned fetch" ); @@ -1616,9 +1493,10 @@ fn an_interval_driven_stream_is_served_to_completion() { }, }))); })"#; - let handle = start_serve(handler, 18229); + let handle = start_serve(handler); + let port = handle.port(); - let body = request(18229, "GET", "/", ""); + let body = request(port, "GET", "/", ""); assert_eq!(dechunk(&body), "tick 1 tick 2 tick 3 tick 4 tick 5 "); handle.stop(); @@ -1627,27 +1505,43 @@ fn an_interval_driven_stream_is_served_to_completion() { /// `--response-body-timeout` truncates a body the handler never finishes: the connection closes /// *without* the last-chunk terminator, so the client sees the truncation (RFC 9112 §7.1) instead /// of waiting forever on a response that looks like it is still coming. +/// +/// Truncating the body ends the request's fetch (step 17.4.20 again), which reaches the handler +/// through `request.signal` as a `TimeoutError`. The phases are independent windows: `waitUntil` +/// work gets its own full `--waituntil-timeout`, counted from the response being over, not +/// whatever the body left of it. Here the lifetime work runs for longer than the body's window and +/// still finishes. #[test] fn a_response_body_timeout_truncates_a_body_that_never_ends() { - let handler = r#"addEventListener('fetch', (event) => { + let handler = r#"let aborted = 'not-aborted'; + let lifetime = 'lifetime-work-did-not-finish'; + addEventListener('fetch', (event) => { + const path = new URL(event.request.url).pathname; + if (path === '/observed') { + event.respondWith(new Response(aborted + ' ' + lifetime)); + return; + } + const signal = event.request.signal; + signal.addEventListener('abort', () => { aborted = 'aborted:' + signal.reason.name; }); + event.waitUntil(new Promise((resolve) => setTimeout(() => { + lifetime = 'lifetime-work-finished'; + resolve(); + }, 600))); event.respondWith(new Response(new ReadableStream({ start(c) { setInterval(() => c.enqueue(new TextEncoder().encode('tick ')), 50); }, }))); - })"#; - let handle = start_serve_config( - RuntimeConfig { - eval_script: Some(handler.to_string()), - legacy_script: true, - serve: Some(18217), - response_body_timeout: Some(Duration::from_secs(1)), - ..Default::default() - }, - 18217, - ); - - let mut stream = TcpStream::connect(("127.0.0.1", 18217)).unwrap(); + });"#; + let handle = start_serve_config(RuntimeConfig { + response_body_timeout: Some(Duration::from_millis(300)), + // The bound only has to clear the lifetime work, with room for a loaded machine. + waituntil_timeout: Some(Duration::from_secs(20)), + ..legacy_script(handler) + }); + let port = handle.port(); + + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); stream - .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") + .write_all(b"GET /stream HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") .unwrap(); let response = read_until_eof(&mut stream, Duration::from_secs(8)) .expect("the timeout should close the connection, not leave it streaming"); @@ -1661,119 +1555,11 @@ fn a_response_body_timeout_truncates_a_body_that_never_ends() { "a cut body must not carry the complete-body terminator, got: {response}" ); - handle.stop(); -} - -/// Truncating the body ends the request's fetch (step 17.4.20 again), which reaches the handler -/// through `request.signal`. Its `waitUntil` work still gets its own full window afterwards, -/// rather than what the truncated body left of one. -#[test] -fn a_response_body_timeout_aborts_the_request_signal() { - let handler = r#"let observed = 'no-request-yet'; - addEventListener('fetch', (event) => { - const path = new URL(event.request.url).pathname; - if (path === '/observed') { - event.respondWith(new Response(observed)); - return; - } - observed = 'not-aborted'; - event.waitUntil(new Promise((resolve) => { - let tries = 0; - const check = () => { - if (event.request.signal.aborted) { - observed = 'aborted:' + event.request.signal.reason.name; - resolve(); - } else if (++tries > 300) { - resolve(); - } else { - setTimeout(check, 10); - } - }; - setTimeout(check, 10); - })); - event.respondWith(new Response(new ReadableStream({ - start(c) { setInterval(() => c.enqueue(new TextEncoder().encode('tick ')), 50); }, - }))); - });"#; - let handle = start_serve_config( - RuntimeConfig { - eval_script: Some(handler.to_string()), - legacy_script: true, - serve: Some(18219), - response_body_timeout: Some(Duration::from_secs(1)), - ..Default::default() - }, - 18219, - ); - - { - let mut stream = TcpStream::connect(("127.0.0.1", 18219)).unwrap(); - stream - .write_all(b"GET /stream HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") - .unwrap(); - read_until_eof(&mut stream, Duration::from_secs(8)) - .expect("the timeout should close the connection"); - } - - // The signal fires while the first request's lifetime work is still being drained, so - // truncating the body left that window open for the `waitUntil` promise to observe it in. - assert_eq!( - eventually(18219, "/observed", |seen| seen.starts_with("aborted:")), - "aborted:TimeoutError" - ); - - handle.stop(); -} - -/// The phases are independent windows: `waitUntil` work gets its own full `--waituntil-timeout` -/// counted from the response being over, not whatever the body left of it. Here the body spends -/// its entire 1s window and is then truncated, and lifetime work that runs for 2s afterwards still -/// finishes. That is longer than the body's window and well inside its own. -#[test] -fn wait_until_gets_its_own_window_after_the_body_is_truncated() { - let handler = r#"let observed = 'no-request-yet'; - addEventListener('fetch', (event) => { - const path = new URL(event.request.url).pathname; - if (path === '/observed') { - event.respondWith(new Response(observed)); - return; - } - observed = 'lifetime-work-did-not-finish'; - event.waitUntil(new Promise((resolve) => setTimeout(() => { - observed = 'lifetime-work-finished'; - resolve(); - }, 2000))); - event.respondWith(new Response(new ReadableStream({ - start(c) { setInterval(() => c.enqueue(new TextEncoder().encode('tick ')), 50); }, - }))); - });"#; - let handle = start_serve_config( - RuntimeConfig { - eval_script: Some(handler.to_string()), - legacy_script: true, - serve: Some(18233), - response_body_timeout: Some(Duration::from_secs(1)), - // The bound only has to clear the 2s of lifetime work, with room for a loaded - // machine. We're testing that truncating the body left the window intact. - waituntil_timeout: Some(Duration::from_secs(20)), - ..Default::default() - }, - 18233, - ); - - { - let mut stream = TcpStream::connect(("127.0.0.1", 18233)).unwrap(); - stream - .write_all(b"GET /stream HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") - .unwrap(); - read_until_eof(&mut stream, Duration::from_secs(8)) - .expect("the body timeout should close the connection"); - } - - // The 2s of lifetime work runs inside its own window and finishes there. assert_eq!( - eventually(18233, "/observed", |seen| seen == "lifetime-work-finished"), - "lifetime-work-finished" + eventually(port, "/observed", |seen| { + seen.starts_with("aborted:") && seen.ends_with("-finished") + }), + "aborted:TimeoutError lifetime-work-finished" ); handle.stop(); @@ -1789,24 +1575,19 @@ fn a_response_body_timeout_truncates_a_response_the_client_will_not_read() { "addEventListener('fetch', (event) => \ event.respondWith(new Response(new Uint8Array({BODY_BYTES}))));" ); - let handle = start_serve_config( - RuntimeConfig { - eval_script: Some(handler), - legacy_script: true, - serve: Some(18221), - response_body_timeout: Some(Duration::from_secs(1)), - ..Default::default() - }, - 18221, - ); + let handle = start_serve_config(RuntimeConfig { + response_body_timeout: Some(Duration::from_millis(250)), + ..legacy_script(&handler) + }); + let port = handle.port(); - let mut stream = TcpStream::connect(("127.0.0.1", 18221)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); stream .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") .unwrap(); // Read nothing while the timeout runs out, so the socket buffers fill and the server's write // parks. Only then start reading: what was buffered arrives, and the connection then ends. - std::thread::sleep(Duration::from_secs(3)); + std::thread::sleep(Duration::from_millis(1000)); let response = read_until_eof(&mut stream, Duration::from_secs(8)) .expect("the timeout should close the connection under an unread response"); assert!( @@ -1828,18 +1609,13 @@ fn a_response_body_timeout_truncates_an_endless_proxied_body() { "addEventListener('fetch', (event) => \ event.respondWith(fetch('http://127.0.0.1:{upstream}/')));" ); - let handle = start_serve_config( - RuntimeConfig { - eval_script: Some(handler), - legacy_script: true, - serve: Some(18235), - response_body_timeout: Some(Duration::from_secs(1)), - ..Default::default() - }, - 18235, - ); + let handle = start_serve_config(RuntimeConfig { + response_body_timeout: Some(Duration::from_millis(300)), + ..legacy_script(&handler) + }); + let port = handle.port(); - let mut stream = TcpStream::connect(("127.0.0.1", 18235)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); stream .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") .unwrap(); @@ -1867,18 +1643,13 @@ fn an_end_to_end_timeout_truncates_a_streaming_body() { start(c) { setInterval(() => c.enqueue(new TextEncoder().encode('tick ')), 50); }, }))); })"#; - let handle = start_serve_config( - RuntimeConfig { - eval_script: Some(handler.to_string()), - legacy_script: true, - serve: Some(18223), - end_to_end_timeout: Some(Duration::from_secs(1)), - ..Default::default() - }, - 18223, - ); - - let mut stream = TcpStream::connect(("127.0.0.1", 18223)).unwrap(); + let handle = start_serve_config(RuntimeConfig { + end_to_end_timeout: Some(Duration::from_millis(300)), + ..legacy_script(handler) + }); + let port = handle.port(); + + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); stream .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") .unwrap(); @@ -1897,18 +1668,13 @@ fn an_end_to_end_timeout_truncates_a_streaming_body() { #[test] fn an_end_to_end_timeout_answers_a_never_settling_dispatch() { let handler = "addEventListener('fetch', (e) => e.respondWith(new Promise(() => {})));"; - let handle = start_serve_config( - RuntimeConfig { - eval_script: Some(handler.to_string()), - legacy_script: true, - serve: Some(18225), - end_to_end_timeout: Some(Duration::from_secs(1)), - ..Default::default() - }, - 18225, - ); - - let mut stream = TcpStream::connect(("127.0.0.1", 18225)).unwrap(); + let handle = start_serve_config(RuntimeConfig { + end_to_end_timeout: Some(Duration::from_millis(250)), + ..legacy_script(handler) + }); + let port = handle.port(); + + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); stream .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") .unwrap(); @@ -1938,24 +1704,19 @@ fn an_end_to_end_timeout_stops_leftover_lifetime_work() { event.waitUntil(new Promise(() => {})); event.respondWith(new Response('started')); });"#; - let handle = start_serve_config( - RuntimeConfig { - eval_script: Some(handler.to_string()), - legacy_script: true, - serve: Some(18227), - end_to_end_timeout: Some(Duration::from_secs(1)), - ..Default::default() - }, - 18227, - ); - - assert_eq!(request(18227, "GET", "/", ""), "started"); + let handle = start_serve_config(RuntimeConfig { + end_to_end_timeout: Some(Duration::from_millis(300)), + ..legacy_script(handler) + }); + let port = handle.port(); + + assert_eq!(request(port, "GET", "/", ""), "started"); // Well past the deadline, the interval must have stopped: two samples with a gap between // them read the same count. - std::thread::sleep(Duration::from_millis(2000)); - let first = request(18227, "GET", "/ticks", ""); - std::thread::sleep(Duration::from_millis(600)); - let second = request(18227, "GET", "/ticks", ""); + std::thread::sleep(Duration::from_millis(700)); + let first = request(port, "GET", "/ticks", ""); + std::thread::sleep(Duration::from_millis(300)); + let second = request(port, "GET", "/ticks", ""); // A frozen counter only means the deadline stopped the work if the work was running in the // first place: an interval that never ticked at all would read the same twice too. assert!( @@ -1972,30 +1733,27 @@ fn an_end_to_end_timeout_stops_leftover_lifetime_work() { /// In isolated mode the content script runs per request, so its startup is request work too: /// `--end-to-end-timeout` bounds a startup that dawdles, sending a 500 rather than making every -/// request wait it out. +/// request wait it out. The script awaits a timer before registering its listener, so the 500 +/// cannot come from a script that finished without one. #[test] fn an_end_to_end_timeout_bounds_isolated_startup() { - let handler = r#"setTimeout(() => { - addEventListener('fetch', (e) => e.respondWith(new Response('late'))); - }, 5000);"#; - let handle = start_serve_config( - RuntimeConfig { - eval_script: Some(handler.to_string()), - legacy_script: true, - serve: Some(18231), - serve_isolated: true, - end_to_end_timeout: Some(Duration::from_secs(1)), - ..Default::default() - }, - 18231, - ); + let handler = r#"await new Promise((resolve) => setTimeout(resolve, 5000)); + addEventListener('fetch', (e) => e.respondWith(new Response('late')));"#; + let deadline = Duration::from_millis(250); + let handle = start_serve_config(RuntimeConfig { + eval_script: Some(handler.to_string()), + serve_isolated: true, + end_to_end_timeout: Some(deadline), + ..Default::default() + }); + let port = handle.port(); let started = std::time::Instant::now(); - assert_eq!(request(18231, "GET", "/", ""), "Internal Server Error"); + assert_eq!(request(port, "GET", "/", ""), "Internal Server Error"); + let elapsed = started.elapsed(); assert!( - started.elapsed() < Duration::from_secs(4), - "the deadline should cut startup short, not wait for it (took {:?})", - started.elapsed() + elapsed >= deadline && elapsed < Duration::from_secs(4), + "the deadline should cut startup short, not wait for it (took {elapsed:?})" ); handle.stop(); @@ -2007,13 +1765,10 @@ fn an_end_to_end_timeout_bounds_isolated_startup() { fn a_script_with_no_fetch_listener_refuses_to_serve() { libstarling::register_builtins(); let config = RuntimeConfig { - eval_script: Some("globalThis.ready = true;".to_string()), - legacy_script: true, - serve: Some(18245), - ..Default::default() + ..legacy_script("globalThis.ready = true;") }; - let error = - libstarling::serve_native::serve_with_shutdown(config, 18245, async {}).unwrap_err(); + let error = libstarling::serve_native::serve_with_shutdown(config, bind_local().0, async {}) + .unwrap_err(); assert!( error.contains("fetch"), "the error should name what is missing, got: {error}" @@ -2032,11 +1787,10 @@ fn a_late_top_level_rejection_refuses_to_serve() { throw new Error('late configuration failure');" .to_string(), ), - serve: Some(18473), ..Default::default() }; - let error = - libstarling::serve_native::serve_with_shutdown(config, 18473, async {}).unwrap_err(); + let error = libstarling::serve_native::serve_with_shutdown(config, bind_local().0, async {}) + .unwrap_err(); assert!(error.contains("late configuration failure"), "got: {error}"); } @@ -2044,75 +1798,45 @@ fn a_late_top_level_rejection_refuses_to_serve() { /// listener fails the request whose evaluation it belongs to, which gets a 500. #[test] fn an_isolated_late_top_level_rejection_fails_the_request() { - let handle = start_serve_config( - RuntimeConfig { - eval_script: Some( - "addEventListener('fetch', (event) => event.respondWith(new Response('served')));\n\ + let handle = start_serve_config(RuntimeConfig { + eval_script: Some( + "addEventListener('fetch', (event) => event.respondWith(new Response('served')));\n\ await new Promise((resolve) => setTimeout(resolve, 10));\n\ throw new Error('late configuration failure');" - .to_string(), - ), - serve: Some(18475), - serve_isolated: true, - ..Default::default() - }, - 18475, - ); + .to_string(), + ), + serve_isolated: true, + ..Default::default() + }); + let port = handle.port(); - assert_eq!(request(18475, "GET", "/", ""), "Internal Server Error"); - assert_eq!(request(18475, "GET", "/", ""), "Internal Server Error"); + assert_eq!(request(port, "GET", "/", ""), "Internal Server Error"); + assert_eq!(request(port, "GET", "/", ""), "Internal Server Error"); handle.stop(); } -/// A phase timeout longer than `--end-to-end-timeout` is a contradiction the server refuses to -/// start with, rather than quietly serving with a window the deadline never lets a phase use. +/// The server refuses to start with a phase timeout longer than `--end-to-end-timeout`, rather +/// than quietly serving with a window that the deadline never lets a phase use. #[test] fn a_contradictory_timeout_config_refuses_to_serve() { libstarling::register_builtins(); let config = RuntimeConfig { - eval_script: Some("addEventListener('fetch', () => {});".to_string()), - legacy_script: true, - serve: Some(18341), end_to_end_timeout: Some(Duration::from_secs(5)), waituntil_timeout: Some(Duration::from_secs(6)), - ..Default::default() + ..legacy_script("addEventListener('fetch', () => {});") }; - let error = - libstarling::serve_native::serve_with_shutdown(config, 18341, async {}).unwrap_err(); + let error = libstarling::serve_native::serve_with_shutdown(config, bind_local().0, async {}) + .unwrap_err(); assert!( error.contains("--waituntil-timeout") && error.contains("--end-to-end-timeout"), "got: {error}" ); } -/// `--dispatch-timeout` bounds a `respondWith` that never settles. Without one — the default — the -/// request would wait for as long as the handler takes, which is why the timeout is opt-in. -#[test] -fn a_dispatch_timeout_answers_a_never_settling_respond_with() { - let handler = "addEventListener('fetch', (e) => e.respondWith(new Promise(() => {})));"; - let handle = start_serve_config( - RuntimeConfig { - eval_script: Some(handler.to_string()), - legacy_script: true, - serve: Some(18213), - dispatch_timeout: Some(Duration::from_secs(1)), - waituntil_timeout: Some(Duration::from_secs(1)), - ..Default::default() - }, - 18213, - ); - - let start = std::time::Instant::now(); - assert_eq!(request(18213, "GET", "/", ""), "Internal Server Error"); - assert!( - start.elapsed() < Duration::from_secs(30), - "the timeout should have answered the request, not the test harness" - ); - - handle.stop(); -} - +/// `--dispatch-timeout` bounds a `respondWith` that never settles, answering it with a 500. Without +/// one, which is the default, the request waits for as long as the handler takes. +/// /// Answering a timed-out request has to release its slot too, or one such request costs a slot for /// the whole `--waituntil-timeout` window — and with none configured, which is the default, costs /// it permanently. `--serve-isolated` gives the server a single slot, so the follow-up request only @@ -2123,21 +1847,16 @@ fn a_dispatch_timeout_frees_the_slot_it_held() { if (new URL(e.request.url).pathname === '/ok') { e.respondWith(new Response('ok')); return; } e.respondWith(new Promise(() => {})); });"#; - let handle = start_serve_config( - RuntimeConfig { - eval_script: Some(handler.to_string()), - legacy_script: true, - serve: Some(18289), - serve_isolated: true, - dispatch_timeout: Some(Duration::from_secs(1)), - ..Default::default() - }, - 18289, - ); - - assert_eq!(request(18289, "GET", "/hang", ""), "Internal Server Error"); + let handle = start_serve_config(RuntimeConfig { + serve_isolated: true, + dispatch_timeout: Some(Duration::from_millis(250)), + ..legacy_script(handler) + }); + let port = handle.port(); + + assert_eq!(request(port, "GET", "/hang", ""), "Internal Server Error"); assert_eq!( - request_within(18289, "/ok", Duration::from_secs(10)).as_deref(), + request_within(port, "/ok", Duration::from_secs(10)).as_deref(), Some("ok"), "the timed-out request never released the server's only in-flight slot" ); @@ -2145,32 +1864,6 @@ fn a_dispatch_timeout_frees_the_slot_it_held() { handle.stop(); } -/// `Response.redirect` on the way out: the status and the `Location` header both have to survive -/// serialization. WPT `resources/redirect-worker.js` (`sw=gen`) covers the same ground. -#[test] -fn serve_delivers_a_redirect_response() { - let handler = - "addEventListener('fetch', (e) => e.respondWith(Response.redirect('http://example.com/there', 302)));"; - let handle = start_serve(handler, 18247); - - let response = raw_request( - 18247, - b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", - ); - assert!( - response.starts_with("HTTP/1.1 302 "), - "expected a 302 status line, got: {response}" - ); - assert!( - response - .to_lowercase() - .contains("location: http://example.com/there"), - "the Location header must reach the wire: {response}" - ); - - handle.stop(); -} - /// Every body source a `Response` can be built from, and the `Content-Type` each implies — /// WPT `resources/fetch-event-respond-with-custom-response-worker.js`. `Blob` and `FormData` are /// not implemented here, so the list is what this runtime actually has. @@ -2192,7 +1885,8 @@ fn serve_delivers_every_body_source() { event.respondWith(make ? make() : new Response('no such source', { status: 404 })); }); "#; - let handle = start_serve(handler, 18249); + let handle = start_serve(handler); + let port = handle.port(); for (path, expected_body, expected_type) in [ ("/string", "plain text", "text/plain;charset=UTF-8"), @@ -2206,7 +1900,7 @@ fn serve_delivers_every_body_source() { ("/null", "", ""), ] { let response = raw_request( - 18249, + port, format!("GET {path} HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n").as_bytes(), ); let (head, body) = response @@ -2239,9 +1933,10 @@ fn a_rejected_wait_until_does_not_break_the_response() { event.waitUntil(Promise.reject(new Error('waitUntil failed'))); event.respondWith(new Response('answered anyway')); })"#; - let handle = start_serve(handler, 18251); + let handle = start_serve(handler); + let port = handle.port(); - assert_eq!(request(18251, "GET", "/", ""), "answered anyway"); + assert_eq!(request(port, "GET", "/", ""), "answered anyway"); handle.stop(); } @@ -2264,9 +1959,10 @@ fn serve_streams_an_empty_chunk_without_ending_the_body() { }, }))); })"#; - let handle = start_serve(handler, 18253); + let handle = start_serve(handler); + let port = handle.port(); - let body = request(18253, "GET", "/", ""); + let body = request(port, "GET", "/", ""); assert_eq!( dechunk(&body), "chunk one chunk two", @@ -2288,45 +1984,28 @@ fn serve_streams_an_empty_chunk_without_ending_the_body() { /// bytes leaves the client waiting for 9995 that never come. #[test] fn serve_frames_a_response_by_its_body_not_the_handlers_content_length() { - let handler = r#" - const cases = { - // Longer than the body. - '/larger': () => new Response('short', { headers: { 'content-length': '10000' } }), - // Not a number at all. - '/bogus': () => new Response('short', { headers: { 'content-length': 'test' } }), - // Two of them, which combine into one comma-joined value. - '/duplicate': () => { - const headers = new Headers(); - headers.append('content-length', '10000'); - headers.append('content-length', '10000'); - return new Response('short', { headers }); - }, - }; - addEventListener('fetch', (event) => { - const make = cases[new URL(event.request.url).pathname]; - event.respondWith(make()); - }); - "#; - let handle = start_serve(handler, 18255); + let handler = r#"addEventListener('fetch', (event) => { + event.respondWith(new Response('short', { headers: { 'content-length': '10000' } })); + })"#; + let handle = start_serve(handler); + let port = handle.port(); - for path in ["/larger", "/bogus", "/duplicate"] { - let response = raw_request( - 18255, - format!("GET {path} HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n").as_bytes(), - ); - let (head, body) = response - .split_once("\r\n\r\n") - .expect("a complete response"); - assert_eq!(body, "short", "body for {path}: {response}"); - assert!( - head.to_lowercase().contains("content-length: 5"), - "the wire length must describe the body, not the handler's header, for {path}: {head}" - ); - assert!( - !head.contains("10000") && !head.to_lowercase().contains("content-length: test"), - "the handler's own length must not reach the wire for {path}: {head}" - ); - } + let response = raw_request( + port, + b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", + ); + let (head, body) = response + .split_once("\r\n\r\n") + .expect("a complete response"); + assert_eq!(body, "short", "got: {response}"); + assert!( + head.to_lowercase().contains("content-length: 5"), + "the wire length must describe the body, not the handler's header: {head}" + ); + assert!( + !head.contains("10000"), + "the handler's own length must not reach the wire: {head}" + ); handle.stop(); } @@ -2349,9 +2028,6 @@ fn serve_frames_a_streamed_body_by_its_declared_content_length() { }); } const cases = { - '/exact': () => new Response(stream(['abc', 'de']), { - headers: { 'content-length': '5' }, - }), // More content than it declared: the excess never reaches the wire. '/longer': () => new Response(stream(['abcde', 'XXXXX']), { headers: { 'content-length': '5' }, @@ -2364,38 +2040,36 @@ fn serve_frames_a_streamed_body_by_its_declared_content_length() { '/bogus': () => new Response(stream(['abcde']), { headers: { 'content-length': 'test' }, }), - '/undeclared': () => new Response(stream(['abcde'])), }; addEventListener('fetch', (event) => { const make = cases[new URL(event.request.url).pathname]; event.respondWith(make()); }); "#; - let handle = start_serve(handler, 18257); + let handle = start_serve(handler); + let port = handle.port(); - for (path, body) in [("/exact", "abcde"), ("/longer", "abcde")] { - let response = raw_request( - 18257, - format!("GET {path} HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n").as_bytes(), - ); - let (head, sent) = response - .split_once("\r\n\r\n") - .expect("a complete response"); - assert!( - head.to_lowercase().contains("content-length: 5"), - "the declared length must frame {path}: {head}" - ); - assert!( - !head.to_lowercase().contains("transfer-encoding"), - "a declared length leaves nothing for chunked to frame, for {path}: {head}" - ); - assert_eq!(sent, body, "body for {path}: {response}"); - } + let response = raw_request( + port, + b"GET /longer HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", + ); + let (head, sent) = response + .split_once("\r\n\r\n") + .expect("a complete response"); + assert!( + head.to_lowercase().contains("content-length: 5"), + "the declared length must frame the response: {head}" + ); + assert!( + !head.to_lowercase().contains("transfer-encoding"), + "a declared length leaves nothing for chunked to frame: {head}" + ); + assert_eq!(sent, "abcde", "got: {response}"); // The head is framed by the declared length, and the body stops short of it, so the client // reads a truncated message rather than a complete-looking one. let response = raw_request( - 18257, + port, b"GET /shorter HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", ); let (head, sent) = response @@ -2410,21 +2084,19 @@ fn serve_frames_a_streamed_body_by_its_declared_content_length() { "the body must stop short of the length it declared: {response:?}" ); - for path in ["/bogus", "/undeclared"] { - let response = raw_request( - 18257, - format!("GET {path} HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n").as_bytes(), - ); - let (head, sent) = response - .split_once("\r\n\r\n") - .expect("a complete response"); - assert!( - head.to_lowercase().contains("transfer-encoding: chunked"), - "with no length to frame by, {path} is chunked: {head}" - ); - assert!(!head.contains("test"), "got: {head}"); - assert_eq!(dechunk(sent), "abcde", "body for {path}: {response}"); - } + let response = raw_request( + port, + b"GET /bogus HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", + ); + let (head, sent) = response + .split_once("\r\n\r\n") + .expect("a complete response"); + assert!( + head.to_lowercase().contains("transfer-encoding: chunked"), + "with no length to frame by, the response is chunked: {head}" + ); + assert!(!head.contains("test"), "got: {head}"); + assert_eq!(dechunk(sent), "abcde", "got: {response}"); handle.stop(); } @@ -2442,10 +2114,11 @@ fn serve_forwards_an_upstreams_content_length() { }})); }})"# ); - let handle = start_serve(&handler, 18259); + let handle = start_serve(&handler); + let port = handle.port(); let response = raw_request( - 18259, + port, b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", ); let (head, body) = response @@ -2464,52 +2137,29 @@ fn serve_forwards_an_upstreams_content_length() { handle.stop(); } -/// A header value carrying a null byte is refused where it is built, not on the way out. WPT -/// `resources/invalid-header-worker.js` expects the *response* to become a network error, which is -/// how a browser reports it; here `Headers` rejects the value outright, so the handler never gets a -/// `Response` to respond with. Either way the bytes never reach the wire, which is the point. -#[test] -fn a_response_header_value_with_a_null_byte_is_refused() { - let handler = r#"addEventListener('fetch', (event) => { - let outcome; - try { - new Response('x', { headers: { 'x-test': 'b\0r' } }); - outcome = 'constructed'; - } catch (e) { - outcome = e.name; - } - event.respondWith(new Response(outcome)); - })"#; - let handle = start_serve(handler, 18343); - - assert_eq!(request(18343, "GET", "/", ""), "TypeError"); - - handle.stop(); -} - /// A response body that errors after sending bytes: the client gets what was produced, and then the /// connection ends *without* the terminating zero-length chunk, so a truncated body cannot be /// mistaken for a complete one. WPT `resources/fetch-error-worker.js` asserts the read rejects /// rather than hanging; on the wire the missing terminator is what carries that. #[test] fn a_response_stream_that_errors_mid_flight_leaves_the_body_unterminated() { - // The error comes from a timer, and a generous one. A body cannot outlive its request's event - // loop going idle, so the stream holds the loop open until it errors rather than waiting for - // the client. The delay only has to outlast the pump reaching the wire, which a GC zeal build - // makes much slower. + // The second `pull()` errors the stream. It only runs once the first chunk has been read out of + // the stream's queue, so the error cannot discard that chunk. let handler = r#"addEventListener('fetch', (event) => { const encoder = new TextEncoder(); + let pulls = 0; event.respondWith(new Response(new ReadableStream({ - start(c) { - c.enqueue(encoder.encode('first-chunk-')); - setTimeout(() => c.error(new Error('stream broke')), 1000); + pull(c) { + if (++pulls === 1) c.enqueue(encoder.encode('first-chunk-')); + else c.error(new Error('stream broke')); }, }))); })"#; - let handle = start_serve(handler, 18339); + let handle = start_serve(handler); + let port = handle.port(); let response = raw_request( - 18339, + port, b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", ); let (_, body) = response.split_once("\r\n\r\n").expect("a complete head"); @@ -2539,8 +2189,9 @@ fn serve_streams_many_queued_chunks_in_order() { }, }))); })"#; - let handle = start_serve(handler, 18471); - let body = request(18471, "GET", "/", ""); + let handle = start_serve(handler); + let port = handle.port(); + let body = request(port, "GET", "/", ""); let expected: String = (0..100) .map(|i| format!("{:03}", i).repeat(341) + "|") .collect(); @@ -2548,8 +2199,9 @@ fn serve_streams_many_queued_chunks_in_order() { handle.stop(); } -/// Chunks queued ahead of an invalid chunk are written before the body errors, and the body then -/// ends unterminated. +/// Chunks queued ahead of a chunk that is not a `Uint8Array` are written before the body errors, +/// and the body then ends unterminated after a `200` head. WPT +/// `resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js`. #[test] fn queued_chunks_before_an_invalid_chunk_are_written_before_the_error() { let handler = r#"addEventListener('fetch', (event) => { @@ -2563,47 +2215,19 @@ fn queued_chunks_before_an_invalid_chunk_are_written_before_the_error() { }, }))); })"#; - let handle = start_serve(handler, 18472); + let handle = start_serve(handler); + let port = handle.port(); let response = raw_request( - 18472, + port, b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", ); - let (_, body) = response.split_once("\r\n\r\n").expect("a complete head"); + let (head, body) = response.split_once("\r\n\r\n").expect("a complete head"); + assert!(head.starts_with("HTTP/1.1 200 "), "got: {response}"); assert!(body.contains("first-second-"), "got: {response}"); assert!(!body.ends_with("0\r\n\r\n"), "got: {response}"); handle.stop(); } -/// The same, for a stream that enqueues something that is not a `Uint8Array`: the body errors -/// before any byte is written, so the head is already committed and the connection ends -/// unterminated. WPT -/// `resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js`. -#[test] -fn a_response_stream_with_an_invalid_chunk_leaves_the_body_unterminated() { - let handler = r#"addEventListener('fetch', (event) => { - event.respondWith(new Response(new ReadableStream({ - start(c) { c.enqueue('a string, not a Uint8Array'); c.close(); }, - }))); - })"#; - let handle = start_serve(handler, 18261); - - let response = raw_request( - 18261, - b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", - ); - let (head, body) = response.split_once("\r\n\r\n").expect("a complete head"); - assert!( - head.starts_with("HTTP/1.1 200 "), - "the head goes out before the first chunk is pulled, so it is still a 200: {response}" - ); - assert!( - !body.ends_with("0\r\n\r\n"), - "a body that never produced a valid chunk must not be terminated: {response}" - ); - - handle.stop(); -} - /// An unusual status reaches the client untouched, and the request behind it is dispatched exactly /// once — nothing retries it. WPT `fetch-event.https.html` pins this with a 421 (`Misdirected /// Request`), the status a client is most tempted to retry elsewhere, and asserts the origin saw @@ -2624,10 +2248,11 @@ fn serve_delivers_an_unusual_status_without_retrying_it() { event.respondWith(new Response('misdirected', { status: 421 })); }); "#; - let handle = start_serve(handler, 18263); + let handle = start_serve(handler); + let port = handle.port(); let response = raw_request( - 18263, + port, b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", ); assert!( @@ -2640,7 +2265,7 @@ fn serve_delivers_an_unusual_status_without_retrying_it() { ); assert_eq!( - request(18263, "GET", "/count", ""), + request(port, "GET", "/count", ""), "dispatched 1 times", "a 421 must not be retried behind the handler's back" ); @@ -2658,7 +2283,8 @@ fn serve_exposes_a_urlencoded_post_to_the_handler() { event.request.method + ':' + event.request.headers.get('content-type') + ':' + body, ))); })"#; - let handle = start_serve(handler, 18265); + let handle = start_serve(handler); + let port = handle.port(); let body = "testName1=testValue1&testName2=testValue2"; let raw = format!( @@ -2666,7 +2292,7 @@ fn serve_exposes_a_urlencoded_post_to_the_handler() { Content-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len() ); - let response = raw_request(18265, raw.as_bytes()); + let response = raw_request(port, raw.as_bytes()); let (_, got) = response .split_once("\r\n\r\n") .expect("a complete response"); @@ -2686,10 +2312,8 @@ fn serve_exposes_a_urlencoded_post_to_the_handler() { /// that. #[test] fn a_handler_can_fetch_its_own_server() { - // The handler addresses itself, so the port is bound once and interpolated: a renumbering that - // updated only `start_serve` would otherwise leave this fetching a dead — or worse, another - // test's — port, and fail as something other than the deadlock this is watching for. - let port = 18267; + // The handler addresses itself, so its port is bound before the handler is written. + let (listener, port) = bind_local(); let handler = format!( r#"addEventListener('fetch', (event) => {{ const path = new URL(event.request.url).pathname; @@ -2704,7 +2328,7 @@ fn a_handler_can_fetch_its_own_server() { ); }})"# ); - let handle = start_serve(&handler, port); + let handle = start_serve_on(listener, legacy_script(&handler)); assert_eq!( request_within(port, "/outer", Duration::from_secs(10)).as_deref(), @@ -2750,12 +2374,13 @@ fn serve_exposes_the_incoming_request_shape() { 'type=' + event.type, ].join('\n'))); })"#; - let handle = start_serve(handler, 18269); + let handle = start_serve(handler); + let port = handle.port(); // The URL is rebuilt from the request target and the `Host` header — this helper sends // `Host: localhost`, with no port, and that is exactly what comes back. assert_eq!( - request(18269, "GET", "/shape?q=1", ""), + request(port, "GET", "/shape?q=1", ""), [ "method=GET", "url=http://localhost/shape?q=1", @@ -2791,10 +2416,11 @@ fn serve_exposes_the_incoming_request_shape() { fn serve_answers_a_head_request_without_a_body() { let handler = "addEventListener('fetch', (e) => e.respondWith(new Response('BODYBYTES', { status: 201 })));"; - let handle = start_serve(handler, 18271); + let handle = start_serve(handler); + let port = handle.port(); let response = raw_request( - 18271, + port, b"HEAD / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", ); let (head, body) = response.split_once("\r\n\r\n").expect("a complete head"); @@ -2829,10 +2455,11 @@ fn a_head_request_does_not_run_the_body_it_will_not_send() { }, }))); })"#; - let handle = start_serve(handler, 18297); + let handle = start_serve(handler); + let port = handle.port(); let response = raw_request( - 18297, + port, b"HEAD / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", ); assert!(response.starts_with("HTTP/1.1 200 "), "got: {response}"); @@ -2845,7 +2472,7 @@ fn a_head_request_does_not_run_the_body_it_will_not_send() { // The stream is constructed either way, which pulls once to fill its queue; what must not // happen is the send pump reading from it on top of that. assert_eq!( - request(18297, "GET", "/pulls", ""), + request(port, "GET", "/pulls", ""), "1", "the handler's pull() ran for a body the HEAD response never carried" ); @@ -2854,57 +2481,32 @@ fn a_head_request_does_not_run_the_body_it_will_not_send() { } /// A `204 No Content` is framed by its status: no `Content-Length`, no `Transfer-Encoding` (RFC -/// 9110 §8.6, RFC 9112 §6.2). The same holds for `304`. +/// 9110 §8.6, RFC 9112 §6.2). #[test] fn a_bodiless_status_has_no_framing_headers() { let handler = r#"addEventListener('fetch', (event) => { - const status = Number(new URL(event.request.url).pathname.slice(1)); - event.respondWith(new Response(null, { status })); + event.respondWith(new Response(null, { status: 204 })); })"#; - let handle = start_serve(handler, 18273); - - for status in [204, 304] { - let response = raw_request( - 18273, - format!("GET /{status} HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n").as_bytes(), - ); - let head = response - .split_once("\r\n\r\n") - .map_or(&*response, |(h, _)| h); - let head = head.to_lowercase(); - - assert!( - !head.contains("content-length"), - "a {status} must not be framed with Content-Length: {response}" - ); - assert!( - !head.contains("transfer-encoding"), - "a {status} must not be framed with Transfer-Encoding: {response}" - ); - } - - handle.stop(); -} - -/// `respondWith(response)` consumes the response's body, so `bodyUsed` reads true afterwards — -/// WPT `fetch-event.https.html` (`?used-check`), which reads the flag back on a later request. -#[test] -fn respond_with_marks_the_response_body_used() { - let handler = r#" - globalThis.stored = null; - addEventListener('fetch', (event) => { - if (new URL(event.request.url).pathname === '/check') { - event.respondWith(new Response('bodyUsed: ' + globalThis.stored.bodyUsed)); - return; - } - globalThis.stored = new Response('payload'); - event.respondWith(globalThis.stored); - }); - "#; - let handle = start_serve(handler, 18275); + let handle = start_serve(handler); + let port = handle.port(); - assert_eq!(request(18275, "GET", "/", ""), "payload"); - assert_eq!(request(18275, "GET", "/check", ""), "bodyUsed: true"); + let response = raw_request( + port, + b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", + ); + let head = response + .split_once("\r\n\r\n") + .map_or(&*response, |(h, _)| h) + .to_lowercase(); + assert!(head.starts_with("http/1.1 204"), "got: {response}"); + assert!( + !head.contains("content-length"), + "a 204 must not be framed with Content-Length: {response}" + ); + assert!( + !head.contains("transfer-encoding"), + "a 204 must not be framed with Transfer-Encoding: {response}" + ); handle.stop(); } @@ -2912,7 +2514,8 @@ fn respond_with_marks_the_response_body_used() { /// Sending a response hands its bytes to the transport rather than copying them, so the `Response` /// is left holding none of the buffer. What content sees must not move with them: the body still /// reads as one that was read — a consumed stream that cannot be read again — rather than as a body -/// that was never there. +/// that was never there. `respondWith` marks it used, so `bodyUsed` reads true, as WPT +/// `fetch-event.https.html` (`?used-check`) checks. #[test] fn a_sent_byte_body_reads_as_consumed_rather_than_absent() { let handler = r#" @@ -2920,6 +2523,7 @@ fn a_sent_byte_body_reads_as_consumed_rather_than_absent() { addEventListener('fetch', (event) => { if (new URL(event.request.url).pathname === '/check') { event.respondWith((async () => { + const used = globalThis.stored.bodyUsed; const body = globalThis.stored.body; let reread; try { @@ -2928,7 +2532,8 @@ fn a_sent_byte_body_reads_as_consumed_rather_than_absent() { } catch (e) { reread = e.name; } - return new Response('body=' + (body === null ? 'null' : 'stream') + return new Response('bodyUsed=' + used + + ' body=' + (body === null ? 'null' : 'stream') + ' locked=' + (body !== null && body.locked) + ' reread=' + reread); })()); @@ -2938,49 +2543,13 @@ fn a_sent_byte_body_reads_as_consumed_rather_than_absent() { event.respondWith(globalThis.stored); }); "#; - let handle = start_serve(handler, 18325); + let handle = start_serve(handler); + let port = handle.port(); - assert_eq!(request(18325, "GET", "/", ""), "payload"); + assert_eq!(request(port, "GET", "/", ""), "payload"); assert_eq!( - request(18325, "GET", "/check", ""), - "body=stream locked=true reread=TypeError" - ); - - handle.stop(); -} - -/// `new Request(input)` takes the input's body over rather than copying it, so the input is left -/// holding no bytes — while still reading as a body that was read, exactly as after a `text()`. -/// -/// The taking is the point: the copy that reaches the new request must be the same bytes, so -/// reading them there has to still produce the payload. -#[test] -fn constructing_a_request_from_another_takes_its_bytes_over() { - let handler = r#"addEventListener('fetch', (event) => { - event.respondWith((async () => { - const input = new Request('https://example.com/', { - method: 'POST', - body: 'payload', - }); - const taken = new Request(input); - let reread; - try { - await input.text(); - reread = 'ok'; - } catch (e) { - reread = e.name; - } - return new Response('used=' + input.bodyUsed - + ' body=' + (input.body === null ? 'null' : 'stream') - + ' reread=' + reread - + ' taken=' + await taken.text()); - })()); - })"#; - let handle = start_serve(handler, 18327); - - assert_eq!( - request(18327, "GET", "/", ""), - "used=true body=stream reread=TypeError taken=payload" + request(port, "GET", "/check", ""), + "bodyUsed=true body=stream locked=true reread=TypeError" ); handle.stop(); @@ -3004,12 +2573,13 @@ fn responding_with_a_used_stream_backed_response_is_a_network_error() { event.respondWith(globalThis.stored); }); "#; - let handle = start_serve(handler, 18277); + let handle = start_serve(handler); + let port = handle.port(); - assert_eq!(dechunk(&request(18277, "GET", "/", "")), "payload"); + assert_eq!(dechunk(&request(port, "GET", "/", "")), "payload"); let response = raw_request( - 18277, + port, b"GET /reuse HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", ); assert!( @@ -3031,9 +2601,10 @@ fn responding_with_a_used_stream_backed_response_is_a_network_error() { fn a_response_header_value_is_sent_as_bytes_not_utf8() { let handler = "addEventListener('fetch', (e) => e.respondWith(new Response('x', { headers: { 'x-test': '\u{df}\u{c0}\u{bf}' } })));"; - let handle = start_serve(handler, 18279); + let handle = start_serve(handler); + let port = handle.port(); - let mut stream = TcpStream::connect(("127.0.0.1", 18279)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); stream .write_all(b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n") .unwrap(); @@ -3085,11 +2656,12 @@ fn respond_with_rejects_the_responses_it_cannot_send() { event.respondWith(new Response('unknown')); } })"#; - let handle = start_serve(handler, 18281); + let handle = start_serve(handler); + let port = handle.port(); for path in ["/resolve-null", "/reject-bare", "/used-body"] { let response = raw_request( - 18281, + port, format!("GET {path} HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n").as_bytes(), ); assert!( @@ -3099,7 +2671,7 @@ fn respond_with_rejects_the_responses_it_cannot_send() { } assert_eq!( - request(18281, "GET", "/unused-body", ""), + request(port, "GET", "/unused-body", ""), "payload", "an untouched body must still be sendable" ); @@ -3123,15 +2695,16 @@ fn prevent_default_does_not_stop_a_later_listener_responding() { } }); "#; - let handle = start_serve(handler, 18283); + let handle = start_serve(handler); + let port = handle.port(); assert_eq!( - request(18283, "GET", "/respond", ""), + request(port, "GET", "/respond", ""), "answered by the second listener" ); let cancelled = raw_request( - 18283, + port, b"GET /cancel-only HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", ); assert!( @@ -3161,10 +2734,11 @@ fn a_client_disconnect_cancels_the_response_stream_in_the_handler() { cancel(reason) { globalThis.cancelled = 'cancelled'; }, }))); })"#; - let handle = start_serve(handler, 18285); + let handle = start_serve(handler); + let port = handle.port(); { - let mut stream = TcpStream::connect(("127.0.0.1", 18285)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); stream .write_all(b"GET /stream HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") .unwrap(); @@ -3182,7 +2756,7 @@ fn a_client_disconnect_cancels_the_response_stream_in_the_handler() { // The cancel reaches JS while the abandoned request's loop drains. assert_eq!( - eventually(18285, "/query", |seen| seen == "cancelled"), + eventually(port, "/query", |seen| seen == "cancelled"), "cancelled", "the handler's stream must be cancelled when the client stops reading" ); @@ -3224,15 +2798,16 @@ fn wait_until_tracks_the_end_of_the_events_active_window() { event.respondWith(new Response('answered')); }); "#; - let handle = start_serve(handler, 18287); + let handle = start_serve(handler); + let port = handle.port(); - assert_eq!(request(18287, "GET", "/immediate", ""), "answered"); - assert_eq!(request(18287, "GET", "/task", ""), "answered"); + assert_eq!(request(port, "GET", "/immediate", ""), "answered"); + assert_eq!(request(port, "GET", "/task", ""), "answered"); // The lifetime work runs after each response. The log is complete once the last of it is in. const LOG: &str = "immediate-sync:ok,immediate-extra:ok,task-sync:ok,task-extra:InvalidStateError"; assert_eq!( - eventually(18287, "/log", |log| log == LOG), + eventually(port, "/log", |log| log == LOG), LOG, "the dispatch flag covers the whole checkpoint; after it, the pending count expires one \ microtask after the promise settles" @@ -3241,41 +2816,6 @@ fn wait_until_tracks_the_end_of_the_events_active_window() { handle.stop(); } -/// Requests on one connection are served in sequence without closing it. The shared global's -/// counter proves both hit the same server; the second response proves the connection survived -/// the first, which therefore must not have promised `Connection: close`. -#[test] -fn serve_reuses_a_connection_for_sequential_requests() { - let handler = r#"globalThis.n = 0; - addEventListener('fetch', (event) => { - globalThis.n += 1; - const url = new URL(event.request.url); - event.respondWith(new Response(url.pathname + ' #' + globalThis.n)); - })"#; - let handle = start_serve(handler, 18301); - - let mut stream = TcpStream::connect(("127.0.0.1", 18301)).unwrap(); - let mut carry = Vec::new(); - stream - .write_all(b"GET /one HTTP/1.1\r\nHost: x\r\n\r\n") - .unwrap(); - let first = read_one_response(&mut stream, &mut carry, PATIENCE).expect("a first response"); - assert!(first.contains("/one #1"), "got: {first}"); - assert!( - !first.to_lowercase().contains("connection: close"), - "a reusable response must not promise a close; got: {first}" - ); - - stream - .write_all(b"GET /two HTTP/1.1\r\nHost: x\r\n\r\n") - .unwrap(); - let second = read_one_response(&mut stream, &mut carry, PATIENCE) - .expect("a second response on the same connection"); - assert!(second.contains("/two #2"), "got: {second}"); - - handle.stop(); -} - /// A request that asks for `Connection: close` gets it: the response repeats the header, and EOF /// follows. #[test] @@ -3283,10 +2823,11 @@ fn serve_honors_a_requests_connection_close() { let handler = r#"addEventListener('fetch', (event) => { event.respondWith(new Response('bye')); })"#; - let handle = start_serve(handler, 18303); + let handle = start_serve(handler); + let port = handle.port(); let response = raw_request( - 18303, + port, b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", ); assert!( @@ -3307,9 +2848,10 @@ fn serve_drains_an_unread_body_and_reuses_the_connection() { globalThis.n += 1; event.respondWith(new Response('ok #' + globalThis.n)); })"#; - let handle = start_serve(handler, 18305); + let handle = start_serve(handler); + let port = handle.port(); - let mut stream = TcpStream::connect(("127.0.0.1", 18305)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); let mut carry = Vec::new(); stream .write_all(b"POST /a HTTP/1.1\r\nHost: x\r\nContent-Length: 11\r\n\r\nhello world") @@ -3343,58 +2885,6 @@ fn serve_drains_an_unread_body_and_reuses_the_connection() { handle.stop(); } -/// Draining an unread body for reuse is only worth so much: past the drain budget the server -/// closes the connection instead of reading megabytes no reader asked for. The close surfaces to -/// client as a failed body write or as EOF where the next response would have been — never as a -/// served next request. -#[test] -fn serve_closes_rather_than_drain_an_oversized_unread_body() { - let handler = r#"addEventListener('fetch', (event) => { - event.respondWith(new Response('ignored your body')); - })"#; - // A drain budget far below the upload, and a short read timeout so the wait for a handler - // that never reads gives up promptly. The upload has to outgrow all passive buffering (the - // incoming-body channel, the body pump's held frame, and hyper's read-ahead, together over - // 1 MiB), so that bytes remain for the drain to give up on. - const UPLOAD: usize = 4 * 1024 * 1024; - let handle = serve_with_limits( - handler, - 18307, - "--max-body-drain-bytes 64KiB --request-read-timeout 1", - ); - - let mut stream = TcpStream::connect(("127.0.0.1", 18307)).unwrap(); - let mut carry = Vec::new(); - stream - .write_all( - format!("POST / HTTP/1.1\r\nHost: x\r\nContent-Length: {UPLOAD}\r\n\r\n").as_bytes(), - ) - .unwrap(); - let first = read_one_response(&mut stream, &mut carry, PATIENCE).expect("the response"); - assert!(first.contains("ignored your body"), "got: {first}"); - - // Feed the declared body. The server gives up draining partway and closes, so these writes - // may start failing at any point; that failure is itself the expected outcome. - let chunk = [b'a'; 32 * 1024]; - let _ = stream.set_write_timeout(Some(Duration::from_secs(5))); - let mut wrote_all = true; - for _ in 0..(UPLOAD / chunk.len()) { - if stream.write_all(&chunk).is_err() { - wrote_all = false; - break; - } - } - if wrote_all { - let _ = stream.write_all(b"GET /after HTTP/1.1\r\nHost: x\r\n\r\n"); - } - assert!( - read_one_response(&mut stream, &mut carry, Duration::from_secs(5)).is_none(), - "the connection must close rather than serve another request past an oversized drain" - ); - - handle.stop(); -} - /// Chunked framing is HTTP/1.1: a 1.0 client gets a streamed body as raw bytes delimited by the /// close. HTTP/1.0 does not persist by default, so the close needs no announcing. #[test] @@ -3408,9 +2898,10 @@ fn serve_answers_http_1_0_with_a_close_delimited_body() { }); event.respondWith(new Response(stream)); })"#; - let handle = start_serve(handler, 18309); + let handle = start_serve(handler); + let port = handle.port(); - let response = raw_request(18309, b"GET / HTTP/1.0\r\nHost: x\r\n\r\n"); + let response = raw_request(port, b"GET / HTTP/1.0\r\nHost: x\r\n\r\n"); let lower = response.to_lowercase(); assert!(!lower.contains("connection: keep-alive"), "got: {response}"); assert!(!lower.contains("transfer-encoding"), "got: {response}"); @@ -3429,9 +2920,10 @@ fn serve_answers_pipelined_requests_in_order() { const url = new URL(event.request.url); event.respondWith(new Response(url.pathname + ' #' + globalThis.n)); })"#; - let handle = start_serve(handler, 18311); + let handle = start_serve(handler); + let port = handle.port(); - let mut stream = TcpStream::connect(("127.0.0.1", 18311)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); let mut carry = Vec::new(); stream .write_all(b"GET /a HTTP/1.1\r\nHost: x\r\n\r\nGET /b HTTP/1.1\r\nHost: x\r\n\r\n") @@ -3456,9 +2948,10 @@ fn serve_does_not_hold_the_next_request_behind_waituntil() { } event.respondWith(new Response(url.pathname)); })"#; - let handle = start_serve(handler, 18313); + let handle = start_serve(handler); + let port = handle.port(); - let mut stream = TcpStream::connect(("127.0.0.1", 18313)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); let mut carry = Vec::new(); stream .write_all(b"GET /lingering HTTP/1.1\r\nHost: x\r\n\r\n") @@ -3485,9 +2978,10 @@ fn serve_does_not_hold_the_next_request_behind_waituntil() { #[test] fn serve_reuses_the_connection_after_a_network_error_response() { let handler = r#"addEventListener('fetch', () => {})"#; - let handle = start_serve(handler, 18315); + let handle = start_serve(handler); + let port = handle.port(); - let mut stream = TcpStream::connect(("127.0.0.1", 18315)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); let mut carry = Vec::new(); stream .write_all(b"GET /a HTTP/1.1\r\nHost: x\r\n\r\n") @@ -3505,14 +2999,13 @@ fn serve_reuses_the_connection_after_a_network_error_response() { handle.stop(); } -/// Start a server whose limit flags come from `flags`, parsed as the CLI parses them, so these +/// Start a server whose limit flags come from `flags`, parsed by the CLI's own parser, so these /// tests exercise the real flag surface rather than a struct literal beside it. -fn serve_with_limits(handler: &str, port: u16, flags: &str) -> common::ServeHandle { +fn serve_with_limits(handler: &str, flags: &str) -> common::ServeHandle { let mut config = RuntimeConfig::from_arg_string(flags).expect("the limit flags parse"); config.eval_script = Some(handler.to_string()); config.legacy_script = true; - config.serve = Some(port); - start_serve_config(config, port) + start_serve_config(config) } /// The handler these limit tests use. None of them is about what it returns. @@ -3520,57 +3013,62 @@ const OK_HANDLER: &str = r#"addEventListener('fetch', (e) => e.respondWith(new R #[test] fn a_configured_head_limit_bounds_the_request_head() { - let handle = serve_with_limits(OK_HANDLER, 18317, "--max-connection-buffer-size 8KiB"); + let handle = serve_with_limits(OK_HANDLER, "--max-connection-buffer-size 8KiB"); + let port = handle.port(); // Well under the default 64 KiB, so only the configured limit can reject this. let mut over = Vec::from(&b"GET / HTTP/1.1\r\nHost: x\r\nX-Big: "[..]); over.extend(std::iter::repeat_n(b'a', 32 * 1024)); over.extend_from_slice(b"\r\nConnection: close\r\n\r\n"); - let response = raw_request(18317, &over); + let response = raw_request(port, &over); assert!(response.starts_with("HTTP/1.1 431"), "got: {response}"); // A head within the limit is still served. - assert_eq!(request(18317, "GET", "/", ""), "ok"); + assert_eq!(request(port, "GET", "/", ""), "ok"); handle.stop(); } #[test] fn a_configured_header_count_bounds_the_number_of_fields() { - let handle = serve_with_limits(OK_HANDLER, 18319, "--max-request-headers 8"); + let handle = serve_with_limits(OK_HANDLER, "--max-request-headers 8"); + let port = handle.port(); let mut over = Vec::from(&b"GET / HTTP/1.1\r\nHost: x\r\n"[..]); for n in 0..20 { over.extend_from_slice(format!("X-Field-{n}: v\r\n").as_bytes()); } over.extend_from_slice(b"Connection: close\r\n\r\n"); - let response = raw_request(18319, &over); + let response = raw_request(port, &over); assert!(response.starts_with("HTTP/1.1 431"), "got: {response}"); - assert_eq!(request(18319, "GET", "/", ""), "ok"); + assert_eq!(request(port, "GET", "/", ""), "ok"); handle.stop(); } #[test] fn a_configured_body_limit_refuses_a_larger_declared_upload() { - let handle = serve_with_limits(OK_HANDLER, 18321, "--max-request-body-bytes 64"); + let handle = serve_with_limits(OK_HANDLER, "--max-request-body-bytes 64"); + let port = handle.port(); let response = raw_request( - 18321, + port, b"POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 4096\r\nConnection: close\r\n\r\n", ); assert!(response.starts_with("HTTP/1.1 413"), "got: {response}"); // An upload within the limit is still served. - assert_eq!(request(18321, "POST", "/", "small"), "ok"); + assert_eq!(request(port, "POST", "/", "small"), "ok"); handle.stop(); } /// The drain budget sets whether a connection whose request body went unread can be reused: the /// same upload against the same handler is reused under a budget that covers it and closed under -/// one that does not. +/// one that does not. Past the budget the server closes the connection instead of reading +/// megabytes not requested by any reader. The close surfaces to the client as a failed body +/// write or as EOF where the next response would have been, never as a served next request. /// /// The upload has to outgrow all passive buffering, or the whole body can be absorbed without /// any of it reaching the drain, leaving the connection reusable under any budget: the @@ -3581,29 +3079,28 @@ fn the_drain_budget_sets_whether_an_unread_body_closes_the_connection() { // A short read timeout so the wait for the consumer that never reads gives up promptly. let generous = serve_with_limits( OK_HANDLER, - 18329, - "--max-body-drain-bytes 8MiB --request-read-timeout 1", + "--max-body-drain-bytes 8MiB --request-read-timeout 0.25", ); assert!( - reuses_after_unread_upload(18329), + reuses_after_unread_upload(generous.port()), "a budget that covers the unread body must leave the connection usable" ); generous.stop(); - let none = serve_with_limits( + let partial = serve_with_limits( OK_HANDLER, - 18331, - "--max-body-drain-bytes 0 --request-read-timeout 1", + "--max-body-drain-bytes 64KiB --request-read-timeout 0.25", ); assert!( - !reuses_after_unread_upload(18331), - "a zero budget must close rather than read a body nobody wanted" + !reuses_after_unread_upload(partial.port()), + "a budget far below the unread body must close rather than read it" ); - none.stop(); + partial.stop(); } -/// Upload four megabytes the handler never reads, then report whether the connection went on to -/// serve another request. +/// Send the head of a four-megabyte upload, read the response the handler sends without reading +/// the body, then upload the body and report whether the connection went on to serve another +/// request. fn reuses_after_unread_upload(port: u16) -> bool { const UPLOAD: usize = 4 * 1024 * 1024; let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); @@ -3613,8 +3110,11 @@ fn reuses_after_unread_upload(port: u16) -> bool { format!("POST / HTTP/1.1\r\nHost: x\r\nContent-Length: {UPLOAD}\r\n\r\n").as_bytes(), ) .unwrap(); + let first = read_one_response(&mut stream, &mut carry, PATIENCE) + .expect("a response before the body is sent"); + assert!(first.starts_with("HTTP/1.1 200"), "got: {first}"); let _ = stream.set_write_timeout(Some(Duration::from_secs(10))); - // The server may stop reading and close partway, which fails these writes — that is the + // The server may stop reading and close partway, which fails these writes. That is the // outcome under test, not an error. let chunk = [b'a'; 32 * 1024]; for _ in 0..(UPLOAD / chunk.len()) { @@ -3622,9 +3122,6 @@ fn reuses_after_unread_upload(port: u16) -> bool { return false; } } - if read_one_response(&mut stream, &mut carry, PATIENCE).is_none() { - return false; - } if stream .write_all(b"GET /again HTTP/1.1\r\nHost: x\r\n\r\n") .is_err() @@ -3636,9 +3133,10 @@ fn reuses_after_unread_upload(port: u16) -> bool { #[test] fn a_configured_keepalive_timeout_closes_an_idle_connection() { - let handle = serve_with_limits(OK_HANDLER, 18333, "--keepalive-timeout 1"); + let handle = serve_with_limits(OK_HANDLER, "--keepalive-timeout 0.25"); + let port = handle.port(); - let mut stream = TcpStream::connect(("127.0.0.1", 18333)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); let mut carry = Vec::new(); stream .write_all(b"GET / HTTP/1.1\r\nHost: x\r\n\r\n") @@ -3659,9 +3157,10 @@ fn a_configured_keepalive_timeout_closes_an_idle_connection() { #[test] fn a_configured_read_timeout_closes_a_stalled_head() { - let handle = serve_with_limits(OK_HANDLER, 18335, "--request-read-timeout 1"); + let handle = serve_with_limits(OK_HANDLER, "--request-read-timeout 0.25"); + let port = handle.port(); - let mut stream = TcpStream::connect(("127.0.0.1", 18335)).unwrap(); + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); // A head that never terminates: the client has started a request and then stalled, so the // connection is closed rather than held open for a request that may never arrive. stream.write_all(b"GET / HTTP/1.1\r\nHost: x\r\n").unwrap(); @@ -3687,20 +3186,21 @@ fn a_configured_connection_limit_serves_one_connection_at_a_time() { event.respondWith(new Promise((resolve) => setTimeout(() => { log.push('slow-done'); resolve(new Response('slow')); - }, 1000))); + }, 400))); return; } event.respondWith(new Response(log.join(',') || 'nothing')); })"#; - let handle = serve_with_limits(handler, 18337, "--max-connections 1"); + let handle = serve_with_limits(handler, "--max-connections 1"); + let port = handle.port(); - let mut slow = TcpStream::connect(("127.0.0.1", 18337)).unwrap(); + let mut slow = TcpStream::connect(("127.0.0.1", port)).unwrap(); slow.write_all(b"GET /slow HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n") .unwrap(); // Let the slow request take the only slot before the second connection asks for one. - std::thread::sleep(Duration::from_millis(300)); + std::thread::sleep(Duration::from_millis(100)); - let mut waiting = TcpStream::connect(("127.0.0.1", 18337)).unwrap(); + let mut waiting = TcpStream::connect(("127.0.0.1", port)).unwrap(); waiting .write_all(b"GET /observed HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n") .unwrap(); @@ -3730,9 +3230,10 @@ fn wait_until_work_does_not_hold_a_connection_slot() { event.respondWith(new Response('ok')); event.waitUntil(new Promise((resolve) => setTimeout(resolve, 5000))); })"#; - let handle = serve_with_limits(handler, 18345, "--max-connections 1"); + let handle = serve_with_limits(handler, "--max-connections 1"); + let port = handle.port(); - let mut first = TcpStream::connect(("127.0.0.1", 18345)).unwrap(); + let mut first = TcpStream::connect(("127.0.0.1", port)).unwrap(); let mut carry = Vec::new(); first .write_all(b"GET / HTTP/1.1\r\nHost: x\r\n\r\n") @@ -3741,7 +3242,7 @@ fn wait_until_work_does_not_hold_a_connection_slot() { drop(first); let started = std::time::Instant::now(); - assert_eq!(request(18345, "GET", "/", ""), "ok"); + assert_eq!(request(port, "GET", "/", ""), "ok"); let elapsed = started.elapsed(); assert!( elapsed < Duration::from_secs(2), diff --git a/crates/libstarling/tests/serve_isolation.rs b/crates/libstarling/tests/serve_isolation.rs deleted file mode 100644 index 43450b4..0000000 --- a/crates/libstarling/tests/serve_isolation.rs +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception -//! Per-request global isolation (`--serve-isolated`): a fresh global per request carries the -//! builtins, keeps one request's top-level declarations out of the next one's way, and receives -//! dispatch through its own listeners. - -#![cfg(not(target_arch = "wasm32"))] - -use core_runtime::runtime::{clear_global_initializers, Runtime}; -use js::conversion::FromJSVal; -use js::gc::scope::Scope; -use web_fetch::request::Request; -use web_globals::events::algorithms::ScriptStackState; - -/// A runtime with the builtins registered, ready for globals to be created from it. Each test -/// clears the initializers first: they accumulate in a process-wide registry, so a test inheriting -/// the previous one's would register each builtin twice over. -fn test_runtime() -> std::rc::Rc { - clear_global_initializers(); - libstarling::register_builtins(); - Runtime::init(&core_runtime::config::RuntimeConfig::default()).expect("runtime init") -} - -fn eval(scope: &Scope<'_>, src: &str) -> Result { - match js::compile::evaluate_with_filename(scope, src, "", 1) { - Ok(v) => String::from_jsval(scope, v, ()).map_err(|e| format!("{e:?}")), - Err(_) => { - let exn = js::error::ExnThrown::capture(scope); - Err(format!("{exn}")) - } - } -} - -#[test] -fn fresh_globals_are_isolated_and_initialized() { - let rt = test_runtime(); - - // Two globals, created the way a per-request server would. - { - let a = rt.new_global(); - // Builtins are installed on it: the initializers run per global. - assert_eq!( - eval(&a, "typeof addEventListener + ',' + typeof Response").unwrap(), - "function,function", - "a fresh global must carry the registered builtins" - ); - // A top-level lexical declaration — the thing that collides across tests today. - eval(&a, "const collide = 1; var marker = 'a';").unwrap(); - assert_eq!(eval(&a, "String(collide)").unwrap(), "1"); - } - { - let b = rt.new_global(); - // The same declaration must not collide, and `var` state must not leak. - eval(&b, "const collide = 2;").expect("redeclaration must not collide across globals"); - assert_eq!(eval(&b, "String(collide)").unwrap(), "2"); - assert_eq!( - eval(&b, "typeof marker").unwrap(), - "undefined", - "globals must not share var state" - ); - } -} - -#[test] -fn a_fetch_handler_registered_in_a_fresh_global_receives_dispatch() { - let rt = test_runtime(); - - let scope = rt.new_global(); - fetch_event::add_to_global(&scope, scope.global()); - - // The content script, as a per-request server would re-evaluate it into this global. - eval( - &scope, - "globalThis.__seen = null; - addEventListener('fetch', (e) => { globalThis.__seen = e.request.url; });", - ) - .unwrap(); - - let el = core_runtime::event_loop::EventLoop::new(); - core_runtime::event_loop::with_event_loop(&el, |_| { - let request = js::compile::evaluate_with_filename( - &scope, - "new Request('http://example.com/')", - "", - 1, - ) - .expect("request value"); - let request = Request::from_jsval(&scope, request, ()).unwrap(); - fetch_event::fetch_event::FetchEvent::dispatch(&scope, request, ScriptStackState::Empty) - .expect("dispatch"); - js::jobs::run_jobs(&scope); - }); - - assert_eq!( - eval(&scope, "String(globalThis.__seen)").unwrap(), - "http://example.com/", - "the fresh global's own listener must receive the event" - ); -} diff --git a/crates/libstarling/tests/wire_response.rs b/crates/libstarling/tests/wire_response.rs new file mode 100644 index 0000000..7f1d1ee --- /dev/null +++ b/crates/libstarling/tests/wire_response.rs @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception + +//! How `prepare_wire_response` reduces a dispatched response to what goes on the wire: which header +//! fields are dropped, when a status or a `HEAD` request rules out content, and which +//! `Content-Length` frames the body. + +#![cfg(not(target_arch = "wasm32"))] + +use libstarling::{prepare_wire_response, WireResponse}; +use platform::http::OutgoingBody; + +fn headers(pairs: &[(&str, &str)]) -> Vec<(String, String)> { + pairs + .iter() + .map(|(n, v)| (n.to_string(), v.to_string())) + .collect() +} + +fn names(response: &WireResponse) -> Vec { + response + .headers + .iter() + .map(|(name, _)| name.as_str().to_string()) + .collect() +} + +#[test] +fn framing_headers_from_the_handler_are_dropped() { + let response = prepare_wire_response( + false, + 200, + headers(&[ + ("Content-Length", "10000"), + ("transfer-encoding", "chunked"), + ("CONNECTION", "keep-alive"), + ("x-keep", "me"), + ]), + OutgoingBody::Bytes(bytes::Bytes::from_static(b"short")), + ); + assert_eq!(names(&response), ["x-keep"]); +} + +#[test] +fn headers_that_could_split_the_response_are_dropped() { + let response = prepare_wire_response( + false, + 200, + headers(&[ + ("x-cr", "a\rb"), + ("x-lf", "a\nb"), + ("x-nul", "a\0b"), + ("x-na\rme", "fine"), + ("x-keep", "me"), + ]), + OutgoingBody::Bytes(bytes::Bytes::new()), + ); + assert_eq!(names(&response), ["x-keep"]); +} + +/// Each code unit of a value becomes one byte. A code unit above U+00FF becomes `?`. +#[test] +fn header_values_are_isomorphic_encoded() { + let response = prepare_wire_response( + false, + 200, + headers(&[("x-latin1", "caf\u{e9}"), ("x-wide", "a\u{2603}b")]), + OutgoingBody::Bytes(bytes::Bytes::new()), + ); + assert_eq!(response.headers["x-latin1"].as_bytes(), b"caf\xe9"); + assert_eq!(response.headers["x-wide"].as_bytes(), b"a?b"); +} + +/// The body a response sends, for a test that only ever builds in-memory ones. +fn content(response: &WireResponse) -> &[u8] { + match &response.body { + OutgoingBody::Bytes(bytes) => bytes, + _ => panic!("the test built an in-memory body"), + } +} + +/// A `1xx`, `204` or `304` has no content and no declared length, whatever the body, the +/// handler's `Content-Length` and the method. +#[test] +fn a_bodiless_status_has_no_content_or_length() { + for status in [103, 204, 304] { + for head_request in [false, true] { + for streamed in [false, true] { + let body = if streamed { + platform::http::body_channel().1 + } else { + OutgoingBody::Bytes(bytes::Bytes::from_static(b"ignored")) + }; + let response = prepare_wire_response( + head_request, + status, + headers(&[("x-keep", "me"), ("content-length", "7")]), + body, + ); + let case = format!("{status} head={head_request} streamed={streamed}"); + assert!(content(&response).is_empty(), "{case}"); + assert_eq!(response.declared_length, None, "{case}"); + assert_eq!(names(&response), ["x-keep"], "{case}"); + } + } + } +} + +#[test] +fn a_head_request_declares_the_length_it_would_have_sent() { + let response = prepare_wire_response( + true, + 200, + Vec::new(), + OutgoingBody::Bytes(bytes::Bytes::from_static(b"BODYBYTES")), + ); + assert!(content(&response).is_empty()); + assert_eq!(response.declared_length, Some(9)); +} + +#[test] +fn a_head_request_declares_no_length_it_would_have_to_produce() { + // A streamed body's length is only knowable by generating the content. + let (_sender, streamed) = platform::http::body_channel(); + let response = prepare_wire_response(true, 200, Vec::new(), streamed); + assert!(content(&response).is_empty()); + assert_eq!(response.declared_length, None); +} + +/// A streamed body has no length until it is produced, so the handler's declaration is the +/// only thing that can frame it. +#[test] +fn a_streamed_body_takes_the_declared_length() { + let (_sender, streamed) = platform::http::body_channel(); + let response = + prepare_wire_response(false, 200, headers(&[("content-length", "42")]), streamed); + assert_eq!(response.declared_length, Some(42)); + assert_eq!(names(&response), Vec::::new()); +} + +/// Content already in memory is measured. A declaration that disagrees with it would frame the +/// response as something other than what it sends. +#[test] +fn an_in_memory_body_is_measured_not_declared() { + let response = prepare_wire_response( + false, + 200, + headers(&[("content-length", "10000")]), + OutgoingBody::Bytes(bytes::Bytes::from_static(b"short")), + ); + assert_eq!(response.declared_length, Some(5)); + assert_eq!(content(&response), b"short"); +} + +/// Only an unambiguous `1*DIGIT` that fits a `u64` frames a response. Repeated fields arrive +/// comma-joined, which is not a number, and two separate fields are refused as well. +#[test] +fn a_declared_length_that_is_not_one_number_is_refused() { + for value in [ + "", + "x", + "10000, 10000", + " 5", + "+5", + "5.0", + "0x5", + "-1", + "18446744073709551616", + ] { + let (_sender, streamed) = platform::http::body_channel(); + let response = + prepare_wire_response(false, 200, headers(&[("content-length", value)]), streamed); + assert_eq!(response.declared_length, None, "{value:?}"); + } + let (_sender, streamed) = platform::http::body_channel(); + let response = prepare_wire_response( + false, + 200, + headers(&[("content-length", "5"), ("Content-Length", "5")]), + streamed, + ); + assert_eq!(response.declared_length, None, "two fields"); +} + +/// A consumed body doesn't send any bytes, so no length frames it. +#[test] +fn a_consumed_body_declares_no_length() { + let response = prepare_wire_response( + false, + 200, + headers(&[("content-length", "42")]), + OutgoingBody::Consumed, + ); + assert_eq!(response.declared_length, None); +} + +/// RFC 9110 §9.3.2: a `HEAD` may report the length its `GET` would have had. The handler's +/// declaration takes precedence over the length of an in-memory body. +#[test] +fn a_head_request_declares_the_length_it_was_given() { + let response = prepare_wire_response( + true, + 200, + headers(&[("content-length", "12345")]), + OutgoingBody::Bytes(bytes::Bytes::from_static(b"BODYBYTES")), + ); + assert!(content(&response).is_empty()); + assert_eq!(response.declared_length, Some(12345)); +} diff --git a/crates/platform/Cargo.toml b/crates/platform/Cargo.toml index 7444791..199b613 100644 --- a/crates/platform/Cargo.toml +++ b/crates/platform/Cargo.toml @@ -7,14 +7,13 @@ authors.workspace = true edition.workspace = true [lib] -# No unit tests; skip building the (SpiderMonkey-sized) lib test harness binary. +# No unit tests. This skips building the (SpiderMonkey-sized) lib test harness binary. test = false [dependencies] bytes.workspace = true futures-channel.workspace = true futures-lite.workspace = true -url.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] libc = "0.2" @@ -22,6 +21,7 @@ reqwest.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] http.workspace = true +url.workspace = true wasip3.workspace = true wit-bindgen.workspace = true diff --git a/crates/platform/src/clock.rs b/crates/platform/src/clock.rs index c1ddb4b..c2dd48c 100644 --- a/crates/platform/src/clock.rs +++ b/crates/platform/src/clock.rs @@ -8,10 +8,6 @@ //! instance's future. [`record_snapshot_reading`] captures the clock as the snapshot is made and //! [`resume_from_snapshot`] adds that reading to every reading afterwards, so a resumed instance //! continues the timeline the snapshot ended on instead of restarting it. -//! -//! SpiderMonkey keeps the same kind of state (GC phase timestamps, the last collection's end) and -//! reads its own copy of the same host clock, so an embedder passes the value -//! [`resume_from_snapshot`] returns on to the engine as well. use std::sync::atomic::{AtomicU64, Ordering}; @@ -86,7 +82,7 @@ pub fn coarse_monotonic_ns() -> u64 { /// Record where the clock stood as a snapshot was made. /// -/// Must be the last thing an instance being wizened does, since a reading taken afterwards +/// An instance being wizened must call this last, since a reading taken afterwards /// would sit past the recorded one and so still be in a resumed instance's future. pub fn record_snapshot_reading() { SNAPSHOT_NS.store(host_monotonic_ns(), Ordering::Relaxed); @@ -114,8 +110,7 @@ pub async fn sleep(duration: std::time::Duration) { /// /// `std::time::Instant` reads the host clock directly, so a deadline taken before a snapshot sits /// in the resumed instance's future by however long the wizened instance ran. This reads -/// [`monotonic_ns`] instead, whose offset makes a resumed instance continue the timeline the -/// snapshot ended on, so such a deadline is already past when the instance resumes. +/// [`monotonic_ns`] instead. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Instant(u64); diff --git a/crates/platform/src/http.rs b/crates/platform/src/http.rs index 8750fd0..fcbbf83 100644 --- a/crates/platform/src/http.rs +++ b/crates/platform/src/http.rs @@ -5,18 +5,19 @@ //! `send` returns once the response status and headers are available; the body //! is a [`IncomingBody`] read lazily from the host (reqwest on native, the //! WASIp3 incoming body stream on wasm). Callers either drain it with -//! [`IncomingBody::read_all`] or pull it chunk by chunk with -//! [`IncomingBody::next_chunk`]. +//! [`IncomingBody::read_all`] or read it piece by piece with +//! [`IncomingBody::read_into`]. Natively, `IncomingBody::next_chunk` also returns +//! each chunk as it was received. //! //! This module holds the platform-agnostic parts; the backends live in //! [`http_native`](crate::http_native) and [`http_wasm`](crate::http_wasm), //! whose platform-specific items are re-exported here. #[cfg(not(target_arch = "wasm32"))] -pub use crate::http_native::{incoming_body_channel, IncomingBody, IncomingBodySender}; +pub use crate::http_native::{incoming_body_channel, send, IncomingBody, IncomingBodySender}; #[cfg(target_arch = "wasm32")] pub use crate::http_wasm::{ - build_outgoing_response, read_incoming_request, AbandonBody, BodyDone, BodySendOutcome, + build_outgoing_response, read_incoming_request, send, AbandonBody, BodyDone, BodySendOutcome, IncomingBody, }; @@ -26,11 +27,8 @@ pub struct Request { pub method: String, /// The absolute request URL. pub url: String, - /// Header (name, value) pairs, in order. - /// - /// Values are `ByteString`s: every code unit is ≤ 0xFF and stands for one - /// byte on the wire, so they are [`isomorphic_encode`]d rather than - /// UTF-8-encoded when sent. See [`isomorphic_encode`] for why. + /// Header (name, value) pairs, in order. Values are `ByteString`s, which are + /// [`isomorphic_encode`]d when sent. pub headers: Vec<(String, String)>, /// The request body. pub body: OutgoingBody, @@ -38,7 +36,7 @@ pub struct Request { /// A body chunk a [`Remaining`] budget can trim. pub trait BodyChunk { - /// How many bytes the chunk carries. + /// The chunk's length in bytes. fn byte_len(&self) -> usize; /// Shorten the chunk to its first `len` bytes. fn truncate_to(&mut self, len: usize); @@ -68,7 +66,7 @@ impl BodyChunk for Vec { /// A response is framed by the `Content-Length` it declares, so content past that never goes on /// the wire, and content short of it leaves the message unfinished. /// -/// `None` where no length was declared. +/// `None` if no length was declared. pub struct Remaining(Option); impl Remaining { @@ -107,16 +105,13 @@ pub type OutgoingBodyReceiver = futures_channel::mpsc::Receiver, /// from elsewhere (such as a JS `ReadableStream` pumped through a channel), or /// a host response body handed straight through. pub enum OutgoingBody { - /// A complete byte sequence (empty for no body). Refcounted, so replaying - /// it across redirects or handing it between layers never copies the bytes. + /// A complete byte sequence, empty for no body. Bytes(bytes::Bytes), - /// Chunks delivered on a bounded channel; a final `Err` aborts the body, and - /// the channel closing ends it. Created via [`body_channel`]. + /// Chunks delivered on a bounded channel. A final `Err` aborts the body, and + /// closing the channel ends it. Created via [`body_channel`]. Stream(OutgoingBodyReceiver), - /// A host response body handed straight to the outgoing request. Used for - /// incoming bodies that are directly used as outgoing ones, or when - /// piped through an identity `TransformStream`. In that case, handling of the - /// body is unobservable by JS and can take a shortcut. + /// A host body sent unchanged, for an incoming body used directly as an outgoing one or + /// piped through an identity `TransformStream`. Host(IncomingBody), /// No body to send, because none is left or none was ever produced: a stream /// body consumed by a previous send (a redirect cannot replay it), or one @@ -124,9 +119,7 @@ pub enum OutgoingBody { Consumed, } -/// How many body chunks may sit in a body channel before the producer must wait -/// — bounds buffering so a fast producer with a slow consumer applies -/// backpressure rather than buffering the whole body. +/// How many chunks a body channel holds before the producer waits. pub(crate) const BODY_CHANNEL_CAPACITY: usize = 8; /// The size up to which [`coalesce_queued_chunks`] appends queued chunks. @@ -160,9 +153,7 @@ pub fn coalesce_queued_chunks( None } -/// Wait for channel capacity, then enqueue. `futures_channel`'s `Sender` has no -/// inherent async `send`, so drive `poll_ready` directly (avoids pulling in -/// `futures-util`'s `SinkExt`). Returns `false` if the receiver was dropped. +/// Wait for channel capacity, then enqueue `item`. Returns `false` if the receiver was dropped. pub(crate) async fn send_on(sender: &mut futures_channel::mpsc::Sender, item: T) -> bool { if futures_lite::future::poll_fn(|cx| sender.poll_ready(cx)) .await @@ -213,12 +204,9 @@ pub fn body_channel() -> (BodySender, OutgoingBody) { (BodySender(sender), OutgoingBody::Stream(receiver)) } -/// A body that fails as soon as the transport reads it. -/// -/// For a caller that cannot produce the bytes it promised but is already -/// committed to sending. The alternative would be sending an empty body, which -/// the peer cannot tell from a legitimately empty one, so a truncated request -/// would look successful. +/// A body that fails as soon as the transport reads it, for a caller that is committed to +/// sending but cannot produce the bytes. An empty body instead would make the truncated request +/// look successful. pub fn failed_body(message: String) -> OutgoingBody { let (mut sender, body) = futures_channel::mpsc::channel(1); // The channel is empty and has capacity, so this cannot fail. @@ -242,13 +230,7 @@ pub struct Response { /// /// -/// Encode a header value to its wire bytes: one byte per code unit. -/// -/// An HTTP header value is a byte sequence, and the WebIDL `ByteString` a script -/// supplies maps each code unit one-to-one onto a byte — `"æ"` (U+00E6) is the -/// single byte 0xE6. Encoding the value as UTF-8 instead would send 0xC3 0xA6, -/// which is a different header value; a peer echoing it back would return two -/// code units, and a server matching on it would not match. +/// Encode a header value to its wire bytes: one byte per code unit, not UTF-8. /// /// A code unit above 0xFF cannot occur in a `ByteString` and is replaced by `?` /// rather than truncated into an unrelated byte. @@ -261,10 +243,8 @@ pub fn isomorphic_encode(value: &str) -> Vec { /// /// -/// Decode wire bytes to a header value: one code unit per byte. -/// -/// The inverse of [`isomorphic_encode`]. This can't fail, since it's not treating -/// the input as, possibly invalid, UTF-8. +/// Decode wire bytes to a header value: one code unit per byte. The inverse of +/// [`isomorphic_encode`]. pub fn isomorphic_decode(bytes: &[u8]) -> String { bytes.iter().map(|&b| char::from(b)).collect() } @@ -281,21 +261,5 @@ impl std::fmt::Display for Error { impl std::error::Error for Error {} -/// A read-only, refcounted byte sequence from a response body. Derefs to -/// `&[u8]`. On native it is reqwest's `Bytes` as-is (the body is never copied -/// into a `Vec` just to be read); on wasm the bytes read from the WASIp3 -/// stream are wrapped without copying. +/// A read-only, refcounted byte sequence from a body. pub type BodyBytes = bytes::Bytes; - -/// Send `request` over the host's HTTP transport, returning once the response -/// status and headers are available. -pub async fn send(request: Request) -> Result { - #[cfg(not(target_arch = "wasm32"))] - { - crate::http_native::send(request).await - } - #[cfg(target_arch = "wasm32")] - { - crate::http_wasm::send(request).await - } -} diff --git a/crates/platform/src/http_native.rs b/crates/platform/src/http_native.rs index 458c4a3..97eb7d8 100644 --- a/crates/platform/src/http_native.rs +++ b/crates/platform/src/http_native.rs @@ -10,37 +10,20 @@ use crate::http::{ }; thread_local! { - // Redirects are followed by the fetch layer (so native and wasm behave identically and - // per spec), so the client itself must not follow them. + // The fetch layer follows redirects, so the client must not. // - // TODO(keep-alive robustness): idle connection pooling is left enabled (reqwest's - // default) for performance, but it has a known robustness gap against non-compliant - // servers. hyper applies RFC 7230 §3.3.3 strictly: a body-exempt response (HEAD, 1xx, - // 204, 304) has a zero-length body regardless of any `Content-Length`. A server that - // nevertheless *sends* a body with such a status, such as WPT `status.py`, - // leaves those bytes unread on the socket; hyper treats the message as - // complete and returns the connection to the pool. The next request to reuse that - // connection reads the stale bytes as a status line and fails with a parse error - // (`hyper::Error(Parse(Version))`). hyper auto-retries idempotent methods on a fresh - // connection, but a POST is not retried (it may already have been processed), so it - // surfaces as a spurious network error. Symptom: the `response-null-body` WPT subtests - // for 204/304 fail intermittently (see their FLAKY expectations). - // - // This cannot be fixed through reqwest: it exposes no connection-eviction or - // idle-flush API, and dropping a body-exempt `Response` unread does not close its - // connection (hyper considers a length-0 body already complete and pools it anyway). - // Instead, fixing this would require building our own pool directly on hyper. - // See wasmtime PR #12635 for the related "ignore a connection error that arrives after - // the response" technique. - // Ideally this is fixed upstream in reqwest/hyper (detect leftover bytes after a - // body-exempt response and refuse to pool that connection) rather than by hand-rolling - // a client here. + // TODO: a server that sends a body with a body-exempt response (HEAD, 1xx, 204, 304) leaves + // those bytes unread on a pooled connection, and the next request on it fails to parse its + // response. hyper retries idempotent methods but not a POST, which makes the + // `response-null-body` WPT subtests for 204 and 304 flaky. reqwest has no API to evict such a + // connection, so fixing this needs a pool built on hyper or a fix upstream. static CLIENT: reqwest::Client = reqwest::Client::builder() .redirect(reqwest::redirect::Policy::none()) .build() .expect("failed to build HTTP client"); } +/// Send `request` with reqwest, returning once the response status and headers are available. pub async fn send(request: Request) -> Result { let client = CLIENT.with(|c| c.clone()); let method = reqwest::Method::from_bytes(request.method.as_bytes()) @@ -52,11 +35,9 @@ pub async fn send(request: Request) -> Result { match request.body { OutgoingBody::Bytes(bytes) if !bytes.is_empty() => builder = builder.body(bytes), OutgoingBody::Bytes(_) | OutgoingBody::Consumed => {} - // Stream the body chunks (read from the channel) as they arrive. OutgoingBody::Stream(receiver) => { builder = builder.body(reqwest::Body::wrap_stream(receiver)) } - // Incoming→outgoing shortcut: stream a host body straight through. OutgoingBody::Host(host_body) => { builder = match host_body.0 { NativeBody::Transport(response) => { @@ -66,7 +47,7 @@ pub async fn send(request: Request) -> Result { } } } - let response = builder.send().await.map_err(|e| Error(e.to_string()))?; + let response = builder.send().await.map_err(|e| transport_error(&e))?; let status = response.status().as_u16(); let headers = response .headers() @@ -85,14 +66,8 @@ pub async fn send(request: Request) -> Result { }) } -/// The write end of a channel-backed [`IncomingBody`]: the native serve loop's -/// connection reader sends the request-body chunks it decodes here, and the -/// handler's `request.body` reads them out. The send awaits channel capacity, so -/// a client uploading faster than the handler consumes is paced by TCP -/// backpressure rather than buffered whole. Dropping it ends the body. -/// -/// Native-only: on wasm an incoming body is already a `wasi:http` stream, which -/// [`IncomingBody`] wraps directly. +/// The write end of a channel-backed [`IncomingBody`]. Sends await channel capacity, and +/// dropping it ends the body. pub struct IncomingBodySender(futures_channel::mpsc::Sender>); impl IncomingBodySender { @@ -126,9 +101,8 @@ pub struct IncomingBody(NativeBody); enum NativeBody { /// A response from the HTTP client, read straight off its connection. Transport(reqwest::Response), - /// Chunks delivered on a channel by whoever owns the connection — the serve - /// loop reading an incoming request body. A final `Err` aborts the body; the - /// channel closing ends it. Created via [`incoming_body_channel`]. + /// Chunks delivered on a channel created by [`incoming_body_channel`]. A final `Err` aborts + /// the body, and closing the channel ends it. Channel(futures_channel::mpsc::Receiver>), } @@ -137,7 +111,7 @@ impl IncomingBody { pub async fn read_all(self) -> Result { match self.0 { NativeBody::Transport(response) => { - response.bytes().await.map_err(|e| Error(e.to_string())) + response.bytes().await.map_err(|e| transport_error(&e)) } NativeBody::Channel(mut receiver) => { let mut buffer = bytes::BytesMut::new(); @@ -149,13 +123,40 @@ impl IncomingBody { } } + /// Read the next chunk, or return `Ok(None)` at the end of the body. A chunk is read whole, so + /// it can hold more bytes than `buf` has capacity for. It is returned in its own allocation if + /// it is the only reference to it, and copied into an allocation of its size otherwise, so + /// `buf` is unused. Empty chunks are skipped. + pub async fn read_into(&mut self, _buf: Vec) -> Result>, Error> { + loop { + let Some(chunk) = self.next_chunk().await? else { + return Ok(None); + }; + if !chunk.is_empty() { + return Ok(Some(Vec::from(chunk))); + } + } + } + /// Read the next chunk, or `Ok(None)` at end of body. pub async fn next_chunk(&mut self) -> Result, Error> { match &mut self.0 { NativeBody::Transport(response) => { - response.chunk().await.map_err(|e| Error(e.to_string())) + response.chunk().await.map_err(|e| transport_error(&e)) } NativeBody::Channel(receiver) => receiver.next().await.transpose(), } } } + +/// The transport error for `error`: its message, followed by the messages of its sources. +fn transport_error(error: &reqwest::Error) -> Error { + let mut message = error.to_string(); + let mut source = std::error::Error::source(error); + while let Some(cause) = source { + message.push_str(": "); + message.push_str(&cause.to_string()); + source = cause.source(); + } + Error(message) +} diff --git a/crates/platform/src/http_wasm.rs b/crates/platform/src/http_wasm.rs index ad96e4b..91a1b6e 100644 --- a/crates/platform/src/http_wasm.rs +++ b/crates/platform/src/http_wasm.rs @@ -34,11 +34,8 @@ struct BodyParts { /// straight through, trailers and all. A body-less request/response /// gets no stream at all. /// -/// Handing a host body over is also handing over the only place its send could be bounded: the -/// host reads that stream itself, for as long as the peer keeps feeding it, so `body_timeout` does -/// not reach one. Keeping hold of it would mean copying every chunk of every proxied body through -/// the guest, which costs far more than it buys — how long a transfer the host is itself pumping -/// may take is the host's to limit. +/// The host reads a host body's stream itself, so `body_timeout` doesn't apply to it. Bounding it +/// would require copying every chunk of a proxied body through the guest. fn body_contents(body: OutgoingBody) -> (BodyParts, TrailersWriter) { let (trailers_tx, trailers_rx) = wit_future::new(|| Ok(None)); let (contents, piped_trailers, writer) = match body { @@ -62,24 +59,21 @@ fn body_contents(body: OutgoingBody) -> (BodyParts, TrailersWriter) { ) } -/// How a body's send ended, reported through [`BodyDone`]. The distinctions -/// mirror the ones the native transport reads off its own socket write, so the -/// serve path can raise the same aborts (a spent clock, a lost connection) on -/// both targets. +/// How a body's send ended, reported through [`BodyDone`]. pub enum BodySendOutcome { /// Every chunk was handed to the host. Sent, /// The body ended before the `Content-Length` the response declared for it. Truncated, - /// The `timeout` given to [`spawn_body_writer`] cut the send. + /// The `timeout` given to [`spawn_body_writer`] expired before the send finished. TimedOut, /// The send failed: the host dropped the stream's read end (the connection /// is gone), or a streamed chunk was itself an error. Failed(String), } -/// Resolves once a body's writer task has ended, with how it ended — or with -/// `None` for a task cancelled before it could say (instance teardown). +/// Resolves once a body's writer task has ended, with how it ended, or with `None` if the task +/// was cancelled first, as at instance teardown. pub struct BodyDone(futures_channel::oneshot::Receiver); impl std::future::Future for BodyDone { @@ -94,19 +88,14 @@ impl std::future::Future for BodyDone { /// Feed the owned body (in-memory, or streamed through the channel) to its /// wit stream from a spawned task, then write the trailers: `Ok(None)` on a -/// clean end, an `ErrorCode` when a streamed chunk errored. The trailers -/// future is the only abort signal a wit stream's consumer can observe, so -/// an aborted body must not look like a clean (truncated) one. +/// clean end, an `ErrorCode` otherwise. The trailers future is the only abort +/// signal a wit stream's consumer can observe. /// -/// `timeout` bounds the write: a handler that never ends its stream would -/// otherwise hold the wit stream open for the life of the instance. Running -/// out is an abort like any other, error trailers and all. It belongs here -/// rather than with the caller watching the same clock, because only this task -/// can end the stream. +/// `timeout` bounds the write, so a handler that never ends its stream doesn't hold the wit +/// stream open for the life of the instance. Running out aborts the body with error trailers. /// /// The returned [`BodyDone`] resolves when the send is over, with its -/// [`BodySendOutcome`]. A caller with no use for that (the request path) just -/// drops it. +/// [`BodySendOutcome`]. fn spawn_body_writer( writer_body: Option<(BodyStreamWriter, OutgoingBody)>, trailers_tx: TrailersWriter, @@ -164,16 +153,16 @@ fn spawn_body_writer( Some(message.clone()), )), }; - // The outcome goes out before the trailers: a host slow to take them - // must not delay the serve path's word that the body phase is over. + // The outcome is sent before the trailers, so a host slow to take the trailers doesn't + // delay the serve path. let _ = done_tx.send(outcome); let _ = trailers_tx.write(trailers).await; }); (BodyDone(done_rx), AbandonBody(abandon_tx)) } -/// A `write_all` that hands back unwritten values hit a dropped read end: the host has torn the -/// response down, so the connection (or the request) is gone. +/// The failure of a `write_all` that returned unwritten values because the host dropped the read +/// end. const HOST_STOPPED_READING: &str = "the host stopped reading the response body"; /// How an abandoned body's send is reported. @@ -191,7 +180,7 @@ fn outcome_at_end(remaining: &crate::http::Remaining) -> BodySendOutcome { } /// Feed a streamed body's chunks to the host as they arrive, until the stream ends, one of them -/// fails, the declared length is full, or [`AbandonBody`] is signaled. +/// fails, the declared length was reached, or [`AbandonBody`] is signaled. async fn write_stream_body( body_tx: &mut BodyStreamWriter, receiver: &mut crate::http::OutgoingBodyReceiver, @@ -210,8 +199,7 @@ async fn write_stream_body( match chunk { Some(Some(Ok(mut chunk))) => { let error = crate::http::coalesce_queued_chunks(receiver, &mut chunk); - // Content past the declared length would leave the host framing a message it has - // no room for, so the body ends here instead. + // Content past the declared length is dropped, and the body ends. let Some(chunk) = remaining.take(chunk) else { return BodySendOutcome::Sent; }; @@ -229,13 +217,12 @@ async fn write_stream_body( } } -/// Finish a body whose pump can no longer run, taking only what the channel already holds: those -/// chunks were put there before the loop ran out, and nothing can add to them now. +/// Finish a body whose pump can no longer run, sending only the chunks the channel already +/// holds. /// -/// If they end the body, the response was complete after all and the signal merely arrived a moment -/// early. If they do not, the handler left a body that will never be finished, and the send is -/// reported failed — which ends the stream as an abort, so the client sees the truncation rather -/// than a complete-looking response (RFC 9112 §7.1). +/// If they end the body, the send succeeded. Otherwise the send is reported failed, which ends +/// the stream as an abort, so the client sees the truncation rather than a complete-looking +/// response (RFC 9112 §7.1). async fn finish_abandoned_body( body_tx: &mut BodyStreamWriter, receiver: &mut crate::http::OutgoingBodyReceiver, @@ -260,11 +247,9 @@ async fn finish_abandoned_body( BodySendOutcome::Failed(ABANDONED_BODY.to_string()) } -/// Tells a streamed body's writer that nothing can produce another chunk: the event loop feeding it -/// has run out of work, so a writer still waiting on the pump would wait forever. The writer takes -/// what the channel already holds and then ends the body — see [`finish_abandoned_body`]. -/// -/// Dropping one instead says nothing; a caller that never learns of such a state simply drops it. +/// Signals a streamed body's writer that the event loop feeding it has run out of work, so no +/// further chunks will be produced. The writer then sends what the channel already holds and ends +/// the body, as [`finish_abandoned_body`] describes. Dropping it doesn't signal the writer. pub struct AbandonBody(futures_channel::oneshot::Sender<()>); impl AbandonBody { @@ -273,12 +258,8 @@ impl AbandonBody { } } -/// Map a method name onto a `wasi:http` method. -/// -/// Matched case-sensitively, and anything unrecognized is passed through as -/// `Other`: Fetch only normalizes the case of `DELETE`, `GET`, `HEAD`, -/// `OPTIONS`, `POST` and `PUT`, so `fetch(url, { method: "patch" })` must go -/// on the wire as `patch`. +/// Map a method name onto a `wasi:http` method, case-sensitively, so `patch` stays `patch`. +/// Anything unrecognized is passed through as `Other`. fn method_of_string(method: &str) -> Method { match method { "GET" => Method::Get, @@ -309,6 +290,8 @@ fn string_of_method(method: Method) -> String { } } +/// Send `request` with the `wasi:http` client, returning once the response status and headers +/// are available. pub async fn send(request: Request) -> Result { let parsed = url::Url::parse(&request.url).map_err(|e| Error(format!("invalid URL: {e}")))?; let scheme = match parsed.scheme() { @@ -386,7 +369,6 @@ pub async fn send(request: Request) -> Result { let _ = transmitted.await; }); - // Send the request via the outgoing HTTP client. let response = wasip3::http::client::send(wasi_request) .await .map_err(|e| Error(format!("fetch failed: {e:?}")))?; @@ -418,7 +400,7 @@ pub async fn send(request: Request) -> Result { /// Read an incoming `wasi:http` request into its parts (method, full URL, headers, body). /// -/// Returns `Err` for a field the host accepted that `http` cannot represent. +/// Returns `Err` for a field that the host accepted but `http` cannot represent. pub fn read_incoming_request( request: WasiRequest, ) -> Result<(String, String, http::HeaderMap, IncomingBody), ErrorCode> { @@ -459,9 +441,8 @@ pub fn read_incoming_request( /// The body is written from a spawned task: an in-memory body inline, /// a host body handed straight through, a stream body forwarded chunk by chunk. /// -/// The returned [`BodyDone`] resolves once the body is out of the guest's -/// hands. That is right away for a host body, which has no writer because the -/// host drains that stream itself. `body_timeout` bounds the send (see +/// The returned [`BodyDone`] resolves once every chunk was handed to the host, +/// which is at once for a host body. `body_timeout` bounds the send (see /// [`spawn_body_writer`]). /// /// `declared_length` is used as the `Content-Length` header, and enforced for guest-produced @@ -475,12 +456,9 @@ pub fn build_outgoing_response( ) -> (WasiResponse, BodyDone, AbandonBody) { let fields = Fields::new(); for (name, value) in &headers { - // Appended one at a time rather than converted with `Fields::try_from`, which goes through - // the all-or-nothing `Fields::from_list`. The host's rules are stricter than - // `prepare_wire_response`'s, since it also reserves the connection-management names it - // writes itself, such as `keep-alive`, and a handler is free to set one, as is an upstream - // whose response is being proxied. Building the fields entry by entry keeps one refusal - // from silently emptying the whole response's headers. + // Appended one at a time rather than converted with the all-or-nothing + // `Fields::from_list`, so a header the host refuses, such as `keep-alive`, is dropped + // without emptying the response's headers. if let Err(e) = fields.append(name.as_str(), value.as_bytes()) { eprintln!( "serve: dropping the response header `{name}`, which the host refused: {e:?}" @@ -490,10 +468,8 @@ pub fn build_outgoing_response( let (body, trailers_tx) = body_contents(body); let (response, _result) = WasiResponse::new(fields, body.contents, body.trailers); - // The caller has already mapped the status onto the range HTTP allows, but what `wasi:http` - // will put on the wire is the host's call and may be narrower still. Leaving a refusal - // unhandled would send the response under `Response`'s default status, which is a wrong answer - // rather than a failed one, so a refused status degrades to a 500. + // The host can refuse a status that HTTP allows. A refused status becomes 500 rather than + // `Response`'s default status. if response.set_status_code(status).is_err() { eprintln!("the host refused status {status}; answering with a 500 instead"); let _ = response.set_status_code(500); @@ -504,8 +480,9 @@ pub fn build_outgoing_response( (response, body_done, abandon) } -/// A body read lazily from the host's incoming `wasi:http` body stream: an HTTP -/// client response, or an incoming request body on the serve path. +/// Bytes read lazily from a host stream: the body of an HTTP client response, an +/// incoming request body on the serve path, or any other `stream` (see +/// [`IncomingBody::from_stream`]). pub struct IncomingBody { stream: BodyStreamReader, /// The body's trailers future, taken when the body ends. @@ -516,6 +493,29 @@ pub struct IncomingBody { } impl IncomingBody { + /// The bytes of `stream`, which has no trailers that could report an abort. + pub fn from_stream(stream: BodyStreamReader) -> Self { + IncomingBody { + stream, + trailers: None, + } + } + + /// The stream the bytes are read from. A stream can't hold an error, so if the body has + /// trailers, a task awaits them and prints an abort they report to stderr. + pub fn into_stream(self) -> BodyStreamReader { + if let Some(trailers) = self.trailers { + wit_bindgen::spawn_local(async move { + if let Err(code) = trailers.await { + eprintln!( + "a stream's source ended the stream early: the body was aborted: {code:?}" + ); + } + }); + } + self.stream + } + /// Read the entire body to a byte sequence. pub async fn read_all(self) -> Result { // `collect` consumes the stream, so destructure first and consult the trailers directly @@ -530,37 +530,34 @@ impl IncomingBody { Ok(bytes) } - /// Read the next chunk, or `Ok(None)` at end of body. - pub async fn next_chunk(&mut self) -> Result, Error> { + /// Read the next bytes into `buf`, at most as many as it has capacity for. Returns the bytes + /// read, or `Ok(None)` at the end of the body. The bytes are never empty. + pub async fn read_into(&mut self, mut buf: Vec) -> Result>, Error> { use wasip3::wit_bindgen::rt::async_support::StreamResult; - let buffer = Vec::with_capacity(8192); - let (status, buffer) = self.stream.read(buffer).await; - match status { - // Values were transferred, so the stream is still open. Report them — even a - // zero-length read, which must *not* fall through to `end_of_body`: the trailers - // future only resolves once the stream is closed, so awaiting it here would hang. - StreamResult::Complete(_) => Ok(Some(bytes::Bytes::from(buffer))), - // The writer dropped its handle: the body has ended. Whether it ended cleanly or - // the peer aborted it is only knowable from the trailers. - StreamResult::Dropped => { - if !buffer.is_empty() { - return Ok(Some(bytes::Bytes::from(buffer))); - } - self.end_of_body().await.map(|()| None) + loop { + let (status, read) = self.stream.read(buf).await; + buf = read; + match status { + // The stream is still open after a zero-length read, so it is read again. + // Awaiting the trailers here would hang, since they only resolve once the stream + // is closed. + StreamResult::Complete(0) => {} + StreamResult::Complete(_) => return Ok(Some(buf)), + // The body has ended. The trailers report whether it ended cleanly. + StreamResult::Dropped if !buf.is_empty() => return Ok(Some(buf)), + StreamResult::Dropped => return self.end_of_body().await.map(|()| None), + // The read was cancelled at teardown. The stream was never reported closed, so + // the trailers may never resolve and aren't awaited. + StreamResult::Cancelled => return Ok(None), } - // The read was cancelled, which is a teardown rather than an end of body. The - // stream was never reported closed, so the trailers may never resolve — do not - // await them. - StreamResult::Cancelled => Ok(None), } } - /// The body's stream has ended: resolve the trailers future to learn whether - /// it ended cleanly or the peer aborted it, surfacing an abort as an error so - /// a truncated body is not mistaken for a complete one. + /// Await the trailers future of a body whose stream has ended, and return an error if the + /// peer aborted the body. /// - /// Only valid once the stream has been reported closed, per `wasi:http`'s - /// `consume-body` docs — the future does not resolve before then. + /// Only valid once the stream has been reported closed. The future does not resolve before + /// then. async fn end_of_body(&mut self) -> Result<(), Error> { let Some(trailers) = self.trailers.take() else { return Ok(()); diff --git a/crates/platform/src/lib.rs b/crates/platform/src/lib.rs index e626f50..2515da0 100644 --- a/crates/platform/src/lib.rs +++ b/crates/platform/src/lib.rs @@ -5,11 +5,8 @@ //! //! Builtins call the backend-agnostic APIs here; each capability holds its //! platform-agnostic parts in one module and its backends in `_native` -//! / `_wasm` modules, selected with `#[cfg(target_arch)]`. The first -//! capability is HTTP ([`http`]), used by `fetch`: native uses an async HTTP -//! client, wasm uses the WASIp3 outgoing-handler. The other is the monotonic -//! clock ([`clock`]). Future host capabilities (sockets, filesystem, …) follow -//! the same shape. +//! / `_wasm` modules, selected with `#[cfg(target_arch)]`. The capabilities +//! are HTTP ([`http`]), used by `fetch`, and the monotonic clock ([`clock`]). pub mod clock; pub mod http; diff --git a/crates/platform/tests/clock.rs b/crates/platform/tests/clock.rs index deb8e31..0a5a08c 100644 --- a/crates/platform/tests/clock.rs +++ b/crates/platform/tests/clock.rs @@ -23,8 +23,8 @@ fn resuming_advances_the_clock_by_the_recorded_reading() { "readings after the resume should sit past the snapshot's" ); - // The reading is consumed by the first resume, so a second one is a no-op. Every later call - // in a resumed instance takes this path. + // The reading is consumed by the first resume, so a second resume does nothing. Every later + // call in a resumed instance takes this path. assert_eq!(resume_from_snapshot(), 0); } diff --git a/crates/serve-test-support/Cargo.toml b/crates/serve-test-support/Cargo.toml index ad0115e..1965699 100644 --- a/crates/serve-test-support/Cargo.toml +++ b/crates/serve-test-support/Cargo.toml @@ -12,7 +12,7 @@ edition.workspace = true # builds only this crate and the component, not the native engine. [lib] -# No unit tests; the integration test in `tests/` links the lib. +# No unit tests. The integration test in `tests/` links the lib. test = false [lints] diff --git a/crates/serve-test-support/src/lib.rs b/crates/serve-test-support/src/lib.rs index 123e15d..5cbeda1 100644 --- a/crates/serve-test-support/src/lib.rs +++ b/crates/serve-test-support/src/lib.rs @@ -17,13 +17,35 @@ use std::time::Duration; /// Like [`request`], but gives up after `patience` and returns `None` — for the cases where a /// server that fails the test would otherwise leave it hanging rather than failing it. pub fn request_within(port: u16, path: &str, patience: Duration) -> Option { - let response = full_request_within(port, "GET", path, "", patience)?; - Some( - response - .split_once("\r\n\r\n") - .map(|(_, body)| body.to_string()) - .unwrap_or(response), - ) + full_request_within(port, "GET", path, "", patience).map(body_of) +} + +/// The part of `response` after the header terminator. See [`split_response`]. +fn body_of(response: String) -> String { + split_response(&response).1.to_string() +} + +/// The head and the body of `response`, split at the header terminator. A response with no header +/// terminator is malformed rather than empty. It has no head, and the whole response is its body, +/// so an assertion on the body cannot pass against nothing at all. +pub(crate) fn split_response(response: &str) -> (Option<&str>, &str) { + match response.split_once("\r\n\r\n") { + Some((head, body)) => (Some(head), body), + None => (None, response), + } +} + +/// The `Content-Length` value in `head`, which must already be lowercase. +fn content_length(head: &str) -> Option { + head.split("content-length:") + .nth(1) + .and_then(|rest| rest.split("\r\n").next()) + .and_then(|value| value.trim().parse().ok()) +} + +/// The size on a chunk-size line, with any chunk extension after `;` ignored. +fn chunk_size(size_line: &str) -> Option { + usize::from_str_radix(size_line.split(';').next()?.trim(), 16).ok() } /// Like [`request_within`], but for any method and body, and returning the whole response — status @@ -48,14 +70,19 @@ pub fn raw_response_within( body: &str, patience: Duration, ) -> Option> { - let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); let req = format!( "{method} {path} HTTP/1.1\r\nHost: localhost\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len(), ); + send_within(port, req.as_bytes(), patience) +} + +/// Send `raw` on a new connection and read the response to EOF within `patience`. +fn send_within(port: u16, raw: &[u8], patience: Duration) -> Option> { + let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); // A refused request is responded to and its connection closed while this is still writing, so // a failed write is part of the outcome rather than an error. - let _ = stream.write_all(req.as_bytes()); + let _ = stream.write_all(raw); read_until_eof(&mut stream, patience) } @@ -64,32 +91,62 @@ pub fn raw_response_within( /// test instead of hanging the run. pub const PATIENCE: Duration = Duration::from_secs(30); +/// How long [`eventually`] polls before returning its last result. Finite so that work which never +/// completes fails its test. +pub const SETTLE_PATIENCE: Duration = Duration::from_secs(20); + +/// Call `poll` every 20 ms until its result satisfies `settled` or [`SETTLE_PATIENCE`] runs out, +/// and return the last result. +/// +/// Work a handler leaves behind runs while the request's loop is drained after the response: a +/// `waitUntil` promise, a signal's reactions, a stream's cancel. Polling until the outcome is there +/// takes as long as the work does, rather than a fixed span chosen for the slowest machine. +/// +/// Only for assertions that wait for something to happen. A request drives the loop it is served +/// on, so an assertion that something does not happen waits a fixed span instead. +pub fn eventually(mut poll: impl FnMut() -> String, settled: impl Fn(&str) -> bool) -> String { + let deadline = std::time::Instant::now() + SETTLE_PATIENCE; + loop { + let result = poll(); + if settled(&result) || std::time::Instant::now() >= deadline { + return result; + } + std::thread::sleep(Duration::from_millis(20)); + } +} + /// Make one HTTP/1.1 request and return the response body (after the headers). pub fn request(port: u16, method: &str, path: &str, body: &str) -> String { - let response = full_request_within(port, method, path, body, PATIENCE) - .expect("the server answered within the patience"); - // A response with no header terminator is malformed rather than empty, so it is handed back - // whole — an assertion on the body would otherwise pass against nothing at all. - response - .split_once("\r\n\r\n") - .map(|(_, body)| body.to_string()) - .unwrap_or(response) + body_of( + full_request_within(port, method, path, body, PATIENCE) + .expect("the server answered within the patience"), + ) } -/// A minimal upstream HTTP server (multi-connection) that always replies with `body`. -pub fn start_upstream(body: &'static str) -> u16 { +/// A listener on a port the system picks on `127.0.0.1`, and that port. +pub fn bind_local() -> (TcpListener, u16) { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let port = listener.local_addr().unwrap().port(); + (listener, port) +} + +/// A `200` response with `body`, framed by its length, closing the connection. +fn ok_response(body: &str) -> String { + format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len(), + ) +} + +/// A minimal upstream HTTP server (multi-connection) that always replies with `body`. +pub fn start_upstream(body: &'static str) -> u16 { + let (listener, port) = bind_local(); std::thread::spawn(move || { for stream in listener.incoming() { let Ok(mut stream) = stream else { continue }; let mut buf = [0u8; 1024]; let _ = stream.read(&mut buf); - let response = format!( - "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", - body.len(), - ); - let _ = stream.write_all(response.as_bytes()); + let _ = stream.write_all(ok_response(body).as_bytes()); } }); port @@ -98,32 +155,19 @@ pub fn start_upstream(body: &'static str) -> u16 { /// An upstream that responds with a chunked body, pausing between chunks, so a handler forwarding it /// has to actually stream rather than receive the whole thing at once. pub fn start_chunked_upstream(chunks: &'static [&'static str]) -> u16 { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let port = listener.local_addr().unwrap().port(); - std::thread::spawn(move || { - for stream in listener.incoming() { - let Ok(mut stream) = stream else { continue }; - let mut buf = [0u8; 1024]; - let _ = stream.read(&mut buf); - let _ = stream.write_all( - b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n", - ); - for chunk in chunks { - let _ = stream.write_all(format!("{:x}\r\n{chunk}\r\n", chunk.len()).as_bytes()); - let _ = stream.flush(); - std::thread::sleep(Duration::from_millis(20)); - } - let _ = stream.write_all(b"0\r\n\r\n"); - } - }); - port + start_chunked(chunks, true) } /// An upstream whose chunked response is cut off: it sends `chunks` and then closes the connection /// without the terminating chunk, so a handler forwarding it is forwarding a truncation. pub fn start_truncated_upstream(chunks: &'static [&'static str]) -> u16 { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let port = listener.local_addr().unwrap().port(); + start_chunked(chunks, false) +} + +/// An upstream that sends `chunks` as a chunked body, 20 ms apart, and the terminating chunk if +/// `terminate` is set. +fn start_chunked(chunks: &'static [&'static str], terminate: bool) -> u16 { + let (listener, port) = bind_local(); std::thread::spawn(move || { for stream in listener.incoming() { let Ok(mut stream) = stream else { continue }; @@ -137,7 +181,9 @@ pub fn start_truncated_upstream(chunks: &'static [&'static str]) -> u16 { let _ = stream.flush(); std::thread::sleep(Duration::from_millis(20)); } - // No terminator: dropping the connection here is the truncation. + if terminate { + let _ = stream.write_all(b"0\r\n\r\n"); + } } }); port @@ -146,8 +192,7 @@ pub fn start_truncated_upstream(chunks: &'static [&'static str]) -> u16 { /// An upstream that waits `delay` before responding with `body`, so a `fetch` to it is still in /// flight while other work — another request, a previous one's drain — runs alongside it. pub fn start_slow_upstream(delay: Duration, body: &'static str) -> u16 { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let port = listener.local_addr().unwrap().port(); + let (listener, port) = bind_local(); std::thread::spawn(move || { for stream in listener.incoming() { let Ok(mut stream) = stream else { continue }; @@ -155,13 +200,7 @@ pub fn start_slow_upstream(delay: Duration, body: &'static str) -> u16 { let mut buf = [0u8; 1024]; let _ = stream.read(&mut buf); std::thread::sleep(delay); - let _ = stream.write_all( - format!( - "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", - body.len(), - ) - .as_bytes(), - ); + let _ = stream.write_all(ok_response(body).as_bytes()); }); } }); @@ -172,8 +211,7 @@ pub fn start_slow_upstream(delay: Duration, body: &'static str) -> u16 { /// handler proxying it sends a *host* body rather than a JS stream, which the serve timeouts have /// to bound just the same. pub fn start_endless_upstream(every: Duration) -> u16 { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let port = listener.local_addr().unwrap().port(); + let (listener, port) = bind_local(); std::thread::spawn(move || { for stream in listener.incoming() { let Ok(mut stream) = stream else { continue }; @@ -230,20 +268,11 @@ impl HeldUpstream { self.released .store(true, std::sync::atomic::Ordering::SeqCst); } - - /// Hold again, and forget the arrivals counted so far, so a test that runs its scenario more - /// than once gates each run from zero. - pub fn rearm(&self) { - self.released - .store(false, std::sync::atomic::Ordering::SeqCst); - while self.arrivals.try_recv().is_ok() {} - } } /// Start a [`HeldUpstream`] that responds with `body` once released. pub fn start_held_upstream(body: &'static str) -> HeldUpstream { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let port = listener.local_addr().unwrap().port(); + let (listener, port) = bind_local(); let (arrived, arrivals) = std::sync::mpsc::channel(); let released = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); let flag = released.clone(); @@ -259,13 +288,7 @@ pub fn start_held_upstream(body: &'static str) -> HeldUpstream { while !flag.load(std::sync::atomic::Ordering::SeqCst) { std::thread::sleep(Duration::from_millis(5)); } - let _ = stream.write_all( - format!( - "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", - body.len(), - ) - .as_bytes(), - ); + let _ = stream.write_all(ok_response(body).as_bytes()); }); } }); @@ -276,8 +299,8 @@ pub fn start_held_upstream(body: &'static str) -> HeldUpstream { } } -/// A proxy that holds each request until released, then forwards it to `target` and copies the -/// response back. +/// A proxy that holds each request until released, then forwards it to the port passed to +/// [`ForwardingGate::release`] and copies the response back. /// /// For the cases where the guest is the client. A handler fetching its own server sends that /// request while it is still running, and a test cannot delay it. Forwarding it from here sets @@ -285,7 +308,8 @@ pub fn start_held_upstream(body: &'static str) -> HeldUpstream { pub struct ForwardingGate { port: u16, arrivals: std::sync::mpsc::Receiver<()>, - released: std::sync::Arc, + /// The port to forward to, or `0` while requests are held. + target: std::sync::Arc, } impl ForwardingGate { @@ -298,25 +322,24 @@ impl ForwardingGate { self.arrivals.recv_timeout(patience).is_ok() } - /// Forward everything held, and everything that arrives from here on. - pub fn release(&self) { - self.released - .store(true, std::sync::atomic::Ordering::SeqCst); + /// Forward everything held, and everything that arrives from here on, to `target`. + pub fn release(&self, target: u16) { + self.target + .store(target, std::sync::atomic::Ordering::SeqCst); } } -/// Start a [`ForwardingGate`] in front of `target`. -pub fn start_forwarding_gate(target: u16) -> ForwardingGate { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let port = listener.local_addr().unwrap().port(); +/// Start a [`ForwardingGate`]. +pub fn start_forwarding_gate() -> ForwardingGate { + let (listener, port) = bind_local(); let (arrived, arrivals) = std::sync::mpsc::channel(); - let released = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); - let flag = released.clone(); + let target = std::sync::Arc::new(std::sync::atomic::AtomicU16::new(0)); + let released = target.clone(); std::thread::spawn(move || { for stream in listener.incoming() { let Ok(mut from) = stream else { continue }; let arrived = arrived.clone(); - let flag = flag.clone(); + let released = released.clone(); std::thread::spawn(move || { let mut head = Vec::new(); let mut buf = [0u8; 1024]; @@ -327,9 +350,12 @@ pub fn start_forwarding_gate(target: u16) -> ForwardingGate { } } let _ = arrived.send(()); - while !flag.load(std::sync::atomic::Ordering::SeqCst) { - std::thread::sleep(Duration::from_millis(5)); - } + let target = loop { + match released.load(std::sync::atomic::Ordering::SeqCst) { + 0 => std::thread::sleep(Duration::from_millis(5)), + target => break target, + } + }; // Forwarded with `Connection: close`, so the response ends at EOF. The rest of the // request is passed on as it arrived. let text = String::from_utf8_lossy(&head).into_owned(); @@ -355,15 +381,14 @@ pub fn start_forwarding_gate(target: u16) -> ForwardingGate { ForwardingGate { port, arrivals, - released, + target, } } /// An upstream that accepts connections and never responds, holding them open until the process /// ends. A `fetch` to it stays in flight for as long as anything is willing to wait for it. pub fn start_silent_upstream() -> u16 { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let port = listener.local_addr().unwrap().port(); + let (listener, port) = bind_local(); std::thread::spawn(move || { let mut held = Vec::new(); for stream in listener.incoming() { @@ -378,8 +403,7 @@ pub fn start_silent_upstream() -> u16 { /// An upstream that reads a request body, chunked or length-delimited, and responds with it, so a /// handler forwarding a body upstream can be checked on what actually arrived there. pub fn start_echo_upstream() -> u16 { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let port = listener.local_addr().unwrap().port(); + let (listener, port) = bind_local(); std::thread::spawn(move || { for stream in listener.incoming() { let Ok(mut stream) = stream else { continue }; @@ -399,12 +423,7 @@ pub fn start_echo_upstream() -> u16 { let complete = if chunked { body.ends_with("0\r\n\r\n") } else { - lower - .split("content-length:") - .nth(1) - .and_then(|rest| rest.split("\r\n").next()) - .and_then(|value| value.trim().parse::().ok()) - .is_some_and(|length| body.len() >= length) + content_length(&lower).is_some_and(|length| body.len() >= length) }; if !complete { continue; @@ -414,13 +433,7 @@ pub fn start_echo_upstream() -> u16 { } else { body.to_string() }; - let _ = stream.write_all( - format!( - "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{payload}", - payload.len(), - ) - .as_bytes(), - ); + let _ = stream.write_all(ok_response(&payload).as_bytes()); break; } } @@ -436,14 +449,14 @@ pub fn start_echo_upstream() -> u16 { pub fn dechunk(body: &str) -> String { let is_chunked = body .split_once("\r\n") - .is_some_and(|(size_line, _)| usize::from_str_radix(size_line.trim(), 16).is_ok()); + .is_some_and(|(size_line, _)| chunk_size(size_line).is_some()); if !is_chunked { return body.to_string(); } let mut out = String::new(); let mut rest = body; while let Some((size_line, after)) = rest.split_once("\r\n") { - let size = usize::from_str_radix(size_line.trim(), 16).unwrap_or(0); + let size = chunk_size(size_line).unwrap_or(0); if size == 0 { break; } @@ -488,22 +501,13 @@ pub fn read_until_eof(stream: &mut TcpStream, patience: Duration) -> Option Option { - let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); - // A refused request is responded to and its connection closed while this is still writing, so - // a failed write is part of the outcome rather than an error. - let _ = stream.write_all(raw); - let response = read_until_eof(&mut stream, patience)?; + let response = send_within(port, raw, patience)?; Some(String::from_utf8_lossy(&response).into_owned()) } /// Send `raw` bytes and return the full response (status line included). pub fn raw_request(port: u16, raw: &[u8]) -> String { - let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); - // A refused request is responded to and its connection closed while this is still writing, so - // a failed write is part of the outcome rather than an error. - let _ = stream.write_all(raw); - let response = read_until_eof(&mut stream, PATIENCE).unwrap_or_default(); - String::from_utf8_lossy(&response).into_owned() + raw_request_within(port, raw, PATIENCE).unwrap_or_default() } /// Read exactly one HTTP response off a connection that may stay open — framed by its @@ -543,12 +547,7 @@ fn response_end(bytes: &[u8]) -> Option { if head.contains("transfer-encoding: chunked") { return chunked_end(bytes, head_end); } - let length = head - .split("content-length:") - .nth(1) - .and_then(|rest| rest.split("\r\n").next()) - .and_then(|value| value.trim().parse::().ok()) - .unwrap_or(0); + let length = content_length(&head).unwrap_or(0); let end = head_end + length; (bytes.len() >= end).then_some(end) } @@ -558,8 +557,7 @@ fn response_end(bytes: &[u8]) -> Option { fn chunked_end(bytes: &[u8], mut pos: usize) -> Option { loop { let line_end = find(bytes, pos, b"\r\n")?; - let size_hex = std::str::from_utf8(&bytes[pos..line_end]).ok()?; - let size = usize::from_str_radix(size_hex.trim().split(';').next()?.trim(), 16).ok()?; + let size = chunk_size(std::str::from_utf8(&bytes[pos..line_end]).ok()?)?; if size == 0 { // The zero-size line, then the final empty line. let end = line_end + 4; diff --git a/crates/serve-test-support/src/wasm_serve.rs b/crates/serve-test-support/src/wasm_serve.rs index fa85db4..4ada5c5 100644 --- a/crates/serve-test-support/src/wasm_serve.rs +++ b/crates/serve-test-support/src/wasm_serve.rs @@ -8,9 +8,9 @@ //! left for what happens after a response is gone — `waitUntil` work, request-signal aborts, the //! server's own diagnostics — so a test asserts on it with [`WasmServer::wait_for_marker`]. -use std::net::TcpListener; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Stdio}; +use std::sync::{Mutex, OnceLock}; use std::time::{Duration, Instant}; /// The WASI capabilities the component needs: `cli` for `stderr`, the environment its configuration @@ -64,30 +64,34 @@ pub enum Ready { /// suits the common case (a legacy script, no extra configuration, a `/ready` route), so a test /// names only what it cares about. pub struct Serve { - port: u16, files: Vec<(String, String)>, flags: Vec, script: Vec, wasmtime_flags: Vec, env: Vec<(String, String)>, ready: Ready, - wizen: bool, + wizer_passes: usize, no_zeal: bool, } +impl Default for Serve { + fn default() -> Self { + Self::new() + } +} + impl Serve { - /// A server on `port`. Ports are assigned per test from 18400 up, since the suite runs its - /// cases in parallel. - pub fn new(port: u16) -> Self { + /// A server on a port the system picks. The port is read back from `wasmtime serve`'s output + /// and reported by [`WasmServer::port`]. + pub fn new() -> Self { Self { - port, files: Vec::new(), flags: Vec::new(), script: Vec::new(), wasmtime_flags: Vec::new(), env: Vec::new(), ready: Ready::Route, - wizen: false, + wizer_passes: 0, no_zeal: false, } } @@ -157,16 +161,17 @@ impl Serve { } /// Snapshot the configured component with `wasmtime wizer` and serve the snapshot: the shape a - /// deployed server has, with the engine stood up and the script evaluated at build time. + /// deployed server has, with the engine stood up and the script evaluated at build time. Each + /// further call snapshots the previous snapshot again, which runs its init export once more. pub fn wizen(mut self) -> Self { - self.wizen = true; + self.wizer_passes += 1; self } /// Start the server, or return `None` when the wasm component or `wasmtime` is missing. pub fn start(self) -> Option { let component = component()?; - let dir = work_dir(self.port); + let dir = work_dir(); for (name, source) in &self.files { std::fs::write(dir.join(name), source).unwrap(); } @@ -180,32 +185,17 @@ impl Serve { // Compiled here rather than by the server below, so every server after the first loads a // component that is already compiled. - let component = if self.wizen { - let snapshot = dir.join("snapshot.wasm"); + let mut component = component; + for pass in 1..=self.wizer_passes { + let snapshot = dir.join(format!("snapshot-{pass}.wasm")); snapshot_with_wizer(&dir, &component, &snapshot, &config, self.no_zeal); - snapshot - } else { - component - }; + component = snapshot; + } let (component, ahead_of_time) = match precompiled(&component) { Some(cwasm) => (cwasm, true), None => (component, false), }; - // A leaked child from an earlier run would serve every request below, so refuse the port - // rather than test whatever is already on it. A child being reaped still holds its port - // for a moment, so wait a little before calling it a leak. - let deadline = Instant::now() + Duration::from_secs(10); - loop { - match TcpListener::bind(("127.0.0.1", self.port)) { - Ok(listener) => break drop(listener), - Err(e) if Instant::now() >= deadline => { - panic!("port {} is still in use ({e})", self.port) - } - Err(_) => std::thread::sleep(Duration::from_millis(100)), - } - } - let log = std::fs::File::create(dir.join("serve.log")).unwrap(); let mut command = Command::new("wasmtime"); command @@ -215,7 +205,7 @@ impl Serve { .args(ahead_of_time.then_some("--allow-precompiled")) .args(["--dir=.::/cwd", "--dir=.", WASI_FLAGS]) .args(&self.wasmtime_flags) - .args(["--addr", &format!("127.0.0.1:{}", self.port)]) + .args(["--addr", "127.0.0.1:0"]) .arg(&component) .env("STARLINGMONKEY_CONFIG", &config) // Read by `WasmServer::await_new_idle`. @@ -233,16 +223,43 @@ impl Serve { let child = command.envs(self.env.iter().cloned()).spawn().unwrap(); let mut server = WasmServer { - port: self.port, + port: 0, child, dir, }; + server.port = server.await_bound_port(); server.await_ready(self.ready); Some(server) } } impl WasmServer { + /// Poll the serve log for the address `wasmtime serve` reports once it has bound its + /// listener, and return that address's port. Panics with the log if the child exits first. + fn await_bound_port(&mut self) -> u16 { + const BOUND: &str = "Serving HTTP on http://"; + let deadline = Instant::now() + READY_PATIENCE; + while Instant::now() < deadline { + if let Some(status) = self.child.try_wait().unwrap() { + panic!( + "wasmtime serve exited with {status} before listening\n{}", + self.log() + ); + } + let log = self.log(); + let port = log + .split_once(BOUND) + .and_then(|(_, rest)| rest.split_once('/')) + .and_then(|(address, _)| address.rsplit_once(':')) + .and_then(|(_, port)| port.parse().ok()); + if let Some(port) = port { + return port; + } + std::thread::sleep(Duration::from_millis(10)); + } + panic!("wasmtime serve never reported its address\n{}", self.log()); + } + /// Poll until the server responds, or panic with the serve log. A child that died on a /// rejected flag or a runtime that refused its configuration is otherwise a silent hang. fn await_ready(&mut self, ready: Ready) { @@ -255,9 +272,8 @@ impl WasmServer { self.log() ); } - // Probed through a connection of its own: until the server binds, connecting is an - // error rather than a slow response, which the request helpers rightly refuse to paper - // over. + // The server is probed through a separate connection. Until it binds, connecting fails + // rather than being slow, and the request helpers don't hide such failures. let listening = std::net::TcpStream::connect(("127.0.0.1", self.port)).is_ok(); if listening && ready == Ready::Listening { return; @@ -298,12 +314,6 @@ impl WasmServer { self.port } - /// The directory the child runs in: where the content script lives and where a test can leave - /// a file for it to read. - pub fn dir(&self) -> &Path { - &self.dir - } - /// Everything the server has written to `stdout`/`stderr` so far, the guest's `console.error` /// output included. pub fn log(&self) -> String { @@ -354,8 +364,8 @@ impl WasmServer { } } - /// The instances the host reports having handed a request to since `from` bytes into the log, - /// and the length to read the next batch from. + /// The instances to which the host reports having handed a request since `from` bytes into the + /// log, and the length from which to read the next batch. /// /// A serving host hands a request to an instance it counts as free, and raises a fresh one /// otherwise. A case whose requests have to meet cannot make it choose, so it reads back what @@ -419,19 +429,20 @@ impl Drop for WasmServer { fn drop(&mut self) { let _ = self.child.kill(); let _ = self.child.wait(); + if !std::thread::panicking() { + let _ = std::fs::remove_dir_all(&self.dir); + } } } /// A response's message body, with the host's chunked framing decoded. The free-standing form of /// [`WasmServer::request`], for the tests that address a server's port from a thread of their own. pub fn message_body(response: &str) -> String { - let Some((head, body)) = response.split_once("\r\n\r\n") else { - return response.to_string(); - }; - if head.to_lowercase().contains("transfer-encoding: chunked") { - crate::dechunk(body) - } else { - body.to_string() + match crate::split_response(response) { + (Some(head), body) if head.to_lowercase().contains("transfer-encoding: chunked") => { + crate::dechunk(body) + } + (_, body) => body.to_string(), } } @@ -450,17 +461,11 @@ pub fn component() -> Option { "STARLING_WASM_COMPONENT is set, but there is no component at {}", component.display() ); - } - let have_wasmtime = Command::new("wasmtime") - .arg("--version") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .is_ok_and(|status| status.success()); - if let (Some(component), true) = (&configured, have_wasmtime) { - // Absolutized: each server runs in a work directory of its own and hands the path to - // `wasmtime` from there. - return Some(component.canonicalize().unwrap()); + if wasmtime_version().is_some() { + // Absolutized: each server runs in a work directory of its own and hands the path to + // `wasmtime` from there. + return Some(component.canonicalize().unwrap()); + } } NOTED.call_once(|| { let missing = if configured.is_some() { @@ -480,6 +485,21 @@ pub fn component() -> Option { None } +/// The output of `wasmtime --version`, or `None` when `wasmtime` is not on PATH. Run once per +/// process. +fn wasmtime_version() -> Option<&'static str> { + static VERSION: OnceLock> = OnceLock::new(); + VERSION + .get_or_init(|| { + let output = Command::new("wasmtime").arg("--version").output().ok()?; + output + .status + .success() + .then(|| String::from_utf8_lossy(&output.stdout).trim().to_string()) + }) + .as_deref() +} + /// The component compiled ahead of time, or `None` to serve the `.wasm` and let each server /// compile it itself. /// @@ -493,13 +513,9 @@ pub fn component() -> Option { fn precompiled(component: &Path) -> Option { let out = component.with_extension("cwasm"); let stamp_path = component.with_extension("cwasm.stamp"); - let version = Command::new("wasmtime").arg("--version").output().ok()?; + let version = wasmtime_version()?; let modified = std::fs::metadata(component).ok()?.modified().ok()?; - let stamp = format!( - "{}\n{modified:?}\n{}", - String::from_utf8_lossy(&version.stdout).trim(), - CODEGEN_FLAGS.join(" "), - ); + let stamp = format!("{version}\n{modified:?}\n{}", CODEGEN_FLAGS.join(" ")); let current = |stamp: &str| { out.is_file() && std::fs::read_to_string(&stamp_path).is_ok_and(|found| found == stamp) }; @@ -507,8 +523,8 @@ fn precompiled(component: &Path) -> Option { return Some(out); } - // One process compiles. The rest wait for it rather than each compiling the same artifact. - // Whoever creates the lock is the one that compiles. + // One process or thread compiles. The rest wait for it rather than each compiling the same + // artifact. Whoever creates the lock is the one that compiles. let lock = component.with_extension("cwasm.lock"); match std::fs::OpenOptions::new() .write(true) @@ -516,15 +532,25 @@ fn precompiled(component: &Path) -> Option { .open(&lock) { Ok(_) => { + // Checked again, since a compile may have finished and released the lock between the + // check above and taking it. + if current(&stamp) { + let _ = std::fs::remove_file(&lock); + return Some(out); + } eprintln!("Precompiling the wasm component (once for this build) ..."); + // Written next to `out` and renamed over it, so a server never loads a partly + // written artifact. + let partial = component.with_extension("cwasm.partial"); let result = Command::new("wasmtime") .arg("compile") .args(CODEGEN_FLAGS) .arg(component) .arg("-o") - .arg(&out) + .arg(&partial) .output(); - let compiled = result.is_ok_and(|result| result.status.success()); + let compiled = result.is_ok_and(|result| result.status.success()) + && std::fs::rename(&partial, &out).is_ok(); if compiled { let _ = std::fs::write(&stamp_path, &stamp); } @@ -546,16 +572,33 @@ fn precompiled(component: &Path) -> Option { } } -/// A clean directory for one server to run in, kept after the run so a failure's serve log and -/// content script can be looked at. -fn work_dir(port: u16) -> PathBuf { - let dir = std::env::temp_dir().join(format!("starling-serve-wasm/{port}")); +/// A clean directory for one server to run in. +/// +/// The directory is named after the current thread, which the test harness names after the test, +/// and after the process, so concurrent runs of one suite do not share it. A test's second and +/// later servers get a numbered suffix. [`WasmServer`]'s drop removes the directory unless the +/// thread is panicking, so a failure's serve log and content script can be looked at. +fn work_dir() -> PathBuf { + static STARTED: Mutex> = Mutex::new(Vec::new()); + let thread = std::thread::current(); + let test = thread.name().unwrap_or("server").replace("::", "-"); + let mut started = STARTED.lock().unwrap_or_else(|e| e.into_inner()); + let earlier = started.iter().filter(|name| **name == test).count(); + started.push(test.clone()); + drop(started); + let process = std::process::id(); + let name = match earlier { + 0 => format!("{test}-{process}"), + _ => format!("{test}-{process}-{}", earlier + 1), + }; + let dir = std::env::temp_dir().join("starling-serve-wasm").join(name); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); dir } -/// Snapshot `component`, configured with `config`, into `out`. +/// Snapshot `component`, configured with `config` and run in `dir`, into `out`. With `no_zeal`, +/// SpiderMonkey runs under its ordinary GC (see [`Serve::without_gc_zeal`]). /// /// `--keep-init-func` because dropping the init export — wizer's default — leaves the component /// referring to a core export that is no longer there, and the snapshot then fails to load at all. diff --git a/crates/serve-test-support/tests/serve_wasm_e2e.rs b/crates/serve-test-support/tests/serve_wasm_e2e.rs index a7d998f..9ee77d7 100644 --- a/crates/serve-test-support/tests/serve_wasm_e2e.rs +++ b/crates/serve-test-support/tests/serve_wasm_e2e.rs @@ -16,9 +16,10 @@ use serve_test_support as common; use common::read_until_eof; -use common::wasm_serve::{Ready, Serve}; +use common::wasm_serve::{Ready, Serve, WasmServer}; use std::io::Write; use std::net::TcpStream; +use std::panic::AssertUnwindSafe; use std::time::Duration; /// Counts the requests one instance has served, so a test can tell whether it is still talking to @@ -38,7 +39,7 @@ addEventListener('fetch', (event) => { /// as they do in the native server's default mode. #[test] fn sequential_requests_share_one_instance() { - let Some(server) = Serve::new(18402) + let Some(server) = Serve::new() .reusing_one_instance() .script(REQUEST_COUNTER) .start() @@ -71,11 +72,11 @@ fn sequential_requests_share_one_instance() { } /// The other side of that contract: reuse is the host's choice, and a host that declines it hands -/// every request a global of its own — the property the native server offers as `--serve-isolated` -/// and the WPT serve harness asks `wasmtime serve` for. +/// every request a global of its own. The native server offers this as `--serve-isolated`, and +/// the WPT serve harness requests it from `wasmtime serve`. #[test] fn a_host_that_retires_instances_gives_each_request_a_fresh_global() { - let Some(server) = Serve::new(18403) + let Some(server) = Serve::new() .wasmtime_flags(["--max-instance-reuse-count", "1"]) .script(REQUEST_COUNTER) .start() @@ -103,11 +104,7 @@ fn a_repeating_timer_keeps_running_between_requests() { addEventListener('fetch', (event) => { event.respondWith(new Response('ticks=' + globalThis.ticks)); });"#; - let Some(server) = Serve::new(18404) - .reusing_one_instance() - .script(TICKER) - .start() - else { + let Some(server) = Serve::new().reusing_one_instance().script(TICKER).start() else { return; }; @@ -120,7 +117,7 @@ fn a_repeating_timer_keeps_running_between_requests() { let first = ticks(&server); // Idle time with no requests at all: the interval has to fire on its own. - std::thread::sleep(Duration::from_millis(500)); + std::thread::sleep(Duration::from_millis(200)); let second = ticks(&server); assert!( second > first, @@ -139,7 +136,7 @@ fn an_unawaited_top_level_fetch_does_not_break_the_instance() { "fetch('http://127.0.0.1:{upstream}/'); addEventListener('fetch', (event) => event.respondWith(new Response('served')));" ); - let Some(server) = Serve::new(18405).script(&handler).start() else { + let Some(server) = Serve::new().script(&handler).start() else { return; }; @@ -152,7 +149,7 @@ fn an_unawaited_top_level_fetch_does_not_break_the_instance() { /// host paths — goes to the log. #[test] fn a_runtime_that_will_not_start_keeps_its_reasons_off_the_wire() { - let Some(server) = Serve::new(18406) + let Some(server) = Serve::new() .flags(["--legacy-script", "not-a-real-script.js"]) .ready(Ready::AnyResponse) .start() @@ -180,7 +177,7 @@ fn a_runtime_that_will_not_start_keeps_its_reasons_off_the_wire() { /// once. #[test] fn a_failed_startup_is_not_retried_by_later_requests() { - let Some(server) = Serve::new(18461) + let Some(server) = Serve::new() .reusing_one_instance() .script("console.error('startup-ran'); throw new Error('startup failed');") .ready(Ready::AnyResponse) @@ -211,32 +208,6 @@ fn a_failed_startup_is_not_retried_by_later_requests() { ); } -/// A `fetch` whose request has a header the host refuses rejects with a `TypeError` naming -/// that header. `http2-settings` is allowed by Fetch but refused by `wasi:http`. -#[test] -fn a_fetch_with_a_header_the_host_refuses_rejects_naming_it() { - let upstream = common::start_upstream("UPSTREAM-BODY"); - let handler = format!( - r#"addEventListener('fetch', (event) => {{ - if (new URL(event.request.url).pathname === '/ready') {{ - event.respondWith(new Response('ready')); - return; - }} - event.respondWith( - fetch('http://127.0.0.1:{upstream}/', {{ headers: {{ 'http2-settings': 'AAMAAABk' }} }}) - .then(() => new Response('sent'), (e) => new Response(e.name + ': ' + e.message)) - ); - }});"# - ); - let Some(server) = Serve::new(18462).script(&handler).start() else { - return; - }; - - let body = server.get("/"); - assert!(body.starts_with("TypeError: "), "{body}"); - assert!(body.contains("http2-settings"), "{body}"); -} - /// An unusual status is the handler's to choose: it reaches the client as itself, and the host /// must not act on it behind the handler's back — a `421` invites a client to retry elsewhere, not /// the server to re-dispatch (`serve_delivers_an_unusual_status_without_retrying_it`). That the @@ -257,11 +228,7 @@ fn serve_delivers_an_unusual_status_without_retrying_it() { globalThis.dispatches++; event.respondWith(new Response('misdirected', { status: 421 })); });"#; - let Some(server) = Serve::new(18407) - .reusing_one_instance() - .script(HANDLER) - .start() - else { + let Some(server) = Serve::new().reusing_one_instance().script(HANDLER).start() else { return; }; @@ -281,26 +248,6 @@ fn serve_delivers_an_unusual_status_without_retrying_it() { ); } -/// A handler returning `Response.error()` — a network-error response with status 0 — must not -/// serialize the protocol-illegal status line `HTTP/1.1 0`; the status is surfaced as a 500 -/// (`serve_maps_invalid_status_to_500`). -#[test] -fn serve_maps_invalid_status_to_500() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - if (new URL(event.request.url).pathname === '/ready') { - event.respondWith(new Response('ready')); - return; - } - event.respondWith(Response.error()); - });"#; - let Some(server) = Serve::new(18408).script(HANDLER).start() else { - return; - }; - - let response = server.full_request("GET", "/", ""); - assert!(response.starts_with("HTTP/1.1 500"), "got: {response}"); -} - /// A script that registers no `fetch` listener can only ever serve 500s. Native refuses to serve /// at all (`a_script_with_no_fetch_listener_refuses_to_serve`) and wizer refuses to snapshot it, /// but a wasm server cannot decline the host's requests, so it reports the reason once and the @@ -308,7 +255,7 @@ fn serve_maps_invalid_status_to_500() { /// follow each say why too. #[test] fn a_script_with_no_fetch_listener_reports_itself_and_answers_500s() { - let Some(server) = Serve::new(18409) + let Some(server) = Serve::new() .script("globalThis.ready = true;") .ready(Ready::AnyResponse) .start() @@ -334,7 +281,7 @@ fn a_script_with_no_fetch_listener_reports_itself_and_answers_500s() { /// rejection is logged once, and every request is answered with a 500. #[test] fn a_late_top_level_rejection_fails_startup() { - let Some(server) = Serve::new(18474) + let Some(server) = Serve::new() .module( "entry.mjs", "addEventListener('fetch', (event) => event.respondWith(new Response('served'))); @@ -356,7 +303,7 @@ fn a_late_top_level_rejection_fails_startup() { "{}", server.log() ); - // Logged when startup fails, not again for each request. + // The rejection is logged once when startup fails, and not again for each request. let log = server.log(); assert_eq!( log.matches("top-level `await` rejected").count(), @@ -390,7 +337,7 @@ fn a_reused_instance_shares_its_global_and_starts_up_once() { state.count += 1; event.respondWith(new Response('ok')); });"#; - let Some(server) = Serve::new(18410) + let Some(server) = Serve::new() .reusing_one_instance() .file("dep.mjs", "export const state = { count: 0 };") .module("entry.mjs", ENTRY) @@ -446,9 +393,9 @@ fn a_later_request_gets_a_fresh_event_and_signal() { start(c) { setInterval(() => c.enqueue(new TextEncoder().encode('tick ')), 50); }, }))); });"#; - let Some(server) = Serve::new(18411) + let Some(server) = Serve::new() .reusing_one_instance() - .flags(["--response-body-timeout", "1"]) + .flags(["--response-body-timeout", "0.2"]) .script(HANDLER) .start() else { @@ -491,7 +438,7 @@ fn a_later_request_gets_its_own_response_body_window() { }))); return; } - // Three chunks over ~900ms: comfortably inside a fresh 3s window, and nowhere near one + // Three chunks over ~300ms: comfortably inside a fresh 900ms window, and nowhere near one // that a previous request had already spent. event.respondWith(new Response(new ReadableStream({ start(c) { @@ -502,13 +449,13 @@ fn a_later_request_gets_its_own_response_body_window() { clearInterval(timer); c.close(); } - }, 300); + }, 100); }, }))); });"#; - let Some(server) = Serve::new(18412) + let Some(server) = Serve::new() .reusing_one_instance() - .flags(["--response-body-timeout", "3"]) + .flags(["--response-body-timeout", "0.9"]) .script(HANDLER) .start() else { @@ -526,7 +473,7 @@ fn a_later_request_gets_its_own_response_body_window() { /// A handler that proxies `/fetch` from an upstream slow enough to still be in flight while /// another request's post-response work finishes, and serves `/lifetime` at once while leaving -/// two seconds of `waitUntil` work behind it. +/// 600ms of `waitUntil` work behind it. fn overlapping_work_handler(upstream: u16) -> String { format!( r#"addEventListener('fetch', (event) => {{ @@ -539,7 +486,7 @@ fn overlapping_work_handler(upstream: u16) -> String { event.waitUntil(new Promise((resolve) => setTimeout(() => {{ console.error('lifetime-done'); resolve(); - }}, 2000))); + }}, 600))); event.respondWith(new Response('lifetime-started')); return; }} @@ -564,8 +511,8 @@ fn overlapping_work_handler(upstream: u16) -> String { /// is what keeps the second request's in-flight `fetch` out of it. #[test] fn post_response_work_does_not_disturb_the_next_request() { - let upstream = common::start_slow_upstream(Duration::from_secs(3), "upstream-body"); - let Some(server) = Serve::new(18413) + let upstream = common::start_slow_upstream(Duration::from_millis(1200), "upstream-body"); + let Some(server) = Serve::new() .reusing_one_instance() .script(&overlapping_work_handler(upstream)) .start() @@ -575,7 +522,7 @@ fn post_response_work_does_not_disturb_the_next_request() { // Both have to run in one instance for the first request's drain to reach the second. let (started, proxied) = met_in_one_instance(&server, || { - // Answered at once, with two seconds of lifetime work left running behind it. + // This request is answered at once, with 600ms of lifetime work left running behind it. let started = server.get("/lifetime"); // Its own `fetch` outlasts that window, so the first request's drain ends, releasing what // its loop still holds, while this one is still waiting on the upstream. @@ -590,15 +537,15 @@ fn post_response_work_does_not_disturb_the_next_request() { ); } -/// The same overlap with the harsher ending: the first request's response-body clock runs out — -/// aborting its signal from its drain task, on the realm the second request is running JS in — -/// while the second is parked mid-dispatch. Each has to see only its own outcome. +/// The same overlap with the harsher ending: the first request's response-body clock runs out +/// while the second is parked mid-dispatch, and the first request's drain task aborts its signal +/// on the realm in which the second is running JS. Each has to see only its own outcome. #[test] fn a_truncated_response_body_does_not_disturb_a_concurrent_request() { - let upstream = common::start_slow_upstream(Duration::from_millis(2500), "upstream-body"); - let Some(server) = Serve::new(18414) + let upstream = common::start_slow_upstream(Duration::from_millis(1000), "upstream-body"); + let Some(server) = Serve::new() .reusing_one_instance() - .flags(["--response-body-timeout", "1"]) + .flags(["--response-body-timeout", "0.3"]) .script(&overlapping_work_handler(upstream)) .start() else { @@ -646,7 +593,7 @@ fn instance_of(response: &str) -> &str { } /// Fail unless every response came from the same instance — otherwise the requests never met, and -/// whatever the test asserts about them not interfering proves nothing. +/// the test's assertions that they don't interfere are meaningless. fn assert_met_in_one_instance(responses: &[&str]) { let ids: Vec<&str> = responses.iter().map(|r| instance_of(r)).collect(); assert!( @@ -690,6 +637,16 @@ fn met_in_one_instance( unreachable!("the loop returns or asserts") } +/// Request `path` from `server` until the body is `settled`, and return the last body. See +/// [`common::eventually`]. +fn eventually( + server: &common::wasm_serve::WasmServer, + path: &str, + settled: impl Fn(&str) -> bool, +) -> String { + common::eventually(|| server.get(path), settled) +} + /// Wait until a handler has parked on `gate` and the instance has reported an idle event loop, so /// the next request meets an instance blocked on I/O rather than one running guest code, which /// would be given an instance of its own. @@ -773,12 +730,12 @@ fn concurrent_requests_in_one_instance_keep_their_own_state() { }});"#, gate = gate.port() ); - let Some(server) = Serve::new(18415) + let Some(server) = Serve::new() .reusing_one_instance() - // Pinned to the ordinary collector. Every request is sent only once the instance has - // reported an idle loop, but under mode 4 the verifier wakes it again between that report - // and the request landing, often enough that the host gives the request an instance of its - // own. + // This server runs with the ordinary collector. Every request is sent only once the + // instance has reported an idle loop, but under mode 4 the verifier wakes it again between + // that report and the request landing, often enough that the host gives the request an + // instance of its own. .without_gc_zeal() .script(&format!("{INSTANCE_ID}\n{handler}")) .start() @@ -824,6 +781,16 @@ fn concurrent_requests_in_one_instance_keep_their_own_state() { assert_met_in_one_instance(&[&slow, &quick, &streamed_body]); } +/// How long a request in the interleaving case below may take. Its own patience, since that case +/// runs its JS with the collector in the way. +const WORK_PATIENCE: Duration = Duration::from_secs(180); + +/// The rounds of await-then-allocate run by each request, and how much each round allocates. The +/// rounds are enough that every request is still working while the others are. The allocation is +/// enough to give the collector real work to do in the middle of one request's JS. +const ROUNDS: usize = 12; +const PER_ROUND: usize = 1000; + /// The realm-stack worry the `RUNTIME` thread-local is written around: entering the global's realm /// per request would mean `JSAutoRealm` scopes dropping out of order under interleaving, restoring /// the wrong realm under whichever requests are still in flight. Requests here are deliberately @@ -833,16 +800,12 @@ fn concurrent_requests_in_one_instance_keep_their_own_state() { /// Held to five concurrent requests: past `--max-instance-concurrent-reuse-count`'s default of 16 /// the host would spread them over instances, and interleaving is the whole point. /// -/// Its own patience, since the GC-stress variant runs the same JS with the collector in the way. -const WORK_PATIENCE: Duration = Duration::from_secs(180); - -/// Rounds of await-then-allocate each request runs, and how much each round allocates. The rounds -/// are enough that every request is still working while the others are. The allocation is enough -/// to give the GC-stress variant's collector real work to do in the middle of one request's JS. -const ROUNDS: usize = 12; -const PER_ROUND: usize = 1000; - -fn interleaved_requests_all_get_their_own_answer(port: u16, zeal: Option<&str>) { +/// The collector runs constantly (zeal mode 2: collect every N allocations), since a rooting or +/// realm mistake surfaces when GC lands in the middle of one request's JS while others are parked. +/// Only a `debugmozjs` component acts on the zeal mode. Against any other component this runs the +/// same interleaving under the ordinary collector. +#[test] +fn deeply_interleaved_requests_keep_the_realm_stack_intact() { let gate = common::start_held_upstream("go"); let handler = format!( r#"addEventListener('fetch', (event) => {{ @@ -851,6 +814,10 @@ fn interleaved_requests_all_get_their_own_answer(port: u16, zeal: Option<&str>) event.respondWith(new Response('ready')); return; }} + if (url.pathname === '/instance') {{ + event.respondWith(new Response('instance=' + globalThis.instance)); + return; + }} const n = Number(url.searchParams.get('n')); event.respondWith((async () => {{ // Park on the gate before doing anything else. The harness starts the next request @@ -872,15 +839,16 @@ fn interleaved_requests_all_get_their_own_answer(port: u16, zeal: Option<&str>) }});"#, gate = gate.port() ); - // `zeal` is this case's collector, not the run's. Twelve rounds of allocate-then-await across + // The zeal mode is this case's own, not the run's. Twelve rounds of allocate-then-await across // five requests make many event loop turns over a large live heap, and mode 4 (verify // pre-barriers) walks that heap at every one, measured at roughly 8 seconds per turn. Mode 2 // and the ordinary collector both finish the whole workload in about five. - let server = Serve::new(port) + let Some(server) = Serve::new() .reusing_one_instance() - .without_gc_zeal() - .script(&format!("{INSTANCE_ID}\n{handler}")); - let Some(server) = zeal.into_iter().fold(server, Serve::gc_zeal).start() else { + .gc_zeal("2,200") + .script(&format!("{INSTANCE_ID}\n{handler}")) + .start() + else { return; }; @@ -921,34 +889,12 @@ fn interleaved_requests_all_get_their_own_answer(port: u16, zeal: Option<&str>) assert_met_in_one_instance(&answers.iter().map(String::as_str).collect::>()); // Still serving after all that, rather than having taken the process down with it. - let after = common::full_request_within(port, "GET", "/work?n=9", "", WORK_PATIENCE) - .expect("the server must still be serving"); assert_eq!( - common::wasm_serve::message_body(&after), - format!( - "n=9 allocated={allocated} instance={}", - instance_of(&answers[0]) - ) + server.get("/instance"), + format!("instance={}", instance_of(&answers[0])) ); } -#[test] -fn deeply_interleaved_requests_keep_the_realm_stack_intact() { - interleaved_requests_all_get_their_own_answer(18416, None); -} - -/// The same interleaving with the collector running constantly (zeal mode 2: collect every N -/// allocations). GC landing in the middle of one request's JS while others are parked is exactly -/// where a rooting or realm mistake surfaces, and nothing else in this suite arranges it. -#[test] -fn deeply_interleaved_requests_survive_gc_stress() { - interleaved_requests_all_get_their_own_answer(18417, Some("2,200")); -} - -/// A content script that is still evaluating for a second and a half after the instance starts, so -/// the requests that arrive meanwhile have to wait it out. Its instance id is drawn before the -/// `await`, so a request served from a half-evaluated script would still name the instance it ran -/// in — what marks the script as fully evaluated is the listener, registered after. /// A content script whose startup waits on a [`common::HeldUpstream`] rather than on a timer. The /// upstream is handed the request the guest made reaching its top-level `await`, which pins /// exactly when the instance got there. @@ -973,7 +919,7 @@ addEventListener('fetch', (event) => {{ #[test] fn concurrent_first_requests_all_wait_out_one_startup() { let gate = common::start_held_upstream("go"); - let Some(server) = Serve::new(18418) + let Some(server) = Serve::new() .reusing_one_instance() // Not `/ready`: a readiness probe would itself be the first request and drive the very // startup this test is about. @@ -1045,7 +991,7 @@ fn concurrent_first_requests_all_wait_out_one_startup() { #[test] fn a_client_that_leaves_mid_startup_does_not_strand_the_instance() { let gate = common::start_held_upstream("go"); - let Some(server) = Serve::new(18419) + let Some(server) = Serve::new() .reusing_one_instance() .ready(Ready::Listening) .module("startup.mjs", &slow_startup(gate.port())) @@ -1078,26 +1024,37 @@ fn a_client_that_leaves_mid_startup_does_not_strand_the_instance() { /// A wizened instance serves many requests, so the repairs a resumed instance needs have to happen /// exactly once, on the first of them. The main one rebases the monotonic clock's origin, which is -/// anchored to the process the snapshot was taken in. Running them again on a later request would rebase -/// the clock under a handler that had already read it, so `performance.now()` is checked for going -/// forwards across requests rather than merely being plausible on the first. The shared global -/// carries across requests here exactly as it does without a snapshot. +/// anchored to the process the snapshot was taken in. Running them again on a later request would +/// rebase the clock under a handler that had already read it, so `performance.now()` is checked for +/// going forwards across requests rather than merely being plausible on the first. The shared +/// global persists across requests here exactly as it does without a snapshot. +/// +/// The content script's top-level `await` finishes before the snapshot is taken, so the listener +/// registered after it is in place for the first request (`test-wizer.sh`'s `awaited.mjs` case, +/// extended here with the ordering the response reports). #[test] fn a_wizened_instance_keeps_its_clock_and_global_across_requests() { - const HANDLER: &str = r#"const atStartup = performance.now(); + // The ordering is recorded in a global and reported in the response rather than through the + // usual stderr markers: writing to stdio before the snapshot is taken leaves the resumed + // instance with a stale stream handle, and every later write traps. + const SCRIPT: &str = r#"globalThis.order = ['evaluating']; + const atStartup = performance.now(); + await new Promise((resolve) => setTimeout(resolve, 300)); + globalThis.order.push('evaluated'); globalThis.served = 0; addEventListener('fetch', (event) => { - if (new URL(event.request.url).pathname === '/ready') { - event.respondWith(new Response('ready')); - return; - } + globalThis.order.push('dispatched'); event.respondWith(new Response('n=' + (++globalThis.served) - + ' startup=' + atStartup.toFixed(0) + ' now=' + performance.now().toFixed(0))); + + ' startup=' + atStartup.toFixed(0) + ' now=' + performance.now().toFixed(0) + + ' order=' + globalThis.order.join(','))); });"#; - let Some(server) = Serve::new(18420) + let Some(server) = Serve::new() .reusing_one_instance() .wizen() - .script(HANDLER) + // Not `/ready`: the listener would serve a readiness probe, which would change the + // `order` and `n` values this test asserts on. + .ready(Ready::Listening) + .module("awaited.mjs", SCRIPT) .start() else { return; @@ -1105,20 +1062,31 @@ fn a_wizened_instance_keeps_its_clock_and_global_across_requests() { let mut previous = 0.0; for expected in 1..=3 { - // Spaced out so a rebased clock is unmistakable rather than within the few milliseconds - // back-to-back requests are apart — and still far inside the pinned idle timeout. - std::thread::sleep(Duration::from_millis(300)); + // The requests are spaced out so a rebased clock is unmistakable rather than within the + // few milliseconds between back-to-back requests, and still far inside the pinned idle + // timeout. + std::thread::sleep(Duration::from_millis(100)); let body = server.get("/served"); - let field = |name: &str| -> f64 { + let field = |name: &str| -> &str { body.split(&format!("{name}=")) .nth(1) .and_then(|rest| rest.split(' ').next()) - .and_then(|value| value.parse().ok()) .unwrap_or_else(|| panic!("no {name} in {body:?}")) }; + let number = |name: &str| -> f64 { + field(name) + .parse() + .unwrap_or_else(|_| panic!("{name} is not a number in {body:?}")) + }; assert!(body.starts_with(&format!("n={expected} ")), "{body}"); + let dispatched = vec!["dispatched"; expected].join(","); + assert_eq!( + field("order"), + format!("evaluating,evaluated,{dispatched}"), + "the script must finish evaluating once, before the first dispatch: {body}" + ); // A clock still reading from the snapshot's time origin would be hours out. - let (startup, now) = (field("startup"), field("now")); + let (startup, now) = (number("startup"), number("now")); assert!((0.0..60_000.0).contains(&startup), "{body}"); assert!((0.0..60_000.0).contains(&now), "{body}"); assert!( @@ -1130,173 +1098,19 @@ fn a_wizened_instance_keeps_its_clock_and_global_across_requests() { } } -/// A header value is a byte sequence, not text: Fetch isomorphic-encodes it, so `U+00DF U+00C0 -/// U+00BF` has to leave as `DF C0 BF` rather than as their UTF-8 encodings -/// (`a_response_header_value_is_sent_as_bytes_not_utf8`). The neighbouring header is there to catch -/// the *other* way this can go wrong: a header list the host refuses must not take the rest with -/// it. -#[test] -fn a_response_header_value_is_sent_as_bytes_not_utf8() { - let handler = "addEventListener('fetch', (event) => {\n\ - if (new URL(event.request.url).pathname === '/ready') {\n\ - event.respondWith(new Response('ready'));\n\ - return;\n\ - }\n\ - event.respondWith(new Response('x', { headers: {\n\ - 'x-test': '\u{df}\u{c0}\u{bf}',\n\ - 'x-other': 'kept',\n\ - } }));\n\ - });"; - let Some(server) = Serve::new(18421).script(handler).start() else { - return; - }; - - let raw = common::raw_response_within(server.port(), "GET", "/", "", Duration::from_secs(20)) - .expect("a complete response"); - let needle = b"x-test: \xdf\xc0\xbf\r\n"; - assert!( - raw.windows(needle.len()).any(|window| window == needle), - "the header must be the three bytes DF C0 BF, got: {:?}", - String::from_utf8_lossy(&raw) - ); - assert!( - raw.windows(14).any(|window| window == b"x-other: kept\r"), - "the other headers must survive: {:?}", - String::from_utf8_lossy(&raw) - ); -} - -/// `wasi:http` refuses the connection-management headers it writes itself, which -/// `prepare_wire_response` does not filter and a handler — or an upstream whose response is being -/// proxied — may perfectly well set. Only that header may be lost. -#[test] -fn a_response_header_the_host_refuses_does_not_take_the_others_with_it() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - if (new URL(event.request.url).pathname === '/ready') { - event.respondWith(new Response('ready')); - return; - } - event.respondWith(new Response('body', { headers: { - 'x-before': 'kept', - 'keep-alive': 'timeout=5', - 'x-after': 'kept', - } })); - });"#; - let Some(server) = Serve::new(18422).script(HANDLER).start() else { - return; - }; - - let response = server.full_request("GET", "/", ""); - assert!(response.contains("x-before: kept"), "{response}"); - assert!(response.contains("x-after: kept"), "{response}"); - assert!( - !response.to_lowercase().contains("keep-alive"), - "the refused header must not be on the wire: {response}" - ); - assert_eq!( - common::wasm_serve::message_body(&response), - "body", - "{response}" - ); - assert!( - server.wait_for_marker( - "dropping the response header `keep-alive`", - Duration::from_secs(5) - ), - "{}", - server.log() - ); -} - -/// A header carrying a byte that would split the response never gets as far as the transport: -/// `Headers` refuses it at construction (`a_response_header_value_with_a_null_byte_is_refused`). -#[test] -fn a_response_header_value_with_a_null_byte_is_refused() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - let outcome; - try { - new Response('x', { headers: { 'x-test': 'b\0r' } }); - outcome = 'constructed'; - } catch (e) { - outcome = e.name; - } - event.respondWith(new Response(outcome)); - });"#; - let Some(server) = Serve::new(18424).script(HANDLER).start() else { - return; - }; - - assert_eq!(server.get("/"), "TypeError"); -} - -/// What the wire's headers look like from JS: a field that arrived in mixed case is found by any -/// casing, the incoming set is immutable — it describes what was received, which nothing can change -/// after the fact — and headers put on the response come back out with `set-cookie` kept as -/// separate fields while everything else combines -/// (`serve_exposes_incoming_headers_and_keeps_them_immutable`). +/// Snapshotting a snapshot runs its init export again, in an instance whose script has already +/// been evaluated. The instance resumed from that snapshot still serves. #[test] -fn serve_exposes_incoming_headers_and_keeps_them_immutable() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - const incoming = event.request.headers; - if (new URL(event.request.url).pathname === '/ready') { - event.respondWith(new Response('ready')); - return; - } - let immutable = 'mutable'; - try { - incoming.delete('user-agent'); - } catch (e) { - immutable = e.constructor.name; - } - const response = new Response('ok', { - headers: [ - ['x-seen', incoming.get('EXAMPLE-HEADER') ?? 'missing'], - ['x-agent', incoming.get('User-Agent') ?? 'missing'], - ['x-immutable', immutable], - ], - }); - response.headers.append('set-cookie', 'A'); - response.headers.append('set-cookie', 'B'); - response.headers.append('another', 'A'); - response.headers.append('another', 'B'); - event.respondWith(response); - });"#; - let Some(server) = Serve::new(18423).script(HANDLER).start() else { +fn a_twice_wizened_instance_serves() { + let Some(server) = Serve::new() + .wizen() + .wizen() + .script("addEventListener('fetch', (event) => event.respondWith(new Response('ok')));") + .start() + else { return; }; - - let response = common::raw_request_within( - server.port(), - b"GET / HTTP/1.1\r\nHost: x\r\neXample-hEader: Header Value\r\nUser-Agent: test-agent\r\n\ - Connection: close\r\n\r\n", - Duration::from_secs(20), - ) - .expect("a complete response"); - - assert!( - response.contains("x-seen: Header Value"), - "a mixed-case field must be found by any casing: {response}" - ); - assert!(response.contains("x-agent: test-agent"), "got: {response}"); - assert!( - response.contains("x-immutable: TypeError"), - "an incoming request's headers must not be mutable: {response}" - ); - // `set-cookie` must stay one field per cookie: combining two cookies into one field changes - // what they mean. Other repeats may go out either as separate fields or as one comma list — - // RFC 9110 §5.2 makes those equivalent — but both values have to survive. - assert_eq!( - response.matches("set-cookie: ").count(), - 2, - "each cookie needs a field of its own: {response}" - ); - assert!(response.contains("set-cookie: A"), "got: {response}"); - assert!(response.contains("set-cookie: B"), "got: {response}"); - assert!( - response.contains("another: A, B") - || (response.contains("another: A") && response.contains("another: B")), - "both values of a repeated header must reach the client: {response}" - ); + assert_eq!(server.get("/"), "ok"); } /// A `ReadableStream` whose `start()` neither enqueues nor closes leaves the pump's read pending in @@ -1316,11 +1130,7 @@ fn an_abandoned_response_body_does_not_hang_the_request() { } event.respondWith(new Response('still serving')); });"#; - let Some(server) = Serve::new(18425) - .reusing_one_instance() - .script(HANDLER) - .start() - else { + let Some(server) = Serve::new().reusing_one_instance().script(HANDLER).start() else { return; }; @@ -1375,7 +1185,7 @@ fn an_abandoned_body_is_told_apart_from_a_complete_one() { }, }))); });"#; - let Some(server) = Serve::new(18426) + let Some(server) = Serve::new() .reusing_one_instance() .ready(Ready::AnyResponse) .script(HANDLER) @@ -1400,7 +1210,7 @@ fn an_abandoned_body_is_told_apart_from_a_complete_one() { /// `--dispatch-timeout` bounds a `respondWith` that never settles — without one, the default, the /// request would wait for as long as the handler takes -/// (`a_dispatch_timeout_answers_a_never_settling_respond_with`). Giving up is step 17.4.20's +/// (`a_dispatch_timeout_frees_the_slot_it_held`). Giving up is step 17.4.20's /// "terminated", so the handler is told through its request signal, and the `waitUntil` window is /// its own on the 500 path as on any other. #[test] @@ -1418,8 +1228,8 @@ fn a_dispatch_timeout_answers_a_never_settling_respond_with() { }, 300))); event.respondWith(new Promise(() => {})); });"#; - let Some(server) = Serve::new(18427) - .flags(["--dispatch-timeout", "1", "--waituntil-timeout", "2"]) + let Some(server) = Serve::new() + .flags(["--dispatch-timeout", "0.2", "--waituntil-timeout", "2"]) .script(HANDLER) .start() else { @@ -1460,11 +1270,11 @@ fn a_waituntil_timeout_ends_only_the_work_that_overruns_it() { event.waitUntil(new Promise((resolve) => setTimeout(() => { console.error('finished:' + label); resolve(); - }, path === '/short' ? 300 : 5000))); + }, path === '/short' ? 100 : 1000))); event.respondWith(new Response('started')); });"#; - let Some(server) = Serve::new(18428) - .flags(["--waituntil-timeout", "1"]) + let Some(server) = Serve::new() + .flags(["--waituntil-timeout", "0.4"]) .script(HANDLER) .start() else { @@ -1479,8 +1289,9 @@ fn a_waituntil_timeout_ends_only_the_work_that_overruns_it() { ); assert_eq!(server.get("/long"), "started"); + // Past the long work's own second, so an ignored bound shows up as its marker. assert!( - !server.wait_for_marker("finished:long", Duration::from_secs(4)), + !server.wait_for_marker("finished:long", Duration::from_millis(1500)), "lifetime work past the window must be cut: {}", server.log() ); @@ -1518,9 +1329,9 @@ fn an_end_to_end_timeout_bounds_every_phase() { event.waitUntil(new Promise(() => {})); event.respondWith(new Response('started')); });"#; - let Some(server) = Serve::new(18429) + let Some(server) = Serve::new() .reusing_one_instance() - .flags(["--end-to-end-timeout", "1"]) + .flags(["--end-to-end-timeout", "0.3"]) .script(HANDLER) .start() else { @@ -1534,7 +1345,8 @@ fn an_end_to_end_timeout_bounds_every_phase() { ); // The 500 has to reach the client in full: its body is measured against the `response_body` - // limit alone (`RequestClock::error_body_bound`), since the window that prompted it is spent. + // limit alone (`RequestClock::error_body_time_limit`), since the window that prompted it is + // spent. let never = server.full_request("GET", "/never", ""); assert!( never.starts_with("HTTP/1.1 500"), @@ -1546,50 +1358,52 @@ fn an_end_to_end_timeout_bounds_every_phase() { ); assert_eq!(server.get("/leftover"), "started"); - // Well past the deadline, the interval must have stopped: two samples with a gap between them - // read the same count. - std::thread::sleep(Duration::from_millis(2000)); + // Past the deadline, the interval must have stopped: two samples with a gap between them read + // the same count, and that count shows the interval ran before the deadline. + std::thread::sleep(Duration::from_millis(600)); let first = server.get("/ticks"); - std::thread::sleep(Duration::from_millis(500)); + std::thread::sleep(Duration::from_millis(300)); assert_eq!( server.get("/ticks"), first, "the deadline must drop the request's loop, timers and all" ); + assert_ne!(first, "0", "the interval must run until the deadline"); } /// Where the end-to-end clock opens differs from native, and deliberately: on wasm it starts after /// `ensure_started`, since standing the instance up is the host's business and it bounds that -/// itself (`serve_wasm.rs`'s `dispatch_request`). So a content script that spends a second and a -/// half on a top-level `await` does not spend the first request's window. There is no wasm analog -/// of `an_end_to_end_timeout_bounds_isolated_startup`, and this case replaces it. +/// itself (`serve_wasm.rs`'s `dispatch_request`). So a content script that spends 800ms on a +/// top-level `await` does not spend the first request's window. There is no wasm analog of +/// `an_end_to_end_timeout_bounds_isolated_startup`, and this case replaces it. #[test] fn a_slow_startup_stays_outside_the_first_requests_end_to_end_window() { - const SCRIPT: &str = r#"await new Promise((resolve) => setTimeout(resolve, 1500)); + const SCRIPT: &str = r#"await new Promise((resolve) => setTimeout(resolve, 800)); addEventListener('fetch', (event) => { event.respondWith(new Promise((resolve) => - setTimeout(() => resolve(new Response('served')), 600))); + setTimeout(() => resolve(new Response('served')), 200))); });"#; - let Some(server) = Serve::new(18430) + let Some(server) = Serve::new() .ready(Ready::Listening) - .flags(["--end-to-end-timeout", "1"]) + .flags(["--end-to-end-timeout", "0.6"]) .module("startup.mjs", SCRIPT) .start() else { return; }; - // 1.5s of startup plus 600ms of handler, against a 1s window that only the latter is inside. + // 800ms of startup plus 200ms of handler, against a 600ms window that only the latter is + // inside. assert_eq!(server.get("/first"), "served"); } /// A configuration that cannot be satisfied — a phase allowed to outlast the whole request — is /// refused. The native server refuses to start at all; a wasm one cannot decline the host's -/// requests, so it responds to each of them with the same bare 500 any unstartable runtime gets, and -/// reports why in the log. +/// requests, so it responds to each of them with the same bare 500 returned by any unstartable +/// runtime, and reports why in the log. #[test] fn a_contradictory_timeout_config_refuses_to_serve() { - let Some(server) = Serve::new(18431) + let Some(server) = Serve::new() .flags([ "--end-to-end-timeout", "5", @@ -1623,230 +1437,6 @@ fn a_contradictory_timeout_config_refuses_to_serve() { ); } -/// `wasmtime serve` forwards a `HEAD` to the guest and sends what the guest produced: no body, and -/// the `Content-Length` a `GET` would have had, which `prepare_wire_response` can supply only for an -/// in-memory body, since learning a streamed one's length means producing it -/// (`serve_answers_a_head_request_without_a_body`). -#[test] -fn serve_answers_a_head_request_without_a_body() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - if (new URL(event.request.url).pathname === '/ready') { - event.respondWith(new Response('ready')); - return; - } - if (new URL(event.request.url).pathname === '/stream') { - event.respondWith(new Response(new ReadableStream({ - start(c) { - c.enqueue(new TextEncoder().encode('BODYBYTES')); - c.close(); - }, - }))); - return; - } - event.respondWith(new Response('BODYBYTES', { status: 201 })); - });"#; - let Some(server) = Serve::new(18432).script(HANDLER).start() else { - return; - }; - - let response = server.full_request("HEAD", "/", ""); - let (head, body) = response.split_once("\r\n\r\n").expect("a complete head"); - assert!( - head.starts_with("HTTP/1.1 201 "), - "the head is still the handler's: {response}" - ); - assert_eq!( - body, "", - "a HEAD response must carry no body, but got: {response}" - ); - assert!( - head.to_lowercase().contains("content-length: 9"), - "a HEAD must declare the length a GET would have sent: {head}" - ); - - let streamed = server.full_request("HEAD", "/stream", ""); - let (head, body) = streamed.split_once("\r\n\r\n").expect("a complete head"); - assert_eq!(body, "", "got: {streamed}"); - assert!( - !head.to_lowercase().contains("content-length"), - "a streamed body's length is only knowable by producing it: {head}" - ); -} - -/// A `204 No Content` is framed by its status: no `Content-Length`, no `Transfer-Encoding` (RFC -/// 9110 §8.6, RFC 9112 §6.2). The same holds for `304` -/// (`a_bodiless_status_has_no_framing_headers`). -#[test] -fn a_bodiless_status_has_no_framing_headers() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - const status = Number(new URL(event.request.url).pathname.slice(1)); - event.respondWith(new Response(null, { status: status || 200 })); - });"#; - let Some(server) = Serve::new(18433).script(HANDLER).start() else { - return; - }; - - for status in [204, 304] { - let response = server.full_request("GET", &format!("/{status}"), ""); - let head = response - .split_once("\r\n\r\n") - .map_or(&*response, |(head, _)| head) - .to_lowercase(); - assert!( - !head.contains("content-length"), - "a {status} must not be framed with Content-Length: {response}" - ); - assert!( - !head.contains("transfer-encoding"), - "a {status} must not be framed with Transfer-Encoding: {response}" - ); - } -} - -/// A handler's own `Content-Length` never frames content already in memory: `prepare_wire_response` -/// measures the body instead -/// (`serve_frames_a_response_by_its_body_not_the_handlers_content_length`). A streamed body has no -/// length until it is produced, so there the declaration does frame it — see -/// `a_streamed_body_is_framed_by_its_declared_content_length`. -/// -/// Where native then writes a `Content-Length` of its own, `wasi:http` picks the framing itself and -/// chooses chunked — the host writes the framing bytes, which is exactly the division of labour the -/// `prepare_wire_response` call in `serve_wasm.rs` describes. What the two targets share, and what -/// this asserts, is that the handler's claim is gone and the body is intact. -#[test] -fn a_fixed_length_body_is_framed_by_its_length() { - // Native declares the length of an in-memory body rather than chunking it (`serve.rs`, - // `serve_delivers_a_response_body`), so the guest declares it here too: the host frames by - // what the guest hands it, and with no length that is chunked for every such response. - let Some(server) = Serve::new(18436) - .script("addEventListener('fetch', (e) => e.respondWith(new Response('BODYBYTES')));") - .start() - else { - return; - }; - - let response = server.full_request("GET", "/", ""); - let head = response - .split_once("\r\n\r\n") - .expect("a complete response") - .0 - .to_lowercase(); - assert!(head.contains("content-length: 9"), "got: {head}"); - assert!(!head.contains("transfer-encoding"), "got: {head}"); - assert_eq!(server.get("/"), "BODYBYTES"); -} - -#[test] -fn serve_frames_a_response_by_its_body_not_the_handlers_content_length() { - const HANDLER: &str = r#"const cases = { - // Longer than the body. - '/larger': () => new Response('short', { headers: { 'content-length': '10000' } }), - // Not a number at all. - '/bogus': () => new Response('short', { headers: { 'content-length': 'test' } }), - // Two of them, which combine into one comma-joined value. - '/duplicate': () => { - const headers = new Headers(); - headers.append('content-length', '10000'); - headers.append('content-length', '10000'); - return new Response('short', { headers }); - }, - }; - addEventListener('fetch', (event) => { - const make = cases[new URL(event.request.url).pathname]; - event.respondWith(make ? make() : new Response('ready')); - });"#; - let Some(server) = Serve::new(18434).script(HANDLER).start() else { - return; - }; - - for path in ["/larger", "/bogus", "/duplicate"] { - let response = server.full_request("GET", path, ""); - let head = response - .split_once("\r\n\r\n") - .expect("a complete response") - .0; - assert_eq!(server.request("GET", path, ""), "short", "body for {path}"); - assert!( - !head.contains("10000") && !head.to_lowercase().contains("content-length: test"), - "the handler's own length must not reach the wire for {path}: {head}" - ); - } -} - -/// A streamed body's length is not knowable before it is produced, so a handler that declares one -/// frames its own response by it, on this target as on native -/// (`serve_frames_a_streamed_body_by_its_declared_content_length`). The guest writes the body here, -/// so it is the guest that holds it to the declared length: content past it is never handed to the -/// host. A declaration that is not a single `1*DIGIT` is refused and the host frames as it likes. -#[test] -fn a_streamed_body_is_framed_by_its_declared_content_length() { - const HANDLER: &str = r#"function stream(parts) { - return new ReadableStream({ - start(c) { - for (const part of parts) c.enqueue(new TextEncoder().encode(part)); - c.close(); - }, - }); - } - const cases = { - '/exact': () => new Response(stream(['abc', 'de']), { - headers: { 'content-length': '5' }, - }), - '/longer': () => new Response(stream(['abcde', 'XXXXX']), { - headers: { 'content-length': '5' }, - }), - // Less than it declared: the guest ends the body as an abort, so the host cannot present - // the truncation as a complete response. - '/shorter': () => new Response(stream(['ab']), { - headers: { 'content-length': '5' }, - }), - '/bogus': () => new Response(stream(['abcde']), { - headers: { 'content-length': 'test' }, - }), - }; - addEventListener('fetch', (event) => { - const make = cases[new URL(event.request.url).pathname]; - event.respondWith(make ? make() : new Response('ready')); - });"#; - let Some(server) = Serve::new(18436).script(HANDLER).start() else { - return; - }; - - for path in ["/exact", "/longer"] { - let response = server.full_request("GET", path, ""); - let (head, body) = response - .split_once("\r\n\r\n") - .expect("a complete response"); - assert!( - head.to_lowercase().contains("content-length: 5"), - "the declared length must frame {path}: {head}" - ); - assert!( - !head.to_lowercase().contains("transfer-encoding"), - "got for {path}: {head}" - ); - assert_eq!(body, "abcde", "body for {path}: {response}"); - } - - let short = server.full_request("GET", "/shorter", ""); - let sent = short - .split_once("\r\n\r\n") - .map(|(_, body)| body) - .unwrap_or(""); - assert!( - sent.len() < 5, - "the body must stop short of the length it declared: {short:?}" - ); - - let response = server.full_request("GET", "/bogus", ""); - let head = response - .split_once("\r\n\r\n") - .expect("a complete response") - .0; - assert!(!head.contains("test"), "got: {head}"); - assert_eq!(server.get("/bogus"), "abcde"); -} - /// A handler responding with a `fetch` response hands the transport an `OutgoingBody::Host`: the /// upstream's body stream goes to the host untouched, with no guest writer in between. Two /// concurrent proxies must each get the upstream's body, since `respondWith` resolves from a @@ -1859,11 +1449,7 @@ fn serve_proxies_via_fetch() { "addEventListener('fetch', (event) => \ event.respondWith(fetch('http://127.0.0.1:{upstream}/')));" ); - let Some(server) = Serve::new(18435) - .reusing_one_instance() - .script(&handler) - .start() - else { + let Some(server) = Serve::new().reusing_one_instance().script(&handler).start() else { return; }; @@ -1876,27 +1462,6 @@ fn serve_proxies_via_fetch() { assert!(b.join().unwrap().contains("UPSTREAM-BODY")); } -/// The upstream body forwarded as a stream rather than collected first: the handler builds its own -/// `Response` around `upstream.body`, and the chunks — which the upstream sends with pauses between -/// them — have to reach the client (`serve_forwards_an_upstream_body_as_a_stream`). -#[test] -fn serve_forwards_an_upstream_body_as_a_stream() { - let upstream = common::start_chunked_upstream(&["one ", "two ", "three"]); - let handler = format!( - r#"addEventListener('fetch', (event) => {{ - event.respondWith((async () => {{ - const upstream = await fetch('http://127.0.0.1:{upstream}/'); - return new Response(upstream.body, upstream); - }})()); - }});"# - ); - let Some(server) = Serve::new(18436).script(&handler).start() else { - return; - }; - - assert_eq!(server.get("/"), "one two three"); -} - /// A proxied response body is out of `--response-body-timeout`'s reach, and deliberately: the /// handler responded with a `fetch` response, whose body is a host stream the host pumps itself, and /// holding onto it to bound it would mean copying every chunk of every proxied body through the @@ -1912,8 +1477,8 @@ fn a_proxied_body_outlives_the_response_body_timeout() { "addEventListener('fetch', (event) => \ event.respondWith(fetch('http://127.0.0.1:{upstream}/')));" ); - let Some(server) = Serve::new(18437) - .flags(["--response-body-timeout", "1"]) + let Some(server) = Serve::new() + .flags(["--response-body-timeout", "0.2"]) // Every route proxies the endless upstream, so no probe of it could ever come back. .ready(Ready::Listening) .script(&handler) @@ -1926,8 +1491,8 @@ fn a_proxied_body_outlives_the_response_body_timeout() { stream .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") .unwrap(); - // Several times over the bound, and still streaming. - let ended = read_until_eof(&mut stream, Duration::from_secs(5)); + // Well over the bound, and still streaming. + let ended = read_until_eof(&mut stream, Duration::from_millis(500)); assert!( ended.is_none(), "the guest must not cut a body the host is pumping: {:?}", @@ -1935,542 +1500,12 @@ fn a_proxied_body_outlives_the_response_body_timeout() { ); } -/// An upstream that dies mid-body has to reach the client as the truncation it is, not as a body -/// that ended cleanly a few bytes short: the terminating chunk asserts completeness (RFC 9112 -/// §7.1). The piped trailers carry that, the guest never having seen a byte of the body. -#[test] -fn a_dying_upstream_truncates_the_proxied_body() { - let upstream = common::start_truncated_upstream(&["one ", "two "]); - let handler = format!( - "addEventListener('fetch', (event) => \ - event.respondWith(fetch('http://127.0.0.1:{upstream}/')));" - ); - let Some(server) = Serve::new(18438) - .ready(Ready::AnyResponse) - .script(&handler) - .start() - else { - return; - }; - - let response = server.full_request("GET", "/", ""); - assert!( - response.contains("one ") && response.contains("two "), - "what the upstream did send must arrive: {response:?}" - ); - assert!( - !response.ends_with("0\r\n\r\n"), - "a truncated upstream must not be forwarded as a complete body: {response:?}" - ); -} - -/// A response body that errors after sending bytes: the client gets what was produced, and then -/// the body ends *without* the terminating chunk, so a truncation cannot be mistaken for a complete -/// response (`a_response_stream_that_errors_mid_flight_leaves_the_body_unterminated`). -/// -/// The error comes from a timer, and a generous one: a body cannot outlive its request's event -/// loop going idle, so the stream has to hold the loop open until it errors rather than wait for -/// the client to say when. The delay only has to outlast the pump reaching the wire, which a GC -/// zeal build makes far slower than an ordinary one. -#[test] -fn a_response_stream_that_errors_mid_flight_leaves_the_body_unterminated() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - const encoder = new TextEncoder(); - // A route of its own for the readiness probe. The body below is slow to end, and a probe - // waiting for one would not see a response inside its window. - if (new URL(event.request.url).pathname === '/ready') { - event.respondWith(new Response('ready')); - return; - } - event.respondWith(new Response(new ReadableStream({ - start(c) { - c.enqueue(encoder.encode('first-chunk-')); - setTimeout(() => c.error(new Error('stream broke')), 1000); - }, - }))); - });"#; - let Some(server) = Serve::new(18440).script(HANDLER).start() else { - return; - }; - - let errored = server.full_request("GET", "/mid-flight", ""); - let (_, body) = errored.split_once("\r\n\r\n").expect("a complete head"); - assert!( - body.contains("first-chunk-"), - "the bytes produced before the error must still arrive: {errored}" - ); - assert!( - !body.ends_with("0\r\n\r\n"), - "an errored body must not be terminated as if it were complete: {errored}" - ); -} - -/// A stream that enqueues something that is not a `Uint8Array` aborts its body before a single byte -/// is written (WPT -/// `resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js`). -/// -/// Host-owned divergence: native writes the head itself, so it sends a `200` and then closes -/// unterminated; `wasmtime serve` has not committed the head by the time the body aborts and sends -/// nothing at all. Which of the two a client sees is the host's call — see the comment where -/// `dispatch_request` hands the response over — and what both guarantee is what is asserted here: -/// the client cannot come away with a response that looks complete. -#[test] -fn a_response_stream_with_an_invalid_chunk_is_not_answered_as_a_success() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - if (new URL(event.request.url).pathname === '/ready') { - event.respondWith(new Response('ready')); - return; - } - event.respondWith(new Response(new ReadableStream({ - start(c) { - c.enqueue('a string, not a Uint8Array'); - c.close(); - }, - }))); - });"#; - let Some(server) = Serve::new(18445).script(HANDLER).start() else { - return; - }; - - // The control, since the response under test is nothing at all: the same server sends a - // normal request, so an empty reply below is the invalid chunk's doing and not a server that - // never came up. - assert_eq!(server.get("/ready"), "ready"); - - let response = server.full_request("GET", "/invalid-chunk", ""); - assert!( - response.is_empty() || !response.ends_with("0\r\n\r\n"), - "a body that never produced a valid chunk must reach the client as nothing at all, or as \ - an unterminated one — never as a complete response: {response:?}" - ); -} - -/// An empty chunk is a chunk like any other on this side: it neither ends the body nor corrupts the -/// framing (`serve_streams_an_empty_chunk_without_ending_the_body`). Worth pinning because -/// `spawn_body_writer`'s dropped-reader check cannot see a drop through one — an empty write leaves -/// an empty remainder either way, so what happens to it turns on everything except that -/// check. -#[test] -fn serve_streams_an_empty_chunk_without_ending_the_body() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - const encoder = new TextEncoder(); - event.respondWith(new Response(new ReadableStream({ - start(c) { - c.enqueue(new Uint8Array(0)); - c.enqueue(encoder.encode('chunk one ')); - c.enqueue(new Uint8Array(0)); - c.enqueue(encoder.encode('chunk two')); - c.close(); - }, - }))); - });"#; - let Some(server) = Serve::new(18441) - .ready(Ready::AnyResponse) - .script(HANDLER) - .start() - else { - return; - }; - - assert_eq!(server.get("/"), "chunk one chunk two"); -} - -/// A streamed response's head reaches the client when it is ready, not when the first chunk is: a -/// handler that streams may take a while to produce anything, and the status and headers are what -/// the client acts on meanwhile (`a_streamed_response_sends_its_head_before_the_first_chunk`). -#[test] -fn a_streamed_response_sends_its_head_before_the_first_chunk() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - const stream = new ReadableStream({ - start(c) { - setTimeout(() => { - c.enqueue(new TextEncoder().encode('late')); - c.close(); - }, 2000); - }, - }); - event.respondWith(new Response(stream, { headers: { 'x-marker': 'early' } })); - });"#; - let Some(server) = Serve::new(18442) - // Every route takes two seconds to produce anything, so no probe of it could come back - // inside the harness's own patience. - .ready(Ready::Listening) - .script(HANDLER) - .start() - else { - return; - }; - - let mut stream = TcpStream::connect(("127.0.0.1", server.port())).unwrap(); - stream - .write_all(b"GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n") - .unwrap(); - let started = std::time::Instant::now(); - - // Read just enough to have seen the head, and time how long that took. - stream - .set_read_timeout(Some(Duration::from_secs(10))) - .unwrap(); - let mut buf = [0u8; 256]; - let read = std::io::Read::read(&mut stream, &mut buf).unwrap(); - let elapsed = started.elapsed(); - let head = String::from_utf8_lossy(&buf[..read]).to_string(); - - assert!(head.starts_with("HTTP/1.1 200"), "got: {head}"); - assert!(head.contains("x-marker: early"), "got: {head}"); - assert!( - elapsed < Duration::from_millis(1500), - "the head must not wait for the first chunk (took {elapsed:?})" - ); -} - -/// A body produced by a timer is served to completion: the pump runs on the request's own loop, and -/// that loop has to keep being driven for as long as the body needs it -/// (`an_interval_driven_stream_is_served_to_completion`). -#[test] -fn an_interval_driven_stream_is_served_to_completion() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - let ticks = 0; - event.respondWith(new Response(new ReadableStream({ - start(c) { - const id = setInterval(() => { - c.enqueue(new TextEncoder().encode('tick ' + ++ticks + ' ')); - if (ticks === 5) { - clearInterval(id); - c.close(); - } - }, 50); - }, - }))); - });"#; - let Some(server) = Serve::new(18443) - .ready(Ready::AnyResponse) - .script(HANDLER) - .start() - else { - return; - }; - - assert_eq!(server.get("/"), "tick 1 tick 2 tick 3 tick 4 tick 5 "); -} - -/// A streamed response must not be held open by `waitUntil` work either: the body's pump runs on -/// the request's loop, so the send drives that loop — but only for as long as the body needs it, or -/// the client waits on an open connection for lifetime work it has no interest in -/// (`wait_until_does_not_delay_a_streamed_response`). -#[test] -fn wait_until_does_not_delay_a_streamed_response() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - const stream = new ReadableStream({ - start(c) { - c.enqueue(new TextEncoder().encode('streamed body')); - c.close(); - }, - }); - event.respondWith(new Response(stream)); - event.waitUntil(new Promise((resolve) => setTimeout(resolve, 4000))); - });"#; - let Some(server) = Serve::new(18444) - .ready(Ready::AnyResponse) - .script(HANDLER) - .start() - else { - return; - }; - - let started = std::time::Instant::now(); - let body = server.get("/"); - let elapsed = started.elapsed(); - assert_eq!(body, "streamed body"); - assert!( - elapsed < Duration::from_secs(3), - "a streamed response must not wait for waitUntil work (took {elapsed:?})" - ); -} - -/// A handler reading `request.body` sees chunks as they arrive, not after the upload ends: the -/// first chunk's echo has to come back before the rest is even sent -/// (`serve_streams_an_incoming_request_body`). -#[test] -fn serve_streams_an_incoming_request_body() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - if (new URL(event.request.url).pathname === '/ready') { - event.respondWith(new Response('ready')); - return; - } - const reader = event.request.body.getReader(); - event.respondWith(new Response(new ReadableStream({ - async pull(controller) { - const { done, value } = await reader.read(); - if (done) { - controller.close(); - return; - } - controller.enqueue(value); - }, - }))); - });"#; - let Some(server) = Serve::new(18446).script(HANDLER).start() else { - return; - }; - - let mut stream = TcpStream::connect(("127.0.0.1", server.port())).unwrap(); - stream - .set_read_timeout(Some(Duration::from_secs(20))) - .unwrap(); - stream - .write_all( - b"POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n", - ) - .unwrap(); - - /// Read from `stream` until the response body so far contains `expected`. - fn read_until(stream: &mut TcpStream, raw: &mut Vec, expected: &str) -> String { - for _ in 0..200 { - let body = String::from_utf8_lossy(raw) - .split_once("\r\n\r\n") - .map(|(_, body)| common::dechunk(body)) - .unwrap_or_default(); - if body.contains(expected) { - return body; - } - let mut buf = [0u8; 1024]; - let read = std::io::Read::read(stream, &mut buf).unwrap(); - assert_ne!(read, 0, "connection closed while waiting for {expected:?}"); - raw.extend_from_slice(&buf[..read]); - } - panic!("never received {expected:?}"); - } - - let mut raw = Vec::new(); - // First chunk: the echo must come back before the rest of the request is sent. - stream.write_all(b"5\r\nfirst\r\n").unwrap(); - read_until(&mut stream, &mut raw, "first"); - - // Only now send the second chunk and the terminator. - stream.write_all(b"6\r\nsecond\r\n0\r\n\r\n").unwrap(); - let body = read_until(&mut stream, &mut raw, "second"); - assert_eq!(body, "firstsecond", "got: {body}"); -} - -/// Which methods reach the handler with a body. `wasi:http` gives a request with no body the same -/// empty stream as one whose body is empty, so `body_framing` goes by the method: `GET` gets -/// `null`, and a `POST` without a body gets an empty stream rather than `null`. -/// -/// Host-owned divergence: native reads the framing headers the client sent, so a bodyless `POST` -/// there gets `null`. -#[test] -fn a_bodyless_post_still_reaches_the_handler_with_a_body() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - const body = event.request.body; - event.respondWith(new Response(body === null ? 'null' : 'stream')); - });"#; - let Some(server) = Serve::new(18460) - .ready(Ready::AnyResponse) - .script(HANDLER) - .start() - else { - return; - }; - - assert_eq!(server.request("GET", "/", ""), "null"); - assert_eq!(server.request("POST", "/", ""), "stream"); - assert_eq!(server.request("POST", "/", "payload"), "stream"); -} - -/// A chunked upload reaches the handler decoded: the host undoes the framing, and `body_framing` -/// exposes a body where there is no `Content-Length` to go by, since `wasi:http` does not pass -/// `Transfer-Encoding` on to the guest (`serve_decodes_a_chunked_request_body`). -/// -/// Host-owned divergence: native also refuses `Transfer-Encoding` together with `Content-Length` -/// (RFC 9112 §6.3, a request-smuggling vector) with a 400. On wasm the host parses the request, so -/// no such ambiguity reaches the guest, as `body_framing`'s own comment records, and refusing it is -/// the host's business, not something the guest could do differently. -#[test] -fn serve_decodes_a_chunked_request_body() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - event.respondWith(event.request.text().then((body) => new Response('got:' + body))); - });"#; - let Some(server) = Serve::new(18447) - .ready(Ready::AnyResponse) - .script(HANDLER) - .start() - else { - return; - }; - - // "hello world" as two chunks: "hello " (0x6) then "world" (0x5). - let response = common::raw_request_within( - server.port(), - b"POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n\ - 6\r\nhello \r\n5\r\nworld\r\n0\r\n\r\n", - Duration::from_secs(20), - ) - .expect("a complete response"); - assert!(response.contains("got:hello world"), "got: {response}"); - - // Both framings at once, with bytes after the chunked terminator that only the `Content-Length` - // reading would take for body: the smuggled request either way is the one that gets two - // different response out of two hops. Native refuses it outright with a 400. `wasmtime serve` - // resolves it in favour of the chunked framing, which RFC 9112 §6.3 also allows a server to do - // — what neither may do is let those bytes through as a body. - let smuggle = common::raw_request_within( - server.port(), - b"POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\n\ - Connection: close\r\n\r\n0\r\n\r\nSMUGGLED", - Duration::from_secs(20), - ) - .unwrap_or_default(); - assert!( - !smuggle.contains("SMUGGLED"), - "the second framing's bytes must not reach the handler: {smuggle:?}" - ); - assert!( - smuggle.is_empty() || common::wasm_serve::message_body(&smuggle) == "got:", - "an ambiguous request is either refused or read one way: {smuggle:?}" - ); -} - -/// A client that closes mid-body must not present the truncation to the handler as a complete -/// body: the request's stream errors, so `text()` rejects (`serve_rejects_a_truncated_request_body`). -/// On wasm that abort arrives through the incoming body's trailers, which is the only channel a -/// `wasi:http` body has for saying it ended badly rather than early. -#[test] -fn serve_rejects_a_truncated_request_body() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - event.respondWith(event.request.text().then( - (body) => new Response('complete:' + body), - () => new Response('truncated'), - )); - });"#; - let Some(server) = Serve::new(18448) - .ready(Ready::AnyResponse) - .script(HANDLER) - .start() - else { - return; - }; - - let mut stream = TcpStream::connect(("127.0.0.1", server.port())).unwrap(); - // Set before the half-close below, and read by hand rather than through `read_until_eof`: - // macOS refuses a read timeout on a socket whose write half is already shut down, and - // `read_until_eof` re-sets one each iteration to keep its patience a total. - stream - .set_read_timeout(Some(Duration::from_secs(20))) - .unwrap(); - // Declare ten bytes, send four, then close the sending side. - stream - .write_all( - b"POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 10\r\nConnection: close\r\n\r\nfour", - ) - .unwrap(); - stream.shutdown(std::net::Shutdown::Write).unwrap(); - let mut raw = Vec::new(); - let mut buf = [0u8; 1024]; - while let Ok(read) = std::io::Read::read(&mut stream, &mut buf) { - if read == 0 { - break; - } - raw.extend_from_slice(&buf[..read]); - } - let response = String::from_utf8_lossy(&raw).into_owned(); - assert!( - response.contains("truncated"), - "a truncated upload must make the handler's read reject: {response:?}" - ); -} - -/// An incoming request body used directly as the response body is handed to the wire without being -/// pumped through JS — the incoming-to-outgoing shortcut, which applies because the incoming body -/// is a host body rather than bytes (`serve_pipes_an_incoming_request_body_to_the_response`). -#[test] -fn serve_pipes_an_incoming_request_body_to_the_response() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - event.respondWith(new Response(event.request.body ?? 'no body')); - });"#; - let Some(server) = Serve::new(18449) - .ready(Ready::AnyResponse) - .script(HANDLER) - .start() - else { - return; - }; - - assert_eq!(server.request("POST", "/", "hello world"), "hello world"); -} - -/// The same body going two ways at once: forwarded upstream as an outgoing request's body while -/// the handler reads the other half of the `tee` -/// (`serve_forwards_an_incoming_body_upstream_while_reading_it`). Both halves have to see the whole -/// upload, which is travelling from one connection to another while a second reader pulls on it. -#[test] -fn serve_forwards_an_incoming_body_upstream_while_reading_it() { - let upstream = common::start_echo_upstream(); - let handler = format!( - r#"addEventListener('fetch', (event) => {{ - event.respondWith((async () => {{ - const [forward, inspect] = event.request.body.tee(); - const sent = fetch('http://127.0.0.1:{upstream}/', {{ - method: 'POST', - body: forward, - duplex: 'half', - }}); - let seen = ''; - const reader = inspect.getReader(); - const decoder = new TextDecoder(); - while (true) {{ - const {{ done, value }} = await reader.read(); - if (done) break; - seen += decoder.decode(value, {{ stream: true }}); - }} - const echoed = await (await sent).text(); - return new Response('upstream=' + echoed + ' local=' + seen); - }})()); - }});"# - ); - let Some(server) = Serve::new(18450) - .ready(Ready::AnyResponse) - .script(&handler) - .start() - else { - return; - }; - - assert_eq!( - server.request("POST", "/", "payload bytes"), - "upstream=payload bytes local=payload bytes" - ); -} - -/// `clone()` on an incoming request tees its body, and both halves have to see all of it -/// (`serve_clones_an_incoming_request_body`). -#[test] -fn serve_clones_an_incoming_request_body() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - const copy = event.request.clone(); - event.respondWith(Promise.all([event.request.text(), copy.text()]) - .then(([a, b]) => new Response(a + '|' + b))); - });"#; - let Some(server) = Serve::new(18451) - .ready(Ready::AnyResponse) - .script(HANDLER) - .start() - else { - return; - }; - - assert_eq!( - server.request("POST", "/", "payload"), - "payload|payload", - "both halves of the tee must see the body" - ); -} - -/// `respondWith(response)` consumes the response's body, so `bodyUsed` reads true afterwards — and -/// responding with one whose body is already gone is a network error, refused before the head is -/// committed since there is no way to report it after (`respond_with_marks_the_response_body_used`, -/// `responding_with_a_used_stream_backed_response_is_a_network_error`). Both read the flag back on -/// a later request, which the reused instance is what makes possible. +/// `respondWith(response)` consumes the response's body, so `bodyUsed` reads true afterwards. +/// Responding with one whose body is already gone is a network error, refused before the head is +/// committed since there is no way to report it after +/// (`a_sent_byte_body_reads_as_consumed_rather_than_absent`, +/// `responding_with_a_used_stream_backed_response_is_a_network_error`). Both read the flag back on +/// a later request, which only a reused instance makes possible. #[test] fn respond_with_marks_the_response_body_used() { const HANDLER: &str = r#"globalThis.stored = null; @@ -2492,7 +1527,7 @@ fn respond_with_marks_the_response_body_used() { })); event.respondWith(globalThis.stored); });"#; - let Some(server) = Serve::new(18452) + let Some(server) = Serve::new() .reusing_one_instance() .ready(Ready::AnyResponse) .script(HANDLER) @@ -2526,11 +1561,16 @@ fn fetch_event_handled_reports_the_outcome() { globalThis.state = undefined; return; } + if (url.pathname === '/ready') { + // Not hooked, so the readiness probe records no outcome for `/check` to read. + event.respondWith(new Response('ok')); + return; + } const identical = event.handled === event.handled; event.handled.then( () => { globalThis.state = 'resolved,same=' + identical; }, (e) => { globalThis.state = 'rejected:' + e.name + ',same=' + identical; }); - if (url.pathname === '/responds' || url.pathname === '/ready') { + if (url.pathname === '/responds') { event.respondWith(new Response('ok')); } else if (url.pathname === '/rejects') { event.respondWith(Promise.reject(new Error('no response for you'))); @@ -2539,7 +1579,7 @@ fn fetch_event_handled_reports_the_outcome() { } // /silent falls through without calling respondWith at all. });"#; - let Some(server) = Serve::new(18453) + let Some(server) = Serve::new() .reusing_one_instance() .ready(Ready::AnyResponse) .script(HANDLER) @@ -2552,14 +1592,7 @@ fn fetch_event_handled_reports_the_outcome() { // loop is drained afterwards, so ask until they have recorded something. let settled = |path: &str| { let _ = server.full_request("GET", path, ""); - let deadline = std::time::Instant::now() + Duration::from_secs(20); - loop { - let state = server.get("/check"); - if state != "never settled" || std::time::Instant::now() >= deadline { - return state; - } - std::thread::sleep(Duration::from_millis(20)); - } + eventually(&server, "/check", |state| state != "never settled") }; // Each case reads back what its own request recorded, so every request here has to reach the @@ -2582,67 +1615,6 @@ fn fetch_event_handled_reports_the_outcome() { assert_eq!(outcomes[3], "rejected:NetworkError,same=true"); } -/// A handler that throws instead of responding gets a 500 rather than a hung connection -/// or an empty 200, and the throw does not take the dispatch down: a listener after it still gets -/// the event (`a_throwing_handler_is_answered_with_500`). The same holds for `preventDefault`, -/// which cancels the event without stopping a later listener from responding — and, on its own, -/// leaves nothing to respond with (`prevent_default_does_not_stop_a_later_listener_responding`). -#[test] -fn a_failing_listener_does_not_stop_a_later_one_responding() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - if (new URL(event.request.url).pathname === '/boom') { - throw new Error('handler blew up'); - } - if (new URL(event.request.url).pathname === '/cancel-only') { - event.preventDefault(); - } - }); - addEventListener('fetch', (event) => { - const path = new URL(event.request.url).pathname; - if (path === '/boom' || path === '/cancel-only') { - return; - } - event.respondWith(new Response(path === '/ready' ? 'ready' : 'recovered')); - });"#; - let Some(server) = Serve::new(18454).script(HANDLER).start() else { - return; - }; - - assert!( - server - .full_request("GET", "/boom", "") - .starts_with("HTTP/1.1 500"), - "a throwing listener is answered with a 500" - ); - assert!( - server - .full_request("GET", "/cancel-only", "") - .starts_with("HTTP/1.1 500 "), - "cancelling without responding is a network error" - ); - // Neither takes the dispatch down: a listener after them still gets the event and responds. - assert_eq!(server.get("/ok"), "recovered"); -} - -/// A `waitUntil` promise that rejects is the handler's business, not the response's -/// (`a_rejected_wait_until_does_not_break_the_response`). -#[test] -fn a_rejected_wait_until_does_not_break_the_response() { - const HANDLER: &str = r#"addEventListener('fetch', (event) => { - event.waitUntil(Promise.reject(new Error('waitUntil failed'))); - event.respondWith(new Response('answered anyway')); - });"#; - let Some(server) = Serve::new(18455) - .ready(Ready::AnyResponse) - .script(HANDLER) - .start() - else { - return; - }; - - assert_eq!(server.get("/"), "answered anyway"); -} - /// `waitUntil` may only extend an event that is still `active`. Once the event is over there is /// nothing left to extend, since the request has been served, so it throws rather than silently /// doing nothing (`wait_until_throws_once_the_event_is_over`). The body left open for a moment @@ -2672,7 +1644,7 @@ fn wait_until_throws_once_the_event_is_over() { }, }))); });"#; - let Some(server) = Serve::new(18456) + let Some(server) = Serve::new() .reusing_one_instance() .ready(Ready::AnyResponse) .script(HANDLER) @@ -2682,9 +1654,11 @@ fn wait_until_throws_once_the_event_is_over() { }; assert_eq!(server.get("/"), "during=allowed"); - // The timer runs while the request's loop is drained after its response, so give it time. - std::thread::sleep(Duration::from_millis(400)); - assert_eq!(server.get("/check"), "InvalidStateError"); + // The timer runs while the request's loop is drained after its response. + assert_eq!( + eventually(&server, "/check", |late| late != "never ran"), + "InvalidStateError" + ); } /// What keeps a request alive past its response is `waitUntil` and nothing else: a bare timer goes @@ -2699,7 +1673,7 @@ fn nothing_but_wait_until_extends_the_event_lifetime() { // outlived the request. The loop is driven while the body goes out, as a streamed body needs, // and an upstream that responds inside that window settles without the event's lifetime having // been extended. - let upstream = common::start_slow_upstream(Duration::from_secs(1), "UPSTREAM-BODY"); + let upstream = common::start_slow_upstream(Duration::from_millis(300), "UPSTREAM-BODY"); let handler = format!( r#"addEventListener('fetch', (event) => {{ const path = new URL(event.request.url).pathname; @@ -2710,12 +1684,12 @@ fn nothing_but_wait_until_extends_the_event_lifetime() { setTimeout(() => {{ globalThis.ran = 'the timer ran'; console.error('leftover-timer-ran'); - }}, 300); + }}, 100); fetch('http://127.0.0.1:{upstream}/').then(() => console.error('leftover-fetch-settled')); event.respondWith(new Response('answered')); }});"# ); - let Some(server) = Serve::new(18457) + let Some(server) = Serve::new() .reusing_one_instance() .ready(Ready::AnyResponse) .script(&handler) @@ -2729,7 +1703,7 @@ fn nothing_but_wait_until_extends_the_event_lifetime() { let answered = server.get("/"); // Well past both the timer's delay and the time the upstream takes to reply: a loop still // being driven would have run them by now. - std::thread::sleep(Duration::from_millis(1500)); + std::thread::sleep(Duration::from_millis(800)); (answered, server.get("/check")) }); assert_eq!(answered, "answered"); @@ -2746,35 +1720,6 @@ fn nothing_but_wait_until_extends_the_event_lifetime() { ); } -/// The first request waits for the content script's top-level `await`, so a handler registered -/// after one is in place for it — including on a snapshot, where wizer captures the script -/// mid-evaluation and the resumed instance's first request drives the rest (`test-wizer.sh`'s -/// `awaited.mjs` case, extended here with the ordering the marker pins). -#[test] -fn a_wizened_instance_waits_for_the_scripts_top_level_await() { - // The ordering is recorded in a global and reported in the response rather than through the - // usual stderr markers: writing to stdio before the snapshot is taken leaves the resumed - // instance with a stale stream handle, and every later write traps (see the wizer item in - // `docs/wasm-serve-parity-todo.md`). - const SCRIPT: &str = r#"globalThis.order = ['evaluating']; - await new Promise((resolve) => setTimeout(resolve, 300)); - globalThis.order.push('evaluated'); - addEventListener('fetch', (event) => { - globalThis.order.push('dispatched'); - event.respondWith(new Response(globalThis.order.join(','))); - });"#; - let Some(server) = Serve::new(18458) - .wizen() - .ready(Ready::Listening) - .module("awaited.mjs", SCRIPT) - .start() - else { - return; - }; - - assert_eq!(server.get("/first"), "evaluating,evaluated,dispatched"); -} - /// A handler fetching its own server: guest-out through `wasi:http`'s client, host-in through the /// same listener (`a_handler_can_fetch_its_own_server`). /// @@ -2784,10 +1729,10 @@ fn a_wizened_instance_waits_for_the_scripts_top_level_await() { /// event loop makes it arrive while that instance is blocked on the fetch. #[test] fn a_handler_can_fetch_its_own_server() { - // The handler addresses itself by port rather than by `request.url`'s origin: what the guest - // sees there is built from the `Host` header, which carries no port here. - const PORT: u16 = 18459; - let gate = common::start_forwarding_gate(PORT); + // The handler addresses its own server through the gate rather than by `request.url`'s + // origin: what the guest sees there is built from the `Host` header, which has no port + // here. + let gate = common::start_forwarding_gate(); let handler = format!( r#"addEventListener('fetch', (event) => {{ const path = new URL(event.request.url).pathname; @@ -2805,7 +1750,7 @@ fn a_handler_can_fetch_its_own_server() { // No readiness probe, so `/outer` is the request that stands the instance up and the only // instance there is. A probe would leave a second one idle beside it, and the inner request // could be handed to either. - let Some(server) = Serve::new(PORT) + let Some(server) = Serve::new() .reusing_one_instance() .ready(Ready::Listening) .script(&format!("{INSTANCE_ID}\n{handler}")) @@ -2829,7 +1774,7 @@ fn a_handler_can_fetch_its_own_server() { server.log() ); server.await_new_idle(&mut seen, IDLE_PATIENCE); - gate.release(); + gate.release(port); common::wasm_serve::message_body(&outer.join().unwrap()) }); assert!( @@ -2851,7 +1796,7 @@ fn a_handler_can_fetch_its_own_server() { } /// How long the two cases below wait for what the guest reports after its response is gone. The -/// work itself takes two seconds, and the rest is room for a machine running the whole suite. +/// work itself takes 600ms, and the rest is room for a machine running the whole suite. const MARKER_PATIENCE: Duration = Duration::from_secs(60); /// A handler whose response body never ends, reporting the request signal's abort and its @@ -2869,28 +1814,33 @@ const ENDLESS_STREAM: &str = r#"addEventListener('fetch', (event) => { event.waitUntil(new Promise((resolve) => setTimeout(() => { console.error('lifetime-work-finished'); resolve(); - }, 2000))); + }, 600))); event.respondWith(new Response(new ReadableStream({ start(c) { setInterval(() => c.enqueue(new TextEncoder().encode('tick ')), 50); }, }))); });"#; /// The guest never performs the response write itself, so how a send ending badly reaches the -/// handler — step 17.4.20's abort, raised from the drain task off the body writer's reported -/// outcome — exists only against a real host. Here the body burns its 1s window, and the writer's -/// verdict must come back as a `TimeoutError` on the request's signal, with the `waitUntil` window -/// still its own: the 2s of lifetime work finishes inside its 4s bound, as on the native path -/// (`wait_until_gets_its_own_window_after_the_body_is_truncated`). +/// handler exists only against a real host: step 17.4.20's abort, raised from the drain task from +/// the body writer's reported outcome. Here the body spends its 200ms window, and the writer's +/// outcome must reach the request's signal as a `TimeoutError`, with the `waitUntil` window still +/// its own: the 600ms of lifetime work finishes inside its 20s bound, as on the native path +/// (`a_response_body_timeout_truncates_a_body_that_never_ends`). #[test] fn a_spent_response_body_clock_aborts_the_request_signal() { - let Some(server) = Serve::new(18400) + let Some(server) = Serve::new() // The lifetime work runs after the response is gone, past the point the host counts the // request as done. Under the default one-second idle timeout the host may drop the // instance while that work is still running, and the marker never arrives. .reusing_one_instance() - // The bound only has to clear the 2s of lifetime work, with room for a loaded machine. + // The bound only has to clear the 600ms of lifetime work, with room for a loaded machine. // Under test is that truncating the body left the window intact. - .flags(["--response-body-timeout", "1", "--waituntil-timeout", "20"]) + .flags([ + "--response-body-timeout", + "0.2", + "--waituntil-timeout", + "20", + ]) .script(ENDLESS_STREAM) .start() else { @@ -2917,14 +1867,14 @@ fn a_spent_response_body_clock_aborts_the_request_signal() { /// The other way a send ends badly, with no clock involved: the client walks away mid-stream, the /// host drops the body stream's read end, and the writer's verdict must come back as an -/// `AbortError`. This is the wasm side of `a_lost_connection_aborts_the_request_signal`. Losing -/// the client leaves the `waitUntil` window intact, as truncating the body does above. +/// `AbortError`. This is the wasm side of `a_client_disconnect_does_not_cancel_wait_until_work`. +/// Losing the client leaves the `waitUntil` window intact, as truncating the body does above. #[test] fn a_client_hanging_up_mid_body_aborts_the_request_signal() { - let Some(server) = Serve::new(18401) + let Some(server) = Serve::new() // The instance has to outlive the response for the same reason as above. .reusing_one_instance() - // Clears the 2s of lifetime work with room for a loaded machine. See above. + // Clears the 600ms of lifetime work with room for a loaded machine. See above. .flags(["--waituntil-timeout", "20"]) .script(ENDLESS_STREAM) .start() @@ -2956,3 +1906,949 @@ fn a_client_hanging_up_mid_body_aborts_the_request_signal() { server.log() ); } + +/// One case of a server that several cases share: the route it serves and the check that requests +/// it. +struct Case { + /// The first segment of every path the case requests, which also names the case in a failure. + name: &'static str, + /// A JS function `(event, rest) => ...` that handles `/`. `rest` is the rest of the + /// path: empty, or starting with `/`. + route: String, + /// Requests the route, and panics if the outcome is wrong. + check: fn(&WasmServer), +} + +/// A classic script that serves `/ready`, and hands every other request to the route of the case +/// that its path's first segment names. +fn routed_script(cases: &[Case]) -> String { + let routes: String = cases + .iter() + .map(|case| format!("'{}': {},\n", case.name, case.route)) + .collect(); + format!( + r#"const routes = {{ +{routes}}}; +addEventListener('fetch', (event) => {{ + const path = new URL(event.request.url).pathname; + if (path === '/ready') {{ + event.respondWith(new Response('ready')); + return; + }} + const name = path.split('/')[1]; + routes[name](event, path.slice(name.length + 1)); +}});"# + ) +} + +/// Run every check against `server`, and fail naming every check that panicked, so one failing +/// case does not hide the others. +fn check_all( + server: &WasmServer, + checks: impl IntoIterator, +) { + let mut ran = 0; + let mut failed = Vec::new(); + for (name, check) in checks { + ran += 1; + if let Err(panic) = std::panic::catch_unwind(AssertUnwindSafe(|| check(server))) { + let message = panic + .downcast_ref::() + .map(String::as_str) + .or_else(|| panic.downcast_ref::<&str>().copied()) + .unwrap_or(""); + failed.push(format!("{name}: {message}")); + } + } + assert!( + failed.is_empty(), + "{} of {ran} cases failed:\n{}", + failed.len(), + failed.join("\n") + ); +} + +/// Cases about the response a handler builds and how it reaches the client. None of them depends +/// on what an earlier request left in the instance, so they share one server with default flags. +#[test] +fn response_cases_share_one_server() { + let cases = response_cases(); + let Some(server) = Serve::new().script(&routed_script(&cases)).start() else { + return; + }; + check_all(&server, cases.iter().map(|case| (case.name, case.check))); +} + +fn response_cases() -> Vec { + let upstream = common::start_upstream("UPSTREAM-BODY"); + let chunked_upstream = common::start_chunked_upstream(&["one ", "two ", "three"]); + let truncated_upstream = common::start_truncated_upstream(&["one ", "two "]); + vec![ + // `Response.error()` is a network error with status 0, which must not be serialized as the + // status line `HTTP/1.1 0`. It is sent as a 500 (`serve_maps_invalid_status_to_500`). + Case { + name: "invalid-status", + route: "(event) => event.respondWith(Response.error())".into(), + check: |server| { + let response = server.full_request("GET", "/invalid-status", ""); + assert!(response.starts_with("HTTP/1.1 500"), "got: {response}"); + }, + }, + // Fetch isomorphic-encodes a header value, so `U+00DF U+00C0 U+00BF` is sent as the bytes + // `DF C0 BF` rather than as their UTF-8 encodings + // (`a_response_header_value_is_sent_as_bytes_not_utf8`). The second header must survive + // too, in case the host refuses the header list as a whole. + Case { + name: "header-bytes", + route: r#"(event) => event.respondWith(new Response('x', { headers: { + 'x-test': 'ßÀ¿', + 'x-other': 'kept', + } }))"# + .into(), + check: |server| { + let raw = common::raw_response_within( + server.port(), + "GET", + "/header-bytes", + "", + Duration::from_secs(20), + ) + .expect("a complete response"); + let needle = b"x-test: \xdf\xc0\xbf\r\n"; + assert!( + raw.windows(needle.len()).any(|window| window == needle), + "the header must be the three bytes DF C0 BF, got: {:?}", + String::from_utf8_lossy(&raw) + ); + assert!( + raw.windows(14).any(|window| window == b"x-other: kept\r"), + "the other headers must survive: {:?}", + String::from_utf8_lossy(&raw) + ); + }, + }, + // `wasi:http` refuses the connection-management headers it writes itself. + // `prepare_wire_response` does not filter them, and a handler, or an upstream whose + // response is proxied, may set one. Only that header may be lost. + Case { + name: "refused-header", + route: r#"(event) => event.respondWith(new Response('body', { headers: { + 'x-before': 'kept', + 'keep-alive': 'timeout=5', + 'x-after': 'kept', + } }))"# + .into(), + check: |server| { + let response = server.full_request("GET", "/refused-header", ""); + assert!(response.contains("x-before: kept"), "{response}"); + assert!(response.contains("x-after: kept"), "{response}"); + assert!( + !response.to_lowercase().contains("keep-alive"), + "the refused header must not be on the wire: {response}" + ); + assert_eq!( + common::wasm_serve::message_body(&response), + "body", + "{response}" + ); + assert!( + server.wait_for_marker( + "dropping the response header `keep-alive`", + Duration::from_secs(5) + ), + "{}", + server.log() + ); + }, + }, + // A field that arrived in mixed case is found by any casing. The incoming headers are + // immutable. Headers put on the response keep `set-cookie` as separate fields, while + // everything else combines (`serve_exposes_incoming_headers_and_keeps_them_immutable`). + Case { + name: "incoming-headers", + route: r#"(event) => { + const incoming = event.request.headers; + let immutable = 'mutable'; + try { + incoming.delete('user-agent'); + } catch (e) { + immutable = e.constructor.name; + } + const response = new Response('ok', { + headers: [ + ['x-seen', incoming.get('EXAMPLE-HEADER') ?? 'missing'], + ['x-agent', incoming.get('User-Agent') ?? 'missing'], + ['x-immutable', immutable], + ], + }); + response.headers.append('set-cookie', 'A'); + response.headers.append('set-cookie', 'B'); + response.headers.append('another', 'A'); + response.headers.append('another', 'B'); + event.respondWith(response); + }"# + .into(), + check: |server| { + let response = common::raw_request_within( + server.port(), + b"GET /incoming-headers HTTP/1.1\r\nHost: x\r\neXample-hEader: Header Value\r\n\ + User-Agent: test-agent\r\nConnection: close\r\n\r\n", + Duration::from_secs(20), + ) + .expect("a complete response"); + assert!( + response.contains("x-seen: Header Value"), + "a mixed-case field must be found by any casing: {response}" + ); + assert!(response.contains("x-agent: test-agent"), "got: {response}"); + assert!( + response.contains("x-immutable: TypeError"), + "an incoming request's headers must not be mutable: {response}" + ); + // `set-cookie` must stay one field per cookie, since combining two cookies into + // one field changes what they mean. Other repeats may be sent as separate fields + // or as one comma list, which RFC 9110 §5.2 makes equivalent, but both values have + // to survive. + assert_eq!( + response.matches("set-cookie: ").count(), + 2, + "each cookie needs a field of its own: {response}" + ); + assert!(response.contains("set-cookie: A"), "got: {response}"); + assert!(response.contains("set-cookie: B"), "got: {response}"); + assert!( + response.contains("another: A, B") + || (response.contains("another: A") && response.contains("another: B")), + "both values of a repeated header must reach the client: {response}" + ); + }, + }, + // `wasmtime serve` forwards a `HEAD` to the guest and sends what the guest produced: no + // body, and the `Content-Length` a `GET` would have had. `prepare_wire_response` can supply + // that length only for an in-memory body, since a streamed body's length is known only + // once it is produced (`serve_answers_a_head_request_without_a_body`). + Case { + name: "head", + route: r#"(event, rest) => { + if (rest === '/stream') { + event.respondWith(new Response(new ReadableStream({ + start(c) { + c.enqueue(new TextEncoder().encode('BODYBYTES')); + c.close(); + }, + }))); + return; + } + event.respondWith(new Response('BODYBYTES', { status: 201 })); + }"# + .into(), + check: |server| { + let response = server.full_request("HEAD", "/head", ""); + let (head, body) = response.split_once("\r\n\r\n").expect("a complete head"); + assert!( + head.starts_with("HTTP/1.1 201 "), + "the head is still the handler's: {response}" + ); + assert_eq!( + body, "", + "a HEAD response must not include a body, but got: {response}" + ); + assert!( + head.to_lowercase().contains("content-length: 9"), + "a HEAD must declare the length a GET would have sent: {head}" + ); + + let streamed = server.full_request("HEAD", "/head/stream", ""); + let (head, body) = streamed.split_once("\r\n\r\n").expect("a complete head"); + assert_eq!(body, "", "got: {streamed}"); + assert!( + !head.to_lowercase().contains("content-length"), + "a streamed body's length is only knowable by producing it: {head}" + ); + }, + }, + // A `204` or `304` is framed by its status, with no `Content-Length` and no + // `Transfer-Encoding` (RFC 9110 §8.6, RFC 9112 §6.2, + // `a_bodiless_status_has_no_framing_headers`). + Case { + name: "bodiless", + route: "(event, rest) => \ + event.respondWith(new Response(null, { status: Number(rest.slice(1)) }))" + .into(), + check: |server| { + for status in [204, 304] { + let response = server.full_request("GET", &format!("/bodiless/{status}"), ""); + let head = response + .split_once("\r\n\r\n") + .map_or(&*response, |(head, _)| head) + .to_lowercase(); + assert!( + !head.contains("content-length"), + "a {status} must not be framed with Content-Length: {response}" + ); + assert!( + !head.contains("transfer-encoding"), + "a {status} must not be framed with Transfer-Encoding: {response}" + ); + } + }, + }, + // A handler's own `Content-Length` never frames content already in memory. + // `prepare_wire_response` measures the body instead + // (`serve_frames_a_response_by_its_body_not_the_handlers_content_length`). + Case { + name: "handler-length", + route: r#"(event, rest) => { + const declared = { + // Longer than the body. + '/larger': ['10000'], + // Not a number at all. + '/bogus': ['test'], + // Two of them, which combine into one comma-joined value. + '/duplicate': ['10000', '10000'], + }[rest]; + const headers = new Headers(); + for (const value of declared) headers.append('content-length', value); + event.respondWith(new Response('short', { headers })); + }"# + .into(), + check: |server| { + for path in [ + "/handler-length/larger", + "/handler-length/bogus", + "/handler-length/duplicate", + ] { + let response = server.full_request("GET", path, ""); + let head = response + .split_once("\r\n\r\n") + .expect("a complete response") + .0; + assert_eq!(server.request("GET", path, ""), "short", "body for {path}"); + let lower = head.to_lowercase(); + assert!( + !lower.contains("10000") && !lower.contains("content-length: test"), + "the handler's own length must not reach the wire for {path}: {head}" + ); + } + }, + }, + // The guest declares the length of an in-memory body, as native does + // (`serve_frames_a_response_by_its_body_not_the_handlers_content_length`). The host frames + // by what the guest hands it, and without a declared length it chunks every such response. + Case { + name: "fixed-length", + route: "(event) => event.respondWith(new Response('BODYBYTES'))".into(), + check: |server| { + let response = server.full_request("GET", "/fixed-length", ""); + let head = response + .split_once("\r\n\r\n") + .expect("a complete response") + .0 + .to_lowercase(); + assert!(head.contains("content-length: 9"), "got: {head}"); + assert!(!head.contains("transfer-encoding"), "got: {head}"); + assert_eq!(server.get("/fixed-length"), "BODYBYTES"); + }, + }, + // A streamed body's length is not known before it is produced, so a length the handler + // declares frames the response, as on native + // (`serve_frames_a_streamed_body_by_its_declared_content_length`). The guest writes the + // body, so content past the declared length is never handed to the host. A declaration + // that is not a single `1*DIGIT` is refused, and the host picks the framing. + Case { + name: "declared-length", + route: r#"(event, rest) => { + const [parts, length] = { + '/exact': [['abc', 'de'], '5'], + '/longer': [['abcde', 'XXXXX'], '5'], + // Less than it declared: the guest ends the body as an abort, so the host + // cannot present the truncation as a complete response. + '/shorter': [['ab'], '5'], + '/bogus': [['abcde'], 'test'], + }[rest]; + event.respondWith(new Response(new ReadableStream({ + start(c) { + for (const part of parts) c.enqueue(new TextEncoder().encode(part)); + c.close(); + }, + }), { headers: { 'content-length': length } })); + }"# + .into(), + check: |server| { + for path in ["/declared-length/exact", "/declared-length/longer"] { + let response = server.full_request("GET", path, ""); + let (head, body) = response + .split_once("\r\n\r\n") + .expect("a complete response"); + assert!( + head.to_lowercase().contains("content-length: 5"), + "the declared length must frame {path}: {head}" + ); + assert!( + !head.to_lowercase().contains("transfer-encoding"), + "got for {path}: {head}" + ); + assert_eq!(body, "abcde", "body for {path}: {response}"); + } + + let short = server.full_request("GET", "/declared-length/shorter", ""); + let sent = short + .split_once("\r\n\r\n") + .map(|(_, body)| body) + .unwrap_or(""); + assert!( + sent.len() < 5, + "the body must stop short of the length it declared: {short:?}" + ); + + let response = server.full_request("GET", "/declared-length/bogus", ""); + let head = response + .split_once("\r\n\r\n") + .expect("a complete response") + .0; + assert!(!head.contains("test"), "got: {head}"); + assert_eq!(server.get("/declared-length/bogus"), "abcde"); + }, + }, + // A body that errors after sending bytes delivers what was produced, and then ends without + // the terminating chunk, so the truncation cannot be mistaken for a complete response + // (`a_response_stream_that_errors_mid_flight_leaves_the_body_unterminated`). + Case { + name: "errors-mid-flight", + // The error comes from a timer. A body cannot outlive its request's event loop going + // idle, so the stream holds the loop open until it errors. The delay only has to + // outlast the first chunk reaching the wire: a host that has not flushed a chunk when + // the body aborts may discard it. + route: r#"(event) => event.respondWith(new Response(new ReadableStream({ + start(c) { + c.enqueue(new TextEncoder().encode('first-chunk-')); + setTimeout(() => c.error(new Error('stream broke')), 300); + }, + })))"# + .into(), + check: |server| { + let errored = server.full_request("GET", "/errors-mid-flight", ""); + let (_, body) = errored.split_once("\r\n\r\n").expect("a complete head"); + assert!( + body.contains("first-chunk-"), + "the bytes produced before the error must still arrive: {errored}" + ); + assert!( + !body.ends_with("0\r\n\r\n"), + "an errored body must not be terminated as if it were complete: {errored}" + ); + }, + }, + // A stream that enqueues something other than a `Uint8Array` aborts its body before a byte + // is written (WPT + // `resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js`). + // + // Host-owned divergence: native sends a `200` head and then closes the body unterminated. + // `wasmtime serve` has not committed the head when the body aborts, and sends no response + // (see the comment where `dispatch_request` hands the response over). Either way, the + // client must not get a response that looks complete. + Case { + name: "invalid-chunk", + route: r#"(event) => event.respondWith(new Response(new ReadableStream({ + start(c) { + c.enqueue('a string, not a Uint8Array'); + c.close(); + }, + })))"# + .into(), + check: |server| { + // The control, since the response under test may be empty: an empty reply below is + // the invalid chunk's doing, not a server that stopped responding. + assert_eq!(server.get("/ready"), "ready"); + let response = server.full_request("GET", "/invalid-chunk", ""); + assert!( + response.is_empty() || !response.ends_with("0\r\n\r\n"), + "a body that never produced a valid chunk must reach the client as nothing at \ + all, or as an unterminated one, never as a complete response: {response:?}" + ); + }, + }, + // An empty chunk neither ends the body nor corrupts the framing + // (`serve_streams_an_empty_chunk_without_ending_the_body`). `spawn_body_writer`'s + // dropped-reader check does not apply to it, since an empty write leaves an empty + // remainder either way. + Case { + name: "empty-chunk", + route: r#"(event) => { + const encoder = new TextEncoder(); + event.respondWith(new Response(new ReadableStream({ + start(c) { + c.enqueue(new Uint8Array(0)); + c.enqueue(encoder.encode('chunk one ')); + c.enqueue(new Uint8Array(0)); + c.enqueue(encoder.encode('chunk two')); + c.close(); + }, + }))); + }"# + .into(), + check: |server| assert_eq!(server.get("/empty-chunk"), "chunk one chunk two"), + }, + // A body produced by a timer is served to completion. The pump runs on the request's own + // loop, which is driven for as long as the body needs it + // (`an_interval_driven_stream_is_served_to_completion`). + Case { + name: "interval-stream", + route: r#"(event) => { + let ticks = 0; + event.respondWith(new Response(new ReadableStream({ + start(c) { + const id = setInterval(() => { + c.enqueue(new TextEncoder().encode('tick ' + ++ticks + ' ')); + if (ticks === 5) { + clearInterval(id); + c.close(); + } + }, 50); + }, + }))); + }"# + .into(), + check: |server| { + assert_eq!( + server.get("/interval-stream"), + "tick 1 tick 2 tick 3 tick 4 tick 5 " + ); + }, + }, + // An upstream body forwarded as a stream rather than collected first. The handler builds + // its own `Response` around `upstream.body`, and the chunks, which the upstream sends with + // pauses between them, reach the client (`body_path.rs`'s + // `single_bodies_take_the_route_they_should`). + Case { + name: "upstream-stream", + route: format!( + "(event) => event.respondWith((async () => {{ + const upstream = await fetch('http://127.0.0.1:{chunked_upstream}/'); + return new Response(upstream.body, upstream); + }})())" + ), + check: |server| assert_eq!(server.get("/upstream-stream"), "one two three"), + }, + // An upstream that dies mid-body reaches the client as a truncation, not as a body that + // ended cleanly a few bytes short, since the terminating chunk asserts completeness (RFC + // 9112 §7.1). The guest never sees the body's bytes, and the truncation reaches the client + // through the piped trailers. + Case { + name: "dying-upstream", + route: format!( + "(event) => event.respondWith(fetch('http://127.0.0.1:{truncated_upstream}/'))" + ), + check: |server| { + let response = server.full_request("GET", "/dying-upstream", ""); + assert!( + response.contains("one ") && response.contains("two "), + "what the upstream did send must arrive: {response:?}" + ); + assert!( + !response.ends_with("0\r\n\r\n"), + "a truncated upstream must not be forwarded as a complete body: {response:?}" + ); + }, + }, + // A `fetch` whose request has a header the host refuses rejects with a `TypeError` naming + // that header. `http2-settings` is allowed by Fetch but refused by `wasi:http`. + Case { + name: "refused-fetch-header", + route: format!( + "(event) => event.respondWith( + fetch('http://127.0.0.1:{upstream}/', {{ + headers: {{ 'http2-settings': 'AAMAAABk' }}, + }}).then( + () => new Response('sent'), + (e) => new Response(e.name + ': ' + e.message)))" + ), + check: |server| { + let body = server.get("/refused-fetch-header"); + assert!(body.starts_with("TypeError: "), "{body}"); + assert!(body.contains("http2-settings"), "{body}"); + }, + }, + // A streamed response's head is sent when it is ready, not with the first chunk. A handler + // that streams may take a while to produce anything, and the client acts on the status and + // headers meanwhile (`a_streamed_response_sends_its_head_before_the_first_chunk`). + Case { + name: "head-first", + route: r#"(event) => { + const stream = new ReadableStream({ + start(c) { + setTimeout(() => { + c.enqueue(new TextEncoder().encode('late')); + c.close(); + }, 2000); + }, + }); + event.respondWith(new Response(stream, { headers: { 'x-marker': 'early' } })); + }"# + .into(), + check: |server| { + let mut stream = TcpStream::connect(("127.0.0.1", server.port())).unwrap(); + stream + .write_all(b"GET /head-first HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n") + .unwrap(); + let started = std::time::Instant::now(); + + // Read just enough to have seen the head, and time how long that took. + stream + .set_read_timeout(Some(Duration::from_secs(10))) + .unwrap(); + let mut buf = [0u8; 256]; + let read = std::io::Read::read(&mut stream, &mut buf).unwrap(); + let elapsed = started.elapsed(); + let head = String::from_utf8_lossy(&buf[..read]).to_string(); + + assert!(head.starts_with("HTTP/1.1 200"), "got: {head}"); + assert!(head.contains("x-marker: early"), "got: {head}"); + assert!( + elapsed < Duration::from_millis(1500), + "the head must not wait for the first chunk (took {elapsed:?})" + ); + }, + }, + // `waitUntil` work does not hold a streamed response open. The send drives the request's + // loop only for as long as the body needs it + // (`wait_until_does_not_delay_a_streamed_response`). + Case { + name: "streamed-wait-until", + route: r#"(event) => { + const stream = new ReadableStream({ + start(c) { + c.enqueue(new TextEncoder().encode('streamed body')); + c.close(); + }, + }); + event.respondWith(new Response(stream)); + event.waitUntil(new Promise((resolve) => setTimeout(resolve, 4000))); + }"# + .into(), + check: |server| { + let started = std::time::Instant::now(); + let body = server.get("/streamed-wait-until"); + let elapsed = started.elapsed(); + assert_eq!(body, "streamed body"); + assert!( + elapsed < Duration::from_secs(3), + "a streamed response must not wait for waitUntil work (took {elapsed:?})" + ); + }, + }, + ] +} + +/// Cases about the request body a handler reads. None of them depends on what an earlier request +/// left in the instance, so they share one server with default flags. +#[test] +fn request_body_cases_share_one_server() { + let cases = request_body_cases(); + let Some(server) = Serve::new().script(&routed_script(&cases)).start() else { + return; + }; + check_all(&server, cases.iter().map(|case| (case.name, case.check))); +} + +fn request_body_cases() -> Vec { + let echo_upstream = common::start_echo_upstream(); + vec![ + // A handler reading `request.body` sees chunks as they arrive, not after the upload ends. + // The first chunk's echo comes back before the rest is sent + // (`serve_streams_an_incoming_request_body`). + Case { + name: "upload-stream", + route: r#"(event) => { + const reader = event.request.body.getReader(); + event.respondWith(new Response(new ReadableStream({ + async pull(controller) { + const { done, value } = await reader.read(); + if (done) { + controller.close(); + return; + } + controller.enqueue(value); + }, + }))); + }"# + .into(), + check: |server| { + /// Read from `stream` until the response body so far contains `expected`. + fn read_until(stream: &mut TcpStream, raw: &mut Vec, expected: &str) -> String { + for _ in 0..200 { + let body = String::from_utf8_lossy(raw) + .split_once("\r\n\r\n") + .map(|(_, body)| common::dechunk(body)) + .unwrap_or_default(); + if body.contains(expected) { + return body; + } + let mut buf = [0u8; 1024]; + let read = std::io::Read::read(stream, &mut buf).unwrap(); + assert_ne!(read, 0, "connection closed while waiting for {expected:?}"); + raw.extend_from_slice(&buf[..read]); + } + panic!("never received {expected:?}"); + } + + let mut stream = TcpStream::connect(("127.0.0.1", server.port())).unwrap(); + stream + .set_read_timeout(Some(Duration::from_secs(20))) + .unwrap(); + stream + .write_all( + b"POST /upload-stream HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n\ + Connection: close\r\n\r\n", + ) + .unwrap(); + let mut raw = Vec::new(); + // First chunk: the echo must come back before the rest of the request is sent. + stream.write_all(b"5\r\nfirst\r\n").unwrap(); + read_until(&mut stream, &mut raw, "first"); + // Only now send the second chunk and the terminator. + stream.write_all(b"6\r\nsecond\r\n0\r\n\r\n").unwrap(); + let body = read_until(&mut stream, &mut raw, "second"); + assert_eq!(body, "firstsecond", "got: {body}"); + }, + }, + // `wasi:http` gives a request with no body the same empty stream as one whose body is + // empty, so `body_framing` goes by the method: `GET` gets `null`, and a `POST` without a + // body gets an empty stream rather than `null`. + // + // Host-owned divergence: native reads the framing headers the client sent, so a bodyless + // `POST` there gets `null`. + Case { + name: "bodyless-post", + route: "(event) => event.respondWith(\ + new Response(event.request.body === null ? 'null' : 'stream'))" + .into(), + check: |server| { + assert_eq!(server.request("GET", "/bodyless-post", ""), "null"); + assert_eq!(server.request("POST", "/bodyless-post", ""), "stream"); + assert_eq!( + server.request("POST", "/bodyless-post", "payload"), + "stream" + ); + }, + }, + // A chunked upload reaches the handler decoded. The host undoes the framing, and + // `body_framing` exposes a body although there is no `Content-Length` to go by, since + // `wasi:http` does not pass `Transfer-Encoding` on to the guest + // (`serve_decodes_a_chunked_request_body`). + // + // Host-owned divergence: native also refuses `Transfer-Encoding` together with + // `Content-Length` (RFC 9112 §6.3, a request-smuggling vector) with a 400. On wasm the host + // parses the request, so refusing it is the host's business. + Case { + name: "chunked-upload", + route: "(event) => event.respondWith(\ + event.request.text().then((body) => new Response('got:' + body)))" + .into(), + check: |server| { + // "hello world" as two chunks: "hello " (0x6) then "world" (0x5). + let response = common::raw_request_within( + server.port(), + b"POST /chunked-upload HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n\ + Connection: close\r\n\r\n6\r\nhello \r\n5\r\nworld\r\n0\r\n\r\n", + Duration::from_secs(20), + ) + .expect("a complete response"); + assert!(response.contains("got:hello world"), "got: {response}"); + + // Both framings at once, with bytes after the chunked terminator that only the + // `Content-Length` reading would take for body. `wasmtime serve` reads the request + // as chunked, which RFC 9112 §6.3 allows a server to do. Either way, those bytes + // must not reach the handler as a body. + let smuggle = common::raw_request_within( + server.port(), + b"POST /chunked-upload HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n\ + Content-Length: 5\r\nConnection: close\r\n\r\n0\r\n\r\nSMUGGLED", + Duration::from_secs(20), + ) + .unwrap_or_default(); + assert!( + !smuggle.contains("SMUGGLED"), + "the second framing's bytes must not reach the handler: {smuggle:?}" + ); + assert!( + smuggle.is_empty() || common::wasm_serve::message_body(&smuggle) == "got:", + "an ambiguous request is either refused or read one way: {smuggle:?}" + ); + }, + }, + // A client that closes mid-body must not present the truncation to the handler as a + // complete body. The request's stream errors, so `text()` rejects + // (`serve_rejects_a_truncated_request_body`). On wasm the abort arrives through the + // incoming body's trailers. + Case { + name: "truncated-upload", + route: r#"(event) => event.respondWith(event.request.text().then( + (body) => new Response('complete:' + body), + () => new Response('truncated'), + ))"# + .into(), + check: |server| { + let mut stream = TcpStream::connect(("127.0.0.1", server.port())).unwrap(); + // Set before the half-close below, and read by hand rather than through + // `read_until_eof`: macOS refuses a read timeout on a socket whose write half is + // already shut down, and `read_until_eof` sets one on each iteration to keep its + // patience a total. + stream + .set_read_timeout(Some(Duration::from_secs(20))) + .unwrap(); + // Declare ten bytes, send four, then close the sending side. + stream + .write_all( + b"POST /truncated-upload HTTP/1.1\r\nHost: x\r\nContent-Length: 10\r\n\ + Connection: close\r\n\r\nfour", + ) + .unwrap(); + stream.shutdown(std::net::Shutdown::Write).unwrap(); + let mut raw = Vec::new(); + let mut buf = [0u8; 1024]; + while let Ok(read) = std::io::Read::read(&mut stream, &mut buf) { + if read == 0 { + break; + } + raw.extend_from_slice(&buf[..read]); + } + let response = String::from_utf8_lossy(&raw).into_owned(); + assert!( + response.contains("truncated"), + "a truncated upload must make the handler's read reject: {response:?}" + ); + }, + }, + // An incoming request body used directly as the response body is handed to the wire + // without being pumped through JS, since the incoming body is a host body rather than + // bytes (`serve_pipes_an_incoming_request_body_to_the_response`). + Case { + name: "piped-body", + route: "(event) => event.respondWith(new Response(event.request.body ?? 'no body'))" + .into(), + check: |server| { + assert_eq!( + server.request("POST", "/piped-body", "hello world"), + "hello world" + ); + }, + }, + // The same body forwarded upstream as an outgoing request's body while the handler reads + // the other half of the `tee`. Both halves see the whole upload + // (`serve_forwards_an_incoming_body_upstream_while_reading_it`). + Case { + name: "tee-upstream", + route: format!( + r#"(event) => event.respondWith((async () => {{ + const [forward, inspect] = event.request.body.tee(); + const sent = fetch('http://127.0.0.1:{echo_upstream}/', {{ + method: 'POST', + body: forward, + duplex: 'half', + }}); + let seen = ''; + const reader = inspect.getReader(); + const decoder = new TextDecoder(); + while (true) {{ + const {{ done, value }} = await reader.read(); + if (done) break; + seen += decoder.decode(value, {{ stream: true }}); + }} + const echoed = await (await sent).text(); + return new Response('upstream=' + echoed + ' local=' + seen); + }})())"# + ), + check: |server| { + assert_eq!( + server.request("POST", "/tee-upstream", "payload bytes"), + "upstream=payload bytes local=payload bytes" + ); + }, + }, + // `clone()` on an incoming request tees its body, and both halves see all of it + // (`serve_clones_an_incoming_request_body`). + Case { + name: "cloned-body", + route: r#"(event) => { + const copy = event.request.clone(); + event.respondWith(Promise.all([event.request.text(), copy.text()]) + .then(([a, b]) => new Response(a + '|' + b))); + }"# + .into(), + check: |server| { + assert_eq!( + server.request("POST", "/cloned-body", "payload"), + "payload|payload", + "both halves of the tee must see the body" + ); + }, + }, + ] +} + +/// Cases about how the event's listeners and their failures turn into a response, on one server +/// with two listeners. +#[test] +fn listener_cases_share_one_server() { + const HANDLER: &str = r#"addEventListener('fetch', (event) => { + const path = new URL(event.request.url).pathname; + if (path === '/boom' || path === '/throw-then-respond') { + throw new Error('handler blew up'); + } + if (path === '/cancel-only' || path === '/cancel-then-respond') { + event.preventDefault(); + } + }); + addEventListener('fetch', (event) => { + const path = new URL(event.request.url).pathname; + if (path === '/boom' || path === '/cancel-only') { + return; + } + if (path === '/rejected-wait-until') { + event.waitUntil(Promise.reject(new Error('waitUntil failed'))); + event.respondWith(new Response('answered anyway')); + return; + } + event.respondWith(new Response(path === '/ready' ? 'ready' : 'recovered')); + });"#; + let Some(server) = Serve::new().script(HANDLER).start() else { + return; + }; + check_all( + &server, + [ + // A handler that throws instead of responding gets a 500, not a hung connection or an + // empty 200 (`a_throwing_handler_is_answered_with_500`). + ("boom", |server: &WasmServer| { + assert!( + server + .full_request("GET", "/boom", "") + .starts_with("HTTP/1.1 500"), + "a throwing listener is answered with a 500" + ); + }), + // `preventDefault` cancels the event, and on its own leaves nothing to respond with + // (`prevent_default_does_not_stop_a_later_listener_responding`). + ("cancel-only", |server: &WasmServer| { + assert!( + server + .full_request("GET", "/cancel-only", "") + .starts_with("HTTP/1.1 500 "), + "cancelling without responding is a network error" + ); + }), + // Neither a throw nor `preventDefault` takes the dispatch down: a listener after them + // still gets the event and responds. + ("later-listener", |server: &WasmServer| { + assert_eq!(server.get("/throw-then-respond"), "recovered"); + assert_eq!(server.get("/cancel-then-respond"), "recovered"); + }), + // A `waitUntil` promise that rejects does not affect the response + // (`a_rejected_wait_until_does_not_break_the_response`). + ("rejected-wait-until", |server: &WasmServer| { + assert_eq!(server.get("/rejected-wait-until"), "answered anyway"); + }), + ] as [(&'static str, fn(&WasmServer)); 4], + ); +} diff --git a/crates/wpt-support/Cargo.toml b/crates/wpt-support/Cargo.toml index 10f2a4c..080c8a3 100644 --- a/crates/wpt-support/Cargo.toml +++ b/crates/wpt-support/Cargo.toml @@ -7,7 +7,7 @@ authors.workspace = true edition.workspace = true [lib] -# No unit tests; skip building the (SpiderMonkey-sized) lib test harness binary. +# No unit tests. This skips building the (SpiderMonkey-sized) lib test harness binary. test = false [dependencies] diff --git a/crates/wpt-support/src/lib.rs b/crates/wpt-support/src/lib.rs index 5fe683c..0870f2d 100644 --- a/crates/wpt-support/src/lib.rs +++ b/crates/wpt-support/src/lib.rs @@ -14,15 +14,7 @@ use js::native::{Handle, RawJSContext}; use js::prelude::FromJSVal; use js::Object; -/// Install WPT-specific globals on the given global object. -/// -/// Currently installs: -/// - `evalScript(source)` — evaluate a script in non-syntactic scope -/// - `__setLocation(url)` — configure the URL backing `globalThis.location` -/// -/// # Safety -/// -/// Must be called with a valid scope and global object. +/// Install `evalScript`, `__setLocation` and `__wptDone` on `global`. pub fn add_to_global(scope: &Scope<'_>, global: Object<'_>) { js::Function::define( scope, @@ -59,8 +51,7 @@ pub fn add_to_global(scope: &Scope<'_>, global: Object<'_>) { /// top-level `let`/`const` bindings visible to subsequent calls. This is /// how the WPT harness loads `META: script=...` dependencies. /// -/// `url` names the script in stack traces and error messages; it is what tells -/// a failure in a META dependency apart from one in the test itself. +/// `url` names the script in stack traces and error messages. unsafe extern "C" fn eval_script_native( raw_cx: *mut RawJSContext, argc: u32, @@ -80,11 +71,8 @@ unsafe extern "C" fn eval_script_native( Err(_) => return false, }; - // The harness passes the script's URL; fall back to the function's own name when it is absent - // (a hand-written `evalScript(source)` call). The argument count is what says "absent": the - // conversion is WebIDL's DOMString one, which turns a missing argument into the *string* - // "undefined" rather than failing, and traces attributed to a script called "undefined" read - // as corrupted rather than as an eval'd script. + // Without a `url` argument, the script is named after the function. This checks the argument + // count because the DOMString conversion turns a missing argument into the string "undefined". let filename = if argc >= 2 { match String::from_jsval(&scope, Handle::from_raw(args.get(1)), ()) { Ok(filename) => filename, @@ -101,7 +89,7 @@ unsafe extern "C" fn eval_script_native( true } Err(_) => { - println!("Eval failed"); + eprintln!("Eval failed"); false } } diff --git a/crates/wpt-support/tests/wpt_integration.rs b/crates/wpt-support/tests/wpt_integration.rs index 16eb88a..92839ec 100644 --- a/crates/wpt-support/tests/wpt_integration.rs +++ b/crates/wpt-support/tests/wpt_integration.rs @@ -17,15 +17,10 @@ fn eval_wpt(code: &str) -> String { ) } +/// `evalScript` places `let` bindings in a non-syntactic scope, so they are visible to later +/// `evalScript` calls. #[test] -fn eval_script_basic() { - assert_eq!(eval_wpt("evalScript('1 + 2').toString()"), "3"); -} - -#[test] -fn eval_script_shares_bindings() { - // evalScript should place `let` bindings in a non-syntactic scope - // so they are visible to subsequent evalScript calls. +fn eval_script_shares_let_bindings() { assert_eq!( eval_wpt("evalScript('let wptFoo = 42;'); evalScript('wptFoo.toString()')"), "42" @@ -33,32 +28,25 @@ fn eval_script_shares_bindings() { } #[test] -fn eval_script_available_in_wpt_mode() { - // evalScript should be available as a global function. - assert_eq!(eval_wpt("typeof evalScript"), "function"); -} - -#[test] -fn set_location_configures_worker_location() { - // After __setLocation runs, every WorkerLocation accessor must - // report the configured URL's components. +fn set_location_rejects_an_invalid_url() { assert_eq!( eval_wpt( - "__setLocation('http://web-platform.test:8000/foo/bar.any.js?x=1'); \ - [location.href, location.origin, location.pathname, location.search].join('|')" + "try { __setLocation('not a url'); 'no-throw' } \ + catch (e) { (e instanceof TypeError) + ',' + e.message.includes('invalid URL') }" ), - "http://web-platform.test:8000/foo/bar.any.js?x=1|\ - http://web-platform.test:8000|/foo/bar.any.js|?x=1" + "true,true" ); } +/// After `__setLocation` runs, every `WorkerLocation` accessor reports the URL's components. #[test] -fn set_location_rejects_invalid_url() { +fn set_location_sets_every_location_accessor() { assert_eq!( eval_wpt( - "try { __setLocation('not a url'); 'no-throw' } \ - catch(e) { (e instanceof TypeError) + ',' + e.message.includes('invalid URL') }" + "__setLocation('http://web-platform.test:8000/foo/bar.any.js?x=1'); \ + [location.href, location.origin, location.pathname, location.search].join('|')" ), - "true,true" + "http://web-platform.test:8000/foo/bar.any.js?x=1|\ + http://web-platform.test:8000|/foo/bar.any.js|?x=1" ); } diff --git a/starling/Cargo.toml b/starling/Cargo.toml index 19d6362..d7b2c20 100644 --- a/starling/Cargo.toml +++ b/starling/Cargo.toml @@ -40,16 +40,15 @@ core-runtime.workspace = true js.workspace = true libstarling.workspace = true wasip3.workspace = true -# Pinned to the version `wasip3` itself uses, rather than the workspace's, so the component -# carries one wit-bindgen. Each version generates its own `run_ctors_once` guard, and -# `__wrap___wasm_call_ctors` in `lib.rs` collapses those onto one, so the C++ constructors run -# once. SpiderMonkey's `InitializeUptime` asserts it is called once. +# The workspace pins `wit-bindgen` to the version `wasip3` uses, so the component contains one +# wit-bindgen. Each version generates its own `run_ctors_once` guard. `__wrap___wasm_call_ctors` +# in `lib.rs` makes the C++ constructors run once. # # `inter-task-wakeup` lets one wit task wake another through plain Rust wakers, which the serve # path needs: the response-body writer task parks on a Rust channel the request's event-loop -# drain task feeds (and the drain's body-done signal crosses back the other way). Without it the -# executor panics the moment such a task is the only thing left to sleep on — any long-lived -# streamed response (server-sent events, say) hit that. +# drain task feeds (and the drain's body-done signal crosses back the other way). Without it, the +# executor panics when such a task is the only one left to wait on, as in a long-lived streamed +# response such as server-sent events. wit-bindgen = { workspace = true, features = ["macros", "inter-task-wakeup"] } libc = { version = "0.2", optional = true } diff --git a/starling/examples/class_methods.rs b/starling/examples/class_methods.rs index 17b8d79..9130202 100644 --- a/starling/examples/class_methods.rs +++ b/starling/examples/class_methods.rs @@ -2,6 +2,10 @@ //! Demonstrates defining a JS class with methods using `#[jsclass]` and `#[jsmethods]`. +// `showcase` covers what this example shows, so `cargo test --examples` builds it as an empty +// test harness. +#![cfg(not(test))] + use js::compile::evaluate_with_filename; use js::error::ExnThrown; use js::prelude::FromJSVal; @@ -103,8 +107,3 @@ c.data println!("All tests passed!"); } - -#[test] -fn class_methods_proc_example() { - main() -} diff --git a/starling/examples/globals.rs b/starling/examples/globals.rs index 95e655e..027c550 100644 --- a/starling/examples/globals.rs +++ b/starling/examples/globals.rs @@ -5,6 +5,10 @@ //! //! This is useful for APIs like `atob`, `self`, etc. +// `showcase` covers what this example shows, so `cargo test --examples` builds it as an empty +// test harness. +#![cfg(not(test))] + use js::compile::evaluate_with_filename; use js::prelude::FromJSVal; use libstarling::config::RuntimeConfig; @@ -152,8 +156,3 @@ fn main() { println!("\nAll jsglobals tests passed!"); } - -#[test] -fn globals_example() { - main() -} diff --git a/starling/examples/native_module.rs b/starling/examples/native_module.rs index 95644b4..aa2cde8 100644 --- a/starling/examples/native_module.rs +++ b/starling/examples/native_module.rs @@ -5,6 +5,10 @@ //! Defines a native module with constants and functions, registers it, //! imports from JS, and also calls the functions from Rust. +// `showcase` covers what this example shows, so `cargo test --examples` builds it as an empty +// test harness. +#![cfg(not(test))] + use js::compile::evaluate_with_filename; use libstarling::config::RuntimeConfig; use libstarling::jsmodule; @@ -199,8 +203,3 @@ fn main() { println!("\nAll 7 tests passed!"); } - -#[test] -fn native_module_example() { - main() -} diff --git a/starling/src/main.rs b/starling/src/main.rs index 673b42c..e4e8dcc 100644 --- a/starling/src/main.rs +++ b/starling/src/main.rs @@ -31,17 +31,3 @@ fn main() { fn main() { unreachable!("On wasm targets the `starling` cdylib is the entry point, not this binary"); } - -#[cfg(all(test, not(target_arch = "wasm32")))] -#[test] -fn cli_runs() { - let config = libstarling::config::RuntimeConfig::from_args( - ["starlingmonkey", "-e", "1 + 1"] - .iter() - .map(|s| s.to_string()), - ) - .unwrap(); - libstarling::run(config) - .map_err(|e| println!("{e}")) - .expect("Run failed"); -} diff --git a/starling/tests/console_output.rs b/starling/tests/console_output.rs index 0e5103c..5f31260 100644 --- a/starling/tests/console_output.rs +++ b/starling/tests/console_output.rs @@ -18,19 +18,33 @@ fn run_module(source: &str) -> Output { } /// `log`, `info` and `debug` print the bare message on stdout. `warn` and -/// `error` print it on stderr, prefixed with the level. +/// `error` print it on stderr, prefixed with the level. `log` returns +/// `undefined`. #[test] fn console_levels_write_to_their_streams() { let out = run_module( "console.log('a', 1); console.info('b'); console.debug('c');\n\ - console.warn('d'); console.error('e');", + console.warn('d'); console.error('e');\n\ + console.log(String(console.log('f')));", ); assert_eq!(out.status.code(), Some(0)); - assert_eq!(String::from_utf8_lossy(&out.stdout), "a 1\nb\nc\n"); + assert_eq!( + String::from_utf8_lossy(&out.stdout), + "a 1\nb\nc\nf\nundefined\n" + ); assert_eq!(String::from_utf8_lossy(&out.stderr), "Warn: d\nError: e\n"); } -/// `console.assert` prints nothing when its condition holds, and otherwise prints +/// Messages are written whole, including NUL characters in them. +#[test] +fn console_messages_keep_nul_characters() { + let out = run_module("console.log('a\\0b', 'c'); console.error('d\\0e');"); + assert_eq!(out.status.code(), Some(0)); + assert_eq!(out.stdout, b"a\0b c\n"); + assert_eq!(out.stderr, b"Error: d\0e\n"); +} + +/// `console.assert` doesn't print anything when its condition holds, and otherwise prints /// "Assertion failed" and its data on stderr. #[test] fn console_assert_reports_failed_assertions() { @@ -94,3 +108,43 @@ fn top_level_rejection_is_reported_once() { assert!(stderr.contains("top level"), "{stderr}"); assert!(!stderr.contains("Uncaught (in promise)"), "{stderr}"); } + +/// `--serve 0` listens on a port the system picks and prints its address on +/// stderr, where a client can read it and connect. +#[test] +fn serve_on_port_zero_prints_the_bound_address() { + use std::io::{BufRead, Read, Write}; + + let mut child = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) + .args([ + "--serve", + "0", + "--legacy-script", + "-e", + "addEventListener('fetch', (e) => e.respondWith(new Response('served')))", + ]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::piped()) + .spawn() + .expect("failed to run starling"); + let stderr = std::io::BufReader::new(child.stderr.take().unwrap()); + let address = stderr + .lines() + .map_while(Result::ok) + .find_map(|line| Some(line.strip_prefix("serving on http://")?.to_string())) + .expect("the server printed its address before closing stderr"); + + let mut stream = std::net::TcpStream::connect(&address).unwrap(); + stream + .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") + .unwrap(); + let mut response = String::new(); + let read = stream.read_to_string(&mut response); + let _ = child.kill(); + let _ = child.wait(); + read.unwrap(); + assert!( + response.starts_with("HTTP/1.1 200") && response.ends_with("served"), + "{response}" + ); +} diff --git a/starling/tests/exit_status.rs b/starling/tests/exit_status.rs index c803b26..8b5b499 100644 --- a/starling/tests/exit_status.rs +++ b/starling/tests/exit_status.rs @@ -4,16 +4,22 @@ #![cfg(not(target_arch = "wasm32"))] -use std::process::Command; +use std::ffi::OsStr; +use std::process::{Command, Output}; + +/// Run `starlingmonkey `, and return the process output. +fn run(args: impl IntoIterator>) -> Output { + Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) + .args(args) + .output() + .expect("failed to run starling") +} /// An error before the event loop runs, here an unreadable `-i` initializer /// script, exits with code 1 rather than aborting. #[test] fn initializer_error_exits_with_code_one() { - let out = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) - .args(["-i", "/nonexistent/init.js", "/nonexistent/app.js"]) - .output() - .expect("failed to run starling"); + let out = run(["-i", "/nonexistent/init.js", "/nonexistent/app.js"]); assert_eq!( out.status.code(), Some(1), @@ -23,9 +29,9 @@ fn initializer_error_exits_with_code_one() { ); } -/// Scheduling APIs work during initializer evaluation, and the initializer's -/// microtasks drain before the content script runs — but async work left -/// behind has nothing to drive it and must be a hard error. +/// Scheduling APIs work during initializer evaluation. The initializer's +/// microtasks drain before the content script runs. Async work left behind by +/// the initializer has nothing to drive it, so the run fails. #[test] fn initializer_event_loop_semantics() { let dir = tempfile::tempdir().unwrap(); @@ -43,10 +49,7 @@ fn initializer_event_loop_semantics() { "if (globalThis._fromInit !== 'ready') { throw new Error('initializer microtask not drained'); }", ) .unwrap(); - let out = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) - .args(["-i", &init.to_string_lossy(), &main.to_string_lossy()]) - .output() - .expect("failed to run starling"); + let out = run([OsStr::new("-i"), init.as_os_str(), main.as_os_str()]); assert_eq!( out.status.code(), Some(0), @@ -54,14 +57,10 @@ fn initializer_event_loop_semantics() { String::from_utf8_lossy(&out.stderr) ); - // setTimeout no longer throws "No active event loop" — but a timer left - // pending when the initializer ends is an error. + // A timer left pending when the initializer ends fails the run. let init_timer = dir.path().join("init_timer.js"); std::fs::write(&init_timer, "setTimeout(() => {}, 1000);").unwrap(); - let out = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) - .args(["-i", &init_timer.to_string_lossy(), &main.to_string_lossy()]) - .output() - .expect("failed to run starling"); + let out = run([OsStr::new("-i"), init_timer.as_os_str(), main.as_os_str()]); assert_eq!(out.status.code(), Some(1)); let stderr = String::from_utf8_lossy(&out.stderr); assert!( @@ -76,14 +75,7 @@ fn initializer_event_loop_semantics() { "globalThis._fromInit = 'ready'; var t = setTimeout(() => {}, 1000); clearTimeout(t);", ) .unwrap(); - let out = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) - .args([ - "-i", - &init_cleared.to_string_lossy(), - &main.to_string_lossy(), - ]) - .output() - .expect("failed to run starling"); + let out = run([OsStr::new("-i"), init_cleared.as_os_str(), main.as_os_str()]); assert_eq!( out.status.code(), Some(0), @@ -92,40 +84,27 @@ fn initializer_event_loop_semantics() { ); } -/// A content script that throws reports the error on stderr and exits with 1. -#[test] -fn throwing_script_exits_with_code_one() { - let out = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) - .args(["-e", "throw new Error('boom')"]) - .output() - .expect("failed to run starling"); - let stderr = String::from_utf8_lossy(&out.stderr); - assert_eq!(out.status.code(), Some(1), "stderr: {stderr}"); - assert!(stderr.contains("boom"), "stderr: {stderr}"); - assert!( - !String::from_utf8_lossy(&out.stdout).contains("boom"), - "the error must not go to stdout" - ); -} - -/// A top-level throw in a module file reports where it was thrown. +/// A top-level throw in a module file reports the error and where it was thrown on stderr, and +/// exits with 1. #[test] -fn top_level_throw_reports_its_location() { +fn top_level_throw_exits_with_code_one_and_reports_its_location() { let dir = tempfile::tempdir().unwrap(); let main = dir.path().join("main.mjs"); std::fs::write(&main, "const x = 1;\nthrow new Error('located');").unwrap(); - let out = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) - .arg(&main) - .output() - .expect("failed to run starling"); + let out = run([&main]); let stderr = String::from_utf8_lossy(&out.stderr); assert_eq!(out.status.code(), Some(1), "stderr: {stderr}"); assert!(stderr.contains("located"), "stderr: {stderr}"); assert!(stderr.contains("main.mjs:2:"), "stderr: {stderr}"); + assert!( + !String::from_utf8_lossy(&out.stdout).contains("located"), + "the error must not go to stdout" + ); } /// A top level that rejects once a timer has fired, after evaluation handed off to the event -/// loop, reports the error and exits with 1. +/// loop, reports the error and exits with 1. The module file case has no other work. The `-e` +/// case also checks that the exit does not wait for an interval that keeps the event loop busy. #[test] fn late_top_level_rejection_exits_with_code_one() { let dir = tempfile::tempdir().unwrap(); @@ -136,49 +115,29 @@ fn late_top_level_rejection_exits_with_code_one() { throw new Error('late failure');", ) .unwrap(); - let out = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) - .arg(&main) - .output() - .expect("failed to run starling"); - let stderr = String::from_utf8_lossy(&out.stderr); - assert_eq!(out.status.code(), Some(1), "stderr: {stderr}"); - assert!(stderr.contains("late failure"), "stderr: {stderr}"); -} - -/// A top level that rejects while an interval keeps the event loop busy reports the error and -/// exits with 1, without waiting for the interval. -#[test] -fn late_rejection_with_work_left_exits_with_code_one() { - let dir = tempfile::tempdir().unwrap(); - let main = dir.path().join("main.mjs"); - std::fs::write( - &main, + let from_file = run([&main]); + let from_eval = run([ + "-e", "setInterval(() => {}, 5);\n\ await new Promise((resolve) => setTimeout(resolve, 10));\n\ throw new Error('late failure');", - ) - .unwrap(); - let out = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) - .arg(&main) - .output() - .expect("failed to run starling"); - let stderr = String::from_utf8_lossy(&out.stderr); - assert_eq!(out.status.code(), Some(1), "stderr: {stderr}"); - assert!(stderr.contains("late failure"), "stderr: {stderr}"); + ]); + for (case, out) in [("module file", from_file), ("-e", from_eval)] { + let stderr = String::from_utf8_lossy(&out.stderr); + assert_eq!(out.status.code(), Some(1), "{case}: stderr: {stderr}"); + assert!(stderr.contains("late failure"), "{case}: stderr: {stderr}"); + } } -/// A top-level `await` on a promise nothing settles exits with 1, whether or not the event loop -/// had other work first. +/// A top-level `await` on a promise that never settles exits with 1, whether or not the event +/// loop had other work first. #[test] fn unsettled_top_level_await_exits_with_code_one() { for source in [ "await new Promise(() => {});", "setTimeout(() => {}, 10);\nawait new Promise(() => {});", ] { - let out = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) - .args(["-e", source]) - .output() - .expect("failed to run starling"); + let out = run(["-e", source]); let stderr = String::from_utf8_lossy(&out.stderr); assert_eq!(out.status.code(), Some(1), "{source}: stderr: {stderr}"); assert!( @@ -191,16 +150,8 @@ fn unsettled_top_level_await_exits_with_code_one() { /// `--help` exits with 0 and an unknown flag with clap's usage-error status 2. #[test] fn argument_errors_use_clap_exit_statuses() { - let help = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) - .arg("--help") - .output() - .expect("failed to run starling"); - assert_eq!(help.status.code(), Some(0)); - let bad = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) - .arg("--no-such-flag") - .output() - .expect("failed to run starling"); - assert_eq!(bad.status.code(), Some(2)); + assert_eq!(run(["--help"]).status.code(), Some(0)); + assert_eq!(run(["--no-such-flag"]).status.code(), Some(2)); } /// A syntax error in an imported file module is reported with its own message @@ -211,10 +162,7 @@ fn imported_module_syntax_error_keeps_its_location() { let main = dir.path().join("main.mjs"); std::fs::write(&main, "import './bad.mjs';").unwrap(); std::fs::write(dir.path().join("bad.mjs"), "export const x = ;").unwrap(); - let out = Command::new(env!("CARGO_BIN_EXE_starlingmonkey")) - .arg(&main) - .output() - .expect("failed to run starling"); + let out = run([&main]); let stderr = String::from_utf8_lossy(&out.stderr); assert_eq!(out.status.code(), Some(1), "stderr: {stderr}"); assert!(stderr.contains("expected expression"), "stderr: {stderr}"); From 1b365fff2859e04a89c4ee7df4cff5612633090d Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Thu, 22 Oct 2026 15:36:40 +0200 Subject: [PATCH 06/11] Pin the CI actions, and trim the scripts, recipes, WPT harness and docs CI pins every action to a commit, runs with read-only permissions, and installs tools through `install-tool.sh`, which checks their SHA-256 digests with `scripts/check-sha256.sh`. Clippy fails on warnings. The justfile's recipe docs are full sentences, and the WPT harness reads its ports from the server config. `docs/builtin-development.md` describes the rules builtins follow. The README documents `--web-security-restrictions`, fractional serve timeouts and the safe `#[jsmodule]` `register`. --- .cargo/config.toml | 2 - .github/actions/setup/action.yml | 19 +- .github/actions/setup/install-tool.sh | 69 +++++ .github/workflows/ci.yml | 23 +- README.md | 198 +++++--------- docs/builtin-development.md | 166 +++++++++++ docs/rooting.md | 295 ++++++++++---------- justfile | 72 +++-- rust-toolchain.toml | 4 +- scripts/check-crown.sh | 18 +- scripts/check-sha256.sh | 26 ++ scripts/test-gc-zeal.sh | 15 - scripts/test-wizer.sh | 37 ++- tests/wpt-harness/post-harness.js | 8 +- tests/wpt-harness/pre-harness.js | 14 +- tests/wpt-harness/run-wpt.mjs | 378 +++++++------------------- tests/wpt-harness/wpt-server.js | 72 ++--- 17 files changed, 675 insertions(+), 741 deletions(-) create mode 100755 .github/actions/setup/install-tool.sh create mode 100644 docs/builtin-development.md create mode 100755 scripts/check-sha256.sh diff --git a/.cargo/config.toml b/.cargo/config.toml index 79d08f1..d64d61c 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,11 +2,9 @@ runner = 'wasmtime run --dir=.::/cwd --dir=. --dir=/tmp -Sinherit-env=y,inherit-network=y,http=y,tcp=y,udp=y -C native-unwind-info=n' [target.wasm32-wasip2] -# Keep in sync with the RUSTFLAGS overrides in scripts/build-runtime.sh and scripts/build-dylib.sh. rustflags = ["-C", "target-feature=+simd128"] runner = 'wasmtime run --dir=.::/cwd --dir=. --dir=/tmp -Sinherit-env=y,inherit-network=y,http=y,tcp=y,udp=y -C native-unwind-info=n' [target.wasm32-wasip3] -# Keep in sync with the RUSTFLAGS overrides in scripts/build-runtime.sh and scripts/build-dylib.sh. rustflags = ["-C", "target-feature=+simd128"] runner = 'wasmtime run --dir=.::/cwd --dir=. --dir=/tmp -Sinherit-env=y,inherit-network=y,http=y,tcp=y,udp=y -C native-unwind-info=n' diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 1931ac3..dd69fe3 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -16,13 +16,10 @@ runs: sudo apt-get update sudo apt-get install -y build-essential python3 m4 libclang-dev llvm + # `install-tool.sh` checks each download's SHA-256 digest. - name: Install just shell: bash - run: | - mkdir -p "$HOME/.just/bin" - curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh \ - | bash -s -- --to "$HOME/.just/bin" - echo "$HOME/.just/bin" >> "$GITHUB_PATH" + run: ${{ github.action_path }}/install-tool.sh just 1.58.0 - name: Install Rust toolchain shell: bash @@ -46,18 +43,12 @@ runs: - name: Install WASI SDK if: inputs.target != 'native' shell: bash - run: | - sudo mkdir -p /opt/wasi-sdk - curl -sSfL "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz" \ - | sudo tar xz --strip-components=1 -C /opt/wasi-sdk - echo "WASI_SDK_PATH=/opt/wasi-sdk" >> "$GITHUB_ENV" + run: ${{ github.action_path }}/install-tool.sh wasi-sdk "$WASI_SDK_VERSION" - name: Install wasmtime if: inputs.target != 'native' shell: bash - run: | - curl -sSf https://wasmtime.dev/install.sh | bash -s -- --version "v$WASMTIME_VERSION" - echo "$HOME/.wasmtime/bin" >> "$GITHUB_PATH" + run: ${{ github.action_path }}/install-tool.sh wasmtime "$WASMTIME_VERSION" - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.9 + uses: mozilla-actions/sccache-action@7d986dd989559c6ecdb630a3fd2557667be217ad # v0.0.9 diff --git a/.github/actions/setup/install-tool.sh b/.github/actions/setup/install-tool.sh new file mode 100755 index 0000000..35a34c5 --- /dev/null +++ b/.github/actions/setup/install-tool.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0-WITH-LLVM-exception +# +# Download the x86_64 Linux release of a tool, check its SHA-256 digest against the digest listed +# below for that version, install it, and add it to `GITHUB_PATH`. The WASI SDK goes to +# /opt/wasi-sdk, which `WASI_SDK_PATH` is set to in `GITHUB_ENV`, and the other tools go to +# `~/./bin`. A version without a listed digest fails. +# +# Usage: +# install-tool.sh + +set -euo pipefail + +TOOL="$1" +VERSION="$2" +GH=https://github.com + +case "$TOOL-$VERSION" in + just-1.58.0) + URL="$GH/casey/just/releases/download/$VERSION/just-$VERSION-x86_64-unknown-linux-musl.tar.gz" + SHA256=4a5cc2f53e6f0f8c59092a6cc38291eb729d46a7dd95d3ae582008881b84931d + ;; + wasi-sdk-34) + URL="$GH/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$VERSION/wasi-sdk-$VERSION.0-x86_64-linux.tar.gz" + SHA256=b761e3a0721dbae9c09a0059e5fdb2bf917d1b4a8a7b430fb3b5aafb0984b2c4 + ;; + wasmtime-49.0.0) + URL="$GH/bytecodealliance/wasmtime/releases/download/v$VERSION/wasmtime-v$VERSION-x86_64-linux.tar.xz" + SHA256=a956c279ac6e80109369a285db30fc46f68239fa1bbeb03ccfa8e913bbca85fc + ;; + wasm-tools-1.258.0) + URL="$GH/bytecodealliance/wasm-tools/releases/download/v$VERSION/wasm-tools-$VERSION-x86_64-linux.tar.gz" + SHA256=b52d14eb74a4852cc249369bd4480c2b2fdd876145f41db51ff52269ded240ce + ;; + *) + echo "No SHA-256 digest for $TOOL $VERSION. Add it to $0." >&2 + exit 1 + ;; +esac + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +ARCHIVE="$TMP/$(basename "$URL")" +curl --fail --silent --show-error --location -o "$ARCHIVE" "$URL" +"$(dirname "$0")/../../../scripts/check-sha256.sh" "$ARCHIVE" "$SHA256" + +case "$TOOL" in + just) + mkdir -p "$HOME/.just/bin" + tar -xzf "$ARCHIVE" -C "$HOME/.just/bin" just + echo "$HOME/.just/bin" >>"$GITHUB_PATH" + ;; + wasi-sdk) + sudo rm -rf /opt/wasi-sdk + sudo mkdir /opt/wasi-sdk + sudo tar -xzf "$ARCHIVE" --no-same-owner --strip-components=1 -C /opt/wasi-sdk + echo "WASI_SDK_PATH=/opt/wasi-sdk" >>"$GITHUB_ENV" + ;; + wasmtime) + mkdir -p "$HOME/.wasmtime/bin" + tar -xJf "$ARCHIVE" --strip-components=1 -C "$HOME/.wasmtime/bin" "wasmtime-v$VERSION-x86_64-linux/wasmtime" + echo "$HOME/.wasmtime/bin" >>"$GITHUB_PATH" + ;; + wasm-tools) + mkdir -p "$HOME/.wasm-tools/bin" + tar -xzf "$ARCHIVE" --strip-components=1 -C "$HOME/.wasm-tools/bin" "wasm-tools-$VERSION-x86_64-linux/wasm-tools" + echo "$HOME/.wasm-tools/bin" >>"$GITHUB_PATH" + ;; +esac diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 693a930..2df2587 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 @@ -24,6 +27,8 @@ env: # Both are pinned, so a new release changes nothing until these are bumped. WASMTIME_VERSION: "49.0.0" STARLING_NIGHTLY: nightly-2026-09-24 + # The Node.js version that the jobs needing Node.js install. + NODE_VERSION: "24.21.0" RUSTC_WRAPPER: sccache CCACHE: sccache # Bump to purge the sccache GHA cache. @@ -37,7 +42,7 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - uses: ./.github/actions/setup with: @@ -74,7 +79,7 @@ jobs: WASMTIME_BACKTRACE_DETAILS: '1' RUST_BACKTRACE: 'full' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - uses: ./.github/actions/setup with: @@ -102,15 +107,15 @@ jobs: WASMTIME_BACKTRACE_DETAILS: '1' RUST_BACKTRACE: 'full' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - uses: ./.github/actions/setup with: target: ${{ matrix.target }} - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: "lts/*" + node-version: ${{ env.NODE_VERSION }} - name: Clone WPT suite run: ./scripts/clone-wpt.sh @@ -135,21 +140,19 @@ jobs: gc_rooting: name: GC Rooting Checks - strategy: - fail-fast: false runs-on: ubuntu-latest env: RUST_BACKTRACE: full steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - uses: ./.github/actions/setup with: target: native - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: "lts/*" + node-version: ${{ env.NODE_VERSION }} - name: Crown static analysis run: | diff --git a/README.md b/README.md index edc8fd7..5ecbb89 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ JS modules, and functions and properties on the global object. - [Building and Testing](#building-and-testing) - [Web Platform Tests (WPT)](#web-platform-tests-wpt) - [GC Rooting Checks](#gc-rooting-checks) -- [Key Design Points](#key-design-points) --- @@ -83,18 +82,14 @@ starlingmonkey --legacy-script old-code.js ## Built-in APIs -StarlingMonkey's suite of builtins is a work in progress for now. - ### C++ built-ins -The previous incarnation of StarlingMonkey was written in C++. This one has -support for running built-ins from that version, in the -[crates/builtins/cpp-builtins](crates/builtins/cpp-builtins) crate. Only the old -`console` builtin is added right now. +[crates/builtins/cpp-builtins](crates/builtins/cpp-builtins) runs builtins written for +StarlingMonkey's earlier C++ implementation through a compatibility layer. It provides `console`. Builtins are tested against the [Web Platform Tests](https://github.com/web-platform-tests/wpt) suite running -on both native and `wasm32-wasip2` targets. +on both native and wasm targets. --- @@ -104,44 +99,41 @@ on both native and `wasm32-wasip2` targets. starlingmonkey [OPTIONS] [SCRIPT_PATH] Arguments: - [SCRIPT_PATH] Path to the entry JS/MJS file (default: ./index.js) + [SCRIPT_PATH] Path to the entry JS/MJS file (default: ./index.js) Options: - -e, --eval