aboutsummaryrefslogtreecommitdiffstats
path: root/components/net
diff options
context:
space:
mode:
authorJack Moffitt <jack@metajack.im>2014-08-28 09:34:23 -0600
committerJack Moffitt <jack@metajack.im>2014-09-08 20:21:42 -0600
commitc6ab60dbfc6da7b4f800c9e40893c8b58413960c (patch)
treed1d74076cf7fa20e4f77ec7cb82cae98b67362cb /components/net
parentdb2f642c32fc5bed445bb6f2e45b0f6f0b4342cf (diff)
downloadservo-c6ab60dbfc6da7b4f800c9e40893c8b58413960c.tar.gz
servo-c6ab60dbfc6da7b4f800c9e40893c8b58413960c.zip
Cargoify servo
Diffstat (limited to 'components/net')
-rw-r--r--components/net/Cargo.toml27
-rw-r--r--components/net/data_loader.rs154
-rw-r--r--components/net/fetch/cors_cache.rs316
-rw-r--r--components/net/fetch/request.rs149
-rw-r--r--components/net/fetch/response.rs144
-rw-r--r--components/net/file_loader.rs50
-rw-r--r--components/net/http_loader.rs167
-rw-r--r--components/net/image/base.rs67
-rw-r--r--components/net/image/holder.rs109
-rw-r--r--components/net/image/test.jpegbin0 -> 4962 bytes
-rw-r--r--components/net/image_cache_task.rs993
-rw-r--r--components/net/lib.rs44
-rw-r--r--components/net/local_image_cache.rs166
-rw-r--r--components/net/resource_task.rs267
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
new file mode 100644
index 00000000000..1a0bdb7acd1
--- /dev/null
+++ b/components/net/image/test.jpeg
Binary files differ
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);
+}