diff --git a/Cargo.lock b/Cargo.lock index f7065091220..6cc5749460d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3516,7 +3516,7 @@ dependencies = [ [[package]] name = "mozjs" version = "0.13.0" -source = "git+https://github.com/servo/rust-mozjs#dbb9bee06e0b0168ccae0619c5077e302669d2fb" +source = "git+https://github.com/servo/rust-mozjs#28248e1d6658e92dd5ecb0866e53a97f043b9b38" dependencies = [ "cc", "lazy_static", diff --git a/components/net/http_loader.rs b/components/net/http_loader.rs index f12cfa4498c..2e84e106a9b 100644 --- a/components/net/http_loader.rs +++ b/components/net/http_loader.rs @@ -32,13 +32,17 @@ use http::header::{ use http::{HeaderMap, Request as HyperRequest}; use hyper::{Body, Client, Method, Response as HyperResponse, StatusCode}; use hyper_serde::Serde; +use ipc_channel::ipc::{self, IpcSender}; +use ipc_channel::router::ROUTER; use msg::constellation_msg::{HistoryStateId, PipelineId}; use net_traits::pub_domains::reg_suffix; use net_traits::quality::{quality_to_value, Quality, QualityItem}; use net_traits::request::Origin::Origin as SpecificOrigin; use net_traits::request::{is_cors_safelisted_method, is_cors_safelisted_request_header}; +use net_traits::request::{ + BodyChunkRequest, RedirectMode, Referrer, Request, RequestBuilder, RequestMode, +}; use net_traits::request::{CacheMode, CredentialsMode, Destination, Origin}; -use net_traits::request::{RedirectMode, Referrer, Request, RequestBuilder, RequestMode}; use net_traits::request::{ResponseTainting, ServiceWorkersMode}; use net_traits::response::{HttpsState, Response, ResponseBody, ResponseType}; use net_traits::{CookieSource, FetchMetadata, NetworkError, ReferrerPolicy}; @@ -52,11 +56,12 @@ use std::iter::FromIterator; use std::mem; use std::ops::Deref; use std::str::FromStr; -use std::sync::{Condvar, Mutex, RwLock}; +use std::sync::{Arc as StdArc, Condvar, Mutex, RwLock}; use std::time::{Duration, SystemTime}; use time::{self, Tm}; -use tokio::prelude::{future, Future, Stream}; +use tokio::prelude::{future, Future, Sink, Stream}; use tokio::runtime::Runtime; +use tokio::sync::mpsc::channel; lazy_static! { pub static ref HANDLE: Mutex> = Mutex::new(Some(Runtime::new().unwrap())); @@ -400,8 +405,10 @@ fn obtain_response( client: &Client, url: &ServoUrl, method: &Method, - headers: &HeaderMap, - data: &Option>, + request_headers: &HeaderMap, + body: Option>, + request_len: Option, + load_data_method: &Method, pipeline_id: &Option, request_id: Option<&str>, is_xhr: bool, @@ -412,7 +419,71 @@ fn obtain_response( Error = NetworkError, >, > { - let request_body = data.as_ref().cloned().unwrap_or(vec![]); + let mut headers = request_headers.clone(); + + let devtools_bytes = StdArc::new(Mutex::new(vec![])); + + let request_body = match body { + Some(chunk_requester) => { + // TODO: If body is a stream, append `Transfer-Encoding`/`chunked`, + // see step 4.2 of https://fetch.spec.whatwg.org/#concept-http-network-fetch + + // Step 5.6 of https://fetch.spec.whatwg.org/#concept-http-network-or-cache-fetch + // If source is non-null, + // set contentLengthValue to httpRequest’s body’s total bytes + if let Some(request_len) = request_len { + headers.typed_insert(ContentLength(request_len as u64)); + } + let (body_chan, body_port) = ipc::channel().unwrap(); + + let (sender, receiver) = channel(1); + + let _ = chunk_requester.send(BodyChunkRequest::Connect(body_chan)); + + // https://fetch.spec.whatwg.org/#concept-request-transmit-body + // Request the first chunk, corresponding to Step 3 and 4. + let _ = chunk_requester.send(BodyChunkRequest::Chunk); + + let devtools_bytes = devtools_bytes.clone(); + + ROUTER.add_route( + body_port.to_opaque(), + Box::new(move |message| { + let bytes: Vec = message.to().unwrap(); + let chunk_requester = chunk_requester.clone(); + let sender = sender.clone(); + + devtools_bytes.lock().unwrap().append(&mut bytes.clone()); + + HANDLE.lock().unwrap().as_mut().unwrap().spawn( + // Step 5.1.2.2 + // Transmit a chunk over the network(and blocking until this is done). + sender + .send(bytes) + .map(move |_| { + // Step 5.1.2.3 + // Request the next chunk. + let _ = chunk_requester.send(BodyChunkRequest::Chunk); + () + }) + .map_err(|_| ()), + ); + }), + ); + + receiver + }, + _ => { + if *load_data_method != Method::GET && *load_data_method != Method::HEAD { + headers.typed_insert(ContentLength(0)) + } + let (_sender, mut receiver) = channel(1); + + receiver.close(); + + receiver + }, + }; context .timing @@ -440,7 +511,7 @@ fn obtain_response( .replace("{", "%7B") .replace("}", "%7D"), ) - .body(request_body.clone().into()); + .body(Body::wrap_stream(request_body)); // TODO: We currently don't know when the handhhake before the connection is done // so our best bet would be to set `secure_connection_start` here when we are currently @@ -488,7 +559,7 @@ fn obtain_response( closure_url, method.clone(), headers, - Some(request_body.clone()), + Some(devtools_bytes.lock().unwrap().clone()), pipeline_id, time::now(), connect_end - connect_start, @@ -804,7 +875,7 @@ pub fn http_redirect_fetch( .status .as_ref() .map_or(true, |s| s.0 != StatusCode::SEE_OTHER) && - request.body.as_ref().map_or(false, |b| b.is_empty()) + request.body.as_ref().map_or(false, |b| b.source_is_null()) { return Response::network_error(NetworkError::Internal("Request body is not done".into())); } @@ -943,7 +1014,7 @@ fn http_network_or_cache_fetch( _ => None, }, // Step 5.6 - Some(ref http_request_body) => Some(http_request_body.len() as u64), + Some(ref http_request_body) => http_request_body.len().map(|size| size as u64), }; // Step 5.7 @@ -1460,7 +1531,7 @@ impl Drop for ResponseEndTimer { /// [HTTP network fetch](https://fetch.spec.whatwg.org/#http-network-fetch) fn http_network_fetch( - request: &Request, + request: &mut Request, credentials_flag: bool, done_chan: &mut DoneChannel, context: &FetchContext, @@ -1503,7 +1574,9 @@ fn http_network_fetch( &url, &request.method, &request.headers, - &request.body, + request.body.as_mut().and_then(|body| body.take_stream()), + request.body.as_ref().and_then(|body| body.len()), + &request.method, &request.pipeline_id, request_id.as_ref().map(Deref::deref), is_xhr, diff --git a/components/net/tests/http_loader.rs b/components/net/tests/http_loader.rs index aa4e20cc8a3..1e4f0782ffa 100644 --- a/components/net/tests/http_loader.rs +++ b/components/net/tests/http_loader.rs @@ -24,13 +24,18 @@ use http::uri::Authority; use http::{Method, StatusCode}; use hyper::body::Body; use hyper::{Request as HyperRequest, Response as HyperResponse}; +use ipc_channel::ipc; +use ipc_channel::router::ROUTER; use msg::constellation_msg::TEST_PIPELINE_ID; use net::cookie::Cookie; use net::cookie_storage::CookieStorage; use net::http_loader::determine_request_referrer; use net::resource_thread::AuthCacheEntry; use net::test::replace_host_table; -use net_traits::request::{CredentialsMode, Destination, RequestBuilder, RequestMode}; +use net_traits::request::{ + BodyChunkRequest, BodySource, CredentialsMode, Destination, RequestBody, RequestBuilder, + RequestMode, +}; use net_traits::response::{HttpsState, ResponseBody}; use net_traits::{CookieSource, NetworkError, ReferrerPolicy}; use servo_url::{ImmutableOrigin, ServoUrl}; @@ -94,6 +99,27 @@ pub fn expect_devtools_http_response( } } +fn create_request_body_with_content(content: Vec) -> RequestBody { + let content_len = content.len(); + + let (chunk_request_sender, chunk_request_receiver) = ipc::channel().unwrap(); + ROUTER.add_route( + chunk_request_receiver.to_opaque(), + Box::new(move |message| { + let request = message.to().unwrap(); + if let BodyChunkRequest::Connect(sender) = request { + let _ = sender.send(content.clone()); + } + }), + ); + + RequestBody { + stream: Some(chunk_request_sender), + source: BodySource::USVString, + total_bytes: Some(content_len), + } +} + #[test] fn test_check_default_headers_loaded_in_every_request() { let expected_headers = Arc::new(Mutex::new(None)); @@ -276,7 +302,7 @@ fn test_request_and_response_data_with_network_messages() { url: url, method: Method::GET, headers: headers, - body: Some(b"".to_vec()), + body: Some(vec![]), pipeline_id: TEST_PIPELINE_ID, startedDateTime: devhttprequest.startedDateTime, timeStamp: devhttprequest.timeStamp, @@ -526,8 +552,11 @@ fn test_load_doesnt_send_request_body_on_any_redirect() { }; let (pre_server, pre_url) = make_server(pre_handler); + let content = b"Body on POST!"; + let request_body = create_request_body_with_content(content.to_vec()); + let mut request = RequestBuilder::new(pre_url.clone()) - .body(Some(b"Body on POST!".to_vec())) + .body(Some(request_body)) .method(Method::POST) .destination(Destination::Document) .origin(mock_origin()) @@ -819,9 +848,11 @@ fn test_load_sets_content_length_to_length_of_request_body() { }; let (server, url) = make_server(handler); + let request_body = create_request_body_with_content(content.to_vec()); + let mut request = RequestBuilder::new(url.clone()) .method(Method::POST) - .body(Some(content.to_vec())) + .body(Some(request_body)) .destination(Destination::Document) .origin(mock_origin()) .pipeline_id(Some(TEST_PIPELINE_ID)) diff --git a/components/net_traits/request.rs b/components/net_traits/request.rs index d61e2496683..b5570f79dec 100644 --- a/components/net_traits/request.rs +++ b/components/net_traits/request.rs @@ -8,6 +8,7 @@ use crate::ResourceTimingType; use content_security_policy::{self as csp, CspList}; use http::HeaderMap; use hyper::Method; +use ipc_channel::ipc::IpcSender; use mime::Mime; use msg::constellation_msg::PipelineId; use servo_url::{ImmutableOrigin, ServoUrl}; @@ -115,6 +116,57 @@ pub enum ParserMetadata { NotParserInserted, } +/// +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub enum BodySource { + Null, + Blob, + BufferSource, + FormData, + URLSearchParams, + USVString, +} + +/// Messages used to implement +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum BodyChunkRequest { + /// Connect a fetch in `net`, with a stream of bytes from `script`. + Connect(IpcSender>), + /// Ask for another chunk. + Chunk, + /// Signal the stream is done. + Done, +} + +/// The net component's view into +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] +pub struct RequestBody { + /// Net's view into a + #[ignore_malloc_size_of = "Channels are hard"] + pub stream: Option>, + /// + pub source: BodySource, + /// + pub total_bytes: Option, +} + +impl RequestBody { + pub fn take_stream(&mut self) -> Option> { + self.stream.take() + } + + pub fn source_is_null(&self) -> bool { + if let BodySource::Null = self.source { + return true; + } + false + } + + pub fn len(&self) -> Option { + self.total_bytes.clone() + } +} + #[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] pub struct RequestBuilder { #[serde( @@ -131,7 +183,7 @@ pub struct RequestBuilder { #[ignore_malloc_size_of = "Defined in hyper"] pub headers: HeaderMap, pub unsafe_request: bool, - pub body: Option>, + pub body: Option, pub service_workers_mode: ServiceWorkersMode, // TODO: client object pub destination: Destination, @@ -210,7 +262,7 @@ impl RequestBuilder { self } - pub fn body(mut self, body: Option>) -> RequestBuilder { + pub fn body(mut self, body: Option) -> RequestBuilder { self.body = body; self } @@ -338,7 +390,7 @@ pub struct Request { /// pub unsafe_request: bool, /// - pub body: Option>, + pub body: Option, // TODO: client object pub window: Window, // TODO: target browsing context diff --git a/components/script/body.rs b/components/script/body.rs index e2d9fb925fb..e2d8be3ae86 100644 --- a/components/script/body.rs +++ b/components/script/body.rs @@ -2,19 +2,34 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::dom::bindings::cell::Ref; +use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::BlobBinding::BlobBinding::BlobMethods; use crate::dom::bindings::codegen::Bindings::FormDataBinding::FormDataMethods; +use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit; use crate::dom::bindings::error::{Error, Fallible}; +use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::reflector::DomObject; use crate::dom::bindings::root::DomRoot; -use crate::dom::bindings::str::USVString; +use crate::dom::bindings::settings_stack::AutoIncumbentScript; +use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::bindings::trace::RootedTraceableBox; use crate::dom::blob::{normalize_type_string, Blob}; use crate::dom::formdata::FormData; use crate::dom::globalscope::GlobalScope; +use crate::dom::htmlformelement::{encode_multipart_form_data, generate_boundary}; use crate::dom::promise::Promise; -use crate::realms::{AlreadyInRealm, InRealm}; +use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler}; +use crate::dom::readablestream::{get_read_promise_bytes, get_read_promise_done, ReadableStream}; +use crate::dom::urlsearchparams::URLSearchParams; +use crate::realms::{enter_realm, AlreadyInRealm, InRealm}; use crate::script_runtime::JSContext; +use crate::task::TaskCanceller; +use crate::task_source::networking::NetworkingTaskSource; +use crate::task_source::TaskSource; +use crate::task_source::TaskSourceName; +use encoding_rs::UTF_8; +use ipc_channel::ipc::{self, IpcSender}; +use ipc_channel::router::ROUTER; use js::jsapi::Heap; use js::jsapi::JSObject; use js::jsapi::JS_ClearPendingException; @@ -23,14 +38,394 @@ use js::jsval::JSVal; use js::jsval::UndefinedValue; use js::rust::wrappers::JS_GetPendingException; use js::rust::wrappers::JS_ParseJSON; +use js::rust::HandleValue; use js::typedarray::{ArrayBuffer, CreateWith}; use mime::{self, Mime}; +use net_traits::request::{BodyChunkRequest, BodySource, RequestBody}; use script_traits::serializable::BlobImpl; use std::ptr; use std::rc::Rc; use std::str; use url::form_urlencoded; +/// The IPC route handler +/// for . +/// This route runs in the script process, +/// and will queue tasks to perform operations +/// on the stream and transmit body chunks over IPC. +struct TransmitBodyConnectHandler { + stream: Trusted, + task_source: NetworkingTaskSource, + canceller: TaskCanceller, + bytes_sender: Option>>, + control_sender: IpcSender, +} + +impl TransmitBodyConnectHandler { + pub fn new( + stream: Trusted, + task_source: NetworkingTaskSource, + canceller: TaskCanceller, + control_sender: IpcSender, + ) -> TransmitBodyConnectHandler { + TransmitBodyConnectHandler { + stream: stream, + task_source, + canceller, + bytes_sender: None, + control_sender, + } + } + + /// Take the IPC sender sent by `net`, so we can send body chunks with it. + pub fn start_reading(&mut self, sender: IpcSender>) { + self.bytes_sender = Some(sender); + } + + /// Drop the IPC sender sent by `net` + pub fn stop_reading(&mut self) { + // Note: this should close the corresponding receiver, + // and terminate the request stream in `net`. + self.bytes_sender = None; + } + + /// The entry point to + pub fn transmit_body_chunk(&mut self) { + let stream = self.stream.clone(); + let control_sender = self.control_sender.clone(); + let bytes_sender = self + .bytes_sender + .clone() + .expect("No bytes sender to transmit chunk."); + + let _ = self.task_source.queue_with_canceller( + task!(setup_native_body_promise_handler: move || { + // Step 1, Let body be request’s body. + // + // TODO: We need the handle the body null case, + // here assuming body is something and we have the corresponding stream. + let rooted_stream = stream.root(); + let global = rooted_stream.global(); + + // TODO: Step 2, If body is null, + // then queue a fetch task on request to process request end-of-body + // for request and abort these steps. + + // TODO: queuing those "process request ..." tasks means we also need a handle on Request here. + + // Step 3, get a reader for stream. + if rooted_stream.start_reading().is_err() { + // Note: this can happen if script starts consuming request body + // before fetch starts transmitting it. + // Not in the spec. + return; + } + + // Step 4, the result of reading a chunk from body’s stream with reader. + let promise = rooted_stream.read_a_chunk(); + + // Step 5, the parallel steps waiting for and handling the result of the read promise, + // are a combination of the promise native handler here, + // and the corresponding IPC route in `component::net::http_loader`. + let promise_handler = Box::new(TransmitBodyPromiseHandler { + bytes_sender, + stream: rooted_stream.clone(), + control_sender, + }); + + let rejection_handler = Box::new(TransmitBodyPromiseRejectionHandler {stream: rooted_stream}); + + let handler = PromiseNativeHandler::new(&global, Some(promise_handler), Some(rejection_handler)); + + // Enter a realm, and a script, + // before appending the native handler. + let _realm = enter_realm(&*global); + let _ais = AutoIncumbentScript::new(&*global); + promise.append_native_handler(&handler); + }), + &self.canceller, + ); + } +} + +/// The handler of read promises of body streams used in +/// . +#[derive(Clone, JSTraceable, MallocSizeOf)] +struct TransmitBodyPromiseHandler { + #[ignore_malloc_size_of = "Channels are hard"] + bytes_sender: IpcSender>, + stream: DomRoot, + #[ignore_malloc_size_of = "Channels are hard"] + control_sender: IpcSender, +} + +impl Callback for TransmitBodyPromiseHandler { + /// Step 5 of + fn callback(&self, cx: JSContext, v: HandleValue, _realm: InRealm) { + let is_done = match get_read_promise_done(cx.clone(), &v) { + Ok(is_done) => is_done, + Err(_) => { + // Step 5.5, the "otherwise" steps. + // TODO: terminate fetch. + let _ = self.control_sender.send(BodyChunkRequest::Done); + return self.stream.stop_reading(); + }, + }; + + if is_done { + // Step 5.3, the "done" steps. + // TODO: queue a fetch task on request to process request end-of-body. + let _ = self.control_sender.send(BodyChunkRequest::Done); + return self.stream.stop_reading(); + } + + let chunk = match get_read_promise_bytes(cx.clone(), &v) { + Ok(chunk) => chunk, + Err(_) => { + // Step 5.5, the "otherwise" steps. + // TODO: terminate fetch. + let _ = self.control_sender.send(BodyChunkRequest::Done); + return self.stream.stop_reading(); + }, + }; + + // Step 5.1 and 5.2, transmit chunk. + // Send the chunk to the body transmitter in net::http_loader::obtain_response. + // TODO: queue a fetch task on request to process request body for request. + let _ = self.bytes_sender.send(chunk); + } +} + +/// The handler of read promises rejection of body streams used in +/// . +#[derive(Clone, JSTraceable, MallocSizeOf)] +struct TransmitBodyPromiseRejectionHandler { + stream: DomRoot, +} + +impl Callback for TransmitBodyPromiseRejectionHandler { + /// + fn callback(&self, _cx: JSContext, _v: HandleValue, _realm: InRealm) { + // Step 5.4, the "rejection" steps. + // TODO: terminate fetch. + return self.stream.stop_reading(); + } +} + +/// The result of https://fetch.spec.whatwg.org/#concept-bodyinit-extract +pub struct ExtractedBody { + pub stream: DomRoot, + pub source: BodySource, + pub total_bytes: Option, + pub content_type: Option, +} + +impl ExtractedBody { + /// Build a request body from the extracted body, + /// to be sent over IPC to net to use with `concept-request-transmit-body`, + /// see https://fetch.spec.whatwg.org/#concept-request-transmit-body. + /// + /// Also returning the corresponding readable stream, + /// to be stored on the request in script, + /// and potentially used as part of `consume_body`, + /// see https://fetch.spec.whatwg.org/#concept-body-consume-body + /// + /// Transmitting a body over fetch, and consuming it in script, + /// are mutually exclusive operations, since each will lock the stream to a reader. + pub fn into_net_request_body(self) -> (RequestBody, DomRoot) { + let ExtractedBody { + stream, + total_bytes, + content_type: _, + source, + } = self; + + // First, setup some infra to be used to transmit body + // from `components::script` to `components::net`. + let (chunk_request_sender, chunk_request_receiver) = ipc::channel().unwrap(); + + let trusted_stream = Trusted::new(&*stream); + + let global = stream.global(); + let task_source = global.networking_task_source(); + let canceller = global.task_canceller(TaskSourceName::Networking); + + let mut body_handler = TransmitBodyConnectHandler::new( + trusted_stream, + task_source, + canceller, + chunk_request_sender.clone(), + ); + + ROUTER.add_route( + chunk_request_receiver.to_opaque(), + Box::new(move |message| { + let request = message.to().unwrap(); + match request { + BodyChunkRequest::Connect(sender) => { + body_handler.start_reading(sender); + }, + BodyChunkRequest::Chunk => body_handler.transmit_body_chunk(), + // Note: this is actually sent from this process + // by the TransmitBodyPromiseHandler when reading stops. + BodyChunkRequest::Done => body_handler.stop_reading(), + } + }), + ); + + // Return `components::net` view into this request body, + // which can be used by `net` to transmit it over the network. + let request_body = RequestBody { + stream: Some(chunk_request_sender), + source, + total_bytes, + }; + + // Also return the stream for this body, which can be used by script to consume it. + (request_body, stream) + } +} + +/// +pub trait Extractable { + fn extract(&self, global: &GlobalScope) -> Fallible; +} + +impl Extractable for BodyInit { + // https://fetch.spec.whatwg.org/#concept-bodyinit-extract + fn extract(&self, global: &GlobalScope) -> Fallible { + match self { + BodyInit::String(ref s) => s.extract(global), + BodyInit::URLSearchParams(ref usp) => usp.extract(global), + BodyInit::Blob(ref b) => b.extract(global), + BodyInit::FormData(ref formdata) => formdata.extract(global), + BodyInit::ArrayBuffer(ref typedarray) => { + let bytes = typedarray.to_vec(); + let total_bytes = bytes.len(); + let stream = ReadableStream::new_from_bytes(&global, bytes); + Ok(ExtractedBody { + stream, + total_bytes: Some(total_bytes), + content_type: None, + source: BodySource::BufferSource, + }) + }, + BodyInit::ArrayBufferView(ref typedarray) => { + let bytes = typedarray.to_vec(); + let total_bytes = bytes.len(); + let stream = ReadableStream::new_from_bytes(&global, bytes); + Ok(ExtractedBody { + stream, + total_bytes: Some(total_bytes), + content_type: None, + source: BodySource::BufferSource, + }) + }, + BodyInit::ReadableStream(stream) => { + // TODO: + // 1. If the keepalive flag is set, then throw a TypeError. + + if stream.is_locked() || stream.is_disturbed() { + return Err(Error::Type( + "The body's stream is disturbed or locked".to_string(), + )); + } + + Ok(ExtractedBody { + stream: stream.clone(), + total_bytes: None, + content_type: None, + source: BodySource::Null, + }) + }, + } + } +} + +impl Extractable for Vec { + fn extract(&self, global: &GlobalScope) -> Fallible { + let bytes = self.clone(); + let total_bytes = self.len(); + let stream = ReadableStream::new_from_bytes(&global, bytes); + Ok(ExtractedBody { + stream, + total_bytes: Some(total_bytes), + content_type: None, + // A vec is used only in `submit_entity_body`. + source: BodySource::FormData, + }) + } +} + +impl Extractable for Blob { + fn extract(&self, _global: &GlobalScope) -> Fallible { + let blob_type = self.Type(); + let content_type = if blob_type.as_ref().is_empty() { + None + } else { + Some(blob_type) + }; + let total_bytes = self.Size() as usize; + Ok(ExtractedBody { + stream: self.get_stream(), + total_bytes: Some(total_bytes), + content_type, + source: BodySource::Blob, + }) + } +} + +impl Extractable for DOMString { + fn extract(&self, global: &GlobalScope) -> Fallible { + let bytes = self.as_bytes().to_owned(); + let total_bytes = bytes.len(); + let content_type = Some(DOMString::from("text/plain;charset=UTF-8")); + let stream = ReadableStream::new_from_bytes(&global, bytes); + Ok(ExtractedBody { + stream, + total_bytes: Some(total_bytes), + content_type, + source: BodySource::USVString, + }) + } +} + +impl Extractable for FormData { + fn extract(&self, global: &GlobalScope) -> Fallible { + let boundary = generate_boundary(); + let bytes = encode_multipart_form_data(&mut self.datums(), boundary.clone(), UTF_8); + let total_bytes = bytes.len(); + let content_type = Some(DOMString::from(format!( + "multipart/form-data;boundary={}", + boundary + ))); + let stream = ReadableStream::new_from_bytes(&global, bytes); + Ok(ExtractedBody { + stream, + total_bytes: Some(total_bytes), + content_type, + source: BodySource::FormData, + }) + } +} + +impl Extractable for URLSearchParams { + fn extract(&self, global: &GlobalScope) -> Fallible { + let bytes = self.serialize_utf8().into_bytes(); + let total_bytes = bytes.len(); + let content_type = Some(DOMString::from( + "application/x-www-form-urlencoded;charset=UTF-8", + )); + let stream = ReadableStream::new_from_bytes(&global, bytes); + Ok(ExtractedBody { + stream, + total_bytes: Some(total_bytes), + content_type, + source: BodySource::URLSearchParams, + }) + } +} + #[derive(Clone, Copy, JSTraceable, MallocSizeOf)] pub enum BodyType { Blob, @@ -49,73 +444,212 @@ pub enum FetchedData { JSException(RootedTraceableBox>), } +#[derive(Clone, JSTraceable, MallocSizeOf)] +struct ConsumeBodyPromiseRejectionHandler { + #[ignore_malloc_size_of = "Rc are hard"] + result_promise: Rc, +} + +impl Callback for ConsumeBodyPromiseRejectionHandler { + /// Continuing Step 4 of + /// Step 3 of , + // the rejection steps. + fn callback(&self, cx: JSContext, v: HandleValue, _realm: InRealm) { + self.result_promise.reject(cx, v); + } +} + +#[derive(Clone, JSTraceable, MallocSizeOf)] +/// The promise handler used to consume the body, +/// +struct ConsumeBodyPromiseHandler { + #[ignore_malloc_size_of = "Rc are hard"] + result_promise: Rc, + stream: Option>, + body_type: DomRefCell>, + mime_type: DomRefCell>>, + bytes: DomRefCell>>, +} + +impl ConsumeBodyPromiseHandler { + /// Step 5 of + fn resolve_result_promise(&self, cx: JSContext) { + let body_type = self.body_type.borrow_mut().take().unwrap(); + let mime_type = self.mime_type.borrow_mut().take().unwrap(); + let body = self.bytes.borrow_mut().take().unwrap(); + + let pkg_data_results = run_package_data_algorithm(cx, body, body_type, mime_type); + + match pkg_data_results { + Ok(results) => { + match results { + FetchedData::Text(s) => self.result_promise.resolve_native(&USVString(s)), + FetchedData::Json(j) => self.result_promise.resolve_native(&j), + FetchedData::BlobData(b) => self.result_promise.resolve_native(&b), + FetchedData::FormData(f) => self.result_promise.resolve_native(&f), + FetchedData::ArrayBuffer(a) => self.result_promise.resolve_native(&a), + FetchedData::JSException(e) => self.result_promise.reject_native(&e.handle()), + }; + }, + Err(err) => self.result_promise.reject_error(err), + } + } +} + +impl Callback for ConsumeBodyPromiseHandler { + /// Continuing Step 4 of + /// Step 3 of . + fn callback(&self, cx: JSContext, v: HandleValue, _realm: InRealm) { + let stream = self + .stream + .as_ref() + .expect("ConsumeBodyPromiseHandler has no stream in callback."); + + let is_done = match get_read_promise_done(cx.clone(), &v) { + Ok(is_done) => is_done, + Err(err) => { + stream.stop_reading(); + // When read is fulfilled with a value that doesn't matches with neither of the above patterns. + return self.result_promise.reject_error(err); + }, + }; + + if is_done { + // When read is fulfilled with an object whose done property is true. + self.resolve_result_promise(cx.clone()); + } else { + let chunk = match get_read_promise_bytes(cx.clone(), &v) { + Ok(chunk) => chunk, + Err(err) => { + stream.stop_reading(); + // When read is fulfilled with a value that matches with neither of the above patterns + return self.result_promise.reject_error(err); + }, + }; + + let mut bytes = self + .bytes + .borrow_mut() + .take() + .expect("No bytes for ConsumeBodyPromiseHandler."); + + // Append the value property to bytes. + bytes.extend_from_slice(&*chunk); + + let global = stream.global(); + + // Run the above step again. + let read_promise = stream.read_a_chunk(); + + let promise_handler = Box::new(ConsumeBodyPromiseHandler { + result_promise: self.result_promise.clone(), + stream: self.stream.clone(), + body_type: DomRefCell::new(self.body_type.borrow_mut().take()), + mime_type: DomRefCell::new(self.mime_type.borrow_mut().take()), + bytes: DomRefCell::new(Some(bytes)), + }); + + let rejection_handler = Box::new(ConsumeBodyPromiseRejectionHandler { + result_promise: self.result_promise.clone(), + }); + + let handler = + PromiseNativeHandler::new(&global, Some(promise_handler), Some(rejection_handler)); + + // Enter a realm, and a script, + // before appending the native handler. + let _realm = enter_realm(&*global); + let _ais = AutoIncumbentScript::new(&*global); + read_promise.append_native_handler(&handler); + } + } +} + // https://fetch.spec.whatwg.org/#concept-body-consume-body #[allow(unrooted_must_root)] -pub fn consume_body(object: &T, body_type: BodyType) -> Rc { - let in_realm_proof = AlreadyInRealm::assert(&object.global()); +pub fn consume_body(object: &T, body_type: BodyType) -> Rc { + let global = object.global(); + let in_realm_proof = AlreadyInRealm::assert(&global); let promise = Promise::new_in_current_realm(&object.global(), InRealm::Already(&in_realm_proof)); // Step 1 - if object.get_body_used() || object.is_locked() { + if object.is_disturbed() || object.is_locked() { promise.reject_error(Error::Type( - "The response's stream is disturbed or locked".to_string(), + "The body's stream is disturbed or locked".to_string(), )); return promise; } - object.set_body_promise(&promise, body_type); - - // Steps 2-4 - // TODO: Body does not yet have a stream. - - consume_body_with_promise(object, body_type, &promise); + consume_body_with_promise(object, body_type, promise.clone()); promise } // https://fetch.spec.whatwg.org/#concept-body-consume-body #[allow(unrooted_must_root)] -pub fn consume_body_with_promise( +fn consume_body_with_promise( object: &T, body_type: BodyType, - promise: &Promise, + promise: Rc, ) { - // Step 5 - let body = match object.take_body() { - Some(body) => body, - None => return, + let global = object.global(); + + // Step 2. + let stream = match object.body() { + Some(stream) => stream, + None => { + let stream = ReadableStream::new_from_bytes(&global, Vec::with_capacity(0)); + stream + }, }; - let pkg_data_results = - run_package_data_algorithm(object, body, body_type, object.get_mime_type()); - - match pkg_data_results { - Ok(results) => { - match results { - FetchedData::Text(s) => promise.resolve_native(&USVString(s)), - FetchedData::Json(j) => promise.resolve_native(&j), - FetchedData::BlobData(b) => promise.resolve_native(&b), - FetchedData::FormData(f) => promise.resolve_native(&f), - FetchedData::ArrayBuffer(a) => promise.resolve_native(&a), - FetchedData::JSException(e) => promise.reject_native(&e.handle()), - }; - }, - Err(err) => promise.reject_error(err), + // Step 3. + if stream.start_reading().is_err() { + return promise.reject_error(Error::Type( + "The response's stream is disturbed or locked".to_string(), + )); } + + // Step 4, read all the bytes. + // Starts here, continues in the promise handler. + + // Step 1 of + // https://fetch.spec.whatwg.org/#concept-read-all-bytes-from-readablestream + let read_promise = stream.read_a_chunk(); + + let promise_handler = Box::new(ConsumeBodyPromiseHandler { + result_promise: promise.clone(), + stream: Some(stream), + body_type: DomRefCell::new(Some(body_type)), + mime_type: DomRefCell::new(Some(object.get_mime_type())), + // Step 2. + bytes: DomRefCell::new(Some(vec![])), + }); + + let rejection_handler = Box::new(ConsumeBodyPromiseRejectionHandler { + result_promise: promise, + }); + + let handler = PromiseNativeHandler::new( + &object.global(), + Some(promise_handler), + Some(rejection_handler), + ); + // We are already in a realm and a script. + read_promise.append_native_handler(&handler); } // https://fetch.spec.whatwg.org/#concept-body-package-data -#[allow(unsafe_code)] -fn run_package_data_algorithm( - object: &T, +fn run_package_data_algorithm( + cx: JSContext, bytes: Vec, body_type: BodyType, - mime_type: Ref>, + mime_type: Vec, ) -> Fallible { - let global = object.global(); - let cx = global.get_cx(); let mime = &*mime_type; + let in_realm_proof = AlreadyInRealm::assert_for_cx(cx); + let global = GlobalScope::from_safe_context(cx, InRealm::Already(&in_realm_proof)); match body_type { BodyType::Text => run_text_data_algorithm(bytes), BodyType::Json => run_json_data_algorithm(cx, bytes), @@ -218,12 +752,14 @@ pub fn run_array_buffer_data_algorithm(cx: JSContext, bytes: Vec) -> Fallibl Ok(FetchedData::ArrayBuffer(rooted_heap)) } -pub trait BodyOperations { - fn get_body_used(&self) -> bool; - fn set_body_promise(&self, p: &Rc, body_type: BodyType); - /// Returns `Some(_)` if the body is complete, `None` if there is more to - /// come. - fn take_body(&self) -> Option>; +/// +pub trait BodyMixin { + /// + fn is_disturbed(&self) -> bool; + /// + fn body(&self) -> Option>; + /// fn is_locked(&self) -> bool; - fn get_mime_type(&self) -> Ref>; + /// + fn get_mime_type(&self) -> Vec; } diff --git a/components/script/dom/bindings/codegen/CodegenRust.py b/components/script/dom/bindings/codegen/CodegenRust.py index b486806e562..0b1187a07fb 100644 --- a/components/script/dom/bindings/codegen/CodegenRust.py +++ b/components/script/dom/bindings/codegen/CodegenRust.py @@ -906,6 +906,40 @@ def getJSToNativeConversionInfo(type, descriptorProvider, failureCode=None, return handleOptional(templateBody, declType, handleDefault("None")) + if type.isReadableStream(): + assert not isEnforceRange and not isClamp + + if failureCode is None: + unwrapFailureCode = '''throw_type_error(*cx, "This object is not \ + an instance of ReadableStream.");\n''' + else: + unwrapFailureCode = failureCode + + templateBody = fill( + """ + { + use crate::realms::{AlreadyInRealm, InRealm}; + let in_realm_proof = AlreadyInRealm::assert_for_cx(cx); + match ReadableStream::from_js(cx, $${val}.get().to_object(), InRealm::Already(&in_realm_proof)) { + Ok(val) => val, + Err(()) => { + $*{failureCode} + } + } + + } + """, + failureCode=unwrapFailureCode + "\n", + ) + + templateBody = wrapObjectTemplate(templateBody, "None", + isDefinitelyObject, type, failureCode) + + declType = CGGeneric("DomRoot") + + return handleOptional(templateBody, declType, + handleDefault("None")) + elif type.isSpiderMonkeyInterface(): raise TypeError("Can't handle SpiderMonkey interface arguments other than typed arrays yet") @@ -4481,6 +4515,9 @@ def getUnionTypeTemplateVars(type, descriptorProvider): elif type.isObject(): name = type.name typeName = "Heap<*mut JSObject>" + elif type.isReadableStream(): + name = type.name + typeName = "DomRoot" elif is_typed_array(type): name = type.name typeName = "typedarray::Heap" + name diff --git a/components/script/dom/bindings/interface.rs b/components/script/dom/bindings/interface.rs index d0b3771890e..1ef57bf5409 100644 --- a/components/script/dom/bindings/interface.rs +++ b/components/script/dom/bindings/interface.rs @@ -138,6 +138,7 @@ pub unsafe fn create_global_object( let mut options = RealmOptions::default(); options.creationOptions_.traceGlobal_ = Some(trace); options.creationOptions_.sharedMemoryAndAtomics_ = true; + options.creationOptions_.streams_ = true; rval.set(JS_NewGlobalObject( *cx, diff --git a/components/script/dom/blob.rs b/components/script/dom/blob.rs index 46203a031b5..44011df9494 100644 --- a/components/script/dom/blob.rs +++ b/components/script/dom/blob.rs @@ -14,14 +14,18 @@ use crate::dom::bindings::str::DOMString; use crate::dom::bindings::structuredclone::StructuredDataHolder; use crate::dom::globalscope::GlobalScope; use crate::dom::promise::Promise; +use crate::dom::readablestream::ReadableStream; use crate::realms::{AlreadyInRealm, InRealm}; +use crate::script_runtime::JSContext; use dom_struct::dom_struct; use encoding_rs::UTF_8; +use js::jsapi::JSObject; use msg::constellation_msg::{BlobId, BlobIndex, PipelineNamespaceId}; use net_traits::filemanager_thread::RelativePos; use script_traits::serializable::BlobImpl; use std::collections::HashMap; use std::num::NonZeroU32; +use std::ptr::NonNull; use std::rc::Rc; use uuid::Uuid; @@ -34,13 +38,7 @@ pub struct Blob { impl Blob { pub fn new(global: &GlobalScope, blob_impl: BlobImpl) -> DomRoot { - let dom_blob = reflect_dom_object( - Box::new(Blob { - reflector_: Reflector::new(), - blob_id: blob_impl.blob_id(), - }), - global, - ); + let dom_blob = reflect_dom_object(Box::new(Blob::new_inherited(&blob_impl)), global); global.track_blob(&dom_blob, blob_impl); dom_blob } @@ -89,6 +87,11 @@ impl Blob { pub fn get_blob_url_id(&self) -> Uuid { self.global().get_blob_url_id(&self.blob_id) } + + /// + pub fn get_stream(&self) -> DomRoot { + self.global().get_blob_stream(&self.blob_id) + } } impl Serializable for Blob { @@ -213,6 +216,11 @@ impl BlobMethods for Blob { DOMString::from(self.type_string()) } + // + fn Stream(&self, _cx: JSContext) -> NonNull { + self.get_stream().get_js_stream() + } + // https://w3c.github.io/FileAPI/#slice-method-algo fn Slice( &self, diff --git a/components/script/dom/globalscope.rs b/components/script/dom/globalscope.rs index d03333e8541..517a1cbe8d3 100644 --- a/components/script/dom/globalscope.rs +++ b/components/script/dom/globalscope.rs @@ -43,6 +43,7 @@ use crate::dom::paintworkletglobalscope::PaintWorkletGlobalScope; use crate::dom::performance::Performance; use crate::dom::performanceobserver::VALID_ENTRY_TYPES; use crate::dom::promise::Promise; +use crate::dom::readablestream::{ExternalUnderlyingSource, ReadableStream}; use crate::dom::serviceworker::ServiceWorker; use crate::dom::serviceworkerregistration::ServiceWorkerRegistration; use crate::dom::window::Window; @@ -319,11 +320,19 @@ struct FileListener { task_canceller: TaskCanceller, } -struct FileListenerCallback(Box, Result, Error>) + Send>); +enum FileListenerCallback { + Promise(Box, Result, Error>) + Send>), + Stream, +} + +enum FileListenerTarget { + Promise(TrustedPromise), + Stream(Trusted), +} enum FileListenerState { - Empty(FileListenerCallback, TrustedPromise), - Receiving(Vec, FileListenerCallback, TrustedPromise), + Empty(FileListenerCallback, FileListenerTarget), + Receiving(Vec, FileListenerCallback, FileListenerTarget), } #[derive(JSTraceable, MallocSizeOf)] @@ -356,6 +365,14 @@ pub enum BlobState { UnManaged, } +/// The result of looking-up the data for a Blob, +/// containing either the in-memory bytes, +/// or the file-id. +enum BlobResult { + Bytes(Vec), + File(Uuid, usize), +} + /// Data representing a message-port managed by this global. #[derive(JSTraceable, MallocSizeOf)] #[unrooted_must_root_lint::must_root] @@ -532,57 +549,137 @@ impl MessageListener { } } +/// Callback used to enqueue file chunks to streams as part of FileListener. +fn stream_handle_incoming(stream: &ReadableStream, bytes: Result, Error>) { + match bytes { + Ok(b) => { + stream.enqueue_native(b); + }, + Err(e) => { + stream.error_native(e); + }, + } +} + +/// Callback used to close streams as part of FileListener. +fn stream_handle_eof(stream: &ReadableStream) { + stream.close_native(); +} + impl FileListener { fn handle(&mut self, msg: FileManagerResult) { match msg { Ok(ReadFileProgress::Meta(blob_buf)) => match self.state.take() { - Some(FileListenerState::Empty(callback, promise)) => { - self.state = Some(FileListenerState::Receiving( - blob_buf.bytes, - callback, - promise, - )); + Some(FileListenerState::Empty(callback, target)) => { + let bytes = if let FileListenerTarget::Stream(ref trusted_stream) = target { + let trusted = trusted_stream.clone(); + + let task = task!(enqueue_stream_chunk: move || { + let stream = trusted.root(); + stream_handle_incoming(&*stream, Ok(blob_buf.bytes)); + }); + + let _ = self + .task_source + .queue_with_canceller(task, &self.task_canceller); + Vec::with_capacity(0) + } else { + blob_buf.bytes + }; + + self.state = Some(FileListenerState::Receiving(bytes, callback, target)); }, _ => panic!( "Unexpected FileListenerState when receiving ReadFileProgress::Meta msg." ), }, Ok(ReadFileProgress::Partial(mut bytes_in)) => match self.state.take() { - Some(FileListenerState::Receiving(mut bytes, callback, promise)) => { - bytes.append(&mut bytes_in); - self.state = Some(FileListenerState::Receiving(bytes, callback, promise)); + Some(FileListenerState::Receiving(mut bytes, callback, target)) => { + if let FileListenerTarget::Stream(ref trusted_stream) = target { + let trusted = trusted_stream.clone(); + + let task = task!(enqueue_stream_chunk: move || { + let stream = trusted.root(); + stream_handle_incoming(&*stream, Ok(bytes_in)); + }); + + let _ = self + .task_source + .queue_with_canceller(task, &self.task_canceller); + } else { + bytes.append(&mut bytes_in); + }; + + self.state = Some(FileListenerState::Receiving(bytes, callback, target)); }, _ => panic!( "Unexpected FileListenerState when receiving ReadFileProgress::Partial msg." ), }, Ok(ReadFileProgress::EOF) => match self.state.take() { - Some(FileListenerState::Receiving(bytes, callback, trusted_promise)) => { - let _ = self.task_source.queue_with_canceller( - task!(resolve_promise: move || { + Some(FileListenerState::Receiving(bytes, callback, target)) => match target { + FileListenerTarget::Promise(trusted_promise) => { + let callback = match callback { + FileListenerCallback::Promise(callback) => callback, + _ => panic!("Expected promise callback."), + }; + let task = task!(resolve_promise: move || { let promise = trusted_promise.root(); let _ac = enter_realm(&*promise.global()); - callback.0(promise, Ok(bytes)); - }), - &self.task_canceller, - ); + callback(promise, Ok(bytes)); + }); + + let _ = self + .task_source + .queue_with_canceller(task, &self.task_canceller); + }, + FileListenerTarget::Stream(trusted_stream) => { + let trusted = trusted_stream.clone(); + + let task = task!(enqueue_stream_chunk: move || { + let stream = trusted.root(); + stream_handle_eof(&*stream); + }); + + let _ = self + .task_source + .queue_with_canceller(task, &self.task_canceller); + }, }, _ => { panic!("Unexpected FileListenerState when receiving ReadFileProgress::EOF msg.") }, }, Err(_) => match self.state.take() { - Some(FileListenerState::Receiving(_, callback, trusted_promise)) | - Some(FileListenerState::Empty(callback, trusted_promise)) => { - let bytes = Err(Error::Network); - let _ = self.task_source.queue_with_canceller( - task!(reject_promise: move || { - let promise = trusted_promise.root(); - let _ac = enter_realm(&*promise.global()); - callback.0(promise, bytes); - }), - &self.task_canceller, - ); + Some(FileListenerState::Receiving(_, callback, target)) | + Some(FileListenerState::Empty(callback, target)) => { + let error = Err(Error::Network); + + match target { + FileListenerTarget::Promise(trusted_promise) => { + let callback = match callback { + FileListenerCallback::Promise(callback) => callback, + _ => panic!("Expected promise callback."), + }; + let _ = self.task_source.queue_with_canceller( + task!(reject_promise: move || { + let promise = trusted_promise.root(); + let _ac = enter_realm(&*promise.global()); + callback(promise, error); + }), + &self.task_canceller, + ); + }, + FileListenerTarget::Stream(trusted_stream) => { + let _ = self.task_source.queue_with_canceller( + task!(error_stream: move || { + let stream = trusted_stream.root(); + stream_handle_incoming(&*stream, error); + }), + &self.task_canceller, + ); + }, + } }, _ => panic!("Unexpected FileListenerState when receiving Err msg."), }, @@ -1565,6 +1662,70 @@ impl GlobalScope { } } + /// Get a slice to the inner data of a Blob, + /// if it's a memory blob, or it's file-id and file-size otherwise. + /// + /// Note: this is almost a duplicate of `get_blob_bytes`, + /// tweaked for integration with streams. + /// TODO: merge with `get_blob_bytes` by way of broader integration with blob streams. + fn get_blob_bytes_or_file_id(&self, blob_id: &BlobId) -> BlobResult { + let parent = { + let blob_state = self.blob_state.borrow(); + if let BlobState::Managed(blobs_map) = &*blob_state { + let blob_info = blobs_map + .get(blob_id) + .expect("get_blob_bytes_or_file_id for an unknown blob."); + match blob_info.blob_impl.blob_data() { + BlobData::Sliced(ref parent, ref rel_pos) => { + Some((parent.clone(), rel_pos.clone())) + }, + _ => None, + } + } else { + panic!("get_blob_bytes_or_file_id called on a global not managing any blobs."); + } + }; + + match parent { + Some((parent_id, rel_pos)) => { + match self.get_blob_bytes_non_sliced_or_file_id(&parent_id) { + BlobResult::Bytes(bytes) => { + let range = rel_pos.to_abs_range(bytes.len()); + BlobResult::Bytes(bytes.index(range).to_vec()) + }, + res => res, + } + }, + None => self.get_blob_bytes_non_sliced_or_file_id(blob_id), + } + } + + /// Get bytes from a non-sliced blob if in memory, or it's file-id and file-size. + /// + /// Note: this is almost a duplicate of `get_blob_bytes_non_sliced`, + /// tweaked for integration with streams. + /// TODO: merge with `get_blob_bytes` by way of broader integration with blob streams. + fn get_blob_bytes_non_sliced_or_file_id(&self, blob_id: &BlobId) -> BlobResult { + let blob_state = self.blob_state.borrow(); + if let BlobState::Managed(blobs_map) = &*blob_state { + let blob_info = blobs_map + .get(blob_id) + .expect("get_blob_bytes_non_sliced_or_file_id called for a unknown blob."); + match blob_info.blob_impl.blob_data() { + BlobData::File(ref f) => match f.get_cache() { + Some(bytes) => BlobResult::Bytes(bytes.clone()), + None => BlobResult::File(f.get_id(), f.get_size() as usize), + }, + BlobData::Memory(ref s) => BlobResult::Bytes(s.clone()), + BlobData::Sliced(_, _) => panic!("This blob doesn't have a parent."), + } + } else { + panic!( + "get_blob_bytes_non_sliced_or_file_id called on a global not managing any blobs." + ); + } + } + /// Get a copy of the type_string of a blob. pub fn get_blob_type_string(&self, blob_id: &BlobId) -> String { let blob_state = self.blob_state.borrow(); @@ -1769,6 +1930,50 @@ impl GlobalScope { GlobalScope::read_msg(recv) } + /// + pub fn get_blob_stream(&self, blob_id: &BlobId) -> DomRoot { + let (file_id, size) = match self.get_blob_bytes_or_file_id(blob_id) { + BlobResult::Bytes(bytes) => { + // If we have all the bytes in memory, queue them and close the stream. + let stream = ReadableStream::new_from_bytes(self, bytes); + return stream; + }, + BlobResult::File(id, size) => (id, size), + }; + + let stream = ReadableStream::new_with_external_underlying_source( + self, + ExternalUnderlyingSource::Blob(size as usize), + ); + + let recv = self.send_msg(file_id); + + let trusted_stream = Trusted::new(&*stream.clone()); + let task_canceller = self.task_canceller(TaskSourceName::FileReading); + let task_source = self.file_reading_task_source(); + + let mut file_listener = FileListener { + state: Some(FileListenerState::Empty( + FileListenerCallback::Stream, + FileListenerTarget::Stream(trusted_stream), + )), + task_source, + task_canceller, + }; + + ROUTER.add_route( + recv.to_opaque(), + Box::new(move |msg| { + file_listener.handle( + msg.to() + .expect("Deserialization of file listener msg failed."), + ); + }), + ); + + stream + } + pub fn read_file_async( &self, id: Uuid, @@ -1783,8 +1988,8 @@ impl GlobalScope { let mut file_listener = FileListener { state: Some(FileListenerState::Empty( - FileListenerCallback(callback), - trusted_promise, + FileListenerCallback::Promise(callback), + FileListenerTarget::Promise(trusted_promise), )), task_source, task_canceller, @@ -1894,6 +2099,12 @@ impl GlobalScope { global_scope_from_global(global, cx) } + /// Returns the global scope for the given SafeJSContext + #[allow(unsafe_code)] + pub fn from_safe_context(cx: SafeJSContext, realm: InRealm) -> DomRoot { + unsafe { Self::from_context(*cx, realm) } + } + /// Returns the global object of the realm that the given JS object /// was created in, after unwrapping any wrappers. #[allow(unsafe_code)] diff --git a/components/script/dom/htmlformelement.rs b/components/script/dom/htmlformelement.rs index d3a23a2e3b0..0c7b09ece74 100644 --- a/components/script/dom/htmlformelement.rs +++ b/components/script/dom/htmlformelement.rs @@ -2,6 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use crate::body::Extractable; use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::AttrBinding::AttrBinding::AttrMethods; use crate::dom::bindings::codegen::Bindings::BlobBinding::BlobMethods; @@ -799,7 +800,15 @@ impl HTMLFormElement { }, }; - load_data.data = Some(bytes); + let global = self.global(); + + let request_body = bytes + .extract(&global) + .expect("Couldn't extract body.") + .into_net_request_body() + .0; + load_data.data = Some(request_body); + self.plan_to_navigate(load_data, target); } diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index 0aa0bd3123a..a8e0a9cdc78 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -481,6 +481,7 @@ pub mod promiserejectionevent; pub mod radionodelist; pub mod range; pub mod raredata; +pub mod readablestream; pub mod request; pub mod response; pub mod rtcicecandidate; diff --git a/components/script/dom/promisenativehandler.rs b/components/script/dom/promisenativehandler.rs index 217bcc851c2..a5039d1f0b6 100644 --- a/components/script/dom/promisenativehandler.rs +++ b/components/script/dom/promisenativehandler.rs @@ -7,13 +7,14 @@ use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::trace::JSTraceable; use crate::dom::globalscope::GlobalScope; use crate::realms::InRealm; +use crate::script_runtime::JSContext as SafeJSContext; use dom_struct::dom_struct; use js::jsapi::JSContext; use js::rust::HandleValue; use malloc_size_of::MallocSizeOf; pub trait Callback: JSTraceable + MallocSizeOf { - fn callback(&self, cx: *mut JSContext, v: HandleValue, realm: InRealm); + fn callback(&self, cx: SafeJSContext, v: HandleValue, realm: InRealm); } #[dom_struct] @@ -39,12 +40,14 @@ impl PromiseNativeHandler { ) } + #[allow(unsafe_code)] fn callback( callback: &Option>, cx: *mut JSContext, v: HandleValue, realm: InRealm, ) { + let cx = unsafe { SafeJSContext::from_ptr(cx) }; if let Some(ref callback) = *callback { callback.callback(cx, v, realm) } diff --git a/components/script/dom/readablestream.rs b/components/script/dom/readablestream.rs new file mode 100644 index 00000000000..d2677e80547 --- /dev/null +++ b/components/script/dom/readablestream.rs @@ -0,0 +1,509 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::dom::bindings::conversions::{ConversionBehavior, ConversionResult}; +use crate::dom::bindings::error::Error; +use crate::dom::bindings::reflector::{reflect_dom_object, DomObject, Reflector}; +use crate::dom::bindings::root::DomRoot; +use crate::dom::bindings::settings_stack::AutoEntryScript; +use crate::dom::bindings::utils::get_dictionary_property; +use crate::dom::globalscope::GlobalScope; +use crate::dom::promise::Promise; +use crate::js::conversions::FromJSValConvertible; +use crate::realms::{enter_realm, InRealm}; +use crate::script_runtime::JSContext as SafeJSContext; +use dom_struct::dom_struct; +use js::glue::{ + CreateReadableStreamUnderlyingSource, DeleteReadableStreamUnderlyingSource, + ReadableStreamUnderlyingSourceTraps, +}; +use js::jsapi::{HandleObject, HandleValue, Heap, JSContext, JSObject}; +use js::jsapi::{ + IsReadableStream, NewReadableExternalSourceStreamObject, ReadableStreamClose, + ReadableStreamDefaultReaderRead, ReadableStreamError, ReadableStreamGetReader, + ReadableStreamIsDisturbed, ReadableStreamIsLocked, ReadableStreamIsReadable, + ReadableStreamReaderMode, ReadableStreamReaderReleaseLock, ReadableStreamUnderlyingSource, + ReadableStreamUpdateDataAvailableFromSource, UnwrapReadableStream, +}; +use js::jsval::JSVal; +use js::jsval::UndefinedValue; +use js::rust::HandleValue as SafeHandleValue; +use js::rust::IntoHandle; +use std::cell::{Cell, RefCell}; +use std::os::raw::c_void; +use std::ptr::{self, NonNull}; +use std::rc::Rc; +use std::slice; + +static UNDERLYING_SOURCE_TRAPS: ReadableStreamUnderlyingSourceTraps = + ReadableStreamUnderlyingSourceTraps { + requestData: Some(request_data), + writeIntoReadRequestBuffer: Some(write_into_read_request_buffer), + cancel: Some(cancel), + onClosed: Some(close), + onErrored: Some(error), + finalize: Some(finalize), + }; + +#[dom_struct] +pub struct ReadableStream { + reflector_: Reflector, + #[ignore_malloc_size_of = "SM handles JS values"] + js_stream: Heap<*mut JSObject>, + #[ignore_malloc_size_of = "SM handles JS values"] + js_reader: Heap<*mut JSObject>, + has_reader: Cell, + #[ignore_malloc_size_of = "Rc is hard"] + external_underlying_source: Option>, +} + +impl ReadableStream { + fn new_inherited( + external_underlying_source: Option>, + ) -> ReadableStream { + ReadableStream { + reflector_: Reflector::new(), + js_stream: Heap::default(), + js_reader: Heap::default(), + has_reader: Default::default(), + external_underlying_source: external_underlying_source, + } + } + + fn new( + global: &GlobalScope, + external_underlying_source: Option>, + ) -> DomRoot { + reflect_dom_object( + Box::new(ReadableStream::new_inherited(external_underlying_source)), + global, + ) + } + + /// Used from RustCodegen.py + #[allow(unsafe_code)] + pub unsafe fn from_js( + cx: SafeJSContext, + obj: *mut JSObject, + realm: InRealm, + ) -> Result, ()> { + if !IsReadableStream(obj) { + return Err(()); + } + + let global = GlobalScope::from_safe_context(cx, realm); + + let stream = ReadableStream::new(&global, None); + stream.js_stream.set(UnwrapReadableStream(obj)); + + Ok(stream) + } + + /// Build a stream backed by a Rust source that has already been read into memory. + pub fn new_from_bytes(global: &GlobalScope, bytes: Vec) -> DomRoot { + let stream = ReadableStream::new_with_external_underlying_source( + &global, + ExternalUnderlyingSource::Memory(bytes.len()), + ); + stream.enqueue_native(bytes); + stream.close_native(); + stream + } + + /// Build a stream backed by a Rust underlying source. + #[allow(unsafe_code)] + pub fn new_with_external_underlying_source( + global: &GlobalScope, + source: ExternalUnderlyingSource, + ) -> DomRoot { + let _ar = enter_realm(global); + let cx = global.get_cx(); + + let source = Rc::new(ExternalUnderlyingSourceController::new(source)); + + let stream = ReadableStream::new(&global, Some(source.clone())); + + unsafe { + let js_wrapper = CreateReadableStreamUnderlyingSource( + &UNDERLYING_SOURCE_TRAPS, + &*source as *const _ as *const c_void, + ); + + rooted!(in(*cx) + let js_stream = NewReadableExternalSourceStreamObject( + *cx, + js_wrapper, + ptr::null_mut(), + HandleObject::null(), + ) + ); + + stream.js_stream.set(UnwrapReadableStream(js_stream.get())); + } + + stream + } + + /// Get a pointer to the underlying JS object. + pub fn get_js_stream(&self) -> NonNull { + NonNull::new(self.js_stream.get()) + .expect("Couldn't get a non-null pointer to JS stream object.") + } + + #[allow(unsafe_code)] + pub fn enqueue_native(&self, bytes: Vec) { + let global = self.global(); + let _ar = enter_realm(&*global); + let cx = global.get_cx(); + + let handle = unsafe { self.js_stream.handle() }; + + self.external_underlying_source + .as_ref() + .expect("No external source to enqueue bytes.") + .enqueue_chunk(cx, handle, bytes); + } + + #[allow(unsafe_code)] + pub fn error_native(&self, error: Error) { + let global = self.global(); + let _ar = enter_realm(&*global); + let cx = global.get_cx(); + + unsafe { + rooted!(in(*cx) let mut js_error = UndefinedValue()); + error.to_jsval(*cx, &global, js_error.handle_mut()); + ReadableStreamError( + *cx, + self.js_stream.handle(), + js_error.handle().into_handle(), + ); + } + } + + #[allow(unsafe_code)] + pub fn close_native(&self) { + let global = self.global(); + let _ar = enter_realm(&*global); + let cx = global.get_cx(); + + let handle = unsafe { self.js_stream.handle() }; + + self.external_underlying_source + .as_ref() + .expect("No external source to close.") + .close(cx, handle); + } + + /// Acquires a reader and locks the stream, + /// must be done before `read_a_chunk`. + #[allow(unsafe_code)] + pub fn start_reading(&self) -> Result<(), ()> { + if self.is_locked() || self.is_disturbed() { + return Err(()); + } + + let global = self.global(); + let _ar = enter_realm(&*global); + let cx = global.get_cx(); + + unsafe { + rooted!(in(*cx) let reader = ReadableStreamGetReader( + *cx, + self.js_stream.handle(), + ReadableStreamReaderMode::Default, + )); + + // Note: the stream is locked to the reader. + self.js_reader.set(reader.get()); + } + + self.has_reader.set(true); + Ok(()) + } + + /// Read a chunk from the stream, + /// must be called after `start_reading`, + /// and before `stop_reading`. + #[allow(unsafe_code)] + pub fn read_a_chunk(&self) -> Rc { + if !self.has_reader.get() { + panic!("Attempt to read stream chunk without having acquired a reader."); + } + + let global = self.global(); + let _ar = enter_realm(&*global); + let _aes = AutoEntryScript::new(&*global); + + let cx = global.get_cx(); + + unsafe { + rooted!(in(*cx) let promise_obj = ReadableStreamDefaultReaderRead( + *cx, + self.js_reader.handle(), + )); + Promise::new_with_js_promise(promise_obj.handle(), cx) + } + } + + /// Releases the lock on the reader, + /// must be done after `start_reading`. + #[allow(unsafe_code)] + pub fn stop_reading(&self) { + if !self.has_reader.get() { + panic!("ReadableStream::stop_reading called on a readerless stream."); + } + + self.has_reader.set(false); + + let global = self.global(); + let _ar = enter_realm(&*global); + let cx = global.get_cx(); + + unsafe { + ReadableStreamReaderReleaseLock(*cx, self.js_reader.handle()); + // Note: is this the way to nullify the Heap? + self.js_reader.set(ptr::null_mut()); + } + } + + #[allow(unsafe_code)] + pub fn is_locked(&self) -> bool { + // If we natively took a reader, we're locked. + if self.has_reader.get() { + return true; + } + + // Otherwise, still double-check that script didn't lock the stream. + let cx = self.global().get_cx(); + let mut locked_or_disturbed = false; + + unsafe { + ReadableStreamIsLocked(*cx, self.js_stream.handle(), &mut locked_or_disturbed); + } + + locked_or_disturbed + } + + #[allow(unsafe_code)] + pub fn is_disturbed(&self) -> bool { + // Check that script didn't disturb the stream. + let cx = self.global().get_cx(); + let mut locked_or_disturbed = false; + + unsafe { + ReadableStreamIsDisturbed(*cx, self.js_stream.handle(), &mut locked_or_disturbed); + } + + locked_or_disturbed + } +} + +#[allow(unsafe_code)] +unsafe extern "C" fn request_data( + source: *const c_void, + cx: *mut JSContext, + stream: HandleObject, + desired_size: usize, +) { + let source = &*(source as *const ExternalUnderlyingSourceController); + source.pull(SafeJSContext::from_ptr(cx), stream, desired_size); +} + +#[allow(unsafe_code)] +unsafe extern "C" fn write_into_read_request_buffer( + source: *const c_void, + _cx: *mut JSContext, + _stream: HandleObject, + buffer: *mut c_void, + length: usize, + bytes_written: *mut usize, +) { + let source = &*(source as *const ExternalUnderlyingSourceController); + let slice = slice::from_raw_parts_mut(buffer as *mut u8, length); + source.write_into_buffer(slice); + + // Currently we're always able to completely fulfill the write request. + *bytes_written = length; +} + +#[allow(unsafe_code)] +unsafe extern "C" fn cancel( + _source: *const c_void, + _cx: *mut JSContext, + _stream: HandleObject, + _reason: HandleValue, + _resolve_to: *mut JSVal, +) { +} + +#[allow(unsafe_code)] +unsafe extern "C" fn close(_source: *const c_void, _cx: *mut JSContext, _stream: HandleObject) {} + +#[allow(unsafe_code)] +unsafe extern "C" fn error( + _source: *const c_void, + _cx: *mut JSContext, + _stream: HandleObject, + _reason: HandleValue, +) { +} + +#[allow(unsafe_code)] +unsafe extern "C" fn finalize(source: *mut ReadableStreamUnderlyingSource) { + DeleteReadableStreamUnderlyingSource(source); +} + +pub enum ExternalUnderlyingSource { + /// Facilitate partial integration with sources + /// that are currently read into memory. + Memory(usize), + /// A blob as underlying source, with a known total size. + Blob(usize), + /// A fetch response as underlying source. + FetchResponse, +} + +#[derive(JSTraceable, MallocSizeOf)] +struct ExternalUnderlyingSourceController { + /// Loosely matches the underlying queue, + /// + buffer: RefCell>, + /// Has the stream been closed by native code? + closed: Cell, +} + +impl ExternalUnderlyingSourceController { + fn new(source: ExternalUnderlyingSource) -> ExternalUnderlyingSourceController { + let buffer = match source { + ExternalUnderlyingSource::Blob(size) | ExternalUnderlyingSource::Memory(size) => { + Vec::with_capacity(size) + }, + ExternalUnderlyingSource::FetchResponse => vec![], + }; + ExternalUnderlyingSourceController { + buffer: RefCell::new(buffer), + closed: Cell::new(false), + } + } + + /// Signal available bytes if the stream is currently readable. + #[allow(unsafe_code)] + fn maybe_signal_available_bytes( + &self, + cx: SafeJSContext, + stream: HandleObject, + available: usize, + ) { + if available == 0 { + return; + } + unsafe { + let mut readable = false; + if !ReadableStreamIsReadable(*cx, stream, &mut readable) { + return; + } + if readable { + ReadableStreamUpdateDataAvailableFromSource(*cx, stream, available as u32); + } + } + } + + /// Close a currently readable js stream. + #[allow(unsafe_code)] + fn maybe_close_js_stream(&self, cx: SafeJSContext, stream: HandleObject) { + unsafe { + let mut readable = false; + if !ReadableStreamIsReadable(*cx, stream, &mut readable) { + return; + } + if readable { + ReadableStreamClose(*cx, stream); + } + } + } + + fn close(&self, cx: SafeJSContext, stream: HandleObject) { + self.closed.set(true); + self.maybe_close_js_stream(cx, stream); + } + + fn enqueue_chunk(&self, cx: SafeJSContext, stream: HandleObject, mut chunk: Vec) { + let available = { + let mut buffer = self.buffer.borrow_mut(); + chunk.append(&mut buffer); + *buffer = chunk; + buffer.len() + }; + self.maybe_signal_available_bytes(cx, stream, available); + } + + #[allow(unsafe_code)] + fn pull(&self, cx: SafeJSContext, stream: HandleObject, _desired_size: usize) { + // Note: for pull sources, + // this would be the time to ask for a chunk. + + if self.closed.get() { + return self.maybe_close_js_stream(cx, stream); + } + + let available = { + let buffer = self.buffer.borrow(); + buffer.len() + }; + + self.maybe_signal_available_bytes(cx, stream, available); + } + + fn get_chunk_with_length(&self, length: usize) -> Vec { + let mut buffer = self.buffer.borrow_mut(); + let buffer_len = buffer.len(); + assert!(buffer_len >= length as usize); + buffer.split_off(buffer_len - length) + } + + fn write_into_buffer(&self, dest: &mut [u8]) { + let length = dest.len(); + let chunk = self.get_chunk_with_length(length); + dest.copy_from_slice(chunk.as_slice()); + } +} + +#[allow(unsafe_code)] +/// Get the `done` property of an object that a read promise resolved to. +pub fn get_read_promise_done(cx: SafeJSContext, v: &SafeHandleValue) -> Result { + unsafe { + rooted!(in(*cx) let object = v.to_object()); + rooted!(in(*cx) let mut done = UndefinedValue()); + match get_dictionary_property(*cx, object.handle(), "done", done.handle_mut()) { + Ok(true) => match bool::from_jsval(*cx, done.handle(), ()) { + Ok(ConversionResult::Success(val)) => Ok(val), + Ok(ConversionResult::Failure(error)) => Err(Error::Type(error.to_string())), + _ => Err(Error::Type("Unknown format for done property.".to_string())), + }, + Ok(false) => Err(Error::Type("Promise has no done property.".to_string())), + Err(()) => Err(Error::JSFailed), + } + } +} + +#[allow(unsafe_code)] +/// Get the `value` property of an object that a read promise resolved to. +pub fn get_read_promise_bytes(cx: SafeJSContext, v: &SafeHandleValue) -> Result, Error> { + unsafe { + rooted!(in(*cx) let object = v.to_object()); + rooted!(in(*cx) let mut bytes = UndefinedValue()); + match get_dictionary_property(*cx, object.handle(), "value", bytes.handle_mut()) { + Ok(true) => { + match Vec::::from_jsval(*cx, bytes.handle(), ConversionBehavior::EnforceRange) { + Ok(ConversionResult::Success(val)) => Ok(val), + Ok(ConversionResult::Failure(error)) => Err(Error::Type(error.to_string())), + _ => Err(Error::Type("Unknown format for bytes read.".to_string())), + } + }, + Ok(false) => Err(Error::Type("Promise has no value property.".to_string())), + Err(()) => Err(Error::JSFailed), + } + } +} diff --git a/components/script/dom/request.rs b/components/script/dom/request.rs index 387b7174791..89d9d54506a 100644 --- a/components/script/dom/request.rs +++ b/components/script/dom/request.rs @@ -2,8 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::body::{consume_body, BodyOperations, BodyType}; -use crate::dom::bindings::cell::{DomRefCell, Ref}; +use crate::body::Extractable; +use crate::body::{consume_body, BodyMixin, BodyType}; +use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::HeadersBinding::{HeadersInit, HeadersMethods}; use crate::dom::bindings::codegen::Bindings::RequestBinding::ReferrerPolicy; use crate::dom::bindings::codegen::Bindings::RequestBinding::RequestCache; @@ -22,11 +23,13 @@ use crate::dom::bindings::trace::RootedTraceableBox; use crate::dom::globalscope::GlobalScope; use crate::dom::headers::{Guard, Headers}; use crate::dom::promise::Promise; -use crate::dom::xmlhttprequest::Extractable; +use crate::dom::readablestream::ReadableStream; +use crate::script_runtime::JSContext as SafeJSContext; use dom_struct::dom_struct; use http::header::{HeaderName, HeaderValue}; use http::method::InvalidMethod; use http::Method as HttpMethod; +use js::jsapi::JSObject; use net_traits::request::CacheMode as NetTraitsRequestCache; use net_traits::request::CredentialsMode as NetTraitsRequestCredentials; use net_traits::request::Destination as NetTraitsRequestDestination; @@ -37,7 +40,7 @@ use net_traits::request::RequestMode as NetTraitsRequestMode; use net_traits::request::{Origin, Window}; use net_traits::ReferrerPolicy as MsgReferrerPolicy; use servo_url::ServoUrl; -use std::cell::Cell; +use std::ptr::NonNull; use std::rc::Rc; use std::str::FromStr; @@ -45,11 +48,9 @@ use std::str::FromStr; pub struct Request { reflector_: Reflector, request: DomRefCell, - body_used: Cell, + body_stream: MutNullableDom, headers: MutNullableDom, mime_type: DomRefCell>, - #[ignore_malloc_size_of = "Rc"] - body_promise: DomRefCell, BodyType)>>, } impl Request { @@ -57,10 +58,9 @@ impl Request { Request { reflector_: Reflector::new(), request: DomRefCell::new(net_request_from_global(global, url)), - body_used: Cell::new(false), + body_stream: MutNullableDom::new(None), headers: Default::default(), mime_type: DomRefCell::new("".to_string().into_bytes()), - body_promise: DomRefCell::new(None), } } @@ -72,7 +72,7 @@ impl Request { #[allow(non_snake_case)] pub fn Constructor( global: &GlobalScope, - input: RequestInfo, + mut input: RequestInfo, init: RootedTraceableBox, ) -> Fallible> { // Step 1 @@ -365,9 +365,9 @@ impl Request { r.request.borrow_mut().headers = r.Headers().get_headers_list(); // Step 33 - let mut input_body = if let RequestInfo::Request(ref input_request) = input { - let input_request_request = input_request.request.borrow(); - input_request_request.body.clone() + let mut input_body = if let RequestInfo::Request(ref mut input_request) = input { + let mut input_request_request = input_request.request.borrow_mut(); + input_request_request.body.take() } else { None }; @@ -398,12 +398,10 @@ impl Request { // Step 36.2 TODO "If init["keepalive"] exists and is true..." // Step 36.3 - let extracted_body_tmp = init_body.extract(); - input_body = Some(extracted_body_tmp.0); - let content_type = extracted_body_tmp.1; + let mut extracted_body = init_body.extract(global)?; // Step 36.4 - if let Some(contents) = content_type { + if let Some(contents) = extracted_body.content_type.take() { let ct_header_name = b"Content-Type"; if !r .Headers() @@ -427,6 +425,10 @@ impl Request { } } } + + let (net_body, stream) = extracted_body.into_net_request_body(); + r.body_stream.set(Some(&*stream)); + input_body = Some(net_body); } // Step 37 "TODO if body is non-null and body's source is null..." @@ -448,13 +450,6 @@ impl Request { // Step 42 Ok(r) } - - // https://fetch.spec.whatwg.org/#concept-body-locked - fn locked(&self) -> bool { - // TODO: ReadableStream is unimplemented. Just return false - // for now. - false - } } impl Request { @@ -467,7 +462,6 @@ impl Request { fn clone_from(r: &Request) -> Fallible> { let req = r.request.borrow(); let url = req.url(); - let body_used = r.body_used.get(); let mime_type = r.mime_type.borrow().clone(); let headers_guard = r.Headers().get_guard(); let r_clone = Request::new(&r.global(), url); @@ -477,7 +471,6 @@ impl Request { borrowed_r_request.origin = req.origin.clone(); } *r_clone.request.borrow_mut() = req.clone(); - r_clone.body_used.set(body_used); *r_clone.mime_type.borrow_mut() = mime_type; r_clone.Headers().copy_from_headers(r.Headers())?; r_clone.Headers().set_guard(headers_guard); @@ -536,16 +529,14 @@ fn includes_credentials(input: &ServoUrl) -> bool { !input.username().is_empty() || input.password().is_some() } -// TODO: `Readable Stream` object is not implemented in Servo yet. // https://fetch.spec.whatwg.org/#concept-body-disturbed -fn request_is_disturbed(_input: &Request) -> bool { - false +fn request_is_disturbed(input: &Request) -> bool { + input.is_disturbed() } -// TODO: `Readable Stream` object is not implemented in Servo yet. // https://fetch.spec.whatwg.org/#concept-body-locked -fn request_is_locked(_input: &Request) -> bool { - false +fn request_is_locked(input: &Request) -> bool { + input.is_locked() } impl RequestMethods for Request { @@ -622,9 +613,14 @@ impl RequestMethods for Request { DOMString::from_string(r.integrity_metadata.clone()) } + /// + fn GetBody(&self, _cx: SafeJSContext) -> Option> { + self.body().map(|stream| stream.get_js_stream()) + } + // https://fetch.spec.whatwg.org/#dom-body-bodyused fn BodyUsed(&self) -> bool { - self.body_used.get() + self.is_disturbed() } // https://fetch.spec.whatwg.org/#dom-request-clone @@ -667,29 +663,25 @@ impl RequestMethods for Request { } } -impl BodyOperations for Request { - fn get_body_used(&self) -> bool { - self.BodyUsed() - } - - fn set_body_promise(&self, p: &Rc, body_type: BodyType) { - assert!(self.body_promise.borrow().is_none()); - self.body_used.set(true); - *self.body_promise.borrow_mut() = Some((p.clone(), body_type)); +impl BodyMixin for Request { + fn is_disturbed(&self) -> bool { + let body_stream = self.body_stream.get(); + body_stream + .as_ref() + .map_or(false, |stream| stream.is_disturbed()) } fn is_locked(&self) -> bool { - self.locked() + let body_stream = self.body_stream.get(); + body_stream.map_or(false, |stream| stream.is_locked()) } - fn take_body(&self) -> Option> { - let mut request = self.request.borrow_mut(); - let body = request.body.take(); - Some(body.unwrap_or(vec![])) + fn body(&self) -> Option> { + self.body_stream.get() } - fn get_mime_type(&self) -> Ref> { - self.mime_type.borrow() + fn get_mime_type(&self) -> Vec { + self.mime_type.borrow().clone() } } diff --git a/components/script/dom/response.rs b/components/script/dom/response.rs index ed55d9962a9..432fcbeb3e4 100644 --- a/components/script/dom/response.rs +++ b/components/script/dom/response.rs @@ -2,8 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::body::{consume_body, consume_body_with_promise, BodyOperations, BodyType}; -use crate::dom::bindings::cell::{DomRefCell, Ref}; +use crate::body::{consume_body, BodyMixin, BodyType}; +use crate::body::{Extractable, ExtractedBody}; +use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::HeadersBinding::HeadersMethods; use crate::dom::bindings::codegen::Bindings::ResponseBinding; use crate::dom::bindings::codegen::Bindings::ResponseBinding::{ @@ -18,16 +19,16 @@ use crate::dom::globalscope::GlobalScope; use crate::dom::headers::{is_obs_text, is_vchar}; use crate::dom::headers::{Guard, Headers}; use crate::dom::promise::Promise; -use crate::dom::xmlhttprequest::Extractable; +use crate::dom::readablestream::{ExternalUnderlyingSource, ReadableStream}; +use crate::script_runtime::JSContext as SafeJSContext; use crate::script_runtime::StreamConsumer; use dom_struct::dom_struct; use http::header::HeaderMap as HyperHeaders; use hyper::StatusCode; use hyper_serde::Serde; -use net_traits::response::ResponseBody as NetTraitsResponseBody; +use js::jsapi::JSObject; use servo_url::ServoUrl; -use std::cell::Cell; -use std::mem; +use std::ptr::NonNull; use std::rc::Rc; use std::str::FromStr; use url::Position; @@ -37,7 +38,6 @@ pub struct Response { reflector_: Reflector, headers_reflector: MutNullableDom, mime_type: DomRefCell>, - body_used: Cell, /// `None` can be considered a StatusCode of `0`. #[ignore_malloc_size_of = "Defined in hyper"] status: DomRefCell>, @@ -45,10 +45,8 @@ pub struct Response { response_type: DomRefCell, url: DomRefCell>, url_list: DomRefCell>, - // For now use the existing NetTraitsResponseBody enum - body: DomRefCell, - #[ignore_malloc_size_of = "Rc"] - body_promise: DomRefCell, BodyType)>>, + /// The stream of https://fetch.spec.whatwg.org/#body. + body_stream: MutNullableDom, #[ignore_malloc_size_of = "StreamConsumer"] stream_consumer: DomRefCell>, redirected: DomRefCell, @@ -56,19 +54,21 @@ pub struct Response { #[allow(non_snake_case)] impl Response { - pub fn new_inherited() -> Response { + pub fn new_inherited(global: &GlobalScope) -> Response { + let stream = ReadableStream::new_with_external_underlying_source( + global, + ExternalUnderlyingSource::FetchResponse, + ); Response { reflector_: Reflector::new(), headers_reflector: Default::default(), mime_type: DomRefCell::new("".to_string().into_bytes()), - body_used: Cell::new(false), status: DomRefCell::new(Some(StatusCode::OK)), raw_status: DomRefCell::new(Some((200, b"".to_vec()))), response_type: DomRefCell::new(DOMResponseType::Default), url: DomRefCell::new(None), url_list: DomRefCell::new(vec![]), - body: DomRefCell::new(NetTraitsResponseBody::Empty), - body_promise: DomRefCell::new(None), + body_stream: MutNullableDom::new(Some(&*stream)), stream_consumer: DomRefCell::new(None), redirected: DomRefCell::new(false), } @@ -76,7 +76,7 @@ impl Response { // https://fetch.spec.whatwg.org/#dom-response pub fn new(global: &GlobalScope) -> DomRoot { - reflect_dom_object(Box::new(Response::new_inherited()), global) + reflect_dom_object(Box::new(Response::new_inherited(global)), global) } pub fn Constructor( @@ -128,8 +128,14 @@ impl Response { }; // Step 7.3 - let (extracted_body, content_type) = body.extract(); - *r.body.borrow_mut() = NetTraitsResponseBody::Done(extracted_body); + let ExtractedBody { + stream, + total_bytes: _, + content_type, + source: _, + } = body.extract(global)?; + + r.body_stream.set(Some(&*stream)); // Step 7.4 if let Some(content_type_contents) = content_type { @@ -211,42 +217,32 @@ impl Response { Ok(r) } - // https://fetch.spec.whatwg.org/#concept-body-locked - fn locked(&self) -> bool { - // TODO: ReadableStream is unimplemented. Just return false - // for now. - false + pub fn error_stream(&self, error: Error) { + if let Some(body) = self.body_stream.get() { + body.error_native(error); + } } } -impl BodyOperations for Response { - fn get_body_used(&self) -> bool { - self.BodyUsed() - } - - fn set_body_promise(&self, p: &Rc, body_type: BodyType) { - assert!(self.body_promise.borrow().is_none()); - self.body_used.set(true); - *self.body_promise.borrow_mut() = Some((p.clone(), body_type)); +impl BodyMixin for Response { + fn is_disturbed(&self) -> bool { + self.body_stream + .get() + .map_or(false, |stream| stream.is_disturbed()) } fn is_locked(&self) -> bool { - self.locked() + self.body_stream + .get() + .map_or(false, |stream| stream.is_locked()) } - fn take_body(&self) -> Option> { - let body = mem::replace(&mut *self.body.borrow_mut(), NetTraitsResponseBody::Empty); - match body { - NetTraitsResponseBody::Done(bytes) => Some(bytes), - body => { - let _ = mem::replace(&mut *self.body.borrow_mut(), body); - None - }, - } + fn body(&self) -> Option> { + self.body_stream.get() } - fn get_mime_type(&self) -> Ref> { - self.mime_type.borrow() + fn get_mime_type(&self) -> Vec { + self.mime_type.borrow().clone() } } @@ -328,7 +324,7 @@ impl ResponseMethods for Response { // https://fetch.spec.whatwg.org/#dom-response-clone fn Clone(&self) -> Fallible> { // Step 1 - if self.is_locked() || self.body_used.get() { + if self.is_locked() || self.is_disturbed() { return Err(Error::Type("cannot clone a disturbed response".to_string())); } @@ -346,8 +342,8 @@ impl ResponseMethods for Response { *new_response.url.borrow_mut() = self.url.borrow().clone(); *new_response.url_list.borrow_mut() = self.url_list.borrow().clone(); - if *self.body.borrow() != NetTraitsResponseBody::Empty { - *new_response.body.borrow_mut() = self.body.borrow().clone(); + if let Some(stream) = self.body_stream.get().clone() { + new_response.body_stream.set(Some(&*stream)); } // Step 3 @@ -359,7 +355,12 @@ impl ResponseMethods for Response { // https://fetch.spec.whatwg.org/#dom-body-bodyused fn BodyUsed(&self) -> bool { - self.body_used.get() + self.is_disturbed() + } + + /// + fn GetBody(&self, _cx: SafeJSContext) -> Option> { + self.body().map(|stream| stream.get_js_stream()) } // https://fetch.spec.whatwg.org/#dom-body-text @@ -424,20 +425,19 @@ impl Response { *self.status.borrow_mut() = None; self.set_raw_status(None); self.set_headers(None); - *self.body.borrow_mut() = NetTraitsResponseBody::Done(vec![]); }, DOMResponseType::Opaque => { *self.url_list.borrow_mut() = vec![]; *self.status.borrow_mut() = None; self.set_raw_status(None); self.set_headers(None); - *self.body.borrow_mut() = NetTraitsResponseBody::Done(vec![]); + self.body_stream.set(None); }, DOMResponseType::Opaqueredirect => { *self.status.borrow_mut() = None; self.set_raw_status(None); self.set_headers(None); - *self.body.borrow_mut() = NetTraitsResponseBody::Done(vec![]); + self.body_stream.set(None); }, DOMResponseType::Default => {}, DOMResponseType::Basic => {}, @@ -449,17 +449,19 @@ impl Response { *self.stream_consumer.borrow_mut() = sc; } - pub fn stream_chunk(&self, stream: &[u8]) { + pub fn stream_chunk(&self, chunk: Vec) { + // Note, are these two actually mutually exclusive? if let Some(stream_consumer) = self.stream_consumer.borrow_mut().as_ref() { - stream_consumer.consume_chunk(stream); + stream_consumer.consume_chunk(chunk.as_slice()); + } else if let Some(body) = self.body_stream.get() { + body.enqueue_native(chunk); } } #[allow(unrooted_must_root)] - pub fn finish(&self, body: Vec) { - *self.body.borrow_mut() = NetTraitsResponseBody::Done(body); - if let Some((p, body_type)) = self.body_promise.borrow_mut().take() { - consume_body_with_promise(self, body_type, &p); + pub fn finish(&self) { + if let Some(body) = self.body_stream.get() { + body.close_native(); } if let Some(stream_consumer) = self.stream_consumer.borrow_mut().take() { stream_consumer.stream_end(); diff --git a/components/script/dom/testbinding.rs b/components/script/dom/testbinding.rs index 37c13c173e3..20fe98e2516 100644 --- a/components/script/dom/testbinding.rs +++ b/components/script/dom/testbinding.rs @@ -53,7 +53,7 @@ use crate::realms::InRealm; use crate::script_runtime::JSContext as SafeJSContext; use crate::timers::OneshotTimerCallback; use dom_struct::dom_struct; -use js::jsapi::{Heap, JSContext, JSObject}; +use js::jsapi::{Heap, JSObject}; use js::jsapi::{JS_NewPlainObject, JS_NewUint8ClampedArray}; use js::jsval::{JSVal, NullValue}; use js::rust::CustomAutoRooterGuard; @@ -1018,9 +1018,8 @@ impl TestBindingMethods for TestBinding { } } impl Callback for SimpleHandler { - #[allow(unsafe_code)] - fn callback(&self, cx: *mut JSContext, v: HandleValue, realm: InRealm) { - let global = unsafe { GlobalScope::from_context(cx, realm) }; + fn callback(&self, cx: SafeJSContext, v: HandleValue, realm: InRealm) { + let global = GlobalScope::from_safe_context(cx, realm); let _ = self.handler.Call_(&*global, v, ExceptionHandling::Report); } } diff --git a/components/script/dom/webidls/Blob.webidl b/components/script/dom/webidls/Blob.webidl index 8a3f5f5185e..572879ec621 100644 --- a/components/script/dom/webidls/Blob.webidl +++ b/components/script/dom/webidls/Blob.webidl @@ -17,6 +17,7 @@ interface Blob { optional [Clamp] long long end, optional DOMString contentType); + [NewObject] object stream(); [NewObject] Promise text(); [NewObject] Promise arrayBuffer(); }; diff --git a/components/script/dom/webidls/Body.webidl b/components/script/dom/webidls/Body.webidl index bfca1ad3990..5de4aa36813 100644 --- a/components/script/dom/webidls/Body.webidl +++ b/components/script/dom/webidls/Body.webidl @@ -7,6 +7,7 @@ [Exposed=(Window,Worker)] interface mixin Body { readonly attribute boolean bodyUsed; + readonly attribute object? body; [NewObject] Promise arrayBuffer(); [NewObject] Promise blob(); diff --git a/components/script/dom/webidls/ReadableStream.webidl b/components/script/dom/webidls/ReadableStream.webidl new file mode 100644 index 00000000000..3662ca75ab8 --- /dev/null +++ b/components/script/dom/webidls/ReadableStream.webidl @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// This interface is entirely internal to Servo, and should not be accessible to +// web pages. + +[NoInterfaceObject, Exposed=(Window,Worker)] +// Need to escape "ReadableStream" so it's treated as an identifier. +interface _ReadableStream { +}; diff --git a/components/script/dom/webidls/XMLHttpRequest.webidl b/components/script/dom/webidls/XMLHttpRequest.webidl index d2211110d73..097b4f7177c 100644 --- a/components/script/dom/webidls/XMLHttpRequest.webidl +++ b/components/script/dom/webidls/XMLHttpRequest.webidl @@ -13,7 +13,7 @@ */ // https://fetch.spec.whatwg.org/#bodyinit -typedef (Blob or BufferSource or FormData or DOMString or URLSearchParams) BodyInit; +typedef (Blob or BufferSource or FormData or DOMString or URLSearchParams or ReadableStream) BodyInit; enum XMLHttpRequestResponseType { "", @@ -21,7 +21,7 @@ enum XMLHttpRequestResponseType { "blob", "document", "json", - "text" + "text", }; [Exposed=(Window,Worker)] diff --git a/components/script/dom/xmlhttprequest.rs b/components/script/dom/xmlhttprequest.rs index 336d7a89f22..1bb0ebd60dd 100644 --- a/components/script/dom/xmlhttprequest.rs +++ b/components/script/dom/xmlhttprequest.rs @@ -2,11 +2,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use crate::body::{Extractable, ExtractedBody}; use crate::document_loader::DocumentLoader; use crate::dom::bindings::cell::DomRefCell; -use crate::dom::bindings::codegen::Bindings::BlobBinding::BlobBinding::BlobMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; -use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit; use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::XMLHttpRequestMethods; use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::XMLHttpRequestResponseType; use crate::dom::bindings::codegen::UnionTypes::DocumentOrBodyInit; @@ -22,15 +21,13 @@ use crate::dom::document::DocumentSource; use crate::dom::document::{Document, HasBrowsingContext, IsHTMLDocument}; use crate::dom::event::{Event, EventBubbles, EventCancelable}; use crate::dom::eventtarget::EventTarget; -use crate::dom::formdata::FormData; use crate::dom::globalscope::GlobalScope; use crate::dom::headers::is_forbidden_header_name; -use crate::dom::htmlformelement::{encode_multipart_form_data, generate_boundary}; use crate::dom::node::Node; use crate::dom::performanceresourcetiming::InitiatorType; use crate::dom::progressevent::ProgressEvent; +use crate::dom::readablestream::ReadableStream; use crate::dom::servoparser::ServoParser; -use crate::dom::urlsearchparams::URLSearchParams; use crate::dom::window::Window; use crate::dom::workerglobalscope::WorkerGlobalScope; use crate::dom::xmlhttprequesteventtarget::XMLHttpRequestEventTarget; @@ -58,7 +55,9 @@ use js::jsval::{JSVal, NullValue, UndefinedValue}; use js::rust::wrappers::JS_ParseJSON; use js::typedarray::{ArrayBuffer, CreateWith}; use mime::{self, Mime, Name}; -use net_traits::request::{CredentialsMode, Destination, Referrer, RequestBuilder, RequestMode}; +use net_traits::request::{ + BodySource, CredentialsMode, Destination, Referrer, RequestBuilder, RequestMode, +}; use net_traits::trim_http_whitespace; use net_traits::CoreResourceMsg::Fetch; use net_traits::{FetchChannels, FetchMetadata, FilteredMetadata}; @@ -562,42 +561,96 @@ impl XMLHttpRequestMethods for XMLHttpRequest { _ => data, }; // Step 4 (first half) - let extracted_or_serialized = match data { + let mut extracted_or_serialized = match data { Some(DocumentOrBodyInit::Document(ref doc)) => { - let data = Vec::from(serialize_document(&doc)?.as_ref()); + let bytes = Vec::from(serialize_document(&doc)?.as_ref()); let content_type = if doc.is_html_document() { "text/html;charset=UTF-8" } else { "application/xml;charset=UTF-8" }; - Some((data, Some(DOMString::from(content_type)))) + let total_bytes = bytes.len(); + let global = self.global(); + let stream = ReadableStream::new_from_bytes(&global, bytes); + Some(ExtractedBody { + stream, + total_bytes: Some(total_bytes), + content_type: Some(DOMString::from(content_type)), + source: BodySource::Null, + }) }, - Some(DocumentOrBodyInit::Blob(ref b)) => Some(b.extract()), - Some(DocumentOrBodyInit::FormData(ref formdata)) => Some(formdata.extract()), - Some(DocumentOrBodyInit::String(ref str)) => Some(str.extract()), - Some(DocumentOrBodyInit::URLSearchParams(ref urlsp)) => Some(urlsp.extract()), + Some(DocumentOrBodyInit::Blob(ref b)) => { + Some(b.extract(&self.global()).expect("Couldn't extract body.")) + }, + Some(DocumentOrBodyInit::FormData(ref formdata)) => Some( + formdata + .extract(&self.global()) + .expect("Couldn't extract body."), + ), + Some(DocumentOrBodyInit::String(ref str)) => { + Some(str.extract(&self.global()).expect("Couldn't extract body.")) + }, + Some(DocumentOrBodyInit::URLSearchParams(ref urlsp)) => Some( + urlsp + .extract(&self.global()) + .expect("Couldn't extract body."), + ), Some(DocumentOrBodyInit::ArrayBuffer(ref typedarray)) => { - Some((typedarray.to_vec(), None)) + let bytes = typedarray.to_vec(); + let total_bytes = bytes.len(); + let global = self.global(); + let stream = ReadableStream::new_from_bytes(&global, bytes); + Some(ExtractedBody { + stream, + total_bytes: Some(total_bytes), + content_type: None, + source: BodySource::BufferSource, + }) }, Some(DocumentOrBodyInit::ArrayBufferView(ref typedarray)) => { - Some((typedarray.to_vec(), None)) + let bytes = typedarray.to_vec(); + let total_bytes = bytes.len(); + let global = self.global(); + let stream = ReadableStream::new_from_bytes(&global, bytes); + Some(ExtractedBody { + stream, + total_bytes: Some(total_bytes), + content_type: None, + source: BodySource::BufferSource, + }) + }, + Some(DocumentOrBodyInit::ReadableStream(ref stream)) => { + // TODO: + // 1. If the keepalive flag is set, then throw a TypeError. + + if stream.is_locked() || stream.is_disturbed() { + return Err(Error::Type( + "The body's stream is disturbed or locked".to_string(), + )); + } + + Some(ExtractedBody { + stream: stream.clone(), + total_bytes: None, + content_type: None, + source: BodySource::Null, + }) }, None => None, }; - self.request_body_len - .set(extracted_or_serialized.as_ref().map_or(0, |e| e.0.len())); + self.request_body_len.set( + extracted_or_serialized + .as_ref() + .map_or(0, |e| e.total_bytes.unwrap_or(0)), + ); // todo preserved headers? // Step 6 self.upload_complete.set(false); // Step 7 - self.upload_complete.set(match extracted_or_serialized { - None => true, - Some(ref e) if e.0.is_empty() => true, - _ => false, - }); + self.upload_complete.set(extracted_or_serialized.is_none()); // Step 8 self.send_flag.set(true); @@ -634,12 +687,17 @@ impl XMLHttpRequestMethods for XMLHttpRequest { unreachable!() }; + let content_type = match extracted_or_serialized.as_mut() { + Some(body) => body.content_type.take(), + None => None, + }; + let mut request = RequestBuilder::new(self.request_url.borrow().clone().unwrap()) .method(self.request_method.borrow().clone()) .headers((*self.request_headers.borrow()).clone()) .unsafe_request(true) // XXXManishearth figure out how to avoid this clone - .body(extracted_or_serialized.as_ref().map(|e| e.0.clone())) + .body(extracted_or_serialized.map(|e| e.into_net_request_body().0)) // XXXManishearth actually "subresource", but it doesn't exist // https://github.com/whatwg/xhr/issues/71 .destination(Destination::None) @@ -658,8 +716,8 @@ impl XMLHttpRequestMethods for XMLHttpRequest { .pipeline_id(Some(self.global().pipeline_id())); // step 4 (second half) - match extracted_or_serialized { - Some((_, ref content_type)) => { + match content_type { + Some(content_type) => { let encoding = match data { Some(DocumentOrBodyInit::String(_)) | Some(DocumentOrBodyInit::Document(_)) => // XHR spec differs from http, and says UTF-8 should be in capitals, @@ -672,13 +730,12 @@ impl XMLHttpRequestMethods for XMLHttpRequest { }; let mut content_type_set = false; - if let Some(ref ct) = *content_type { - if !request.headers.contains_key(header::CONTENT_TYPE) { - request - .headers - .insert(header::CONTENT_TYPE, HeaderValue::from_str(ct).unwrap()); - content_type_set = true; - } + if !request.headers.contains_key(header::CONTENT_TYPE) { + request.headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_str(&content_type).unwrap(), + ); + content_type_set = true; } if !content_type_set { @@ -1555,56 +1612,6 @@ impl XHRTimeoutCallback { } } -pub trait Extractable { - fn extract(&self) -> (Vec, Option); -} - -impl Extractable for Blob { - fn extract(&self) -> (Vec, Option) { - let content_type = if self.Type().as_ref().is_empty() { - None - } else { - Some(self.Type()) - }; - let bytes = self.get_bytes().unwrap_or(vec![]); - (bytes, content_type) - } -} - -impl Extractable for DOMString { - fn extract(&self) -> (Vec, Option) { - ( - self.as_bytes().to_owned(), - Some(DOMString::from("text/plain;charset=UTF-8")), - ) - } -} - -impl Extractable for FormData { - fn extract(&self) -> (Vec, Option) { - let boundary = generate_boundary(); - let bytes = encode_multipart_form_data(&mut self.datums(), boundary.clone(), UTF_8); - ( - bytes, - Some(DOMString::from(format!( - "multipart/form-data;boundary={}", - boundary - ))), - ) - } -} - -impl Extractable for URLSearchParams { - fn extract(&self) -> (Vec, Option) { - ( - self.serialize_utf8().into_bytes(), - Some(DOMString::from( - "application/x-www-form-urlencoded;charset=UTF-8", - )), - ) - } -} - fn serialize_document(doc: &Document) -> Fallible { let mut writer = vec![]; match serialize(&mut writer, &doc.upcast::(), SerializeOpts::default()) { @@ -1613,20 +1620,6 @@ fn serialize_document(doc: &Document) -> Fallible { } } -impl Extractable for BodyInit { - // https://fetch.spec.whatwg.org/#concept-bodyinit-extract - fn extract(&self) -> (Vec, Option) { - match *self { - BodyInit::String(ref s) => s.extract(), - BodyInit::URLSearchParams(ref usp) => usp.extract(), - BodyInit::Blob(ref b) => b.extract(), - BodyInit::FormData(ref formdata) => formdata.extract(), - BodyInit::ArrayBuffer(ref typedarray) => ((typedarray.to_vec(), None)), - BodyInit::ArrayBufferView(ref typedarray) => ((typedarray.to_vec(), None)), - } - } -} - /// Returns whether `bs` is a `field-value`, as defined by /// [RFC 2616](http://tools.ietf.org/html/rfc2616#page-32). pub fn is_field_value(slice: &[u8]) -> bool { diff --git a/components/script/fetch.rs b/components/script/fetch.rs index 3f8e27c556c..e26370016e6 100644 --- a/components/script/fetch.rs +++ b/components/script/fetch.rs @@ -36,14 +36,12 @@ use net_traits::{FetchChannels, FetchResponseListener, NetworkError}; use net_traits::{FetchMetadata, FilteredMetadata, Metadata}; use net_traits::{ResourceFetchTiming, ResourceTimingType}; use servo_url::ServoUrl; -use std::mem; use std::rc::Rc; use std::sync::{Arc, Mutex}; struct FetchContext { fetch_promise: Option, response_object: Trusted, - body: Vec, resource_timing: ResourceFetchTiming, } @@ -149,6 +147,7 @@ pub fn Fetch( // Step 2 let request = match Request::Constructor(global, input, init) { Err(e) => { + response.error_stream(e.clone()); promise.reject_error(e); return promise; }, @@ -172,7 +171,6 @@ pub fn Fetch( let fetch_context = Arc::new(Mutex::new(FetchContext { fetch_promise: Some(TrustedPromise::new(promise.clone())), response_object: Trusted::new(&*response), - body: vec![], resource_timing: ResourceFetchTiming::new(timing_type), })); let listener = NetworkListener { @@ -222,7 +220,9 @@ impl FetchResponseListener for FetchContext { Err(_) => { promise.reject_error(Error::Type("Network error occurred".to_string())); self.fetch_promise = Some(TrustedPromise::new(promise)); - self.response_object.root().set_type(DOMResponseType::Error); + let response = self.response_object.root(); + response.set_type(DOMResponseType::Error); + response.error_stream(Error::Type("Network error occurred".to_string())); return; }, // Step 4.2 @@ -260,15 +260,15 @@ impl FetchResponseListener for FetchContext { self.fetch_promise = Some(TrustedPromise::new(promise)); } - fn process_response_chunk(&mut self, mut chunk: Vec) { - self.response_object.root().stream_chunk(chunk.as_slice()); - self.body.append(&mut chunk); + fn process_response_chunk(&mut self, chunk: Vec) { + let response = self.response_object.root(); + response.stream_chunk(chunk); } fn process_response_eof(&mut self, _response: Result) { let response = self.response_object.root(); let _ac = enter_realm(&*response); - response.finish(mem::replace(&mut self.body, vec![])); + response.finish(); // TODO // ... trailerObject is not supported in Servo yet. } diff --git a/components/script/script_module.rs b/components/script/script_module.rs index 39933121899..c12b745af47 100644 --- a/components/script/script_module.rs +++ b/components/script/script_module.rs @@ -675,7 +675,7 @@ impl ModuleHandler { } impl Callback for ModuleHandler { - fn callback(&self, _cx: *mut JSContext, _v: HandleValue, _realm: InRealm) { + fn callback(&self, _cx: SafeJSContext, _v: HandleValue, _realm: InRealm) { let task = self.task.borrow_mut().take().unwrap(); task.run_box(); } diff --git a/components/script/script_runtime.rs b/components/script/script_runtime.rs index 6c725f8cbd3..4cf2055c649 100644 --- a/components/script/script_runtime.rs +++ b/components/script/script_runtime.rs @@ -7,7 +7,7 @@ #![allow(dead_code)] -use crate::body::BodyOperations; +use crate::body::BodyMixin; use crate::dom::bindings::codegen::Bindings::PromiseBinding::PromiseJobCallback; use crate::dom::bindings::codegen::Bindings::ResponseBinding::ResponseBinding::ResponseMethods; use crate::dom::bindings::codegen::Bindings::ResponseBinding::ResponseType as DOMResponseType; @@ -979,7 +979,7 @@ unsafe extern "C" fn consume_stream( } // Step 2.6.2 If response body is alreaady consumed, return with a TypeError and abort these substeps. - if unwrapped_source.get_body_used() { + if unwrapped_source.is_disturbed() { throw_dom_exception( cx, &global, diff --git a/components/script_traits/lib.rs b/components/script_traits/lib.rs index 84314244f87..fc532ff2866 100644 --- a/components/script_traits/lib.rs +++ b/components/script_traits/lib.rs @@ -49,7 +49,7 @@ use msg::constellation_msg::{ use msg::constellation_msg::{PipelineNamespaceId, TopLevelBrowsingContextId}; use net_traits::image::base::Image; use net_traits::image_cache::ImageCache; -use net_traits::request::Referrer; +use net_traits::request::{Referrer, RequestBody}; use net_traits::storage_thread::StorageType; use net_traits::{FetchResponseMsg, ReferrerPolicy, ResourceThreads}; use pixels::PixelFormat; @@ -171,8 +171,8 @@ pub struct LoadData { serialize_with = "::hyper_serde::serialize" )] pub headers: HeaderMap, - /// The data. - pub data: Option>, + /// The data that will be used as the body of the request. + pub data: Option, /// The result of evaluating a javascript scheme url. pub js_eval_result: Option, /// The referrer. diff --git a/tests/wpt/include.ini b/tests/wpt/include.ini index e6405398227..2394e6020d3 100644 --- a/tests/wpt/include.ini +++ b/tests/wpt/include.ini @@ -153,6 +153,10 @@ skip: true skip: false [selection] skip: false +[streams] + skip: true + [readable-streams] + skip: false [subresource-integrity] skip: false [touch-events] diff --git a/tests/wpt/metadata/FileAPI/blob/Blob-stream.any.js.ini b/tests/wpt/metadata/FileAPI/blob/Blob-stream.any.js.ini deleted file mode 100644 index facf4573cb3..00000000000 --- a/tests/wpt/metadata/FileAPI/blob/Blob-stream.any.js.ini +++ /dev/null @@ -1,27 +0,0 @@ -[Blob-stream.any.worker.html] - [Blob.stream() empty Blob] - expected: FAIL - - [Blob.stream()] - expected: FAIL - - [Blob.stream() non-unicode input] - expected: FAIL - - [Blob.stream() garbage collection of blob shouldn't break streamconsumption] - expected: FAIL - - -[Blob-stream.any.html] - [Blob.stream() empty Blob] - expected: FAIL - - [Blob.stream()] - expected: FAIL - - [Blob.stream() non-unicode input] - expected: FAIL - - [Blob.stream() garbage collection of blob shouldn't break streamconsumption] - expected: FAIL - diff --git a/tests/wpt/metadata/FileAPI/idlharness.html.ini b/tests/wpt/metadata/FileAPI/idlharness.html.ini index a19d956ea41..aa610053984 100644 --- a/tests/wpt/metadata/FileAPI/idlharness.html.ini +++ b/tests/wpt/metadata/FileAPI/idlharness.html.ini @@ -18,18 +18,9 @@ [File API automated IDL tests] expected: FAIL - [Blob interface: operation stream()] - expected: FAIL - [Blob interface: operation text()] expected: FAIL - [Blob interface: new Blob(["TEST"\]) must inherit property "stream()" with the proper type] - expected: FAIL - [Blob interface: operation arrayBuffer()] expected: FAIL - [Blob interface: new File(["myFileBits"\], "myFileName") must inherit property "stream()" with the proper type] - expected: FAIL - diff --git a/tests/wpt/metadata/FileAPI/idlharness.worker.js.ini b/tests/wpt/metadata/FileAPI/idlharness.worker.js.ini index b8d0a66f512..b47ec2ab313 100644 --- a/tests/wpt/metadata/FileAPI/idlharness.worker.js.ini +++ b/tests/wpt/metadata/FileAPI/idlharness.worker.js.ini @@ -30,18 +30,9 @@ [idlharness] expected: FAIL - [Blob interface: operation stream()] - expected: FAIL - [Blob interface: operation text()] expected: FAIL - [Blob interface: new Blob(["TEST"\]) must inherit property "stream()" with the proper type] - expected: FAIL - [Blob interface: operation arrayBuffer()] expected: FAIL - [Blob interface: new File(["myFileBits"\], "myFileName") must inherit property "stream()" with the proper type] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/basic/stream-response.any.js.ini b/tests/wpt/metadata/fetch/api/basic/stream-response.any.js.ini deleted file mode 100644 index 108c42fa26e..00000000000 --- a/tests/wpt/metadata/fetch/api/basic/stream-response.any.js.ini +++ /dev/null @@ -1,11 +0,0 @@ -[stream-response.any.html] - type: testharness - [Stream response's body] - expected: FAIL - - -[stream-response.any.worker.html] - type: testharness - [Stream response's body] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/idlharness.any.js.ini b/tests/wpt/metadata/fetch/api/idlharness.any.js.ini index 81849d034d6..c5e292ce4cf 100644 --- a/tests/wpt/metadata/fetch/api/idlharness.any.js.ini +++ b/tests/wpt/metadata/fetch/api/idlharness.any.js.ini @@ -5,12 +5,6 @@ [idlharness.any.html] - [Response interface: new Response() must inherit property "body" with the proper type] - expected: FAIL - - [Response interface: attribute body] - expected: FAIL - [Response interface: operation blob()] expected: FAIL @@ -68,9 +62,6 @@ [Request interface: new Request('about:blank') must inherit property "signal" with the proper type] expected: FAIL - [Request interface: new Request('about:blank') must inherit property "body" with the proper type] - expected: FAIL - [Request interface: operation arrayBuffer()] expected: FAIL @@ -80,9 +71,6 @@ [Response interface: attribute trailer] expected: FAIL - [Request interface: attribute body] - expected: FAIL - [Window interface: calling fetch(RequestInfo, optional RequestInit) on window with too few arguments must throw TypeError] expected: FAIL @@ -91,12 +79,6 @@ [idlharness.any.worker.html] - [Response interface: new Response() must inherit property "body" with the proper type] - expected: FAIL - - [Response interface: attribute body] - expected: FAIL - [Response interface: operation blob()] expected: FAIL @@ -154,9 +136,6 @@ [Request interface: new Request('about:blank') must inherit property "signal" with the proper type] expected: FAIL - [Request interface: new Request('about:blank') must inherit property "body" with the proper type] - expected: FAIL - [Request interface: operation arrayBuffer()] expected: FAIL @@ -166,9 +145,6 @@ [Response interface: attribute trailer] expected: FAIL - [Request interface: attribute body] - expected: FAIL - [WorkerGlobalScope interface: calling fetch(RequestInfo, optional RequestInit) on self with too few arguments must throw TypeError] expected: FAIL diff --git a/tests/wpt/metadata/fetch/api/request/request-bad-port.html.ini b/tests/wpt/metadata/fetch/api/request/request-bad-port.html.ini deleted file mode 100644 index 6143800f34d..00000000000 --- a/tests/wpt/metadata/fetch/api/request/request-bad-port.html.ini +++ /dev/null @@ -1,89 +0,0 @@ -[request-bad-port.html] - expected: TIMEOUT - [Request on bad port 427 should throw TypeError.] - expected: TIMEOUT - - [Request on bad port 465 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 512 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 513 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 514 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 515 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 526 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 530 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 531 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 532 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 540 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 548 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 556 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 563 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 587 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 601 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 636 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 993 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 995 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 2049 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 3659 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 4045 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6000 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6665 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6666 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6667 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6668 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6669 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6697 should throw TypeError.] - expected: NOTRUN - diff --git a/tests/wpt/metadata/fetch/api/request/request-consume-empty.html.ini b/tests/wpt/metadata/fetch/api/request/request-consume-empty.html.ini index df31b559a78..d176dd066d4 100644 --- a/tests/wpt/metadata/fetch/api/request/request-consume-empty.html.ini +++ b/tests/wpt/metadata/fetch/api/request/request-consume-empty.html.ini @@ -1,32 +1,11 @@ [request-consume-empty.html] type: testharness - [Consume request's body as arrayBuffer] - expected: FAIL - [Consume request's body as formData] expected: FAIL - [Consume request's body as text] - expected: FAIL - - [Consume request's body as blob] - expected: FAIL - [Consume request's body as json] expected: FAIL [Consume empty FormData request body as text] expected: FAIL - [Consume request's body as json (error case)] - expected: FAIL - - [Consume request's body as formData with correct multipart type (error case)] - expected: FAIL - - [Consume request's body as formData with correct urlencoded type] - expected: FAIL - - [Consume request's body as formData without correct type (error case)] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/request/request-disturbed.html.ini b/tests/wpt/metadata/fetch/api/request/request-disturbed.html.ini index 15cf180a2a8..2c4ad55e9dd 100644 --- a/tests/wpt/metadata/fetch/api/request/request-disturbed.html.ini +++ b/tests/wpt/metadata/fetch/api/request/request-disturbed.html.ini @@ -1,23 +1,11 @@ [request-disturbed.html] type: testharness - [Check cloning a disturbed request] - expected: FAIL - - [Check creating a new request from a disturbed request] - expected: FAIL - [Input request used for creating new request became disturbed] expected: FAIL [Request construction failure should not set "bodyUsed"] expected: FAIL - [Request without body cannot be disturbed] - expected: FAIL - - [Request's body: initial state] - expected: FAIL - [Input request used for creating new request became disturbed even if body is not used] expected: FAIL diff --git a/tests/wpt/metadata/fetch/api/request/request-init-stream.any.js.ini b/tests/wpt/metadata/fetch/api/request/request-init-stream.any.js.ini index 9548739f4e7..1357491a94c 100644 --- a/tests/wpt/metadata/fetch/api/request/request-init-stream.any.js.ini +++ b/tests/wpt/metadata/fetch/api/request/request-init-stream.any.js.ini @@ -2,18 +2,9 @@ [Constructing a Request with a Request on which read() and releaseLock() are called] expected: FAIL - [Constructing a Request with a stream on which read() and releaseLock() are called] - expected: FAIL - - [Constructing a Request with a stream on which getReader() is called] - expected: FAIL - [Constructing a Request with a Request on which body.getReader() is called] expected: FAIL - [Constructing a Request with a stream on which read() is called] - expected: FAIL - [Constructing a Request with a Request on which body.getReader().read() is called] expected: FAIL @@ -28,18 +19,9 @@ [Constructing a Request with a Request on which read() and releaseLock() are called] expected: FAIL - [Constructing a Request with a stream on which read() and releaseLock() are called] - expected: FAIL - - [Constructing a Request with a stream on which getReader() is called] - expected: FAIL - [Constructing a Request with a Request on which body.getReader() is called] expected: FAIL - [Constructing a Request with a stream on which read() is called] - expected: FAIL - [Constructing a Request with a Request on which body.getReader().read() is called] expected: FAIL diff --git a/tests/wpt/metadata/fetch/api/response/response-cancel-stream.html.ini b/tests/wpt/metadata/fetch/api/response/response-cancel-stream.html.ini index e5bd06a3de3..71c9c15559d 100644 --- a/tests/wpt/metadata/fetch/api/response/response-cancel-stream.html.ini +++ b/tests/wpt/metadata/fetch/api/response/response-cancel-stream.html.ini @@ -1,23 +1,5 @@ [response-cancel-stream.html] type: testharness - [Cancelling a starting blob Response stream] - expected: FAIL - - [Cancelling a loading blob Response stream] - expected: FAIL - - [Cancelling a closed blob Response stream] - expected: FAIL - - [Cancelling a starting Response stream] - expected: FAIL - - [Cancelling a loading Response stream] - expected: FAIL - - [Cancelling a closed Response stream] - expected: FAIL - [Response consume blob and http bodies] expected: FAIL diff --git a/tests/wpt/metadata/fetch/api/response/response-clone.html.ini b/tests/wpt/metadata/fetch/api/response/response-clone.html.ini index e0cfc1152e8..93257723665 100644 --- a/tests/wpt/metadata/fetch/api/response/response-clone.html.ini +++ b/tests/wpt/metadata/fetch/api/response/response-clone.html.ini @@ -1,8 +1,5 @@ [response-clone.html] type: testharness - [Check orginal response's body after cloning] - expected: FAIL - [Check cloned response's body] expected: FAIL @@ -44,3 +41,4 @@ [Check response clone use structureClone for teed ReadableStreams (DataViewchunk)] expected: FAIL + diff --git a/tests/wpt/metadata/fetch/api/response/response-consume-stream.html.ini b/tests/wpt/metadata/fetch/api/response/response-consume-stream.html.ini index ccf31287f00..4961a896740 100644 --- a/tests/wpt/metadata/fetch/api/response/response-consume-stream.html.ini +++ b/tests/wpt/metadata/fetch/api/response/response-consume-stream.html.ini @@ -1,29 +1,8 @@ [response-consume-stream.html] type: testharness - [Read empty text response's body as readableStream] - expected: FAIL - - [Read empty blob response's body as readableStream] - expected: FAIL - - [Read blob response's body as readableStream] - expected: FAIL - - [Read text response's body as readableStream] - expected: FAIL - - [Read array buffer response's body as readableStream] - expected: FAIL - - [Read form data response's body as readableStream] - expected: FAIL - [Getting an error Response stream] expected: FAIL [Getting a redirect Response stream] expected: FAIL - [Read URLSearchParams response's body as readableStream] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/response/response-consume.html.ini b/tests/wpt/metadata/fetch/api/response/response-consume.html.ini index d8f9054d16b..06d0bec7842 100644 --- a/tests/wpt/metadata/fetch/api/response/response-consume.html.ini +++ b/tests/wpt/metadata/fetch/api/response/response-consume.html.ini @@ -1,6 +1,5 @@ [response-consume.html] type: testharness - expected: ERROR [Consume response's body as arrayBuffer] expected: FAIL @@ -34,30 +33,9 @@ [Consume response's body: from FormData to formData] expected: FAIL - [Consume response's body: from stream to blob] - expected: FAIL - - [Consume response's body: from stream to text] - expected: FAIL - - [Consume response's body: from stream to arrayBuffer] - expected: FAIL - - [Consume response's body: from stream to json] - expected: FAIL - [Consume response's body: from stream with correct multipart type to formData] expected: FAIL - [Consume response's body: from stream without correct multipart type to formData (error case)] - expected: FAIL - - [Consume response's body: from stream with correct urlencoded type to formData] - expected: FAIL - - [Consume response's body: from stream without correct urlencoded type to formData (error case)] - expected: FAIL - [Consume response's body: from multipart form data blob to formData] expected: FAIL diff --git a/tests/wpt/metadata/fetch/api/response/response-error-from-stream.html.ini b/tests/wpt/metadata/fetch/api/response/response-error-from-stream.html.ini index 55576d655d7..7ad406d65fa 100644 --- a/tests/wpt/metadata/fetch/api/response/response-error-from-stream.html.ini +++ b/tests/wpt/metadata/fetch/api/response/response-error-from-stream.html.ini @@ -1,8 +1,2 @@ [response-error-from-stream.html] expected: ERROR - [ReadableStreamDefaultReader Promise receives ReadableStream start() Error] - expected: FAIL - - [ReadableStreamDefaultReader Promise receives ReadableStream pull() Error] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/response/response-from-stream.any.js.ini b/tests/wpt/metadata/fetch/api/response/response-from-stream.any.js.ini index ada02e692de..9758e5727c3 100644 --- a/tests/wpt/metadata/fetch/api/response/response-from-stream.any.js.ini +++ b/tests/wpt/metadata/fetch/api/response/response-from-stream.any.js.ini @@ -5,29 +5,11 @@ [response-from-stream.any.html] - [Constructing a Response with a stream on which getReader() is called] - expected: FAIL - - [Constructing a Response with a stream on which read() and releaseLock() are called] - expected: FAIL - - [Constructing a Response with a stream on which read() is called] - expected: FAIL - [response-from-stream.any.serviceworker.html] expected: TIMEOUT [response-from-stream.any.worker.html] - [Constructing a Response with a stream on which getReader() is called] - expected: FAIL - - [Constructing a Response with a stream on which read() and releaseLock() are called] - expected: FAIL - - [Constructing a Response with a stream on which read() is called] - expected: FAIL - [response-from-stream.any.serviceworker.html] expected: ERROR diff --git a/tests/wpt/metadata/fetch/api/response/response-init-002.html.ini b/tests/wpt/metadata/fetch/api/response/response-init-002.html.ini index 3340acba7e7..aaa1f04a0f1 100644 --- a/tests/wpt/metadata/fetch/api/response/response-init-002.html.ini +++ b/tests/wpt/metadata/fetch/api/response/response-init-002.html.ini @@ -1,8 +1,5 @@ [response-init-002.html] type: testharness - [Read Response's body as readableStream] - expected: FAIL - [Testing null Response body] expected: FAIL diff --git a/tests/wpt/metadata/fetch/api/response/response-stream-disturbed-1.html.ini b/tests/wpt/metadata/fetch/api/response/response-stream-disturbed-1.html.ini deleted file mode 100644 index c8f955c526f..00000000000 --- a/tests/wpt/metadata/fetch/api/response/response-stream-disturbed-1.html.ini +++ /dev/null @@ -1,14 +0,0 @@ -[response-stream-disturbed-1.html] - type: testharness - [Getting blob after getting the Response body - not disturbed, not locked] - expected: FAIL - - [Getting text after getting the Response body - not disturbed, not locked] - expected: FAIL - - [Getting json after getting the Response body - not disturbed, not locked] - expected: FAIL - - [Getting arrayBuffer after getting the Response body - not disturbed, not locked] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/response/response-stream-disturbed-2.html.ini b/tests/wpt/metadata/fetch/api/response/response-stream-disturbed-2.html.ini deleted file mode 100644 index b1431ca6e30..00000000000 --- a/tests/wpt/metadata/fetch/api/response/response-stream-disturbed-2.html.ini +++ /dev/null @@ -1,14 +0,0 @@ -[response-stream-disturbed-2.html] - type: testharness - [Getting blob after getting a locked Response body] - expected: FAIL - - [Getting text after getting a locked Response body] - expected: FAIL - - [Getting json after getting a locked Response body] - expected: FAIL - - [Getting arrayBuffer after getting a locked Response body] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/response/response-stream-disturbed-3.html.ini b/tests/wpt/metadata/fetch/api/response/response-stream-disturbed-3.html.ini deleted file mode 100644 index 07c1348b8a2..00000000000 --- a/tests/wpt/metadata/fetch/api/response/response-stream-disturbed-3.html.ini +++ /dev/null @@ -1,14 +0,0 @@ -[response-stream-disturbed-3.html] - type: testharness - [Getting blob after reading the Response body] - expected: FAIL - - [Getting text after reading the Response body] - expected: FAIL - - [Getting json after reading the Response body] - expected: FAIL - - [Getting arrayBuffer after reading the Response body] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/response/response-stream-disturbed-4.html.ini b/tests/wpt/metadata/fetch/api/response/response-stream-disturbed-4.html.ini deleted file mode 100644 index d5092502494..00000000000 --- a/tests/wpt/metadata/fetch/api/response/response-stream-disturbed-4.html.ini +++ /dev/null @@ -1,14 +0,0 @@ -[response-stream-disturbed-4.html] - type: testharness - [Getting blob after cancelling the Response body] - expected: FAIL - - [Getting text after cancelling the Response body] - expected: FAIL - - [Getting json after cancelling the Response body] - expected: FAIL - - [Getting arrayBuffer after cancelling the Response body] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/response/response-stream-disturbed-6.html.ini b/tests/wpt/metadata/fetch/api/response/response-stream-disturbed-6.html.ini deleted file mode 100644 index 5004396f961..00000000000 --- a/tests/wpt/metadata/fetch/api/response/response-stream-disturbed-6.html.ini +++ /dev/null @@ -1,17 +0,0 @@ -[response-stream-disturbed-6.html] - type: testharness - [A non-closed stream on which read() has been called] - expected: FAIL - - [A non-closed stream on which cancel() has been called] - expected: FAIL - - [A closed stream on which read() has been called] - expected: FAIL - - [An errored stream on which read() has been called] - expected: FAIL - - [An errored stream on which cancel() has been called] - expected: FAIL - diff --git a/tests/wpt/metadata/fetch/api/response/response-stream-with-broken-then.any.js.ini b/tests/wpt/metadata/fetch/api/response/response-stream-with-broken-then.any.js.ini index 87ac9fa17ab..52cfd491dfe 100644 --- a/tests/wpt/metadata/fetch/api/response/response-stream-with-broken-then.any.js.ini +++ b/tests/wpt/metadata/fetch/api/response/response-stream-with-broken-then.any.js.ini @@ -1,25 +1,22 @@ [response-stream-with-broken-then.any.html] [Untitled] expected: FAIL + [Inject {done: false, value: bye} via Object.prototype.then.] expected: FAIL + [Inject {done: false, value: undefined} via Object.prototype.then.] expected: FAIL + [Inject undefined via Object.prototype.then.] expected: FAIL + [Inject 8.2 via Object.prototype.then.] expected: FAIL [response-stream-with-broken-then] expected: FAIL - [Attempt to inject {done: false, value: bye} via Object.prototype.then.] - expected: FAIL - [Attempt to inject value: undefined via Object.prototype.then.] - expected: FAIL - [Attempt to inject undefined via Object.prototype.then.] - expected: FAIL - [Attempt to inject 8.2 via Object.prototype.then.] - expected: FAIL + [intercepting arraybuffer to body readable stream conversion via Object.prototype.then should not be possible] expected: FAIL @@ -27,26 +24,22 @@ [response-stream-with-broken-then.any.worker.html] [Untitled] expected: FAIL + [Inject {done: false, value: bye} via Object.prototype.then.] expected: FAIL + [Inject {done: false, value: undefined} via Object.prototype.then.] expected: FAIL + [Inject undefined via Object.prototype.then.] expected: FAIL + [Inject 8.2 via Object.prototype.then.] expected: FAIL - [Attempt to inject {done: false, value: bye} via Object.prototype.then.] - expected: FAIL - [Attempt to inject value: undefined via Object.prototype.then.] - expected: FAIL - [Attempt to inject undefined via Object.prototype.then.] - expected: FAIL - [Attempt to inject 8.2 via Object.prototype.then.] - expected: FAIL + [intercepting arraybuffer to body readable stream conversion via Object.prototype.then should not be possible] expected: FAIL - [response-stream-with-broken-then] expected: FAIL diff --git a/tests/wpt/metadata/fetch/content-type/response.window.js.ini b/tests/wpt/metadata/fetch/content-type/response.window.js.ini index ed08877281b..7221e30f417 100644 --- a/tests/wpt/metadata/fetch/content-type/response.window.js.ini +++ b/tests/wpt/metadata/fetch/content-type/response.window.js.ini @@ -311,28 +311,3 @@ [fetch(): separate response Content-Type: text/plain ] expected: NOTRUN - - [