use deno_error::JsError;
use serde::Deserialize;
use serde::Serialize;
use std::borrow::Cow;
use std::io::ErrorKind;
use std::path::PathBuf;
use std::time::SystemTime;
use thiserror::Error;
use url::Url;
use crate::common::base_url_to_filename_parts;
use crate::common::checksum;
use crate::common::HeadersMap;
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum GlobalToLocalCopy {
Allow,
Disallow,
}
impl GlobalToLocalCopy {
pub fn is_true(&self) -> bool {
matches!(self, GlobalToLocalCopy::Allow)
}
}
#[derive(Debug, Error, JsError)]
#[class(type)]
#[error("Integrity check failed for {}\n\nActual: {}\nExpected: {}", .url, .actual, .expected)]
pub struct ChecksumIntegrityError {
pub url: Url,
pub actual: String,
pub expected: String,
}
#[derive(Debug, Clone, Copy)]
pub struct Checksum<'a>(&'a str);
impl<'a> Checksum<'a> {
pub fn new(checksum: &'a str) -> Self {
Self(checksum)
}
pub fn as_str(&self) -> &str {
self.0
}
pub fn check(
&self,
url: &Url,
content: &[u8],
) -> Result<(), Box<ChecksumIntegrityError>> {
let actual = checksum(content);
if self.as_str() != actual {
Err(Box::new(ChecksumIntegrityError {
url: url.clone(),
expected: self.as_str().to_string(),
actual,
}))
} else {
Ok(())
}
}
}
pub fn url_to_filename(url: &Url) -> std::io::Result<PathBuf> {
let Some(cache_parts) = base_url_to_filename_parts(url, "_PORT") else {
return Err(std::io::Error::new(
ErrorKind::InvalidInput,
format!("Can't convert url (\"{}\") to filename.", url),
));
};
let rest_str = if let Some(query) = url.query() {
let mut rest_str =
String::with_capacity(url.path().len() + 1 + query.len());
rest_str.push_str(url.path());
rest_str.push('?');
rest_str.push_str(query);
Cow::Owned(rest_str)
} else {
Cow::Borrowed(url.path())
};
let hashed_filename = checksum(rest_str.as_bytes());
let capacity = cache_parts.iter().map(|s| s.len() + 1).sum::<usize>()
+ 1
+ hashed_filename.len();
let mut cache_filename = PathBuf::with_capacity(capacity);
cache_filename.extend(cache_parts.iter().map(|s| s.as_ref()));
cache_filename.push(hashed_filename);
debug_assert_eq!(cache_filename.capacity(), capacity);
Ok(cache_filename)
}
#[derive(Debug, Error, JsError)]
pub enum CacheReadFileError {
#[class(inherit)]
#[error(transparent)]
Io(#[from] std::io::Error),
#[class(inherit)]
#[error(transparent)]
ChecksumIntegrity(Box<ChecksumIntegrityError>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SerializedCachedUrlMetadata {
pub headers: HeadersMap,
pub url: String,
#[serde(default)]
pub time: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CacheEntry {
pub metadata: SerializedCachedUrlMetadata,
pub content: Cow<'static, [u8]>,
}
pub struct HttpCacheItemKey<'a> {
#[cfg(debug_assertions)]
pub(super) is_local_key: bool,
pub(super) url: &'a Url,
pub(super) file_path: Option<PathBuf>,
}
pub trait HttpCache: Send + Sync + std::fmt::Debug {
fn cache_item_key<'a>(
&self,
url: &'a Url,
) -> std::io::Result<HttpCacheItemKey<'a>>;
fn contains(&self, url: &Url) -> bool;
fn set(
&self,
url: &Url,
headers: HeadersMap,
content: &[u8],
) -> std::io::Result<()>;
fn get(
&self,
key: &HttpCacheItemKey,
maybe_checksum: Option<Checksum>,
) -> Result<Option<CacheEntry>, CacheReadFileError>;
fn read_modified_time(
&self,
key: &HttpCacheItemKey,
) -> std::io::Result<Option<SystemTime>>;
fn read_headers(
&self,
key: &HttpCacheItemKey,
) -> std::io::Result<Option<HeadersMap>>;
fn read_download_time(
&self,
key: &HttpCacheItemKey,
) -> std::io::Result<Option<SystemTime>>;
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn deserialized_no_time() {
let json = r#"{
"headers": {
"content-type": "application/javascript"
},
"url": "https://deno.land/std/http/file_server.ts"
}"#;
let data: SerializedCachedUrlMetadata = serde_json::from_str(json).unwrap();
assert_eq!(
data,
SerializedCachedUrlMetadata {
headers: HeadersMap::from([(
"content-type".to_string(),
"application/javascript".to_string()
)]),
time: None,
url: "https://deno.land/std/http/file_server.ts".to_string(),
}
);
}
#[test]
fn serialize_deserialize_time() {
let json = r#"{
"headers": {
"content-type": "application/javascript"
},
"url": "https://deno.land/std/http/file_server.ts",
"time": 123456789
}"#;
let data: SerializedCachedUrlMetadata = serde_json::from_str(json).unwrap();
let expected = SerializedCachedUrlMetadata {
headers: HeadersMap::from([(
"content-type".to_string(),
"application/javascript".to_string(),
)]),
time: Some(123456789),
url: "https://deno.land/std/http/file_server.ts".to_string(),
};
assert_eq!(data, expected);
}
}