diff --git a/components/net/blob_loader.rs b/components/net/blob_loader.rs index 1bf931dac32..9bd4eec04c9 100644 --- a/components/net/blob_loader.rs +++ b/components/net/blob_loader.rs @@ -18,12 +18,13 @@ use resource_thread::{send_error, start_sending_sniffed_opt}; use resource_thread::CancellationListener; use std::boxed::FnBox; use std::sync::Arc; +use url::Url; use util::thread::spawn_named; // TODO: Check on GET // https://w3c.github.io/FileAPI/#requestResponseModel -pub fn factory(filemanager: Arc>) +pub fn factory(filemanager: FileManager) -> Box, CancellationListener) + Send> { box move |load_data: LoadData, start_chan, classifier, cancel_listener| { spawn_named(format!("blob loader for {}", load_data.url), move || { @@ -35,7 +36,7 @@ pub fn factory(filemanager: Arc>) fn load_blob (load_data: LoadData, start_chan: LoadConsumer, classifier: Arc, - filemanager: Arc>, + filemanager: FileManager, cancel_listener: CancellationListener) { let (chan, recv) = ipc::channel().unwrap(); if let Ok((id, origin, _fragment)) = parse_blob_url(&load_data.url.clone()) { @@ -119,3 +120,72 @@ fn load_blob send_error(load_data.url.clone(), format_err, start_chan); } } + +/// https://fetch.spec.whatwg.org/#concept-basic-fetch (partial) +// TODO: make async. +pub fn load_blob_sync + (url: Url, + filemanager: FileManager) + -> Result<(Headers, Vec), NetworkError> { + let (id, origin) = match parse_blob_url(&url) { + Ok((id, origin, _fragment)) => (id, origin), + Err(()) => { + let e = format!("Invalid blob URL format {:?}", url); + return Err(NetworkError::Internal(e)); + } + }; + + let (sender, receiver) = ipc::channel().unwrap(); + let check_url_validity = true; + let msg = FileManagerThreadMsg::ReadFile(sender, id, check_url_validity, origin); + let _ = filemanager.handle(msg, None); + + let blob_buf = match receiver.recv().unwrap() { + Ok(ReadFileProgress::Meta(blob_buf)) => blob_buf, + Ok(_) => { + return Err(NetworkError::Internal("Invalid filemanager reply".to_string())); + } + Err(e) => { + return Err(NetworkError::Internal(format!("{:?}", e))); + } + }; + + let content_type: Mime = blob_buf.type_string.parse().unwrap_or(mime!(Text / Plain)); + let charset = content_type.get_param(Attr::Charset); + + let mut headers = Headers::new(); + + if let Some(name) = blob_buf.filename { + let charset = charset.and_then(|c| c.as_str().parse().ok()); + headers.set(ContentDisposition { + disposition: DispositionType::Inline, + parameters: vec![ + DispositionParam::Filename(charset.unwrap_or(Charset::Us_Ascii), + None, name.as_bytes().to_vec()) + ] + }); + } + + // Basic fetch, Step 4. + headers.set(ContentLength(blob_buf.size as u64)); + // Basic fetch, Step 5. + headers.set(ContentType(content_type.clone())); + + let mut bytes = blob_buf.bytes; + loop { + match receiver.recv().unwrap() { + Ok(ReadFileProgress::Partial(ref mut new_bytes)) => { + bytes.append(new_bytes); + } + Ok(ReadFileProgress::EOF) => { + return Ok((headers, bytes)); + } + Ok(_) => { + return Err(NetworkError::Internal("Invalid filemanager reply".to_string())); + } + Err(e) => { + return Err(NetworkError::Internal(format!("{:?}", e))); + } + } + } +} diff --git a/components/net/fetch/methods.rs b/components/net/fetch/methods.rs index e1e66b37f9d..0d5b2d2835e 100644 --- a/components/net/fetch/methods.rs +++ b/components/net/fetch/methods.rs @@ -2,10 +2,12 @@ * 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 blob_loader::load_blob_sync; use connector::create_http_connector; use data_loader::decode; use devtools_traits::DevtoolsControlMsg; use fetch::cors_cache::CORSCache; +use filemanager_thread::{FileManager, UIProvider}; use http_loader::{HttpState, set_default_accept_encoding, set_request_cookies}; use http_loader::{NetworkHttpRequestFactory, ReadResult, StreamedResponse, obtain_response, read_block}; use http_loader::{auth_from_cache, determine_request_referrer}; @@ -50,23 +52,28 @@ enum Data { Done, } -pub struct FetchContext { +pub struct FetchContext { pub state: HttpState, pub user_agent: Cow<'static, str>, pub devtools_chan: Option>, + pub filemanager: FileManager, } type DoneChannel = Option<(Sender, Receiver)>; /// [Fetch](https://fetch.spec.whatwg.org#concept-fetch) -pub fn fetch(request: Rc, target: &mut Target, context: FetchContext) -> Response { +pub fn fetch(request: Rc, + target: &mut Target, + context: FetchContext) + -> Response { fetch_with_cors_cache(request, &mut CORSCache::new(), target, context) } -pub fn fetch_with_cors_cache(request: Rc, - cache: &mut CORSCache, - target: &mut Target, - context: FetchContext) -> Response { +pub fn fetch_with_cors_cache(request: Rc, + cache: &mut CORSCache, + target: &mut Target, + context: FetchContext) + -> Response { // Step 1 if request.window.get() == Window::Client { // TODO: Set window to request's client object if client is a Window object @@ -131,9 +138,14 @@ pub fn fetch_with_cors_cache(request: Rc, } /// [Main fetch](https://fetch.spec.whatwg.org/#concept-main-fetch) -fn main_fetch(request: Rc, cache: &mut CORSCache, cors_flag: bool, - recursive_flag: bool, target: &mut Target, done_chan: &mut DoneChannel, - context: &FetchContext) -> Response { +fn main_fetch(request: Rc, + cache: &mut CORSCache, + cors_flag: bool, + recursive_flag: bool, + target: &mut Target, + done_chan: &mut DoneChannel, + context: &FetchContext) + -> Response { // TODO: Implement main fetch spec // Step 1 @@ -389,9 +401,12 @@ fn main_fetch(request: Rc, cache: &mut CORSCache, cors_flag: bool, } /// [Basic fetch](https://fetch.spec.whatwg.org#basic-fetch) -fn basic_fetch(request: Rc, cache: &mut CORSCache, - target: &mut Target, done_chan: &mut DoneChannel, - context: &FetchContext) -> Response { +fn basic_fetch(request: Rc, + cache: &mut CORSCache, + target: &mut Target, + done_chan: &mut DoneChannel, + context: &FetchContext) + -> Response { let url = request.current_url(); match url.scheme() { @@ -450,7 +465,29 @@ fn basic_fetch(request: Rc, cache: &mut CORSCache, } }, - "blob" | "ftp" => { + "blob" => { + println!("Loading blob {}", url.as_str()); + // Step 2. + if *request.method.borrow() != Method::Get { + return Response::network_error(); + } + + match load_blob_sync(url.clone(), context.filemanager.clone()) { + Ok((headers, bytes)) => { + let mut response = Response::new(); + response.url = Some(url.clone()); + response.headers = headers; + *response.body.lock().unwrap() = ResponseBody::Done(bytes); + response + }, + Err(e) => { + debug!("Failed to load {}: {:?}", url, e); + Response::network_error() + }, + } + }, + + "ftp" => { // XXXManishearth handle these panic!("Unimplemented scheme for Fetch") }, @@ -460,14 +497,15 @@ fn basic_fetch(request: Rc, cache: &mut CORSCache, } /// [HTTP fetch](https://fetch.spec.whatwg.org#http-fetch) -fn http_fetch(request: Rc, - cache: &mut CORSCache, - cors_flag: bool, - cors_preflight_flag: bool, - authentication_fetch_flag: bool, - target: &mut Target, - done_chan: &mut DoneChannel, - context: &FetchContext) -> Response { +fn http_fetch(request: Rc, + cache: &mut CORSCache, + cors_flag: bool, + cors_preflight_flag: bool, + authentication_fetch_flag: bool, + target: &mut Target, + done_chan: &mut DoneChannel, + context: &FetchContext) + -> Response { // This is a new async fetch, reset the channel we are waiting on *done_chan = None; // Step 1 @@ -631,13 +669,14 @@ fn http_fetch(request: Rc, } /// [HTTP redirect fetch](https://fetch.spec.whatwg.org#http-redirect-fetch) -fn http_redirect_fetch(request: Rc, - cache: &mut CORSCache, - response: Rc, - cors_flag: bool, - target: &mut Target, - done_chan: &mut DoneChannel, - context: &FetchContext) -> Response { +fn http_redirect_fetch(request: Rc, + cache: &mut CORSCache, + response: Rc, + cors_flag: bool, + target: &mut Target, + done_chan: &mut DoneChannel, + context: &FetchContext) + -> Response { // Step 1 assert_eq!(response.return_internal.get(), true); @@ -711,11 +750,12 @@ fn http_redirect_fetch(request: Rc, } /// [HTTP network or cache fetch](https://fetch.spec.whatwg.org#http-network-or-cache-fetch) -fn http_network_or_cache_fetch(request: Rc, - credentials_flag: bool, - authentication_fetch_flag: bool, - done_chan: &mut DoneChannel, - context: &FetchContext) -> Response { +fn http_network_or_cache_fetch(request: Rc, + credentials_flag: bool, + authentication_fetch_flag: bool, + done_chan: &mut DoneChannel, + context: &FetchContext) + -> Response { // TODO: Implement Window enum for Request let request_has_no_window = true; @@ -1108,8 +1148,10 @@ fn http_network_fetch(request: Rc, } /// [CORS preflight fetch](https://fetch.spec.whatwg.org#cors-preflight-fetch) -fn cors_preflight_fetch(request: Rc, cache: &mut CORSCache, - context: &FetchContext) -> Response { +fn cors_preflight_fetch(request: Rc, + cache: &mut CORSCache, + context: &FetchContext) + -> Response { // Step 1 let mut preflight = Request::new(request.current_url(), Some(request.origin.borrow().clone()), request.is_service_worker_global_scope, request.pipeline_id.get()); diff --git a/components/net/filemanager_thread.rs b/components/net/filemanager_thread.rs index ac8ad70d1bc..5b520b9e31d 100644 --- a/components/net/filemanager_thread.rs +++ b/components/net/filemanager_thread.rs @@ -116,6 +116,15 @@ pub struct FileManager { store: Arc>, } +// Not derived to avoid an unnecessary `UI: Clone` bound. +impl Clone for FileManager { + fn clone(&self) -> Self { + FileManager { + store: self.store.clone(), + } + } +} + impl FileManager { pub fn new(ui: &'static UI) -> FileManager { FileManager { @@ -139,7 +148,7 @@ impl FileManager { } FileManagerThreadMsg::ReadFile(sender, id, check_url_validity, origin) => { spawn_named("read file".to_owned(), move || { - if let Err(e) = store.try_read_file(sender.clone(), id, check_url_validity, + if let Err(e) = store.try_read_file(&sender, id, check_url_validity, origin, cancel_listener) { let _ = sender.send(Err(FileManagerThreadError::BlobURLStoreError(e))); } @@ -151,24 +160,16 @@ impl FileManager { }) } FileManagerThreadMsg::AddSlicedURLEntry(id, rel_pos, sender, origin) =>{ - spawn_named("add sliced URL entry".to_owned(), move || { - store.add_sliced_url_entry(id, rel_pos, sender, origin); - }) + store.add_sliced_url_entry(id, rel_pos, sender, origin); } FileManagerThreadMsg::DecRef(id, origin, sender) => { - spawn_named("dec ref".to_owned(), move || { - let _ = sender.send(store.dec_ref(&id, &origin)); - }) + let _ = sender.send(store.dec_ref(&id, &origin)); } FileManagerThreadMsg::RevokeBlobURL(id, origin, sender) => { - spawn_named("revoke blob url".to_owned(), move || { - let _ = sender.send(store.set_blob_url_validity(false, &id, &origin)); - }) + let _ = sender.send(store.set_blob_url_validity(false, &id, &origin)); } FileManagerThreadMsg::ActivateBlobURL(id, sender, origin) => { - spawn_named("activate blob url".to_owned(), move || { - let _ = sender.send(store.set_blob_url_validity(true, &id, &origin)); - }); + let _ = sender.send(store.set_blob_url_validity(true, &id, &origin)); } } } @@ -365,7 +366,7 @@ impl FileManagerStore { }) } - fn get_blob_buf(&self, sender: IpcSender>, + fn get_blob_buf(&self, sender: &IpcSender>, id: &Uuid, origin_in: &FileOrigin, rel_pos: RelativePos, check_url_validity: bool, cancel_listener: Option) -> Result<(), BlobURLStoreError> { @@ -428,7 +429,7 @@ impl FileManagerStore { } // Convenient wrapper over get_blob_buf - fn try_read_file(&self, sender: IpcSender>, + fn try_read_file(&self, sender: &IpcSender>, id: Uuid, check_url_validity: bool, origin_in: FileOrigin, cancel_listener: Option) -> Result<(), BlobURLStoreError> { self.get_blob_buf(sender, &id, &origin_in, RelativePos::full_range(), check_url_validity, cancel_listener) @@ -541,7 +542,7 @@ fn select_files_pref_enabled() -> bool { const CHUNK_SIZE: usize = 8192; -fn chunked_read(sender: IpcSender>, +fn chunked_read(sender: &IpcSender>, file: &mut File, size: usize, opt_filename: Option, type_string: String, cancel_listener: Option) { // First chunk diff --git a/components/net/resource_thread.rs b/components/net/resource_thread.rs index 86e382e4bb3..e6606b87b8c 100644 --- a/components/net/resource_thread.rs +++ b/components/net/resource_thread.rs @@ -475,7 +475,7 @@ pub struct CoreResourceManager { devtools_chan: Option>, swmanager_chan: Option>, profiler_chan: ProfilerChan, - filemanager: Arc>, + filemanager: FileManager, cancel_load_map: HashMap>, next_resource_id: ResourceId, } @@ -490,7 +490,7 @@ impl CoreResourceManager { devtools_chan: devtools_channel, swmanager_chan: None, profiler_chan: profiler_chan, - filemanager: Arc::new(FileManager::new(TFD_PROVIDER)), + filemanager: FileManager::new(TFD_PROVIDER), cancel_load_map: HashMap::new(), next_resource_id: ResourceId(0), } @@ -592,6 +592,7 @@ impl CoreResourceManager { }; let ua = self.user_agent.clone(); let dc = self.devtools_chan.clone(); + let filemanager = self.filemanager.clone(); spawn_named(format!("fetch thread for {}", init.url), move || { let request = Request::from_init(init); // XXXManishearth: Check origin against pipeline id (also ensure that the mode is allowed) @@ -599,7 +600,12 @@ impl CoreResourceManager { // todo referrer policy? // todo service worker stuff let mut target = Some(Box::new(sender) as Box); - let context = FetchContext { state: http_state, user_agent: ua, devtools_chan: dc }; + let context = FetchContext { + state: http_state, + user_agent: ua, + devtools_chan: dc, + filemanager: filemanager, + }; fetch(Rc::new(request), &mut target, context); }) } diff --git a/components/script/dom/xmlhttprequest.rs b/components/script/dom/xmlhttprequest.rs index 39cb2f3a956..a9a2b0cb476 100644 --- a/components/script/dom/xmlhttprequest.rs +++ b/components/script/dom/xmlhttprequest.rs @@ -366,7 +366,7 @@ impl XMLHttpRequestMethods for XMLHttpRequest { // Step 11 - abort existing requests self.terminate_ongoing_fetch(); - // TODO(izgzhen): In the WPT test: FileAPI/blob/Blob-XHR-revoke.html, + // FIXME(#13767): In the WPT test: FileAPI/blob/Blob-XHR-revoke.html, // the xhr.open(url) is expected to hold a reference to the URL, // thus renders following revocations invalid. Though we won't // implement this for now, if ever needed, we should check blob diff --git a/tests/unit/net/fetch.rs b/tests/unit/net/fetch.rs index 7f368516656..60f6456ed0a 100644 --- a/tests/unit/net/fetch.rs +++ b/tests/unit/net/fetch.rs @@ -5,6 +5,7 @@ use devtools_traits::DevtoolsControlMsg; use devtools_traits::HttpRequest as DevtoolsHttpRequest; use devtools_traits::HttpResponse as DevtoolsHttpResponse; +use filemanager_thread::{TestProvider, TEST_PROVIDER}; use http_loader::{expect_devtools_http_request, expect_devtools_http_response}; use hyper::LanguageTag; use hyper::header::{Accept, AccessControlAllowCredentials, AccessControlAllowHeaders, AccessControlAllowOrigin}; @@ -22,6 +23,7 @@ use hyper::uri::RequestUri; use msg::constellation_msg::{ReferrerPolicy, TEST_PIPELINE_ID}; use net::fetch::cors_cache::CORSCache; use net::fetch::methods::{FetchContext, fetch, fetch_with_cors_cache}; +use net::filemanager_thread::FileManager; use net::http_loader::HttpState; use net_traits::FetchTaskTarget; use net_traits::request::{Origin, RedirectMode, Referrer, Request, RequestMode}; @@ -46,11 +48,12 @@ struct FetchResponseCollector { sender: Sender, } -fn new_fetch_context(dc: Option>) -> FetchContext { +fn new_fetch_context(dc: Option>) -> FetchContext { FetchContext { state: HttpState::new(), user_agent: DEFAULT_USER_AGENT.into(), devtools_chan: dc, + filemanager: FileManager::new(TEST_PROVIDER), } } impl FetchTaskTarget for FetchResponseCollector { @@ -164,6 +167,48 @@ fn test_fetch_data() { } } +#[test] +fn test_fetch_blob() { + use ipc_channel::ipc; + use net_traits::blob_url_store::BlobBuf; + use net_traits::filemanager_thread::FileManagerThreadMsg; + + let context = new_fetch_context(None); + + let bytes = b"content"; + let blob_buf = BlobBuf { + filename: Some("test.txt".into()), + type_string: "text/plain".into(), + size: bytes.len() as u64, + bytes: bytes.to_vec(), + }; + + let origin = Url::parse("http://www.example.org/").unwrap(); + + let (sender, receiver) = ipc::channel().unwrap(); + let message = FileManagerThreadMsg::PromoteMemory(blob_buf, true, sender, "http://www.example.org".into()); + context.filemanager.handle(message, None); + let id = receiver.recv().unwrap().unwrap(); + let url = Url::parse(&format!("blob:{}{}", origin.as_str(), id.simple())).unwrap(); + + + let request = Request::new(url, Some(Origin::Origin(origin.origin())), false, None); + let fetch_response = fetch(Rc::new(request), &mut None, context); + + assert!(!fetch_response.is_network_error()); + + assert_eq!(fetch_response.headers.len(), 2); + + let content_type: &ContentType = fetch_response.headers.get().unwrap(); + assert_eq!(**content_type, Mime(TopLevel::Text, SubLevel::Plain, vec![])); + + let content_length: &ContentLength = fetch_response.headers.get().unwrap(); + assert_eq!(**content_length, bytes.len() as u64); + + assert_eq!(*fetch_response.body.lock().unwrap(), + ResponseBody::Done(bytes.to_vec())); +} + #[test] fn test_fetch_file() { let mut path = resources_dir_path().expect("Cannot find resource dir"); diff --git a/tests/unit/net/filemanager_thread.rs b/tests/unit/net/filemanager_thread.rs index 7266157481a..75facb0b6aa 100644 --- a/tests/unit/net/filemanager_thread.rs +++ b/tests/unit/net/filemanager_thread.rs @@ -10,9 +10,9 @@ use std::fs::File; use std::io::Read; use std::path::PathBuf; -const TEST_PROVIDER: &'static TestProvider = &TestProvider; +pub const TEST_PROVIDER: &'static TestProvider = &TestProvider; -struct TestProvider; +pub struct TestProvider; impl UIProvider for TestProvider { fn open_file_dialog(&self, _path: &str, _patterns: Vec) -> Option { diff --git a/tests/wpt/metadata/FileAPI/blob/Blob-XHR-revoke.html.ini b/tests/wpt/metadata/FileAPI/blob/Blob-XHR-revoke.html.ini index 6fb7d0ba5ca..75e645a94c9 100644 --- a/tests/wpt/metadata/FileAPI/blob/Blob-XHR-revoke.html.ini +++ b/tests/wpt/metadata/FileAPI/blob/Blob-XHR-revoke.html.ini @@ -1,4 +1,6 @@ [Blob-XHR-revoke.html] type: testharness - expected: CRASH - bug: https://github.com/servo/servo/issues/10539 + bug: https://github.com/servo/servo/issues/13767 + [Revoking blob URL used with XMLHttpRequest] + expected: FAIL + diff --git a/tests/wpt/metadata/fetch/api/basic/scheme-blob-worker.html.ini b/tests/wpt/metadata/fetch/api/basic/scheme-blob-worker.html.ini index 74240439891..7c2d1d343b3 100644 --- a/tests/wpt/metadata/fetch/api/basic/scheme-blob-worker.html.ini +++ b/tests/wpt/metadata/fetch/api/basic/scheme-blob-worker.html.ini @@ -1,3 +1,6 @@ [scheme-blob-worker.html] type: testharness - expected: CRASH + [Fetching [GET\] URL.createObjectURL(blob) is OK] + bug: https://github.com/servo/servo/issues/13766 + expected: FAIL + diff --git a/tests/wpt/metadata/fetch/api/basic/scheme-blob.html.ini b/tests/wpt/metadata/fetch/api/basic/scheme-blob.html.ini index ef3371f4e38..599e25f7f0b 100644 --- a/tests/wpt/metadata/fetch/api/basic/scheme-blob.html.ini +++ b/tests/wpt/metadata/fetch/api/basic/scheme-blob.html.ini @@ -1,3 +1,6 @@ [scheme-blob.html] type: testharness - expected: CRASH + [Fetching [GET\] URL.createObjectURL(blob) is OK] + bug: https://github.com/servo/servo/issues/13766 + expected: FAIL + diff --git a/tests/wpt/metadata/webgl/conformance-1.0.3/conformance/more/glsl/uniformOutOfBounds.html.ini b/tests/wpt/metadata/webgl/conformance-1.0.3/conformance/more/glsl/uniformOutOfBounds.html.ini index a4c47444af4..c2797480f56 100644 --- a/tests/wpt/metadata/webgl/conformance-1.0.3/conformance/more/glsl/uniformOutOfBounds.html.ini +++ b/tests/wpt/metadata/webgl/conformance-1.0.3/conformance/more/glsl/uniformOutOfBounds.html.ini @@ -1,2 +1,3 @@ [uniformOutOfBounds.html] + type: testharness disabled: https://github.com/servo/servo/issues/13662