mirror of
https://github.com/servo/servo.git
synced 2025-08-05 13:40:08 +01:00
Cargoify servo
This commit is contained in:
parent
db2f642c32
commit
c6ab60dbfc
1761 changed files with 8423 additions and 2294 deletions
27
components/net/Cargo.toml
Normal file
27
components/net/Cargo.toml
Normal file
|
@ -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"
|
154
components/net/data_loader.rs
Normal file
154
components/net/data_loader.rs
Normal file
|
@ -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)));
|
||||
}
|
316
components/net/fetch/cors_cache.rs
Normal file
316
components/net/fetch/cors_cache.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
149
components/net/fetch/request.rs
Normal file
149
components/net/fetch/request.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
144
components/net/fetch/response.rs
Normal file
144
components/net/fetch/response.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
50
components/net/file_loader.rs
Normal file
50
components/net/file_loader.rs
Normal file
|
@ -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
|
||||
}
|
167
components/net/http_loader.rs
Normal file
167
components/net/http_loader.rs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
67
components/net/image/base.rs
Normal file
67
components/net/image/base.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
109
components/net/image/holder.rs
Normal file
109
components/net/image/holder.rs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
BIN
components/net/image/test.jpeg
Normal file
BIN
components/net/image/test.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
993
components/net/image_cache_task.rs
Normal file
993
components/net/image_cache_task.rs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
44
components/net/lib.rs
Normal file
44
components/net/lib.rs
Normal file
|
@ -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;
|
||||
}
|
166
components/net/local_image_cache.rs
Normal file
166
components/net/local_image_cache.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
267
components/net/resource_task.rs
Normal file
267
components/net/resource_task.rs
Normal file
|
@ -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);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue