pub use bytes::Bytes;
pub use http;
pub use http::{HeaderMap, Method, StatusCode};
use std::sync::atomic::{AtomicU64, Ordering};
pub use url::Url;
pub trait FetchProvider: Send + Sync + 'static {
fn fetch(&self, request: FetchRequest, handler: Box<dyn FetchHandler>);
}
pub trait FetchHandler: Send + Sync + 'static {
fn complete(self: Box<Self>, result: Result<FetchResponse, FetchError>);
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct FetchRequest {
pub url: Url,
pub method: Method,
pub headers: HeaderMap,
pub body: Option<Bytes>,
}
impl FetchRequest {
pub fn get(url: Url) -> Self {
Self {
url,
method: Method::GET,
headers: HeaderMap::new(),
body: None,
}
}
pub fn method(mut self, method: Method) -> Self {
self.method = method;
self
}
pub fn body(mut self, body: Bytes) -> Self {
self.body = Some(body);
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct FetchResponse {
pub url: Url,
pub status: StatusCode,
pub headers: HeaderMap,
pub body: Bytes,
}
impl FetchResponse {
pub fn new(url: Url, status: StatusCode) -> Self {
Self {
url,
status,
headers: HeaderMap::new(),
body: Bytes::new(),
}
}
pub fn headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
pub fn body(mut self, body: Bytes) -> Self {
self.body = body;
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FetchError {
Network(String),
UnsupportedScheme(String),
InvalidRequest(String),
Blocked(String),
NoProvider,
}
impl std::fmt::Display for FetchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Network(detail) => write!(f, "network error: {detail}"),
Self::UnsupportedScheme(scheme) => write!(f, "unsupported URL scheme: {scheme}"),
Self::InvalidRequest(detail) => write!(f, "invalid request: {detail}"),
Self::Blocked(reason) => write!(f, "blocked: {reason}"),
Self::NoProvider => write!(f, "no fetch provider is installed"),
}
}
}
impl std::error::Error for FetchError {}
#[derive(Default)]
pub struct DummyFetchProvider;
impl FetchProvider for DummyFetchProvider {
fn fetch(&self, _request: FetchRequest, handler: Box<dyn FetchHandler>) {
handler.complete(Err(FetchError::NoProvider));
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OriginKey {
key: String,
persistable: bool,
}
static NEXT_OPAQUE: AtomicU64 = AtomicU64::new(0);
impl OriginKey {
pub fn for_document(url: &Url) -> Self {
let origin = url.origin();
if origin.is_tuple() {
Self {
key: origin.ascii_serialization(),
persistable: true,
}
} else {
Self::opaque()
}
}
pub fn opaque() -> Self {
let nth = NEXT_OPAQUE.fetch_add(1, Ordering::Relaxed);
Self {
key: format!("null:{nth}"),
persistable: false,
}
}
pub fn as_str(&self) -> &str {
&self.key
}
pub fn is_persistable(&self) -> bool {
self.persistable
}
}
pub trait StorageProvider: Send + Sync + 'static {
fn get(&self, origin: &OriginKey, key: &str) -> Option<String>;
fn set(&self, origin: &OriginKey, key: &str, value: &str) -> Result<(), StorageError>;
fn remove(&self, origin: &OriginKey, key: &str);
fn clear(&self, origin: &OriginKey);
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StorageError {
QuotaExceeded,
Backend(String),
}
impl std::fmt::Display for StorageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::QuotaExceeded => write!(f, "storage quota exceeded for this origin"),
Self::Backend(detail) => write!(f, "storage backend error: {detail}"),
}
}
}
impl std::error::Error for StorageError {}
#[cfg(test)]
mod tests {
use super::*;
fn url(text: &str) -> Url {
Url::parse(text).unwrap()
}
#[test]
fn same_origin_urls_share_a_key() {
let a = OriginKey::for_document(&url("https://example.com/one?x=1#frag"));
let b = OriginKey::for_document(&url("https://example.com/two"));
assert_eq!(a, b);
assert!(a.is_persistable());
}
#[test]
fn scheme_port_and_host_all_separate_origins() {
let https = OriginKey::for_document(&url("https://example.com/"));
let http = OriginKey::for_document(&url("http://example.com/"));
let port = OriginKey::for_document(&url("https://example.com:8443/"));
let host = OriginKey::for_document(&url("https://other.example.com/"));
assert_ne!(https, http);
assert_ne!(https, port);
assert_ne!(https, host);
}
#[test]
fn every_file_url_gets_its_own_opaque_origin() {
let one = OriginKey::for_document(&url("file:///home/user/page.html"));
let same_again = OriginKey::for_document(&url("file:///home/user/page.html"));
let other = OriginKey::for_document(&url("file:///home/user/other.html"));
assert_ne!(one, same_again);
assert_ne!(one, other);
assert!(!one.is_persistable());
assert!(!same_again.is_persistable());
}
#[test]
fn data_urls_are_opaque_too() {
let key = OriginKey::for_document(&url("data:text/html,<p>hi"));
assert!(!key.is_persistable());
}
#[test]
fn opaque_keys_are_distinguishable_from_each_other() {
let one = OriginKey::opaque();
let two = OriginKey::opaque();
assert_ne!(one.as_str(), two.as_str());
assert_ne!(one.as_str(), "null");
}
#[test]
fn a_blob_url_takes_the_origin_it_was_minted_from() {
let blob = OriginKey::for_document(&url("blob:https://example.com/uuid-goes-here"));
let page = OriginKey::for_document(&url("https://example.com/index.html"));
assert_eq!(blob, page);
}
#[test]
fn the_dummy_provider_answers_rather_than_going_quiet() {
use std::sync::{Arc, Mutex};
struct Record(Arc<Mutex<Option<Result<FetchResponse, FetchError>>>>);
impl FetchHandler for Record {
fn complete(self: Box<Self>, result: Result<FetchResponse, FetchError>) {
*self.0.lock().unwrap() = Some(result);
}
}
let seen = Arc::new(Mutex::new(None));
DummyFetchProvider.fetch(
FetchRequest::get(url("https://example.com/")),
Box::new(Record(seen.clone())),
);
let answer = seen.lock().unwrap().take();
assert_eq!(
answer.expect("the dummy must answer").unwrap_err(),
FetchError::NoProvider
);
}
}