diff options
author | Jack Moffitt <jack@metajack.im> | 2014-08-28 09:34:23 -0600 |
---|---|---|
committer | Jack Moffitt <jack@metajack.im> | 2014-09-08 20:21:42 -0600 |
commit | c6ab60dbfc6da7b4f800c9e40893c8b58413960c (patch) | |
tree | d1d74076cf7fa20e4f77ec7cb82cae98b67362cb /components/net | |
parent | db2f642c32fc5bed445bb6f2e45b0f6f0b4342cf (diff) | |
download | servo-c6ab60dbfc6da7b4f800c9e40893c8b58413960c.tar.gz servo-c6ab60dbfc6da7b4f800c9e40893c8b58413960c.zip |
Cargoify servo
Diffstat (limited to 'components/net')
-rw-r--r-- | components/net/Cargo.toml | 27 | ||||
-rw-r--r-- | components/net/data_loader.rs | 154 | ||||
-rw-r--r-- | components/net/fetch/cors_cache.rs | 316 | ||||
-rw-r--r-- | components/net/fetch/request.rs | 149 | ||||
-rw-r--r-- | components/net/fetch/response.rs | 144 | ||||
-rw-r--r-- | components/net/file_loader.rs | 50 | ||||
-rw-r--r-- | components/net/http_loader.rs | 167 | ||||
-rw-r--r-- | components/net/image/base.rs | 67 | ||||
-rw-r--r-- | components/net/image/holder.rs | 109 | ||||
-rw-r--r-- | components/net/image/test.jpeg | bin | 0 -> 4962 bytes | |||
-rw-r--r-- | components/net/image_cache_task.rs | 993 | ||||
-rw-r--r-- | components/net/lib.rs | 44 | ||||
-rw-r--r-- | components/net/local_image_cache.rs | 166 | ||||
-rw-r--r-- | components/net/resource_task.rs | 267 |
14 files changed, 2653 insertions, 0 deletions
diff --git a/components/net/Cargo.toml b/components/net/Cargo.toml new file mode 100644 index 00000000000..17560f4aa5c --- /dev/null +++ b/components/net/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "net" +version = "0.0.1" +authors = ["The Servo Project Developers"] + +[lib] +name = "net" +path = "lib.rs" + +[dependencies.util] +path = "../util" + +[dependencies.geom] +git = "https://github.com/servo/rust-geom" + +[dependencies.http] +git = "https://github.com/servo/rust-http" +branch = "servo" + +[dependencies.png] +git = "https://github.com/servo/rust-png" + +[dependencies.stb_image] +git = "https://github.com/servo/rust-stb-image" + +[dependencies.url] +git = "https://github.com/servo/rust-url" diff --git a/components/net/data_loader.rs b/components/net/data_loader.rs new file mode 100644 index 00000000000..5d9fb776674 --- /dev/null +++ b/components/net/data_loader.rs @@ -0,0 +1,154 @@ +/* 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 std::str; + +use resource_task::{Done, Payload, Metadata, LoadData, LoadResponse, LoaderTask, start_sending}; + +use serialize::base64::FromBase64; + +use http::headers::test_utils::from_stream_with_str; +use http::headers::content_type::MediaType; +use url::{percent_decode, NonRelativeSchemeData}; + + +pub fn factory() -> LoaderTask { + proc(url, start_chan) { + // NB: we don't spawn a new task. + // Hypothesis: data URLs are too small for parallel base64 etc. to be worth it. + // Should be tested at some point. + load(url, start_chan) + } +} + +fn load(load_data: LoadData, start_chan: Sender<LoadResponse>) { + let url = load_data.url; + assert!("data" == url.scheme.as_slice()); + + let mut metadata = Metadata::default(url.clone()); + + // Split out content type and data. + let mut scheme_data = match url.scheme_data { + NonRelativeSchemeData(scheme_data) => scheme_data, + _ => fail!("Expected a non-relative scheme URL.") + }; + match url.query { + Some(query) => { + scheme_data.push_str("?"); + scheme_data.push_str(query.as_slice()); + }, + None => () + } + let parts: Vec<&str> = scheme_data.as_slice().splitn(',', 1).collect(); + if parts.len() != 2 { + start_sending(start_chan, metadata).send(Done(Err("invalid data uri".to_string()))); + return; + } + + // ";base64" must come at the end of the content type, per RFC 2397. + // rust-http will fail to parse it because there's no =value part. + let mut is_base64 = false; + let mut ct_str = parts[0]; + if ct_str.ends_with(";base64") { + is_base64 = true; + ct_str = ct_str.slice_to(ct_str.as_bytes().len() - 7); + } + + // Parse the content type using rust-http. + // FIXME: this can go into an infinite loop! (rust-http #25) + let content_type: Option<MediaType> = from_stream_with_str(ct_str); + metadata.set_content_type(&content_type); + + let progress_chan = start_sending(start_chan, metadata); + let bytes = percent_decode(parts[1].as_bytes()); + + if is_base64 { + // FIXME(#2909): It’s unclear what to do with non-alphabet characters, + // but Acid 3 apparently depends on spaces being ignored. + let bytes = bytes.move_iter().filter(|&b| b != ' ' as u8).collect::<Vec<u8>>(); + // FIXME(#2877): use bytes.as_slice().from_base64() when we upgrade to a Rust version + // that includes https://github.com/rust-lang/rust/pull/15810 + let fake_utf8 = unsafe { str::raw::from_utf8(bytes.as_slice()) }; + match fake_utf8.from_base64() { + Err(..) => { + progress_chan.send(Done(Err("non-base64 data uri".to_string()))); + } + Ok(data) => { + progress_chan.send(Payload(data)); + progress_chan.send(Done(Ok(()))); + } + } + } else { + progress_chan.send(Payload(bytes)); + progress_chan.send(Done(Ok(()))); + } +} + +#[cfg(test)] +fn assert_parse(url: &'static str, + content_type: Option<(String, String)>, + charset: Option<String>, + data: Option<Vec<u8>>) { + use std::comm; + use url::Url; + + let (start_chan, start_port) = comm::channel(); + load(LoadData::new(Url::parse(url).unwrap()), start_chan); + + let response = start_port.recv(); + assert_eq!(&response.metadata.content_type, &content_type); + assert_eq!(&response.metadata.charset, &charset); + + let progress = response.progress_port.recv(); + + match data { + None => { + assert_eq!(progress, Done(Err("invalid data uri".to_string()))); + } + Some(dat) => { + assert_eq!(progress, Payload(dat)); + assert_eq!(response.progress_port.recv(), Done(Ok(()))); + } + } +} + +#[test] +fn empty_invalid() { + assert_parse("data:", None, None, None); +} + +#[test] +fn plain() { + assert_parse("data:,hello%20world", None, None, Some(b"hello world".iter().map(|&x| x).collect())); +} + +#[test] +fn plain_ct() { + assert_parse("data:text/plain,hello", + Some(("text".to_string(), "plain".to_string())), None, Some(b"hello".iter().map(|&x| x).collect())); +} + +#[test] +fn plain_charset() { + assert_parse("data:text/plain;charset=latin1,hello", + Some(("text".to_string(), "plain".to_string())), Some("latin1".to_string()), Some(b"hello".iter().map(|&x| x).collect())); +} + +#[test] +fn base64() { + assert_parse("data:;base64,C62+7w==", None, None, Some(vec!(0x0B, 0xAD, 0xBE, 0xEF))); +} + +#[test] +fn base64_ct() { + assert_parse("data:application/octet-stream;base64,C62+7w==", + Some(("application".to_string(), "octet-stream".to_string())), None, Some(vec!(0x0B, 0xAD, 0xBE, 0xEF))); +} + +#[test] +fn base64_charset() { + assert_parse("data:text/plain;charset=koi8-r;base64,8PLl9+XkIO3l5Pfl5A==", + Some(("text".to_string(), "plain".to_string())), Some("koi8-r".to_string()), + Some(vec!(0xF0, 0xF2, 0xE5, 0xF7, 0xE5, 0xE4, 0x20, 0xED, 0xE5, 0xE4, 0xF7, 0xE5, 0xE4))); +} diff --git a/components/net/fetch/cors_cache.rs b/components/net/fetch/cors_cache.rs new file mode 100644 index 00000000000..fb6676e8064 --- /dev/null +++ b/components/net/fetch/cors_cache.rs @@ -0,0 +1,316 @@ +/* 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/. */ + +//! An implementation of the [CORS preflight cache](http://fetch.spec.whatwg.org/#cors-preflight-cache) +//! For now this library is XHR-specific. +//! For stuff involving `<img>`, `<iframe>`, `<form>`, etc please check what +//! the request mode should be and compare with the fetch spec +//! This library will eventually become the core of the Fetch crate +//! with CORSRequest being expanded into FetchRequest (etc) + +use http::method::Method; +use std::ascii::StrAsciiExt; +use std::comm::{Sender, Receiver, channel}; +use time; +use time::{now, Timespec}; +use url::Url; + +/// Union type for CORS cache entries +/// +/// Each entry might pertain to a header or method +#[deriving(Clone)] +pub enum HeaderOrMethod { + HeaderData(String), + MethodData(Method) +} + +impl HeaderOrMethod { + fn match_header(&self, header_name: &str) -> bool { + match *self { + HeaderData(ref s) => s.as_slice().eq_ignore_ascii_case(header_name), + _ => false + } + } + + fn match_method(&self, method: &Method) -> bool { + match *self { + MethodData(ref m) => m == method, + _ => false + } + } +} + +/// An entry in the CORS cache +#[deriving(Clone)] +pub struct CORSCacheEntry { + pub origin: Url, + pub url: Url, + pub max_age: uint, + pub credentials: bool, + pub header_or_method: HeaderOrMethod, + created: Timespec +} + +impl CORSCacheEntry { + fn new (origin:Url, url: Url, max_age: uint, credentials: bool, header_or_method: HeaderOrMethod) -> CORSCacheEntry { + CORSCacheEntry { + origin: origin, + url: url, + max_age: max_age, + credentials: credentials, + header_or_method: header_or_method, + created: time::now().to_timespec() + } + } +} + +/// Properties of Request required to cache match. +pub struct CacheRequestDetails { + pub origin: Url, + pub destination: Url, + pub credentials: bool +} + +/// Trait for a generic CORS Cache +pub trait CORSCache { + /// [Clear the cache](http://fetch.spec.whatwg.org/#concept-cache-clear) + fn clear (&mut self, request: CacheRequestDetails); + + /// Remove old entries + fn cleanup(&mut self); + + /// Returns true if an entry with a [matching header](http://fetch.spec.whatwg.org/#concept-cache-match-header) is found + fn match_header(&mut self, request: CacheRequestDetails, header_name: &str) -> bool; + + /// Updates max age if an entry for a [matching header](http://fetch.spec.whatwg.org/#concept-cache-match-header) is found. + /// + /// If not, it will insert an equivalent entry + fn match_header_and_update(&mut self, request: CacheRequestDetails, header_name: &str, new_max_age: uint) -> bool; + + /// Returns true if an entry with a [matching method](http://fetch.spec.whatwg.org/#concept-cache-match-method) is found + fn match_method(&mut self, request: CacheRequestDetails, method: Method) -> bool; + + /// Updates max age if an entry for [a matching method](http://fetch.spec.whatwg.org/#concept-cache-match-method) is found. + /// + /// If not, it will insert an equivalent entry + fn match_method_and_update(&mut self, request: CacheRequestDetails, method: Method, new_max_age: uint) -> bool; + /// Insert an entry + fn insert(&mut self, entry: CORSCacheEntry); +} + +/// A simple, vector-based CORS Cache +#[deriving(Clone)] +#[unstable = "This might later be replaced with a HashMap-like entity, though that requires a separate Origin struct"] +pub struct BasicCORSCache(Vec<CORSCacheEntry>); + +impl BasicCORSCache { + fn find_entry_by_header<'a>(&'a mut self, request: &CacheRequestDetails, header_name: &str) -> Option<&'a mut CORSCacheEntry> { + self.cleanup(); + let BasicCORSCache(ref mut buf) = *self; + let entry = buf.mut_iter().find(|e| e.origin.scheme == request.origin.scheme && + e.origin.host() == request.origin.host() && + e.origin.port() == request.origin.port() && + e.url == request.destination && + e.credentials == request.credentials && + e.header_or_method.match_header(header_name)); + entry + } + + fn find_entry_by_method<'a>(&'a mut self, request: &CacheRequestDetails, method: Method) -> Option<&'a mut CORSCacheEntry> { + // we can take the method from CORSRequest itself + self.cleanup(); + let BasicCORSCache(ref mut buf) = *self; + let entry = buf.mut_iter().find(|e| e.origin.scheme == request.origin.scheme && + e.origin.host() == request.origin.host() && + e.origin.port() == request.origin.port() && + e.url == request.destination && + e.credentials == request.credentials && + e.header_or_method.match_method(&method)); + entry + } +} + +impl CORSCache for BasicCORSCache { + /// http://fetch.spec.whatwg.org/#concept-cache-clear + #[allow(dead_code)] + fn clear (&mut self, request: CacheRequestDetails) { + let BasicCORSCache(buf) = self.clone(); + let new_buf: Vec<CORSCacheEntry> = buf.move_iter().filter(|e| e.origin == request.origin && request.destination == e.url).collect(); + *self = BasicCORSCache(new_buf); + } + + // Remove old entries + fn cleanup(&mut self) { + let BasicCORSCache(buf) = self.clone(); + let now = time::now().to_timespec(); + let new_buf: Vec<CORSCacheEntry> = buf.move_iter().filter(|e| now.sec > e.created.sec + e.max_age as i64).collect(); + *self = BasicCORSCache(new_buf); + } + + fn match_header(&mut self, request: CacheRequestDetails, header_name: &str) -> bool { + self.find_entry_by_header(&request, header_name).is_some() + } + + fn match_header_and_update(&mut self, request: CacheRequestDetails, header_name: &str, new_max_age: uint) -> bool { + match self.find_entry_by_header(&request, header_name).map(|e| e.max_age = new_max_age) { + Some(_) => true, + None => { + self.insert(CORSCacheEntry::new(request.origin, request.destination, new_max_age, + request.credentials, HeaderData(header_name.to_string()))); + false + } + } + } + + fn match_method(&mut self, request: CacheRequestDetails, method: Method) -> bool { + self.find_entry_by_method(&request, method).is_some() + } + + fn match_method_and_update(&mut self, request: CacheRequestDetails, method: Method, new_max_age: uint) -> bool { + match self.find_entry_by_method(&request, method.clone()).map(|e| e.max_age = new_max_age) { + Some(_) => true, + None => { + self.insert(CORSCacheEntry::new(request.origin, request.destination, new_max_age, + request.credentials, MethodData(method))); + false + } + } + } + + fn insert(&mut self, entry: CORSCacheEntry) { + self.cleanup(); + let BasicCORSCache(ref mut buf) = *self; + buf.push(entry); + } +} + +/// Various messages that can be sent to a CORSCacheTask +pub enum CORSCacheTaskMsg { + Clear(CacheRequestDetails, Sender<()>), + Cleanup(Sender<()>), + MatchHeader(CacheRequestDetails, String, Sender<bool>), + MatchHeaderUpdate(CacheRequestDetails, String, uint, Sender<bool>), + MatchMethod(CacheRequestDetails, Method, Sender<bool>), + MatchMethodUpdate(CacheRequestDetails, Method, uint, Sender<bool>), + Insert(CORSCacheEntry, Sender<()>), + ExitMsg +} + +/// A Sender to a CORSCacheTask +/// +/// This can be used as a CORS Cache. +/// The methods on this type block until they can run, and it behaves similar to a mutex +pub type CORSCacheSender = Sender<CORSCacheTaskMsg>; + +impl CORSCache for CORSCacheSender { + fn clear (&mut self, request: CacheRequestDetails) { + let (tx, rx) = channel(); + self.send(Clear(request, tx)); + let _ = rx.recv_opt(); + } + + fn cleanup(&mut self) { + let (tx, rx) = channel(); + self.send(Cleanup(tx)); + let _ = rx.recv_opt(); + } + + fn match_header(&mut self, request: CacheRequestDetails, header_name: &str) -> bool { + let (tx, rx) = channel(); + self.send(MatchHeader(request, header_name.to_string(), tx)); + rx.recv_opt().unwrap_or(false) + } + + fn match_header_and_update(&mut self, request: CacheRequestDetails, header_name: &str, new_max_age: uint) -> bool { + let (tx, rx) = channel(); + self.send(MatchHeaderUpdate(request, header_name.to_string(), new_max_age, tx)); + rx.recv_opt().unwrap_or(false) + } + + fn match_method(&mut self, request: CacheRequestDetails, method: Method) -> bool { + let (tx, rx) = channel(); + self.send(MatchMethod(request, method, tx)); + rx.recv_opt().unwrap_or(false) + } + + fn match_method_and_update(&mut self, request: CacheRequestDetails, method: Method, new_max_age: uint) -> bool { + let (tx, rx) = channel(); + self.send(MatchMethodUpdate(request, method, new_max_age, tx)); + rx.recv_opt().unwrap_or(false) + } + + fn insert(&mut self, entry: CORSCacheEntry) { + let (tx, rx) = channel(); + self.send(Insert(entry, tx)); + let _ = rx.recv_opt(); + } +} + +/// A simple task-based CORS Cache that can be sent messages +/// +/// #Example +/// ``` +/// let task = CORSCacheTask::new(); +/// let builder = TaskBuilder::new().named("XHRTask"); +/// let mut sender = task.get_sender(); +/// builder.spawn(proc() { task.run() }); +/// sender.insert(CORSCacheEntry::new(/* parameters here */)); +/// ``` +pub struct CORSCacheTask { + receiver: Receiver<CORSCacheTaskMsg>, + cache: BasicCORSCache, + sender: CORSCacheSender +} + +impl CORSCacheTask { + pub fn new() -> CORSCacheTask { + let (tx, rx) = channel(); + CORSCacheTask { + receiver: rx, + cache: BasicCORSCache(vec![]), + sender: tx + } + } + + /// Provides a sender to the cache task + pub fn get_sender(&self) -> CORSCacheSender { + self.sender.clone() + } + + /// Runs the cache task + /// This blocks the current task, so it is advised + /// to spawn a new task for this + /// Send ExitMsg to the associated Sender to exit + pub fn run(&mut self) { + loop { + match self.receiver.recv() { + Clear(request, tx) => { + self.cache.clear(request); + tx.send(()); + }, + Cleanup(tx) => { + self.cache.cleanup(); + tx.send(()); + }, + MatchHeader(request, header, tx) => { + tx.send(self.cache.match_header(request, header.as_slice())); + }, + MatchHeaderUpdate(request, header, new_max_age, tx) => { + tx.send(self.cache.match_header_and_update(request, header.as_slice(), new_max_age)); + }, + MatchMethod(request, method, tx) => { + tx.send(self.cache.match_method(request, method)); + }, + MatchMethodUpdate(request, method, new_max_age, tx) => { + tx.send(self.cache.match_method_and_update(request, method, new_max_age)); + }, + Insert(entry, tx) => { + self.cache.insert(entry); + tx.send(()); + }, + ExitMsg => break + } + } + } +} diff --git a/components/net/fetch/request.rs b/components/net/fetch/request.rs new file mode 100644 index 00000000000..c14efe9c59e --- /dev/null +++ b/components/net/fetch/request.rs @@ -0,0 +1,149 @@ +/* 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 url::Url; +use http::method::{Get, Method}; +use http::headers::request::HeaderCollection; +use fetch::cors_cache::CORSCache; +use fetch::response::Response; + +/// A [request context](http://fetch.spec.whatwg.org/#concept-request-context) +pub enum Context { + Audio, Beacon, CSPreport, Download, Embed, Eventsource, + Favicon, Fetch, Font, Form, Frame, Hyperlink, IFrame, Image, + ImageSet, Import, Internal, Location, Manifest, Object, Ping, + Plugin, Prefetch, Script, ServiceWorker, SharedWorker, Subresource, + Style, Track, Video, Worker, XMLHttpRequest, XSLT +} + +/// A [request context frame type](http://fetch.spec.whatwg.org/#concept-request-context-frame-type) +pub enum ContextFrameType { + Auxiliary, + TopLevel, + Nested, + ContextNone +} + +/// A [referer](http://fetch.spec.whatwg.org/#concept-request-referrer) +pub enum Referer { + RefererNone, + Client, + RefererUrl(Url) +} + +/// A [request mode](http://fetch.spec.whatwg.org/#concept-request-mode) +pub enum RequestMode { + SameOrigin, + NoCORS, + CORSMode, + ForcedPreflightMode +} + +/// Request [credentials mode](http://fetch.spec.whatwg.org/#concept-request-credentials-mode) +pub enum CredentialsMode { + Omit, + CredentialsSameOrigin, + Include +} + +/// [Response tainting](http://fetch.spec.whatwg.org/#concept-request-response-tainting) +pub enum ResponseTainting { + Basic, + CORSTainting, + Opaque +} + +/// A [Request](http://fetch.spec.whatwg.org/#requests) as defined by the Fetch spec +pub struct Request { + pub method: Method, + pub url: Url, + pub headers: HeaderCollection, + pub unsafe_request: bool, + pub body: Option<Vec<u8>>, + pub preserve_content_codings: bool, + // pub client: GlobalRef, // XXXManishearth copy over only the relevant fields of the global scope, + // not the entire scope to avoid the libscript dependency + pub skip_service_worker: bool, + pub context: Context, + pub context_frame_type: ContextFrameType, + pub origin: Option<Url>, + pub force_origin_header: bool, + pub same_origin_data: bool, + pub referer: Referer, + pub authentication: bool, + pub sync: bool, + pub mode: RequestMode, + pub credentials_mode: CredentialsMode, + pub use_url_credentials: bool, + pub manual_redirect: bool, + pub redirect_count: uint, + pub response_tainting: ResponseTainting, + pub cache: Option<Box<CORSCache>> +} + +impl Request { + pub fn new(url: Url, context: Context) -> Request { + Request { + method: Get, + url: url, + headers: HeaderCollection::new(), + unsafe_request: false, + body: None, + preserve_content_codings: false, + skip_service_worker: false, + context: context, + context_frame_type: ContextNone, + origin: None, + force_origin_header: false, + same_origin_data: false, + referer: Client, + authentication: false, + sync: false, + mode: NoCORS, + credentials_mode: Omit, + use_url_credentials: false, + manual_redirect: false, + redirect_count: 0, + response_tainting: Basic, + cache: None + } + } + + /// [Basic fetch](http://fetch.spec.whatwg.org#basic-fetch) + pub fn basic_fetch(&mut self) -> Response { + match self.url.scheme.as_slice() { + "about" => match self.url.non_relative_scheme_data() { + Some(s) if s.as_slice() == "blank" => { + let mut response = Response::new(); + let _ = response.headers.insert_raw("Content-Type".to_string(), b"text/html;charset=utf-8"); + response + }, + _ => Response::network_error() + }, + "http" | "https" => { + self.http_fetch(false, false, false) + }, + "blob" | "data" | "file" | "ftp" => { + // XXXManishearth handle these + fail!("Unimplemented scheme for Fetch") + }, + + _ => Response::network_error() + } + } + + // [HTTP fetch](http://fetch.spec.whatwg.org#http-fetch) + pub fn http_fetch(&mut self, _cors_flag: bool, cors_preflight_flag: bool, _authentication_fetch_flag: bool) -> Response { + let response = Response::new(); + // TODO: Service worker fetch + // Step 3 + // Substep 1 + self.skip_service_worker = true; + // Substep 2 + if cors_preflight_flag { + // XXXManishearth stuff goes here + } + response + } +} diff --git a/components/net/fetch/response.rs b/components/net/fetch/response.rs new file mode 100644 index 00000000000..359ec6aa394 --- /dev/null +++ b/components/net/fetch/response.rs @@ -0,0 +1,144 @@ +/* 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 url::Url; +use http::status::{Status, UnregisteredStatus}; +use StatusOk = http::status::Ok; +use http::headers::HeaderEnum; +use http::headers::response::HeaderCollection; +use std::ascii::OwnedStrAsciiExt; +use std::comm::Receiver; + +/// [Response type](http://fetch.spec.whatwg.org/#concept-response-type) +#[deriving(Clone, PartialEq)] +pub enum ResponseType { + Basic, + CORS, + Default, + Error, + Opaque +} + +/// [Response termination reason](http://fetch.spec.whatwg.org/#concept-response-termination-reason) +#[deriving(Clone)] +pub enum TerminationReason { + EndUserAbort, + Fatal, + Timeout +} + +/// The response body can still be pushed to after fetch +/// This provides a way to store unfinished response bodies +#[unstable = "I haven't yet decided exactly how the interface for this will be"] +#[deriving(Clone)] +pub enum ResponseBody { + Empty, // XXXManishearth is this necessary, or is Done(vec![]) enough? + Receiving(Vec<u8>), + Done(Vec<u8>), +} + +#[unstable = "I haven't yet decided exactly how the interface for this will be"] +pub enum ResponseMsg { + Chunk(Vec<u8>), + Finished, + Errored +} + +#[unstable = "I haven't yet decided exactly how the interface for this will be"] +pub struct ResponseLoader { + response: Response, + chan: Receiver<ResponseMsg> +} + +/// A [Response](http://fetch.spec.whatwg.org/#concept-response) as defined by the Fetch spec +#[deriving(Clone)] +pub struct Response { + pub response_type: ResponseType, + pub termination_reason: Option<TerminationReason>, + pub url: Option<Url>, + pub status: Status, + pub headers: HeaderCollection, + pub body: ResponseBody, + /// [Internal response](http://fetch.spec.whatwg.org/#concept-internal-response), only used if the Response is a filtered response + pub internal_response: Option<Box<Response>>, +} + +impl Response { + pub fn new() -> Response { + Response { + response_type: Default, + termination_reason: None, + url: None, + status: StatusOk, + headers: HeaderCollection::new(), + body: Empty, + internal_response: None + } + } + + pub fn network_error() -> Response { + Response { + response_type: Error, + termination_reason: None, + url: None, + status: UnregisteredStatus(0, "".to_string()), + headers: HeaderCollection::new(), + body: Empty, + internal_response: None + } + } + + pub fn is_network_error(&self) -> bool { + match self.response_type { + Error => true, + _ => false + } + } + + /// Convert to a filtered response, of type `filter_type`. + /// Do not use with type Error or Default + pub fn to_filtered(self, filter_type: ResponseType) -> Response { + assert!(filter_type != Error); + assert!(filter_type != Default); + if self.is_network_error() { + return self; + } + let old_headers = self.headers.clone(); + let mut response = self.clone(); + response.internal_response = Some(box self); + match filter_type { + Default | Error => unreachable!(), + Basic => { + let mut headers = HeaderCollection::new(); + for h in old_headers.iter() { + match h.header_name().into_ascii_lower().as_slice() { + "set-cookie" | "set-cookie2" => {}, + _ => headers.insert(h) + } + } + response.headers = headers; + response.response_type = filter_type; + }, + CORS => { + let mut headers = HeaderCollection::new(); + for h in old_headers.iter() { + match h.header_name().into_ascii_lower().as_slice() { + "cache-control" | "content-language" | + "content-type" | "expires" | "last-modified" | "Pragma" => {}, + // XXXManishearth handle Access-Control-Expose-Headers + _ => headers.insert(h) + } + } + response.headers = headers; + response.response_type = filter_type; + }, + Opaque => { + response.headers = HeaderCollection::new(); + response.status = UnregisteredStatus(0, "".to_string()); + response.body = Empty; + } + } + response + } +} diff --git a/components/net/file_loader.rs b/components/net/file_loader.rs new file mode 100644 index 00000000000..43c3191c600 --- /dev/null +++ b/components/net/file_loader.rs @@ -0,0 +1,50 @@ +/* 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 resource_task::{ProgressMsg, Metadata, Payload, Done, LoaderTask, start_sending}; + +use std::io; +use std::io::File; +use servo_util::task::spawn_named; + +static READ_SIZE: uint = 8192; + +fn read_all(reader: &mut io::Stream, progress_chan: &Sender<ProgressMsg>) + -> Result<(), String> { + loop { + let mut buf = vec!(); + match reader.push_at_least(READ_SIZE, READ_SIZE, &mut buf) { + Ok(_) => progress_chan.send(Payload(buf)), + Err(e) => match e.kind { + io::EndOfFile => { + if buf.len() > 0 { + progress_chan.send(Payload(buf)); + } + return Ok(()); + } + _ => return Err(e.desc.to_string()), + } + } + } +} + +pub fn factory() -> LoaderTask { + let f: LoaderTask = proc(load_data, start_chan) { + let url = load_data.url; + assert!("file" == url.scheme.as_slice()); + let progress_chan = start_sending(start_chan, Metadata::default(url.clone())); + spawn_named("file_loader", proc() { + match File::open_mode(&Path::new(url.serialize_path().unwrap()), io::Open, io::Read) { + Ok(ref mut reader) => { + let res = read_all(reader as &mut io::Stream, &progress_chan); + progress_chan.send(Done(res)); + } + Err(e) => { + progress_chan.send(Done(Err(e.desc.to_string()))); + } + }; + }); + }; + f +} diff --git a/components/net/http_loader.rs b/components/net/http_loader.rs new file mode 100644 index 00000000000..c7cb56d4231 --- /dev/null +++ b/components/net/http_loader.rs @@ -0,0 +1,167 @@ +/* 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 resource_task::{Metadata, Payload, Done, LoadResponse, LoadData, LoaderTask, start_sending_opt}; + +use std::collections::hashmap::HashSet; +use http::client::{RequestWriter, NetworkStream}; +use http::headers::HeaderEnum; +use std::io::Reader; +use servo_util::task::spawn_named; +use url::Url; + +pub fn factory() -> LoaderTask { + let f: LoaderTask = proc(url, start_chan) { + spawn_named("http_loader", proc() load(url, start_chan)) + }; + f +} + +fn send_error(url: Url, err: String, start_chan: Sender<LoadResponse>) { + match start_sending_opt(start_chan, Metadata::default(url)) { + Ok(p) => p.send(Done(Err(err))), + _ => {} + }; +} + +fn load(load_data: LoadData, start_chan: Sender<LoadResponse>) { + // FIXME: At the time of writing this FIXME, servo didn't have any central + // location for configuration. If you're reading this and such a + // repository DOES exist, please update this constant to use it. + let max_redirects = 50u; + let mut iters = 0u; + let mut url = load_data.url.clone(); + let mut redirected_to = HashSet::new(); + + // Loop to handle redirects. + loop { + iters = iters + 1; + + if iters > max_redirects { + send_error(url, "too many redirects".to_string(), start_chan); + return; + } + + if redirected_to.contains(&url) { + send_error(url, "redirect loop".to_string(), start_chan); + return; + } + + redirected_to.insert(url.clone()); + + match url.scheme.as_slice() { + "http" | "https" => {} + _ => { + let s = format!("{:s} request, but we don't support that scheme", url.scheme); + send_error(url, s, start_chan); + return; + } + } + + info!("requesting {:s}", url.serialize()); + + let request = RequestWriter::<NetworkStream>::new(load_data.method.clone(), url.clone()); + let mut writer = match request { + Ok(w) => box w, + Err(e) => { + send_error(url, e.desc.to_string(), start_chan); + return; + } + }; + + // Preserve the `host` header set automatically by RequestWriter. + let host = writer.headers.host.clone(); + writer.headers = box load_data.headers.clone(); + writer.headers.host = host; + if writer.headers.accept_encoding.is_none() { + // We currently don't support HTTP Compression (FIXME #2587) + writer.headers.accept_encoding = Some(String::from_str("identity".as_slice())) + } + match load_data.data { + Some(ref data) => { + writer.headers.content_length = Some(data.len()); + match writer.write(data.as_slice()) { + Err(e) => { + send_error(url, e.desc.to_string(), start_chan); + return; + } + _ => {} + } + }, + _ => {} + } + let mut response = match writer.read_response() { + Ok(r) => r, + Err((_, e)) => { + send_error(url, e.desc.to_string(), start_chan); + return; + } + }; + + // Dump headers, but only do the iteration if info!() is enabled. + info!("got HTTP response {:s}, headers:", response.status.to_string()); + info!("{:?}", + for header in response.headers.iter() { + info!(" - {:s}: {:s}", header.header_name(), header.header_value()); + }); + + if 3 == (response.status.code() / 100) { + match response.headers.location { + Some(new_url) => { + // CORS (http://fetch.spec.whatwg.org/#http-fetch, status section, point 9, 10) + match load_data.cors { + Some(ref c) => { + if c.preflight { + // The preflight lied + send_error(url, "Preflight fetch inconsistent with main fetch".to_string(), start_chan); + return; + } else { + // XXXManishearth There are some CORS-related steps here, + // but they don't seem necessary until credentials are implemented + } + } + _ => {} + } + info!("redirecting to {:s}", new_url.serialize()); + url = new_url; + continue; + } + None => () + } + } + + let mut metadata = Metadata::default(url); + metadata.set_content_type(&response.headers.content_type); + metadata.headers = Some(*response.headers.clone()); + metadata.status = response.status.clone(); + + let progress_chan = match start_sending_opt(start_chan, metadata) { + Ok(p) => p, + _ => return + }; + loop { + let mut buf = Vec::with_capacity(1024); + + unsafe { buf.set_len(1024); } + match response.read(buf.as_mut_slice()) { + Ok(len) => { + unsafe { buf.set_len(len); } + if progress_chan.send_opt(Payload(buf)).is_err() { + // The send errors when the receiver is out of scope, + // which will happen if the fetch has timed out (or has been aborted) + // so we don't need to continue with the loading of the file here. + return; + } + } + Err(_) => { + let _ = progress_chan.send_opt(Done(Ok(()))); + break; + } + } + } + + // We didn't get redirected. + break; + } +} diff --git a/components/net/image/base.rs b/components/net/image/base.rs new file mode 100644 index 00000000000..deda4ee8556 --- /dev/null +++ b/components/net/image/base.rs @@ -0,0 +1,67 @@ +/* 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 std::iter::range_step; +use stb_image = stb_image::image; +use png; + +// FIXME: Images must not be copied every frame. Instead we should atomically +// reference count them. +pub type Image = png::Image; + + +static TEST_IMAGE: &'static [u8] = include_bin!("test.jpeg"); + +pub fn test_image_bin() -> Vec<u8> { + TEST_IMAGE.iter().map(|&x| x).collect() +} + +// TODO(pcwalton): Speed up with SIMD, or better yet, find some way to not do this. +fn byte_swap(data: &mut [u8]) { + let length = data.len(); + for i in range_step(0, length, 4) { + let r = data[i + 2]; + data[i + 2] = data[i + 0]; + data[i + 0] = r; + } +} + +pub fn load_from_memory(buffer: &[u8]) -> Option<Image> { + if buffer.len() == 0 { + return None; + } + + if png::is_png(buffer) { + match png::load_png_from_memory(buffer) { + Ok(mut png_image) => { + match png_image.pixels { + png::RGB8(ref mut data) | png::RGBA8(ref mut data) => { + byte_swap(data.as_mut_slice()); + } + _ => {} + } + Some(png_image) + } + Err(_err) => None, + } + } else { + // For non-png images, we use stb_image + // Can't remember why we do this. Maybe it's what cairo wants + static FORCE_DEPTH: uint = 4; + + match stb_image::load_from_memory_with_depth(buffer, FORCE_DEPTH, true) { + stb_image::ImageU8(mut image) => { + assert!(image.depth == 4); + byte_swap(image.data.as_mut_slice()); + Some(png::Image { + width: image.width as u32, + height: image.height as u32, + pixels: png::RGBA8(image.data) + }) + } + stb_image::ImageF32(_image) => fail!("HDR images not implemented"), + stb_image::Error(_) => None + } + } +} diff --git a/components/net/image/holder.rs b/components/net/image/holder.rs new file mode 100644 index 00000000000..11f055aad9d --- /dev/null +++ b/components/net/image/holder.rs @@ -0,0 +1,109 @@ +/* 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 image::base::Image; +use image_cache_task::{ImageReady, ImageNotReady, ImageFailed}; +use local_image_cache::LocalImageCache; + +use geom::size::Size2D; +use std::mem; +use sync::{Arc, Mutex}; +use url::Url; + +// FIXME: Nasty coupling here This will be a problem if we want to factor out image handling from +// the network stack. This should probably be factored out into an interface and use dependency +// injection. + +/// A struct to store image data. The image will be loaded once the first time it is requested, +/// and an Arc will be stored. Clones of this Arc are given out on demand. +#[deriving(Clone)] +pub struct ImageHolder { + url: Url, + image: Option<Arc<Box<Image>>>, + cached_size: Size2D<int>, + local_image_cache: Arc<Mutex<LocalImageCache>>, +} + +impl ImageHolder { + pub fn new(url: Url, local_image_cache: Arc<Mutex<LocalImageCache>>) -> ImageHolder { + debug!("ImageHolder::new() {}", url.serialize()); + let holder = ImageHolder { + url: url, + image: None, + cached_size: Size2D(0,0), + local_image_cache: local_image_cache.clone(), + }; + + // Tell the image cache we're going to be interested in this url + // FIXME: These two messages must be sent to prep an image for use + // but they are intended to be spread out in time. Ideally prefetch + // should be done as early as possible and decode only once we + // are sure that the image will be used. + { + let val = holder.local_image_cache.lock(); + let mut local_image_cache = val; + local_image_cache.prefetch(&holder.url); + local_image_cache.decode(&holder.url); + } + + holder + } + + /// This version doesn't perform any computation, but may be stale w.r.t. newly-available image + /// data that determines size. + /// + /// The intent is that the impure version is used during layout when dimensions are used for + /// computing layout. + pub fn size(&self) -> Size2D<int> { + self.cached_size + } + + /// Query and update the current image size. + pub fn get_size(&mut self) -> Option<Size2D<int>> { + debug!("get_size() {}", self.url.serialize()); + self.get_image().map(|img| { + self.cached_size = Size2D(img.width as int, + img.height as int); + self.cached_size.clone() + }) + } + + pub fn get_image_if_present(&self) -> Option<Arc<Box<Image>>> { + debug!("get_image_if_present() {}", self.url.serialize()); + self.image.clone() + } + + pub fn get_image(&mut self) -> Option<Arc<Box<Image>>> { + debug!("get_image() {}", self.url.serialize()); + + // If this is the first time we've called this function, load + // the image and store it for the future + if self.image.is_none() { + let port = { + let val = self.local_image_cache.lock(); + let mut local_image_cache = val; + local_image_cache.get_image(&self.url) + }; + match port.recv() { + ImageReady(image) => { + self.image = Some(image); + } + ImageNotReady => { + debug!("image not ready for {:s}", self.url.serialize()); + } + ImageFailed => { + debug!("image decoding failed for {:s}", self.url.serialize()); + } + } + } + + // Clone isn't pure so we have to swap out the mutable image option + let image = mem::replace(&mut self.image, None); + let result = image.clone(); + mem::replace(&mut self.image, image); + + return result; + } +} + diff --git a/components/net/image/test.jpeg b/components/net/image/test.jpeg Binary files differnew file mode 100644 index 00000000000..1a0bdb7acd1 --- /dev/null +++ b/components/net/image/test.jpeg diff --git a/components/net/image_cache_task.rs b/components/net/image_cache_task.rs new file mode 100644 index 00000000000..de0c978c3cf --- /dev/null +++ b/components/net/image_cache_task.rs @@ -0,0 +1,993 @@ +/* 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 image::base::{Image, load_from_memory}; +use resource_task; +use resource_task::{LoadData, ResourceTask}; + +use std::comm::{channel, Receiver, Sender}; +use std::collections::hashmap::HashMap; +use std::mem::replace; +use std::task::spawn; +use std::result; +use sync::{Arc, Mutex}; +use serialize::{Encoder, Encodable}; +use url::Url; + +pub enum Msg { + /// Tell the cache that we may need a particular image soon. Must be posted + /// before Decode + Prefetch(Url), + + /// Tell the cache to decode an image. Must be posted before GetImage/WaitForImage + Decode(Url), + + /// Request an Image object for a URL. If the image is not is not immediately + /// available then ImageNotReady is returned. + GetImage(Url, Sender<ImageResponseMsg>), + + /// Wait for an image to become available (or fail to load). + WaitForImage(Url, Sender<ImageResponseMsg>), + + /// Clients must wait for a response before shutting down the ResourceTask + Exit(Sender<()>), + + /// Used by the prefetch tasks to post back image binaries + StorePrefetchedImageData(Url, Result<Vec<u8>, ()>), + + /// Used by the decoder tasks to post decoded images back to the cache + StoreImage(Url, Option<Arc<Box<Image>>>), + + /// For testing + WaitForStore(Sender<()>), + + /// For testing + WaitForStorePrefetched(Sender<()>), +} + +#[deriving(Clone)] +pub enum ImageResponseMsg { + ImageReady(Arc<Box<Image>>), + ImageNotReady, + ImageFailed +} + +impl PartialEq for ImageResponseMsg { + fn eq(&self, other: &ImageResponseMsg) -> bool { + match (self, other) { + (&ImageReady(..), &ImageReady(..)) => fail!("unimplemented comparison"), + (&ImageNotReady, &ImageNotReady) => true, + (&ImageFailed, &ImageFailed) => true, + + (&ImageReady(..), _) | (&ImageNotReady, _) | (&ImageFailed, _) => false + } + } +} + +#[deriving(Clone)] +pub struct ImageCacheTask { + chan: Sender<Msg>, +} + +impl<E, S: Encoder<E>> Encodable<S, E> for ImageCacheTask { + fn encode(&self, _: &mut S) -> Result<(), E> { + Ok(()) + } +} + +type DecoderFactory = fn() -> proc(&[u8]) -> Option<Image>; + +impl ImageCacheTask { + pub fn new(resource_task: ResourceTask) -> ImageCacheTask { + let (chan, port) = channel(); + let chan_clone = chan.clone(); + + spawn(proc() { + let mut cache = ImageCache { + resource_task: resource_task, + port: port, + chan: chan_clone, + state_map: HashMap::new(), + wait_map: HashMap::new(), + need_exit: None + }; + cache.run(); + }); + + ImageCacheTask { + chan: chan, + } + } + + pub fn new_sync(resource_task: ResourceTask) -> ImageCacheTask { + let (chan, port) = channel(); + + spawn(proc() { + let inner_cache = ImageCacheTask::new(resource_task); + + loop { + let msg: Msg = port.recv(); + + match msg { + GetImage(url, response) => { + inner_cache.send(WaitForImage(url, response)); + } + Exit(response) => { + inner_cache.send(Exit(response)); + break; + } + msg => inner_cache.send(msg) + } + } + }); + + ImageCacheTask { + chan: chan, + } + } +} + +struct ImageCache { + /// A handle to the resource task for fetching the image binaries + resource_task: ResourceTask, + /// The port on which we'll receive client requests + port: Receiver<Msg>, + /// A copy of the shared chan to give to child tasks + chan: Sender<Msg>, + /// The state of processsing an image for a URL + state_map: HashMap<Url, ImageState>, + /// List of clients waiting on a WaitForImage response + wait_map: HashMap<Url, Arc<Mutex<Vec<Sender<ImageResponseMsg>>>>>, + need_exit: Option<Sender<()>>, +} + +#[deriving(Clone)] +enum ImageState { + Init, + Prefetching(AfterPrefetch), + Prefetched(Vec<u8>), + Decoding, + Decoded(Arc<Box<Image>>), + Failed +} + +#[deriving(Clone)] +enum AfterPrefetch { + DoDecode, + DoNotDecode +} + +impl ImageCache { + pub fn run(&mut self) { + let mut store_chan: Option<Sender<()>> = None; + let mut store_prefetched_chan: Option<Sender<()>> = None; + + loop { + let msg = self.port.recv(); + + debug!("image_cache_task: received: {:?}", msg); + + match msg { + Prefetch(url) => self.prefetch(url), + StorePrefetchedImageData(url, data) => { + store_prefetched_chan.map(|chan| { + chan.send(()); + }); + store_prefetched_chan = None; + + self.store_prefetched_image_data(url, data); + } + Decode(url) => self.decode(url), + StoreImage(url, image) => { + store_chan.map(|chan| { + chan.send(()); + }); + store_chan = None; + + self.store_image(url, image) + } + GetImage(url, response) => self.get_image(url, response), + WaitForImage(url, response) => { + self.wait_for_image(url, response) + } + WaitForStore(chan) => store_chan = Some(chan), + WaitForStorePrefetched(chan) => store_prefetched_chan = Some(chan), + Exit(response) => { + assert!(self.need_exit.is_none()); + self.need_exit = Some(response); + } + } + + let need_exit = replace(&mut self.need_exit, None); + + match need_exit { + Some(response) => { + // Wait until we have no outstanding requests and subtasks + // before exiting + let mut can_exit = true; + for (_, state) in self.state_map.iter() { + match *state { + Prefetching(..) => can_exit = false, + Decoding => can_exit = false, + + Init | Prefetched(..) | Decoded(..) | Failed => () + } + } + + if can_exit { + response.send(()); + break; + } else { + self.need_exit = Some(response); + } + } + None => () + } + } + } + + fn get_state(&self, url: Url) -> ImageState { + match self.state_map.find(&url) { + Some(state) => state.clone(), + None => Init + } + } + + fn set_state(&mut self, url: Url, state: ImageState) { + self.state_map.insert(url, state); + } + + fn prefetch(&mut self, url: Url) { + match self.get_state(url.clone()) { + Init => { + let to_cache = self.chan.clone(); + let resource_task = self.resource_task.clone(); + let url_clone = url.clone(); + + spawn(proc() { + let url = url_clone; + debug!("image_cache_task: started fetch for {:s}", url.serialize()); + + let image = load_image_data(url.clone(), resource_task.clone()); + + let result = if image.is_ok() { + Ok(image.unwrap()) + } else { + Err(()) + }; + to_cache.send(StorePrefetchedImageData(url.clone(), result)); + debug!("image_cache_task: ended fetch for {:s}", url.serialize()); + }); + + self.set_state(url, Prefetching(DoNotDecode)); + } + + Prefetching(..) | Prefetched(..) | Decoding | Decoded(..) | Failed => { + // We've already begun working on this image + } + } + } + + fn store_prefetched_image_data(&mut self, url: Url, data: Result<Vec<u8>, ()>) { + match self.get_state(url.clone()) { + Prefetching(next_step) => { + match data { + Ok(data) => { + self.set_state(url.clone(), Prefetched(data)); + match next_step { + DoDecode => self.decode(url), + _ => () + } + } + Err(..) => { + self.set_state(url.clone(), Failed); + self.purge_waiters(url, || ImageFailed); + } + } + } + + Init + | Prefetched(..) + | Decoding + | Decoded(..) + | Failed => { + fail!("wrong state for storing prefetched image") + } + } + } + + fn decode(&mut self, url: Url) { + match self.get_state(url.clone()) { + Init => fail!("decoding image before prefetch"), + + Prefetching(DoNotDecode) => { + // We don't have the data yet, queue up the decode + self.set_state(url, Prefetching(DoDecode)) + } + + Prefetching(DoDecode) => { + // We don't have the data yet, but the decode request is queued up + } + + Prefetched(data) => { + let to_cache = self.chan.clone(); + let url_clone = url.clone(); + + spawn(proc() { + let url = url_clone; + debug!("image_cache_task: started image decode for {:s}", url.serialize()); + let image = load_from_memory(data.as_slice()); + let image = if image.is_some() { + Some(Arc::new(box image.unwrap())) + } else { + None + }; + to_cache.send(StoreImage(url.clone(), image)); + debug!("image_cache_task: ended image decode for {:s}", url.serialize()); + }); + + self.set_state(url, Decoding); + } + + Decoding | Decoded(..) | Failed => { + // We've already begun decoding + } + } + } + + fn store_image(&mut self, url: Url, image: Option<Arc<Box<Image>>>) { + + match self.get_state(url.clone()) { + Decoding => { + match image { + Some(image) => { + self.set_state(url.clone(), Decoded(image.clone())); + self.purge_waiters(url, || ImageReady(image.clone()) ); + } + None => { + self.set_state(url.clone(), Failed); + self.purge_waiters(url, || ImageFailed ); + } + } + } + + Init + | Prefetching(..) + | Prefetched(..) + | Decoded(..) + | Failed => { + fail!("incorrect state in store_image") + } + } + + } + + fn purge_waiters(&mut self, url: Url, f: || -> ImageResponseMsg) { + match self.wait_map.pop(&url) { + Some(waiters) => { + let mut items = waiters.lock(); + for response in items.iter() { + response.send(f()); + } + } + None => () + } + } + + fn get_image(&self, url: Url, response: Sender<ImageResponseMsg>) { + match self.get_state(url.clone()) { + Init => fail!("request for image before prefetch"), + Prefetching(DoDecode) => response.send(ImageNotReady), + Prefetching(DoNotDecode) | Prefetched(..) => fail!("request for image before decode"), + Decoding => response.send(ImageNotReady), + Decoded(image) => response.send(ImageReady(image.clone())), + Failed => response.send(ImageFailed), + } + } + + fn wait_for_image(&mut self, url: Url, response: Sender<ImageResponseMsg>) { + match self.get_state(url.clone()) { + Init => fail!("request for image before prefetch"), + + Prefetching(DoNotDecode) | Prefetched(..) => fail!("request for image before decode"), + + Prefetching(DoDecode) | Decoding => { + // We don't have this image yet + if self.wait_map.contains_key(&url) { + let waiters = self.wait_map.find_mut(&url).unwrap(); + let mut response = Some(response); + let mut items = waiters.lock(); + items.push(response.take().unwrap()); + } else { + let response = vec!(response); + let wrapped = Arc::new(Mutex::new(response)); + self.wait_map.insert(url, wrapped); + } + } + + Decoded(image) => { + response.send(ImageReady(image.clone())); + } + + Failed => { + response.send(ImageFailed); + } + } + } + +} + + +pub trait ImageCacheTaskClient { + fn exit(&self); +} + +impl ImageCacheTaskClient for ImageCacheTask { + fn exit(&self) { + let (response_chan, response_port) = channel(); + self.send(Exit(response_chan)); + response_port.recv(); + } +} + +impl ImageCacheTask { + pub fn send(&self, msg: Msg) { + self.chan.send(msg); + } + + #[cfg(test)] + fn wait_for_store(&self) -> Receiver<()> { + let (chan, port) = channel(); + self.send(WaitForStore(chan)); + port + } + + #[cfg(test)] + fn wait_for_store_prefetched(&self) -> Receiver<()> { + let (chan, port) = channel(); + self.send(WaitForStorePrefetched(chan)); + port + } +} + +fn load_image_data(url: Url, resource_task: ResourceTask) -> Result<Vec<u8>, ()> { + let (response_chan, response_port) = channel(); + resource_task.send(resource_task::Load(LoadData::new(url), response_chan)); + + let mut image_data = vec!(); + + let progress_port = response_port.recv().progress_port; + loop { + match progress_port.recv() { + resource_task::Payload(data) => { + image_data.push_all(data.as_slice()); + } + resource_task::Done(result::Ok(..)) => { + return Ok(image_data.move_iter().collect()); + } + resource_task::Done(result::Err(..)) => { + return Err(()); + } + } + } +} + + +pub fn spawn_listener<A: Send>(f: proc(Receiver<A>):Send) -> Sender<A> { + let (setup_chan, setup_port) = channel(); + + spawn(proc() { + let (chan, port) = channel(); + setup_chan.send(chan); + f(port); + }); + setup_port.recv() +} + + +#[cfg(test)] +mod tests { + use super::*; + + use resource_task; + use resource_task::{ResourceTask, Metadata, start_sending}; + use image::base::test_image_bin; + use std::comm; + use url::Url; + + trait Closure { + fn invoke(&self, _response: Sender<resource_task::ProgressMsg>) { } + } + struct DoesNothing; + impl Closure for DoesNothing { } + + struct JustSendOK { + url_requested_chan: Sender<()>, + } + impl Closure for JustSendOK { + fn invoke(&self, response: Sender<resource_task::ProgressMsg>) { + self.url_requested_chan.send(()); + response.send(resource_task::Done(Ok(()))); + } + } + + struct SendTestImage; + impl Closure for SendTestImage { + fn invoke(&self, response: Sender<resource_task::ProgressMsg>) { + response.send(resource_task::Payload(test_image_bin())); + response.send(resource_task::Done(Ok(()))); + } + } + + struct SendBogusImage; + impl Closure for SendBogusImage { + fn invoke(&self, response: Sender<resource_task::ProgressMsg>) { + response.send(resource_task::Payload(vec!())); + response.send(resource_task::Done(Ok(()))); + } + } + + struct SendTestImageErr; + impl Closure for SendTestImageErr { + fn invoke(&self, response: Sender<resource_task::ProgressMsg>) { + response.send(resource_task::Payload(test_image_bin())); + response.send(resource_task::Done(Err("".to_string()))); + } + } + + struct WaitSendTestImage { + wait_port: Receiver<()>, + } + impl Closure for WaitSendTestImage { + fn invoke(&self, response: Sender<resource_task::ProgressMsg>) { + // Don't send the data until after the client requests + // the image + self.wait_port.recv(); + response.send(resource_task::Payload(test_image_bin())); + response.send(resource_task::Done(Ok(()))); + } + } + + struct WaitSendTestImageErr { + wait_port: Receiver<()>, + } + impl Closure for WaitSendTestImageErr { + fn invoke(&self, response: Sender<resource_task::ProgressMsg>) { + // Don't send the data until after the client requests + // the image + self.wait_port.recv(); + response.send(resource_task::Payload(test_image_bin())); + response.send(resource_task::Done(Err("".to_string()))); + } + } + + fn mock_resource_task<T: Closure+Send>(on_load: Box<T>) -> ResourceTask { + spawn_listener(proc(port: Receiver<resource_task::ControlMsg>) { + loop { + match port.recv() { + resource_task::Load(_, response) => { + let chan = start_sending(response, Metadata::default( + Url::parse("file:///fake").unwrap())); + on_load.invoke(chan); + } + resource_task::Exit => break + } + } + }) + } + + #[test] + fn should_exit_on_request() { + let mock_resource_task = mock_resource_task(box DoesNothing); + + let image_cache_task = ImageCacheTask::new(mock_resource_task.clone()); + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + } + + #[test] + #[should_fail] + fn should_fail_if_unprefetched_image_is_requested() { + let mock_resource_task = mock_resource_task(box DoesNothing); + + let image_cache_task = ImageCacheTask::new(mock_resource_task.clone()); + let url = Url::parse("file:///").unwrap(); + + let (chan, port) = channel(); + image_cache_task.send(GetImage(url, chan)); + port.recv(); + } + + #[test] + fn should_request_url_from_resource_task_on_prefetch() { + let (url_requested_chan, url_requested) = channel(); + + let mock_resource_task = mock_resource_task(box JustSendOK { url_requested_chan: url_requested_chan}); + + let image_cache_task = ImageCacheTask::new(mock_resource_task.clone()); + let url = Url::parse("file:///").unwrap(); + + image_cache_task.send(Prefetch(url)); + url_requested.recv(); + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + } + + #[test] + fn should_not_request_url_from_resource_task_on_multiple_prefetches() { + let (url_requested_chan, url_requested) = comm::channel(); + + let mock_resource_task = mock_resource_task(box JustSendOK { url_requested_chan: url_requested_chan}); + + let image_cache_task = ImageCacheTask::new(mock_resource_task.clone()); + let url = Url::parse("file:///").unwrap(); + + image_cache_task.send(Prefetch(url.clone())); + image_cache_task.send(Prefetch(url)); + url_requested.recv(); + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + match url_requested.try_recv() { + Err(_) => (), + Ok(_) => fail!(), + }; + } + + #[test] + fn should_return_image_not_ready_if_data_has_not_arrived() { + let (wait_chan, wait_port) = comm::channel(); + + let mock_resource_task = mock_resource_task(box WaitSendTestImage{wait_port: wait_port}); + + let image_cache_task = ImageCacheTask::new(mock_resource_task.clone()); + let url = Url::parse("file:///").unwrap(); + + image_cache_task.send(Prefetch(url.clone())); + image_cache_task.send(Decode(url.clone())); + let (response_chan, response_port) = comm::channel(); + image_cache_task.send(GetImage(url, response_chan)); + assert!(response_port.recv() == ImageNotReady); + wait_chan.send(()); + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + } + + #[test] + fn should_return_decoded_image_data_if_data_has_arrived() { + let mock_resource_task = mock_resource_task(box SendTestImage); + + let image_cache_task = ImageCacheTask::new(mock_resource_task.clone()); + let url = Url::parse("file:///").unwrap(); + + let join_port = image_cache_task.wait_for_store(); + + image_cache_task.send(Prefetch(url.clone())); + image_cache_task.send(Decode(url.clone())); + + // Wait until our mock resource task has sent the image to the image cache + join_port.recv(); + + let (response_chan, response_port) = comm::channel(); + image_cache_task.send(GetImage(url, response_chan)); + match response_port.recv() { + ImageReady(_) => (), + _ => fail!("bleh") + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + } + + #[test] + fn should_return_decoded_image_data_for_multiple_requests() { + let mock_resource_task = mock_resource_task(box SendTestImage); + + let image_cache_task = ImageCacheTask::new(mock_resource_task.clone()); + let url = Url::parse("file:///").unwrap(); + + let join_port = image_cache_task.wait_for_store(); + + image_cache_task.send(Prefetch(url.clone())); + image_cache_task.send(Decode(url.clone())); + + // Wait until our mock resource task has sent the image to the image cache + join_port.recv(); + + for _ in range(0u32, 2u32) { + let (response_chan, response_port) = comm::channel(); + image_cache_task.send(GetImage(url.clone(), response_chan)); + match response_port.recv() { + ImageReady(_) => (), + _ => fail!("bleh") + } + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + } + + #[test] + fn should_not_request_image_from_resource_task_if_image_is_already_available() { + let (image_bin_sent_chan, image_bin_sent) = comm::channel(); + + let (resource_task_exited_chan, resource_task_exited) = comm::channel(); + + let mock_resource_task = spawn_listener(proc(port: Receiver<resource_task::ControlMsg>) { + loop { + match port.recv() { + resource_task::Load(_, response) => { + let chan = start_sending(response, Metadata::default( + Url::parse("file:///fake").unwrap())); + chan.send(resource_task::Payload(test_image_bin())); + chan.send(resource_task::Done(Ok(()))); + image_bin_sent_chan.send(()); + } + resource_task::Exit => { + resource_task_exited_chan.send(()); + break + } + } + } + }); + + let image_cache_task = ImageCacheTask::new(mock_resource_task.clone()); + let url = Url::parse("file:///").unwrap(); + + image_cache_task.send(Prefetch(url.clone())); + + // Wait until our mock resource task has sent the image to the image cache + image_bin_sent.recv(); + + image_cache_task.send(Prefetch(url.clone())); + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + + resource_task_exited.recv(); + + // Our resource task should not have received another request for the image + // because it's already cached + match image_bin_sent.try_recv() { + Err(_) => (), + Ok(_) => fail!(), + } + } + + #[test] + fn should_not_request_image_from_resource_task_if_image_fetch_already_failed() { + let (image_bin_sent_chan, image_bin_sent) = comm::channel(); + + let (resource_task_exited_chan, resource_task_exited) = comm::channel(); + + let mock_resource_task = spawn_listener(proc(port: Receiver<resource_task::ControlMsg>) { + loop { + match port.recv() { + resource_task::Load(_, response) => { + let chan = start_sending(response, Metadata::default( + Url::parse("file:///fake").unwrap())); + chan.send(resource_task::Payload(test_image_bin())); + chan.send(resource_task::Done(Err("".to_string()))); + image_bin_sent_chan.send(()); + } + resource_task::Exit => { + resource_task_exited_chan.send(()); + break + } + } + } + }); + + let image_cache_task = ImageCacheTask::new(mock_resource_task.clone()); + let url = Url::parse("file:///").unwrap(); + + image_cache_task.send(Prefetch(url.clone())); + image_cache_task.send(Decode(url.clone())); + + // Wait until our mock resource task has sent the image to the image cache + image_bin_sent.recv(); + + image_cache_task.send(Prefetch(url.clone())); + image_cache_task.send(Decode(url.clone())); + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + + resource_task_exited.recv(); + + // Our resource task should not have received another request for the image + // because it's already cached + match image_bin_sent.try_recv() { + Err(_) => (), + Ok(_) => fail!(), + } + } + + #[test] + fn should_return_failed_if_image_bin_cannot_be_fetched() { + let mock_resource_task = mock_resource_task(box SendTestImageErr); + + let image_cache_task = ImageCacheTask::new(mock_resource_task.clone()); + let url = Url::parse("file:///").unwrap(); + + let join_port = image_cache_task.wait_for_store_prefetched(); + + image_cache_task.send(Prefetch(url.clone())); + image_cache_task.send(Decode(url.clone())); + + // Wait until our mock resource task has sent the image to the image cache + join_port.recv(); + + let (response_chan, response_port) = comm::channel(); + image_cache_task.send(GetImage(url, response_chan)); + match response_port.recv() { + ImageFailed => (), + _ => fail!("bleh") + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + } + + #[test] + fn should_return_failed_for_multiple_get_image_requests_if_image_bin_cannot_be_fetched() { + let mock_resource_task = mock_resource_task(box SendTestImageErr); + + let image_cache_task = ImageCacheTask::new(mock_resource_task.clone()); + let url = Url::parse("file:///").unwrap(); + + let join_port = image_cache_task.wait_for_store_prefetched(); + + image_cache_task.send(Prefetch(url.clone())); + image_cache_task.send(Decode(url.clone())); + + // Wait until our mock resource task has sent the image to the image cache + join_port.recv(); + + let (response_chan, response_port) = comm::channel(); + image_cache_task.send(GetImage(url.clone(), response_chan)); + match response_port.recv() { + ImageFailed => (), + _ => fail!("bleh") + } + + // And ask again, we should get the same response + let (response_chan, response_port) = comm::channel(); + image_cache_task.send(GetImage(url, response_chan)); + match response_port.recv() { + ImageFailed => (), + _ => fail!("bleh") + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + } + + #[test] + fn should_return_failed_if_image_decode_fails() { + let mock_resource_task = mock_resource_task(box SendBogusImage); + + let image_cache_task = ImageCacheTask::new(mock_resource_task.clone()); + let url = Url::parse("file:///").unwrap(); + + let join_port = image_cache_task.wait_for_store(); + + image_cache_task.send(Prefetch(url.clone())); + image_cache_task.send(Decode(url.clone())); + + // Wait until our mock resource task has sent the image to the image cache + join_port.recv(); + + // Make the request + let (response_chan, response_port) = comm::channel(); + image_cache_task.send(GetImage(url, response_chan)); + + match response_port.recv() { + ImageFailed => (), + _ => fail!("bleh") + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + } + + #[test] + fn should_return_image_on_wait_if_image_is_already_loaded() { + let mock_resource_task = mock_resource_task(box SendTestImage); + + let image_cache_task = ImageCacheTask::new(mock_resource_task.clone()); + let url = Url::parse("file:///").unwrap(); + + let join_port = image_cache_task.wait_for_store(); + + image_cache_task.send(Prefetch(url.clone())); + image_cache_task.send(Decode(url.clone())); + + // Wait until our mock resource task has sent the image to the image cache + join_port.recv(); + + let (response_chan, response_port) = comm::channel(); + image_cache_task.send(WaitForImage(url, response_chan)); + match response_port.recv() { + ImageReady(..) => (), + _ => fail!("bleh") + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + } + + #[test] + fn should_return_image_on_wait_if_image_is_not_yet_loaded() { + let (wait_chan, wait_port) = comm::channel(); + + let mock_resource_task = mock_resource_task(box WaitSendTestImage {wait_port: wait_port}); + + let image_cache_task = ImageCacheTask::new(mock_resource_task.clone()); + let url = Url::parse("file:///").unwrap(); + + image_cache_task.send(Prefetch(url.clone())); + image_cache_task.send(Decode(url.clone())); + + let (response_chan, response_port) = comm::channel(); + image_cache_task.send(WaitForImage(url, response_chan)); + + wait_chan.send(()); + + match response_port.recv() { + ImageReady(..) => (), + _ => fail!("bleh") + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + } + + #[test] + fn should_return_image_failed_on_wait_if_image_fails_to_load() { + let (wait_chan, wait_port) = comm::channel(); + + let mock_resource_task = mock_resource_task(box WaitSendTestImageErr{wait_port: wait_port}); + + let image_cache_task = ImageCacheTask::new(mock_resource_task.clone()); + let url = Url::parse("file:///").unwrap(); + + image_cache_task.send(Prefetch(url.clone())); + image_cache_task.send(Decode(url.clone())); + + let (response_chan, response_port) = comm::channel(); + image_cache_task.send(WaitForImage(url, response_chan)); + + wait_chan.send(()); + + match response_port.recv() { + ImageFailed => (), + _ => fail!("bleh") + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + } + + #[test] + fn sync_cache_should_wait_for_images() { + let mock_resource_task = mock_resource_task(box SendTestImage); + + let image_cache_task = ImageCacheTask::new_sync(mock_resource_task.clone()); + let url = Url::parse("file:///").unwrap(); + + image_cache_task.send(Prefetch(url.clone())); + image_cache_task.send(Decode(url.clone())); + + let (response_chan, response_port) = comm::channel(); + image_cache_task.send(GetImage(url, response_chan)); + match response_port.recv() { + ImageReady(_) => (), + _ => fail!("bleh") + } + + image_cache_task.exit(); + mock_resource_task.send(resource_task::Exit); + } +} diff --git a/components/net/lib.rs b/components/net/lib.rs new file mode 100644 index 00000000000..bb1c5d47b1a --- /dev/null +++ b/components/net/lib.rs @@ -0,0 +1,44 @@ +/* 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/. */ + +#![feature(default_type_params, globs, managed_boxes, phase)] + +extern crate debug; +extern crate collections; +extern crate geom; +extern crate http; +extern crate png; +#[phase(plugin, link)] +extern crate log; +extern crate serialize; +extern crate servo_util = "util"; +extern crate stb_image; +extern crate sync; +extern crate time; +extern crate url; + +/// Image handling. +/// +/// It may be surprising that this goes in the network crate as opposed to the graphics crate. +/// However, image handling is generally very integrated with the network stack (especially where +/// caching is involved) and as a result it must live in here. +pub mod image { + pub mod base; + pub mod holder; +} + +pub mod file_loader; +pub mod http_loader; +pub mod data_loader; +pub mod image_cache_task; +pub mod local_image_cache; +pub mod resource_task; + +/// An implementation of the [Fetch spec](http://fetch.spec.whatwg.org/) +pub mod fetch { + #![allow(dead_code)] // XXXManishearth this is only temporary until the Fetch mod starts being used + pub mod request; + pub mod response; + pub mod cors_cache; +} diff --git a/components/net/local_image_cache.rs b/components/net/local_image_cache.rs new file mode 100644 index 00000000000..1427c831654 --- /dev/null +++ b/components/net/local_image_cache.rs @@ -0,0 +1,166 @@ +/* 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/. */ + +/*! +An adapter for ImageCacheTask that does local caching to avoid +extra message traffic, it also avoids waiting on the same image +multiple times and thus triggering reflows multiple times. +*/ + +use image_cache_task::{Decode, GetImage, ImageCacheTask, ImageFailed, ImageNotReady, ImageReady}; +use image_cache_task::{ImageResponseMsg, Prefetch, WaitForImage}; + +use std::comm::{Receiver, channel}; +use std::collections::hashmap::HashMap; +use servo_util::task::spawn_named; +use url::Url; + +pub trait ImageResponder { + fn respond(&self) -> proc(ImageResponseMsg):Send; +} + +pub struct LocalImageCache { + image_cache_task: ImageCacheTask, + round_number: uint, + on_image_available: Option<Box<ImageResponder+Send>>, + state_map: HashMap<Url, ImageState> +} + +impl LocalImageCache { + pub fn new(image_cache_task: ImageCacheTask) -> LocalImageCache { + LocalImageCache { + image_cache_task: image_cache_task, + round_number: 1, + on_image_available: None, + state_map: HashMap::new() + } + } +} + +#[deriving(Clone)] +struct ImageState { + prefetched: bool, + decoded: bool, + last_request_round: uint, + last_response: ImageResponseMsg +} + +impl LocalImageCache { + /// The local cache will only do a single remote request for a given + /// URL in each 'round'. Layout should call this each time it begins + pub fn next_round(&mut self, on_image_available: Box<ImageResponder+Send>) { + self.round_number += 1; + self.on_image_available = Some(on_image_available); + } + + pub fn prefetch(&mut self, url: &Url) { + { + let state = self.get_state(url); + if state.prefetched { + return + } + + state.prefetched = true; + } + + self.image_cache_task.send(Prefetch((*url).clone())); + } + + pub fn decode(&mut self, url: &Url) { + { + let state = self.get_state(url); + if state.decoded { + return + } + state.decoded = true; + } + + self.image_cache_task.send(Decode((*url).clone())); + } + + // FIXME: Should return a Future + pub fn get_image(&mut self, url: &Url) -> Receiver<ImageResponseMsg> { + { + let round_number = self.round_number; + let state = self.get_state(url); + + // Save the previous round number for comparison + let last_round = state.last_request_round; + // Set the current round number for this image + state.last_request_round = round_number; + + match state.last_response { + ImageReady(ref image) => { + let (chan, port) = channel(); + chan.send(ImageReady(image.clone())); + return port; + } + ImageNotReady => { + if last_round == round_number { + let (chan, port) = channel(); + chan.send(ImageNotReady); + return port; + } else { + // We haven't requested the image from the + // remote cache this round + } + } + ImageFailed => { + let (chan, port) = channel(); + chan.send(ImageFailed); + return port; + } + } + } + + let (response_chan, response_port) = channel(); + self.image_cache_task.send(GetImage((*url).clone(), response_chan)); + + let response = response_port.recv(); + match response { + ImageNotReady => { + // Need to reflow when the image is available + // FIXME: Instead we should be just passing a Future + // to the caller, then to the display list. Finally, + // the compositor should be resonsible for waiting + // on the image to load and triggering layout + let image_cache_task = self.image_cache_task.clone(); + assert!(self.on_image_available.is_some()); + let on_image_available: proc(ImageResponseMsg):Send = self.on_image_available.as_ref().unwrap().respond(); + let url = (*url).clone(); + spawn_named("LocalImageCache", proc() { + let (response_chan, response_port) = channel(); + image_cache_task.send(WaitForImage(url.clone(), response_chan)); + on_image_available(response_port.recv()); + }); + } + _ => () + } + + // Put a copy of the response in the cache + let response_copy = match response { + ImageReady(ref image) => ImageReady(image.clone()), + ImageNotReady => ImageNotReady, + ImageFailed => ImageFailed + }; + self.get_state(url).last_response = response_copy; + + let (chan, port) = channel(); + chan.send(response); + return port; + } + + fn get_state<'a>(&'a mut self, url: &Url) -> &'a mut ImageState { + let state = self.state_map.find_or_insert_with(url.clone(), |_| { + let new_state = ImageState { + prefetched: false, + decoded: false, + last_request_round: 0, + last_response: ImageNotReady + }; + new_state + }); + state + } +} diff --git a/components/net/resource_task.rs b/components/net/resource_task.rs new file mode 100644 index 00000000000..bdc1c3f2339 --- /dev/null +++ b/components/net/resource_task.rs @@ -0,0 +1,267 @@ +/* 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/. */ + +//! A task that takes a URL and streams back the binary data. + +use file_loader; +use http_loader; +use data_loader; + +use std::comm::{channel, Receiver, Sender}; +use std::task::TaskBuilder; +use std::os; +use http::headers::content_type::MediaType; +use ResponseHeaderCollection = http::headers::response::HeaderCollection; +use RequestHeaderCollection = http::headers::request::HeaderCollection; +use http::method::{Method, Get}; +use url::Url; + +use StatusOk = http::status::Ok; +use http::status::Status; + + +pub enum ControlMsg { + /// Request the data associated with a particular URL + Load(LoadData, Sender<LoadResponse>), + Exit +} + +#[deriving(Clone)] +pub struct LoadData { + pub url: Url, + pub method: Method, + pub headers: RequestHeaderCollection, + pub data: Option<Vec<u8>>, + pub cors: Option<ResourceCORSData> +} + +impl LoadData { + pub fn new(url: Url) -> LoadData { + LoadData { + url: url, + method: Get, + headers: RequestHeaderCollection::new(), + data: None, + cors: None + } + } +} + +#[deriving(Clone)] +pub struct ResourceCORSData { + /// CORS Preflight flag + pub preflight: bool, + /// Origin of CORS Request + pub origin: Url +} + +/// Metadata about a loaded resource, such as is obtained from HTTP headers. +pub struct Metadata { + /// Final URL after redirects. + pub final_url: Url, + + /// MIME type / subtype. + pub content_type: Option<(String, String)>, + + /// Character set. + pub charset: Option<String>, + + /// Headers + pub headers: Option<ResponseHeaderCollection>, + + /// HTTP Status + pub status: Status +} + +impl Metadata { + /// Metadata with defaults for everything optional. + pub fn default(url: Url) -> Metadata { + Metadata { + final_url: url, + content_type: None, + charset: None, + headers: None, + status: StatusOk // http://fetch.spec.whatwg.org/#concept-response-status-message + } + } + + /// Extract the parts of a MediaType that we care about. + pub fn set_content_type(&mut self, content_type: &Option<MediaType>) { + match *content_type { + None => (), + Some(MediaType { type_: ref type_, + subtype: ref subtype, + parameters: ref parameters }) => { + self.content_type = Some((type_.clone(), subtype.clone())); + for &(ref k, ref v) in parameters.iter() { + if "charset" == k.as_slice() { + self.charset = Some(v.clone()); + } + } + } + } + } +} + +/// Message sent in response to `Load`. Contains metadata, and a port +/// for receiving the data. +/// +/// Even if loading fails immediately, we send one of these and the +/// progress_port will provide the error. +pub struct LoadResponse { + /// Metadata, such as from HTTP headers. + pub metadata: Metadata, + /// Port for reading data. + pub progress_port: Receiver<ProgressMsg>, +} + +/// Messages sent in response to a `Load` message +#[deriving(PartialEq,Show)] +pub enum ProgressMsg { + /// Binary data - there may be multiple of these + Payload(Vec<u8>), + /// Indicates loading is complete, either successfully or not + Done(Result<(), String>) +} + +/// For use by loaders in responding to a Load message. +pub fn start_sending(start_chan: Sender<LoadResponse>, metadata: Metadata) -> Sender<ProgressMsg> { + start_sending_opt(start_chan, metadata).ok().unwrap() +} + +/// For use by loaders in responding to a Load message. +pub fn start_sending_opt(start_chan: Sender<LoadResponse>, metadata: Metadata) -> Result<Sender<ProgressMsg>, ()> { + let (progress_chan, progress_port) = channel(); + let result = start_chan.send_opt(LoadResponse { + metadata: metadata, + progress_port: progress_port, + }); + match result { + Ok(_) => Ok(progress_chan), + Err(_) => Err(()) + } +} + +/// Convenience function for synchronously loading a whole resource. +pub fn load_whole_resource(resource_task: &ResourceTask, url: Url) + -> Result<(Metadata, Vec<u8>), String> { + let (start_chan, start_port) = channel(); + resource_task.send(Load(LoadData::new(url), start_chan)); + let response = start_port.recv(); + + let mut buf = vec!(); + loop { + match response.progress_port.recv() { + Payload(data) => buf.push_all(data.as_slice()), + Done(Ok(())) => return Ok((response.metadata, buf)), + Done(Err(e)) => return Err(e) + } + } +} + +/// Handle to a resource task +pub type ResourceTask = Sender<ControlMsg>; + +pub type LoaderTask = proc(load_data: LoadData, Sender<LoadResponse>); + +/** +Creates a task to load a specific resource + +The ResourceManager delegates loading to a different type of loader task for +each URL scheme +*/ +type LoaderTaskFactory = extern "Rust" fn() -> LoaderTask; + +/// Create a ResourceTask +pub fn new_resource_task() -> ResourceTask { + let (setup_chan, setup_port) = channel(); + let builder = TaskBuilder::new().named("ResourceManager"); + builder.spawn(proc() { + ResourceManager::new(setup_port).start(); + }); + setup_chan +} + +struct ResourceManager { + from_client: Receiver<ControlMsg>, +} + + +impl ResourceManager { + fn new(from_client: Receiver<ControlMsg>) -> ResourceManager { + ResourceManager { + from_client : from_client, + } + } +} + + +impl ResourceManager { + fn start(&self) { + loop { + match self.from_client.recv() { + Load(load_data, start_chan) => { + self.load(load_data, start_chan) + } + Exit => { + break + } + } + } + } + + fn load(&self, mut load_data: LoadData, start_chan: Sender<LoadResponse>) { + let loader = match load_data.url.scheme.as_slice() { + "file" => file_loader::factory(), + "http" | "https" => http_loader::factory(), + "data" => data_loader::factory(), + "about" => { + match load_data.url.non_relative_scheme_data().unwrap() { + "crash" => fail!("Loading the about:crash URL."), + "failure" => { + // FIXME: Find a way to load this without relying on the `../src` directory. + let mut path = os::self_exe_path().expect("can't get exe path"); + path.pop(); + path.push_many(["src", "test", "html", "failure.html"]); + load_data.url = Url::from_file_path(&path).unwrap(); + file_loader::factory() + } + _ => { + start_sending(start_chan, Metadata::default(load_data.url)) + .send(Done(Err("Unknown about: URL.".to_string()))); + return + } + } + }, + _ => { + debug!("resource_task: no loader for scheme {:s}", load_data.url.scheme); + start_sending(start_chan, Metadata::default(load_data.url)) + .send(Done(Err("no loader for scheme".to_string()))); + return + } + }; + debug!("resource_task: loading url: {:s}", load_data.url.serialize()); + loader(load_data, start_chan); + } +} + +#[test] +fn test_exit() { + let resource_task = new_resource_task(); + resource_task.send(Exit); +} + +#[test] +fn test_bad_scheme() { + let resource_task = new_resource_task(); + let (start_chan, start) = channel(); + let url = Url::parse("bogus://whatever").unwrap(); + resource_task.send(Load(LoadData::new(url), start_chan)); + let response = start.recv(); + match response.progress_port.recv() { + Done(result) => { assert!(result.is_err()) } + _ => fail!("bleh") + } + resource_task.send(Exit); +} |