mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 16:00:54 +00:00
fix(fetch): create binary body results in receiver realm
Allocate Body.arrayBuffer() and Body.bytes() results in the receiver's creation context for buffered, author-stream and pending network bodies. Retain that context across asynchronous completion and recognize readable bodies by their internal brand when methods are borrowed across realms. Add Request/Response regression coverage for both iframe borrowing directions, null bodies, byte contents, asynchronous and cloned streams, prototype changes, network rewrapping, and original stream error identity. Validation: cargo fmt --all; strict workspace Clippy; nextest (17,958 passed, 13 skipped). The 81-case WPT selection improves from 73 to 75 passing cases with no regressions or new failed subtests. All four CLI regression groups pass (176 checks).
This commit is contained in:
@@ -5213,6 +5213,8 @@ fetch/api/request/url-encoding.html
|
||||
fetch/api/response/json.any.js?moli-wpt-any=dedicatedworker
|
||||
fetch/api/response/json.any.js?moli-wpt-any=window
|
||||
fetch/api/response/multi-globals/url-parsing.html
|
||||
fetch/api/response/response-arraybuffer-realm.window.js?moli-wpt-script=window
|
||||
fetch/api/response/response-blob-realm.any.js?moli-wpt-any=window
|
||||
fetch/api/response/response-body-read-task-handling.html
|
||||
fetch/api/response/response-clone.any.js?moli-wpt-any=dedicatedworker
|
||||
fetch/api/response/response-clone.any.js?moli-wpt-any=window
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
async function runFetchBodyRealmProbe(scenario, bodyURL, networkBytes) {
|
||||
if (document.readyState !== 'complete') {
|
||||
await new Promise(resolve => addEventListener('load', resolve, {once: true}));
|
||||
}
|
||||
const frame = document.createElement('iframe');
|
||||
const loaded = new Promise(resolve => frame.onload = resolve);
|
||||
frame.srcdoc = '<!doctype html><meta charset=utf-8><body>';
|
||||
document.body.append(frame);
|
||||
await loaded;
|
||||
const errors = [];
|
||||
let checked = 0;
|
||||
const check = (condition, label) => { if (!condition) errors.push(label); };
|
||||
try {
|
||||
for (const direction of ['parent-method', 'child-method']) {
|
||||
const methodRealm = direction === 'parent-method' ? self : frame.contentWindow;
|
||||
const receiverRealm = direction === 'parent-method' ? frame.contentWindow : self;
|
||||
const expectedBufferPrototype = receiverRealm.ArrayBuffer.prototype;
|
||||
const expectedBytesPrototype = receiverRealm.Uint8Array.prototype;
|
||||
for (const type of ['Request', 'Response']) {
|
||||
const sources = scenario === 'network'
|
||||
? ['network', 'rewrapped-network', 'cloned-network']
|
||||
: ['null', 'buffered', 'receiver-stream', 'method-stream', 'async-stream', 'changed-prototype', 'cloned-stream'];
|
||||
for (const source of sources) {
|
||||
for (const method of ['arrayBuffer', 'bytes']) {
|
||||
const label = [direction, type, source, method].join('/');
|
||||
try {
|
||||
let expected = source === 'null' ? [] : [65, 0, 66, 255];
|
||||
let body = source === 'null' ? null : new receiverRealm.Uint8Array(expected);
|
||||
let object;
|
||||
if (scenario === 'network') {
|
||||
expected = networkBytes;
|
||||
const fetchRealm = source === 'rewrapped-network' ? methodRealm : receiverRealm;
|
||||
object = await fetchRealm.fetch(bodyURL);
|
||||
body = object.body;
|
||||
if (type === 'Request' || source === 'rewrapped-network') object = undefined;
|
||||
} else if (source.endsWith('stream')) {
|
||||
const streamRealm = source === 'receiver-stream' ? receiverRealm : methodRealm;
|
||||
body = new streamRealm.ReadableStream({start(controller) {
|
||||
const send = () => {
|
||||
controller.enqueue(new streamRealm.Uint8Array([9, ...expected, 9]).subarray(1, 5));
|
||||
controller.close();
|
||||
};
|
||||
if (source === 'async-stream') setTimeout(send, 5); else send();
|
||||
}});
|
||||
}
|
||||
if (!object) {
|
||||
object = type === 'Request'
|
||||
? new receiverRealm.Request(bodyURL, {method: 'POST', body, duplex: 'half'})
|
||||
: new receiverRealm.Response(body);
|
||||
}
|
||||
if (source.startsWith('cloned-')) object = object.clone();
|
||||
if (source === 'changed-prototype') {
|
||||
Object.setPrototypeOf(object, methodRealm[type].prototype);
|
||||
}
|
||||
const result = await methodRealm[type].prototype[method].call(object);
|
||||
const buffer = method === 'bytes' ? result.buffer : result;
|
||||
check(Object.getPrototypeOf(buffer) === expectedBufferPrototype, label + ': buffer realm');
|
||||
if (method === 'bytes') {
|
||||
check(Object.getPrototypeOf(result) === expectedBytesPrototype, label + ': bytes realm');
|
||||
}
|
||||
const bytes = new Uint8Array(buffer);
|
||||
check(bytes.length === expected.length && bytes.every((byte, index) => byte === expected[index]), label + ': contents');
|
||||
check(object.bodyUsed === (source !== 'null'), label + ': bodyUsed');
|
||||
checked++;
|
||||
} catch (error) {
|
||||
errors.push(label + ': ' + String(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scenario !== 'network') {
|
||||
for (const method of ['arrayBuffer', 'bytes']) {
|
||||
const reason = new receiverRealm.Error('stream failure');
|
||||
const body = new receiverRealm.ReadableStream({start(controller) {controller.error(reason)}});
|
||||
const object = type === 'Request'
|
||||
? new receiverRealm.Request(bodyURL, {method: 'POST', body, duplex: 'half'})
|
||||
: new receiverRealm.Response(body);
|
||||
try {
|
||||
await methodRealm[type].prototype[method].call(object);
|
||||
errors.push(direction + '/' + type + '/' + method + ': error stream fulfilled');
|
||||
} catch (error) {
|
||||
check(error === reason, direction + '/' + type + '/' + method + ': error identity');
|
||||
}
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
frame.remove();
|
||||
}
|
||||
return {errors, checked};
|
||||
}
|
||||
@@ -20,6 +20,8 @@ mod callback_cleanup;
|
||||
mod event_dispatch;
|
||||
#[path = "web_apis/fetch_body_native.rs"]
|
||||
mod fetch_body_native;
|
||||
#[path = "web_apis/fetch_body_realm.rs"]
|
||||
mod fetch_body_realm;
|
||||
#[path = "web_apis/pipe_disturbed.rs"]
|
||||
mod pipe_disturbed;
|
||||
#[path = "web_apis/request_init.rs"]
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use super::*;
|
||||
|
||||
async fn assert_binary_body_realms(scenario: &str, expected_cases: usize) -> Result<()> {
|
||||
let server = FixtureServer::spawn().await?;
|
||||
let browser = Browser::new(AppConfig::default())?;
|
||||
let expected = b"<!doctype html><html><body><main id=\"stream\">naive-\xe4\xbd\xa0\xe5\xa5\xbd</main><script>document.body.setAttribute('data-stream-script','seen');</script></body></html>";
|
||||
let source = format!(
|
||||
"{}\nrunFetchBodyRealmProbe({}, {}, {}).then(finish, error => finish({{error: String(error)}}));",
|
||||
include_str!("../fixtures/runtime/fetch_body_realm.js"),
|
||||
serde_json::to_string(scenario)?,
|
||||
serde_json::to_string(&server.url("/streaming/chunked-html"))?,
|
||||
serde_json::to_string(expected.as_slice())?,
|
||||
);
|
||||
for target in ["window", "child"] {
|
||||
let observed = tokio::time::timeout(
|
||||
Duration::from_secs(20),
|
||||
super::event_dispatch::run_probe(&browser, &server, target, &source),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
observed,
|
||||
serde_json::json!({"errors": [], "checked": expected_cases}),
|
||||
"{scenario}/{target}",
|
||||
);
|
||||
}
|
||||
server.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn fetch_binary_body_results_use_receiver_realm() -> Result<()> {
|
||||
assert_binary_body_realms("memory", 64).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn fetch_binary_network_body_results_use_receiver_realm() -> Result<()> {
|
||||
assert_binary_body_realms("network", 24).await
|
||||
}
|
||||
@@ -240,8 +240,8 @@ impl Drop for NetworkBodySourceState {
|
||||
enum PendingBodyMaterializationKind {
|
||||
Text,
|
||||
Json,
|
||||
ArrayBuffer,
|
||||
Bytes,
|
||||
ArrayBuffer(v8::Global<v8::Context>),
|
||||
Bytes(v8::Global<v8::Context>),
|
||||
Blob { mime_type: String },
|
||||
FormData { content_type: String },
|
||||
}
|
||||
@@ -289,28 +289,41 @@ pub(in crate::network_host) enum NetworkBodyConsumptionKind {
|
||||
FormData { content_type: String },
|
||||
}
|
||||
|
||||
impl From<NetworkBodyConsumptionKind> for PendingBodyMaterializationKind {
|
||||
fn from(kind: NetworkBodyConsumptionKind) -> Self {
|
||||
impl PendingBodyMaterializationKind {
|
||||
fn new<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
kind: NetworkBodyConsumptionKind,
|
||||
) -> Self {
|
||||
match kind {
|
||||
NetworkBodyConsumptionKind::Text => Self::Text,
|
||||
NetworkBodyConsumptionKind::Json => Self::Json,
|
||||
NetworkBodyConsumptionKind::ArrayBuffer => Self::ArrayBuffer,
|
||||
NetworkBodyConsumptionKind::Bytes => Self::Bytes,
|
||||
NetworkBodyConsumptionKind::ArrayBuffer | NetworkBodyConsumptionKind::Bytes => {
|
||||
// Fetch explicitly allocates binary results in the Body
|
||||
// receiver's relevant realm, even for a borrowed method or a
|
||||
// stream created in another realm. Retain it until completion.
|
||||
let realm = object
|
||||
.get_creation_context(scope)
|
||||
.expect("body owner must have a creation context");
|
||||
let realm = v8::Global::new(scope, realm);
|
||||
if matches!(kind, NetworkBodyConsumptionKind::ArrayBuffer) {
|
||||
Self::ArrayBuffer(realm)
|
||||
} else {
|
||||
Self::Bytes(realm)
|
||||
}
|
||||
}
|
||||
NetworkBodyConsumptionKind::Blob { mime_type } => Self::Blob { mime_type },
|
||||
NetworkBodyConsumptionKind::FormData { content_type } => {
|
||||
Self::FormData { content_type }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PendingBodyMaterializationKind {
|
||||
fn clone_for_ready(&self) -> Self {
|
||||
match self {
|
||||
Self::Text => Self::Text,
|
||||
Self::Json => Self::Json,
|
||||
Self::ArrayBuffer => Self::ArrayBuffer,
|
||||
Self::Bytes => Self::Bytes,
|
||||
Self::ArrayBuffer(realm) => Self::ArrayBuffer(realm.clone()),
|
||||
Self::Bytes(realm) => Self::Bytes(realm.clone()),
|
||||
Self::Blob { mime_type } => Self::Blob {
|
||||
mime_type: mime_type.clone(),
|
||||
},
|
||||
@@ -1226,6 +1239,7 @@ pub(in crate::network_host) fn consume_filtered_response_internal_body_value_fro
|
||||
{
|
||||
return Some(consume_readable_body_stream(
|
||||
scope,
|
||||
object,
|
||||
stream,
|
||||
kind,
|
||||
Some(chunk_callback),
|
||||
@@ -1280,7 +1294,7 @@ fn consume_network_body_value_from_source_inner<'s>(
|
||||
if let Some(chunk_callback) = chunk_callback
|
||||
&& let Some(stream) = readable_body_stream_from_object(scope, object)
|
||||
{
|
||||
return consume_readable_body_stream(scope, stream, kind, Some(chunk_callback));
|
||||
return consume_readable_body_stream(scope, object, stream, kind, Some(chunk_callback));
|
||||
}
|
||||
let Some(id) = registry_body_source_id(scope, source) else {
|
||||
return (NetworkBodyConsumption::Failed, None);
|
||||
@@ -1289,7 +1303,7 @@ fn consume_network_body_value_from_source_inner<'s>(
|
||||
return (NetworkBodyConsumption::Failed, None);
|
||||
};
|
||||
let promise = resolver.get_promise(scope);
|
||||
let materialization_kind = PendingBodyMaterializationKind::from(kind);
|
||||
let materialization_kind = PendingBodyMaterializationKind::new(scope, object, kind);
|
||||
let mut ready = None;
|
||||
let mut rejected = None;
|
||||
if let Some(host) = context_host_mut(scope) {
|
||||
@@ -1339,7 +1353,7 @@ fn consume_network_body_value_from_source_inner<'s>(
|
||||
if explicit_source.is_none()
|
||||
&& let Some(stream) = readable_body_stream_from_object(scope, object)
|
||||
{
|
||||
return consume_readable_body_stream(scope, stream, kind, chunk_callback);
|
||||
return consume_readable_body_stream(scope, object, stream, kind, chunk_callback);
|
||||
}
|
||||
|
||||
let bytes = match try_network_body_bytes_from_storage(scope, source, true) {
|
||||
@@ -1350,7 +1364,8 @@ fn consume_network_body_value_from_source_inner<'s>(
|
||||
Ok(None) => return (NetworkBodyConsumption::Failed, None),
|
||||
Err(_) => return (NetworkBodyConsumption::Failed, None),
|
||||
};
|
||||
match body_materialization_value(scope, &bytes, PendingBodyMaterializationKind::from(kind)) {
|
||||
let kind = PendingBodyMaterializationKind::new(scope, object, kind);
|
||||
match body_materialization_value(scope, &bytes, kind) {
|
||||
Ok(value) => (NetworkBodyConsumption::Ready(value), None),
|
||||
Err(error) => (NetworkBodyConsumption::Rejected(error), None),
|
||||
}
|
||||
@@ -1775,17 +1790,11 @@ fn body_materialization_value<'s>(
|
||||
.unwrap_or_else(|| v8::undefined(&scope).into())
|
||||
})
|
||||
}
|
||||
PendingBodyMaterializationKind::ArrayBuffer => {
|
||||
blob::array_buffer_from_bytes(scope, bytes.to_vec())
|
||||
.map(Into::into)
|
||||
.ok_or_else(|| v8::undefined(scope).into())
|
||||
PendingBodyMaterializationKind::ArrayBuffer(realm) => {
|
||||
binary_body_materialization_value(scope, bytes, realm, false)
|
||||
}
|
||||
PendingBodyMaterializationKind::Bytes => {
|
||||
let byte_len = bytes.len();
|
||||
blob::array_buffer_from_bytes(scope, bytes.to_vec())
|
||||
.and_then(|buffer| v8::Uint8Array::new(scope, buffer, 0, byte_len))
|
||||
.map(Into::into)
|
||||
.ok_or_else(|| v8::undefined(scope).into())
|
||||
PendingBodyMaterializationKind::Bytes(realm) => {
|
||||
binary_body_materialization_value(scope, bytes, realm, true)
|
||||
}
|
||||
PendingBodyMaterializationKind::Blob { mime_type } => {
|
||||
blob::build_blob_object(scope, bytes.to_vec(), mime_type)
|
||||
@@ -1821,6 +1830,30 @@ fn body_materialization_value<'s>(
|
||||
}
|
||||
}
|
||||
|
||||
fn binary_body_materialization_value<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
bytes: &[u8],
|
||||
realm: v8::Global<v8::Context>,
|
||||
as_uint8_array: bool,
|
||||
) -> Result<v8::Local<'s, v8::Value>, v8::Local<'s, v8::Value>> {
|
||||
let realm = v8::Local::new(scope, realm);
|
||||
let value = {
|
||||
let scope = &mut v8::ContextScope::new(scope, realm);
|
||||
blob::array_buffer_from_bytes(scope, bytes.to_vec())
|
||||
.and_then(|buffer| {
|
||||
if as_uint8_array {
|
||||
v8::Uint8Array::new(scope, buffer, 0, bytes.len()).map(Into::into)
|
||||
} else {
|
||||
Some(v8::Local::<v8::Value>::from(buffer))
|
||||
}
|
||||
})
|
||||
.map(|value| v8::Global::new(scope, value))
|
||||
};
|
||||
value
|
||||
.map(|value| v8::Local::new(scope, value))
|
||||
.ok_or_else(|| v8::undefined(scope).into())
|
||||
}
|
||||
|
||||
fn registry_body_source_id<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
source: v8::Local<'s, v8::Object>,
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::context_bootstrap::{
|
||||
use crate::util::set_null_prototype;
|
||||
|
||||
const STREAM: &str = "__moliBodyConsumerStream";
|
||||
const BODY_OWNER: &str = "__moliBodyConsumerOwner";
|
||||
const RESOLVER: &str = "__moliBodyConsumerResolver";
|
||||
const CHUNKS: &str = "__moliBodyConsumerChunks";
|
||||
const KIND: &str = "__moliBodyConsumerKind";
|
||||
@@ -26,6 +27,8 @@ const REASON: &str = "__moliBodyConsumerReason";
|
||||
#[derive(WebApiObject)]
|
||||
#[webapi(plain)]
|
||||
struct BodyConsumerDeclaration<'scope> {
|
||||
#[webapi(slot = BODY_OWNER)]
|
||||
body_owner: v8::Local<'scope, v8::Object>,
|
||||
#[webapi(slot = STREAM)]
|
||||
stream: v8::Local<'scope, v8::Object>,
|
||||
#[webapi(slot = RESOLVER)]
|
||||
@@ -134,6 +137,7 @@ impl<'s> Consumer<'s> {
|
||||
|
||||
pub(super) fn consume_readable_body_stream<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
stream: v8::Local<'s, v8::Object>,
|
||||
kind: NetworkBodyConsumptionKind,
|
||||
chunk_callback: Option<v8::Local<'s, v8::Function>>,
|
||||
@@ -157,9 +161,17 @@ pub(super) fn consume_readable_body_stream<'s>(
|
||||
let chunks = v8::Array::new(scope, 0);
|
||||
set_null_prototype(scope, chunks.into());
|
||||
let consumer = Consumer(
|
||||
BodyConsumerDeclaration::new(stream, resolver.into(), chunks, kind, mime, chunk_callback)
|
||||
.bind(scope)
|
||||
.expect("body consumer declaration must bind"),
|
||||
BodyConsumerDeclaration::new(
|
||||
object,
|
||||
stream,
|
||||
resolver.into(),
|
||||
chunks,
|
||||
kind,
|
||||
mime,
|
||||
chunk_callback,
|
||||
)
|
||||
.bind(scope)
|
||||
.expect("body consumer declaration must bind"),
|
||||
);
|
||||
let callbacks = [
|
||||
v8::Function::builder(chunk_callback_step)
|
||||
@@ -295,20 +307,24 @@ fn materialize_callback<'s>(
|
||||
.to_rust_string_lossy(scope)
|
||||
.as_str()
|
||||
{
|
||||
"text" => PendingBodyMaterializationKind::Text,
|
||||
"json" => PendingBodyMaterializationKind::Json,
|
||||
"arrayBuffer" => PendingBodyMaterializationKind::ArrayBuffer,
|
||||
"bytes" => PendingBodyMaterializationKind::Bytes,
|
||||
"blob" => PendingBodyMaterializationKind::Blob { mime_type: mime },
|
||||
"formData" => PendingBodyMaterializationKind::FormData { content_type: mime },
|
||||
"text" => NetworkBodyConsumptionKind::Text,
|
||||
"json" => NetworkBodyConsumptionKind::Json,
|
||||
"arrayBuffer" => NetworkBodyConsumptionKind::ArrayBuffer,
|
||||
"bytes" => NetworkBodyConsumptionKind::Bytes,
|
||||
"blob" => NetworkBodyConsumptionKind::Blob { mime_type: mime },
|
||||
"formData" => NetworkBodyConsumptionKind::FormData { content_type: mime },
|
||||
_ => unreachable!("body materialization kind is private"),
|
||||
};
|
||||
let object = v8::Local::try_from(consumer.value(scope, BODY_OWNER))
|
||||
.expect("body materialization owner must exist");
|
||||
let kind = PendingBodyMaterializationKind::new(scope, object, kind);
|
||||
resolve_body_materialization(scope, resolver, bytes, kind);
|
||||
} else {
|
||||
let error = consumer.value(scope, REASON);
|
||||
let _ = resolver.reject(scope, error);
|
||||
}
|
||||
for slot in [
|
||||
BODY_OWNER,
|
||||
STREAM,
|
||||
RESOLVER,
|
||||
CHUNKS,
|
||||
|
||||
Reference in New Issue
Block a user