summaryrefslogtreecommitdiff
path: root/cli/cache/http_cache/local.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/cache/http_cache/local.rs')
-rw-r--r--cli/cache/http_cache/local.rs1425
1 files changed, 0 insertions, 1425 deletions
diff --git a/cli/cache/http_cache/local.rs b/cli/cache/http_cache/local.rs
deleted file mode 100644
index 04883b3ba..000000000
--- a/cli/cache/http_cache/local.rs
+++ /dev/null
@@ -1,1425 +0,0 @@
-// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
-
-use std::borrow::Cow;
-use std::collections::HashMap;
-use std::collections::HashSet;
-use std::path::Path;
-use std::path::PathBuf;
-use std::sync::Arc;
-use std::time::SystemTime;
-
-use deno_ast::MediaType;
-use deno_core::error::AnyError;
-use deno_core::parking_lot::RwLock;
-use deno_core::url::Url;
-use indexmap::IndexMap;
-use once_cell::sync::Lazy;
-
-use crate::cache::CACHE_PERM;
-use crate::http_util::HeadersMap;
-use crate::util;
-use crate::util::fs::atomic_write_file;
-
-use super::common::base_url_to_filename_parts;
-use super::common::read_file_bytes;
-use super::global::GlobalHttpCache;
-use super::global::UrlToFilenameConversionError;
-use super::CachedUrlMetadata;
-use super::HttpCache;
-use super::HttpCacheItemKey;
-
-/// A vendor/ folder http cache for the lsp that provides functionality
-/// for doing a reverse mapping.
-#[derive(Debug)]
-pub struct LocalLspHttpCache {
- cache: LocalHttpCache,
-}
-
-impl LocalLspHttpCache {
- pub fn new(path: PathBuf, global_cache: Arc<GlobalHttpCache>) -> Self {
- assert!(path.is_absolute());
- let manifest = LocalCacheManifest::new_for_lsp(path.join("manifest.json"));
- Self {
- cache: LocalHttpCache {
- path,
- manifest,
- global_cache,
- },
- }
- }
-
- pub fn get_file_url(&self, url: &Url) -> Option<Url> {
- let sub_path = {
- let data = self.cache.manifest.data.read();
- let maybe_content_type =
- data.get(url).and_then(|d| d.content_type_header());
- url_to_local_sub_path(url, maybe_content_type).ok()?
- };
- let path = sub_path.as_path_from_root(&self.cache.path);
- if path.exists() {
- Url::from_file_path(path).ok()
- } else {
- None
- }
- }
-
- pub fn get_remote_url(&self, path: &Path) -> Option<Url> {
- let Ok(path) = path.strip_prefix(&self.cache.path) else {
- return None; // not in this directory
- };
- let components = path
- .components()
- .map(|c| c.as_os_str().to_string_lossy())
- .collect::<Vec<_>>();
- if components
- .last()
- .map(|c| c.starts_with('#'))
- .unwrap_or(false)
- {
- // the file itself will have an entry in the manifest
- let data = self.cache.manifest.data.read();
- data.get_reverse_mapping(path)
- } else if let Some(last_index) =
- components.iter().rposition(|c| c.starts_with('#'))
- {
- // get the mapping to the deepest hashed directory and
- // then add the remaining path components to the url
- let dir_path: PathBuf = components[..last_index + 1].iter().fold(
- PathBuf::new(),
- |mut path, c| {
- path.push(c.as_ref());
- path
- },
- );
- let dir_url = self
- .cache
- .manifest
- .data
- .read()
- .get_reverse_mapping(&dir_path)?;
- let file_url =
- dir_url.join(&components[last_index + 1..].join("/")).ok()?;
- Some(file_url)
- } else {
- // we can work backwards from the path to the url
- let mut parts = Vec::new();
- for (i, part) in path.components().enumerate() {
- let part = part.as_os_str().to_string_lossy();
- if i == 0 {
- let mut result = String::new();
- let part = if let Some(part) = part.strip_prefix("http_") {
- result.push_str("http://");
- part
- } else {
- result.push_str("https://");
- &part
- };
- if let Some((domain, port)) = part.rsplit_once('_') {
- result.push_str(&format!("{}:{}", domain, port));
- } else {
- result.push_str(part);
- }
- parts.push(result);
- } else {
- parts.push(part.to_string());
- }
- }
- Url::parse(&parts.join("/")).ok()
- }
- }
-}
-
-impl HttpCache for LocalLspHttpCache {
- fn cache_item_key<'a>(
- &self,
- url: &'a Url,
- ) -> Result<HttpCacheItemKey<'a>, AnyError> {
- self.cache.cache_item_key(url)
- }
-
- fn contains(&self, url: &Url) -> bool {
- self.cache.contains(url)
- }
-
- fn set(
- &self,
- url: &Url,
- headers: HeadersMap,
- content: &[u8],
- ) -> Result<(), AnyError> {
- self.cache.set(url, headers, content)
- }
-
- fn read_modified_time(
- &self,
- key: &HttpCacheItemKey,
- ) -> Result<Option<SystemTime>, AnyError> {
- self.cache.read_modified_time(key)
- }
-
- fn read_file_bytes(
- &self,
- key: &HttpCacheItemKey,
- ) -> Result<Option<Vec<u8>>, AnyError> {
- self.cache.read_file_bytes(key)
- }
-
- fn read_metadata(
- &self,
- key: &HttpCacheItemKey,
- ) -> Result<Option<CachedUrlMetadata>, AnyError> {
- self.cache.read_metadata(key)
- }
-}
-
-#[derive(Debug)]
-pub struct LocalHttpCache {
- path: PathBuf,
- manifest: LocalCacheManifest,
- global_cache: Arc<GlobalHttpCache>,
-}
-
-impl LocalHttpCache {
- pub fn new(path: PathBuf, global_cache: Arc<GlobalHttpCache>) -> Self {
- assert!(path.is_absolute());
- let manifest = LocalCacheManifest::new(path.join("manifest.json"));
- Self {
- path,
- manifest,
- global_cache,
- }
- }
-
- /// Copies the file from the global cache to the local cache returning
- /// if the data was successfully copied to the local cache.
- fn check_copy_global_to_local(&self, url: &Url) -> Result<bool, AnyError> {
- let global_key = self.global_cache.cache_item_key(url)?;
- let Some(metadata) = self.global_cache.read_metadata(&global_key)? else {
- return Ok(false);
- };
-
- let local_path =
- url_to_local_sub_path(url, headers_content_type(&metadata.headers))?;
-
- if !metadata.is_redirect() {
- let Some(cached_bytes) = self.global_cache.read_file_bytes(&global_key)? else {
- return Ok(false);
- };
-
- let local_file_path = local_path.as_path_from_root(&self.path);
- // if we're here, then this will be set
- atomic_write_file(&local_file_path, cached_bytes, CACHE_PERM)?;
- }
-
- self
- .manifest
- .insert_data(local_path, url.clone(), metadata.headers);
-
- Ok(true)
- }
-
- fn get_url_metadata_checking_global_cache(
- &self,
- url: &Url,
- ) -> Result<Option<CachedUrlMetadata>, AnyError> {
- if let Some(metadata) = self.manifest.get_metadata(url) {
- Ok(Some(metadata))
- } else if self.check_copy_global_to_local(url)? {
- // try again now that it's saved
- Ok(self.manifest.get_metadata(url))
- } else {
- Ok(None)
- }
- }
-}
-
-impl HttpCache for LocalHttpCache {
- fn cache_item_key<'a>(
- &self,
- url: &'a Url,
- ) -> Result<HttpCacheItemKey<'a>, AnyError> {
- Ok(HttpCacheItemKey {
- #[cfg(debug_assertions)]
- is_local_key: true,
- url,
- file_path: None, // need to compute this every time
- })
- }
-
- fn contains(&self, url: &Url) -> bool {
- self.manifest.get_metadata(url).is_some()
- }
-
- fn read_modified_time(
- &self,
- key: &HttpCacheItemKey,
- ) -> Result<Option<SystemTime>, AnyError> {
- #[cfg(debug_assertions)]
- debug_assert!(key.is_local_key);
-
- self
- .get_url_metadata_checking_global_cache(key.url)
- .map(|m| m.map(|m| m.time))
- }
-
- fn set(
- &self,
- url: &Url,
- headers: crate::http_util::HeadersMap,
- content: &[u8],
- ) -> Result<(), AnyError> {
- let is_redirect = headers.contains_key("location");
- let sub_path = url_to_local_sub_path(url, headers_content_type(&headers))?;
-
- if !is_redirect {
- // Cache content
- atomic_write_file(
- &sub_path.as_path_from_root(&self.path),
- content,
- CACHE_PERM,
- )?;
- }
-
- self.manifest.insert_data(sub_path, url.clone(), headers);
-
- Ok(())
- }
-
- fn read_file_bytes(
- &self,
- key: &HttpCacheItemKey,
- ) -> Result<Option<Vec<u8>>, AnyError> {
- #[cfg(debug_assertions)]
- debug_assert!(key.is_local_key);
-
- let metadata = self.get_url_metadata_checking_global_cache(key.url)?;
- match metadata {
- Some(data) => {
- if data.is_redirect() {
- // return back an empty file for redirect
- Ok(Some(Vec::new()))
- } else {
- // if it's not a redirect, then it should have a file path
- let cache_file_path = url_to_local_sub_path(
- key.url,
- headers_content_type(&data.headers),
- )?
- .as_path_from_root(&self.path);
- Ok(read_file_bytes(&cache_file_path)?)
- }
- }
- None => Ok(None),
- }
- }
-
- fn read_metadata(
- &self,
- key: &HttpCacheItemKey,
- ) -> Result<Option<CachedUrlMetadata>, AnyError> {
- #[cfg(debug_assertions)]
- debug_assert!(key.is_local_key);
-
- self.get_url_metadata_checking_global_cache(key.url)
- }
-}
-
-pub(super) struct LocalCacheSubPath {
- pub has_hash: bool,
- pub parts: Vec<String>,
-}
-
-impl LocalCacheSubPath {
- pub fn as_path_from_root(&self, root_path: &Path) -> PathBuf {
- let mut path = root_path.to_path_buf();
- for part in &self.parts {
- path.push(part);
- }
- path
- }
-
- pub fn as_relative_path(&self) -> PathBuf {
- let mut path = PathBuf::with_capacity(self.parts.len());
- for part in &self.parts {
- path.push(part);
- }
- path
- }
-}
-
-fn headers_content_type(headers: &HeadersMap) -> Option<&str> {
- headers.get("content-type").map(|s| s.as_str())
-}
-
-fn url_to_local_sub_path(
- url: &Url,
- content_type: Option<&str>,
-) -> Result<LocalCacheSubPath, UrlToFilenameConversionError> {
- // https://stackoverflow.com/a/31976060/188246
- static FORBIDDEN_CHARS: Lazy<HashSet<char>> = Lazy::new(|| {
- HashSet::from(['?', '<', '>', ':', '*', '|', '\\', ':', '"', '\'', '/'])
- });
- // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
- static FORBIDDEN_WINDOWS_NAMES: Lazy<HashSet<&'static str>> =
- Lazy::new(|| {
- let set = HashSet::from([
- "con", "prn", "aux", "nul", "com0", "com1", "com2", "com3", "com4",
- "com5", "com6", "com7", "com8", "com9", "lpt0", "lpt1", "lpt2", "lpt3",
- "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9",
- ]);
- // ensure everything is lowercase because we'll be comparing
- // lowercase filenames against this
- debug_assert!(set.iter().all(|s| s.to_lowercase() == *s));
- set
- });
-
- fn has_forbidden_chars(segment: &str) -> bool {
- segment.chars().any(|c| {
- let is_uppercase = c.is_ascii_alphabetic() && !c.is_ascii_lowercase();
- FORBIDDEN_CHARS.contains(&c)
- // do not allow uppercase letters in order to make this work
- // the same on case insensitive file systems
- || is_uppercase
- })
- }
-
- fn has_known_extension(path: &str) -> bool {
- let path = path.to_lowercase();
- path.ends_with(".js")
- || path.ends_with(".ts")
- || path.ends_with(".jsx")
- || path.ends_with(".tsx")
- || path.ends_with(".mts")
- || path.ends_with(".mjs")
- || path.ends_with(".json")
- || path.ends_with(".wasm")
- }
-
- fn get_extension(url: &Url, content_type: Option<&str>) -> &'static str {
- MediaType::from_specifier_and_content_type(url, content_type)
- .as_ts_extension()
- }
-
- fn short_hash(data: &str, last_ext: Option<&str>) -> String {
- // This function is a bit of a balancing act between readability
- // and avoiding collisions.
- let hash = util::checksum::gen(&[data.as_bytes()]);
- // keep the paths short because of windows path limit
- const MAX_LENGTH: usize = 20;
- let mut sub = String::with_capacity(MAX_LENGTH);
- for c in data.chars().take(MAX_LENGTH) {
- // don't include the query string (only use it in the hash)
- if c == '?' {
- break;
- }
- if FORBIDDEN_CHARS.contains(&c) {
- sub.push('_');
- } else {
- sub.extend(c.to_lowercase());
- }
- }
- let sub = match last_ext {
- Some(ext) => sub.strip_suffix(ext).unwrap_or(&sub),
- None => &sub,
- };
- let ext = last_ext.unwrap_or("");
- if sub.is_empty() {
- format!("#{}{}", &hash[..7], ext)
- } else {
- format!("#{}_{}{}", &sub, &hash[..5], ext)
- }
- }
-
- fn should_hash_part(part: &str, last_ext: Option<&str>) -> bool {
- if part.is_empty() || part.len() > 30 {
- // keep short due to windows path limit
- return true;
- }
- let hash_context_specific = if let Some(last_ext) = last_ext {
- // if the last part does not have a known extension, hash it in order to
- // prevent collisions with a directory of the same name
- !has_known_extension(part) || !part.ends_with(last_ext)
- } else {
- // if any non-ending path part has a known extension, hash it in order to
- // prevent collisions where a filename has the same name as a directory name
- has_known_extension(part)
- };
-
- // the hash symbol at the start designates a hash for the url part
- hash_context_specific
- || part.starts_with('#')
- || has_forbidden_chars(part)
- || last_ext.is_none() && FORBIDDEN_WINDOWS_NAMES.contains(part)
- || part.ends_with('.')
- }
-
- // get the base url
- let port_separator = "_"; // make this shorter with just an underscore
- let Some(mut base_parts) = base_url_to_filename_parts(url, port_separator) else {
- return Err(UrlToFilenameConversionError { url: url.to_string() });
- };
-
- if base_parts[0] == "https" {
- base_parts.remove(0);
- } else {
- let scheme = base_parts.remove(0);
- base_parts[0] = format!("{}_{}", scheme, base_parts[0]);
- }
-
- // first, try to get the filename of the path
- let path_segments = url_path_segments(url);
- let mut parts = base_parts
- .into_iter()
- .chain(path_segments.map(|s| s.to_string()))
- .collect::<Vec<_>>();
-
- // push the query parameter onto the last part
- if let Some(query) = url.query() {
- let last_part = parts.last_mut().unwrap();
- last_part.push('?');
- last_part.push_str(query);
- }
-
- let mut has_hash = false;
- let parts_len = parts.len();
- let parts = parts
- .into_iter()
- .enumerate()
- .map(|(i, part)| {
- let is_last = i == parts_len - 1;
- let last_ext = if is_last {
- Some(get_extension(url, content_type))
- } else {
- None
- };
- if should_hash_part(&part, last_ext) {
- has_hash = true;
- short_hash(&part, last_ext)
- } else {
- part
- }
- })
- .collect::<Vec<_>>();
-
- Ok(LocalCacheSubPath { has_hash, parts })
-}
-
-#[derive(Debug)]
-struct LocalCacheManifest {
- file_path: PathBuf,
- data: RwLock<manifest::LocalCacheManifestData>,
-}
-
-impl LocalCacheManifest {
- pub fn new(file_path: PathBuf) -> Self {
- Self::new_internal(file_path, false)
- }
-
- pub fn new_for_lsp(file_path: PathBuf) -> Self {
- Self::new_internal(file_path, true)
- }
-
- fn new_internal(file_path: PathBuf, use_reverse_mapping: bool) -> Self {
- let text = std::fs::read_to_string(&file_path).ok();
- Self {
- data: RwLock::new(manifest::LocalCacheManifestData::new(
- text.as_deref(),
- use_reverse_mapping,
- )),
- file_path,
- }
- }
-
- pub fn insert_data(
- &self,
- sub_path: LocalCacheSubPath,
- url: Url,
- mut original_headers: HashMap<String, String>,
- ) {
- fn should_keep_content_type_header(
- url: &Url,
- headers: &HashMap<String, String>,
- ) -> bool {
- // only keep the location header if it can't be derived from the url
- MediaType::from_specifier(url)
- != MediaType::from_specifier_and_headers(url, Some(headers))
- }
-
- let mut headers_subset = IndexMap::new();
-
- const HEADER_KEYS_TO_KEEP: [&str; 4] = [
- // keep alphabetical for cleanliness in the output
- "content-type",
- "location",
- "x-deno-warning",
- "x-typescript-types",
- ];
- for key in HEADER_KEYS_TO_KEEP {
- if key == "content-type"
- && !should_keep_content_type_header(&url, &original_headers)
- {
- continue;
- }
- if let Some((k, v)) = original_headers.remove_entry(key) {
- headers_subset.insert(k, v);
- }
- }
-
- let mut data = self.data.write();
- let add_module_entry = headers_subset.is_empty()
- && !sub_path
- .parts
- .last()
- .map(|s| s.starts_with('#'))
- .unwrap_or(false);
- let mut has_changed = if add_module_entry {
- data.remove(&url, &sub_path)
- } else {
- let new_data = manifest::SerializedLocalCacheManifestDataModule {
- headers: headers_subset,
- };
- if data.get(&url) == Some(&new_data) {
- false
- } else {
- data.insert(url.clone(), &sub_path, new_data);
- true
- }
- };
-
- if sub_path.has_hash {
- let url_path_parts = url_path_segments(&url).collect::<Vec<_>>();
- let base_url = {
- let mut url = url.clone();
- url.set_path("/");
- url.set_query(None);
- url.set_fragment(None);
- url
- };
- for (i, local_part) in sub_path.parts[1..sub_path.parts.len() - 1]
- .iter()
- .enumerate()
- {
- if local_part.starts_with('#') {
- let mut url = base_url.clone();
- url.set_path(&format!("{}/", url_path_parts[..i + 1].join("/")));
- if data.add_directory(url, sub_path.parts[..i + 2].join("/")) {
- has_changed = true;
- }
- }
- }
- }
-
- if has_changed {
- // don't bother ensuring the directory here because it will
- // eventually be created by files being added to the cache
- let result =
- atomic_write_file(&self.file_path, data.as_json(), CACHE_PERM);
- if let Err(err) = result {
- log::debug!("Failed saving local cache manifest: {:#}", err);
- }
- }
- }
-
- pub fn get_metadata(&self, url: &Url) -> Option<CachedUrlMetadata> {
- let data = self.data.read();
- match data.get(url) {
- Some(module) => {
- let headers = module
- .headers
- .iter()
- .map(|(k, v)| (k.to_string(), v.to_string()))
- .collect::<HashMap<_, _>>();
- let sub_path = if headers.contains_key("location") {
- Cow::Borrowed(&self.file_path)
- } else {
- let sub_path =
- url_to_local_sub_path(url, headers_content_type(&headers)).ok()?;
- let folder_path = self.file_path.parent().unwrap();
- Cow::Owned(sub_path.as_path_from_root(folder_path))
- };
-
- let Ok(metadata) = sub_path.metadata() else {
- return None;
- };
-
- Some(CachedUrlMetadata {
- headers,
- url: url.to_string(),
- time: metadata.modified().unwrap_or_else(|_| SystemTime::now()),
- })
- }
- None => {
- let folder_path = self.file_path.parent().unwrap();
- let sub_path = url_to_local_sub_path(url, None).ok()?;
- if sub_path
- .parts
- .last()
- .map(|s| s.starts_with('#'))
- .unwrap_or(false)
- {
- // only filenames without a hash are considered as in the cache
- // when they don't have a metadata entry
- return None;
- }
- let file_path = sub_path.as_path_from_root(folder_path);
- if let Ok(metadata) = file_path.metadata() {
- Some(CachedUrlMetadata {
- headers: Default::default(),
- url: url.to_string(),
- time: metadata.modified().unwrap_or_else(|_| SystemTime::now()),
- })
- } else {
- None
- }
- }
- }
- }
-}
-
-// This is in a separate module in order to enforce keeping
-// the internal implementation private.
-mod manifest {
- use std::collections::HashMap;
- use std::path::Path;
- use std::path::PathBuf;
-
- use deno_core::serde_json;
- use deno_core::url::Url;
- use indexmap::IndexMap;
- use serde::Deserialize;
- use serde::Serialize;
-
- use super::url_to_local_sub_path;
- use super::LocalCacheSubPath;
-
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
- pub struct SerializedLocalCacheManifestDataModule {
- #[serde(
- default = "IndexMap::new",
- skip_serializing_if = "IndexMap::is_empty"
- )]
- pub headers: IndexMap<String, String>,
- }
-
- impl SerializedLocalCacheManifestDataModule {
- pub fn content_type_header(&self) -> Option<&str> {
- self.headers.get("content-type").map(|s| s.as_str())
- }
- }
-
- #[derive(Debug, Default, Clone, Serialize, Deserialize)]
- struct SerializedLocalCacheManifestData {
- #[serde(
- default = "IndexMap::new",
- skip_serializing_if = "IndexMap::is_empty"
- )]
- pub folders: IndexMap<Url, String>,
- #[serde(
- default = "IndexMap::new",
- skip_serializing_if = "IndexMap::is_empty"
- )]
- pub modules: IndexMap<Url, SerializedLocalCacheManifestDataModule>,
- }
-
- #[derive(Debug, Default, Clone)]
- pub(super) struct LocalCacheManifestData {
- serialized: SerializedLocalCacheManifestData,
- // reverse mapping used in the lsp
- reverse_mapping: Option<HashMap<PathBuf, Url>>,
- }
-
- impl LocalCacheManifestData {
- pub fn new(maybe_text: Option<&str>, use_reverse_mapping: bool) -> Self {
- let serialized: SerializedLocalCacheManifestData = maybe_text
- .and_then(|text| match serde_json::from_str(text) {
- Ok(data) => Some(data),
- Err(err) => {
- log::debug!("Failed deserializing local cache manifest: {:#}", err);
- None
- }
- })
- .unwrap_or_default();
- let reverse_mapping = if use_reverse_mapping {
- Some(
- serialized
- .modules
- .iter()
- .filter_map(|(url, module)| {
- if module.headers.contains_key("location") {
- return None;
- }
- url_to_local_sub_path(url, module.content_type_header())
- .ok()
- .map(|local_path| {
- let path = if cfg!(windows) {
- PathBuf::from(local_path.parts.join("\\"))
- } else {
- PathBuf::from(local_path.parts.join("/"))
- };
- (path, url.clone())
- })
- })
- .chain(serialized.folders.iter().map(|(url, local_path)| {
- let path = if cfg!(windows) {
- PathBuf::from(local_path.replace('/', "\\"))
- } else {
- PathBuf::from(local_path)
- };
- (path, url.clone())
- }))
- .collect::<HashMap<_, _>>(),
- )
- } else {
- None
- };
- Self {
- serialized,
- reverse_mapping,
- }
- }
-
- pub fn get(
- &self,
- url: &Url,
- ) -> Option<&SerializedLocalCacheManifestDataModule> {
- self.serialized.modules.get(url)
- }
-
- pub fn get_reverse_mapping(&self, path: &Path) -> Option<Url> {
- debug_assert!(self.reverse_mapping.is_some()); // only call this if you're in the lsp
- self
- .reverse_mapping
- .as_ref()
- .and_then(|mapping| mapping.get(path))
- .cloned()
- }
-
- pub fn add_directory(&mut self, url: Url, local_path: String) -> bool {
- if let Some(current) = self.serialized.folders.get(&url) {
- if *current == local_path {
- return false;
- }
- }
-
- if let Some(reverse_mapping) = &mut self.reverse_mapping {
- reverse_mapping.insert(
- if cfg!(windows) {
- PathBuf::from(local_path.replace('/', "\\"))
- } else {
- PathBuf::from(&local_path)
- },
- url.clone(),
- );
- }
-
- self.serialized.folders.insert(url, local_path);
- true
- }
-
- pub fn insert(
- &mut self,
- url: Url,
- sub_path: &LocalCacheSubPath,
- new_data: SerializedLocalCacheManifestDataModule,
- ) {
- if let Some(reverse_mapping) = &mut self.reverse_mapping {
- reverse_mapping.insert(sub_path.as_relative_path(), url.clone());
- }
- self.serialized.modules.insert(url, new_data);
- }
-
- pub fn remove(&mut self, url: &Url, sub_path: &LocalCacheSubPath) -> bool {
- if self.serialized.modules.remove(url).is_some() {
- if let Some(reverse_mapping) = &mut self.reverse_mapping {
- reverse_mapping.remove(&sub_path.as_relative_path());
- }
- true
- } else {
- false
- }
- }
-
- pub fn as_json(&self) -> String {
- serde_json::to_string_pretty(&self.serialized).unwrap()
- }
- }
-}
-
-fn url_path_segments(url: &Url) -> impl Iterator<Item = &str> {
- url
- .path()
- .strip_prefix('/')
- .unwrap_or(url.path())
- .split('/')
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
-
- use deno_core::serde_json::json;
- use pretty_assertions::assert_eq;
- use test_util::TempDir;
-
- #[test]
- fn test_url_to_local_sub_path() {
- run_test("https://deno.land/x/mod.ts", &[], "deno.land/x/mod.ts");
- run_test(
- "http://deno.land/x/mod.ts",
- &[],
- // http gets added to the folder name, but not https
- "http_deno.land/x/mod.ts",
- );
- run_test(
- // capital letter in filename
- "https://deno.land/x/MOD.ts",
- &[],
- "deno.land/x/#mod_fa860.ts",
- );
- run_test(
- // query string
- "https://deno.land/x/mod.ts?testing=1",
- &[],
- "deno.land/x/#mod_2eb80.ts",
- );
- run_test(
- // capital letter in directory
- "https://deno.land/OTHER/mod.ts",
- &[],
- "deno.land/#other_1c55d/mod.ts",
- );
- run_test(
- // under max of 30 chars
- "https://deno.land/x/012345678901234567890123456.js",
- &[],
- "deno.land/x/012345678901234567890123456.js",
- );
- run_test(
- // max 30 chars
- "https://deno.land/x/0123456789012345678901234567.js",
- &[],
- "deno.land/x/#01234567890123456789_836de.js",
- );
- run_test(
- // forbidden char
- "https://deno.land/x/mod's.js",
- &[],
- "deno.land/x/#mod_s_44fc8.js",
- );
- run_test(
- // no extension
- "https://deno.land/x/mod",
- &[("content-type", "application/typescript")],
- "deno.land/x/#mod_e55cf.ts",
- );
- run_test(
- // known extension in directory is not allowed
- // because it could conflict with a file of the same name
- "https://deno.land/x/mod.js/mod.js",
- &[],
- "deno.land/x/#mod.js_59c58/mod.js",
- );
- run_test(
- // slash slash in path
- "http://localhost//mod.js",
- &[],
- "http_localhost/#e3b0c44/mod.js",
- );
- run_test(
- // headers same extension
- "https://deno.land/x/mod.ts",
- &[("content-type", "application/typescript")],
- "deno.land/x/mod.ts",
- );
- run_test(
- // headers different extension... We hash this because
- // if someone deletes the manifest file, then we don't want
- // https://deno.land/x/mod.ts to resolve as a typescript file
- "https://deno.land/x/mod.ts",
- &[("content-type", "application/javascript")],
- "deno.land/x/#mod.ts_e8c36.js",
- );
- run_test(
- // not allowed windows folder name
- "https://deno.land/x/con/con.ts",
- &[],
- "deno.land/x/#con_1143d/con.ts",
- );
- run_test(
- // disallow ending a directory with a period
- // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
- "https://deno.land/x/test./main.ts",
- &[],
- "deno.land/x/#test._4ee3d/main.ts",
- );
-
- #[track_caller]
- fn run_test(url: &str, headers: &[(&str, &str)], expected: &str) {
- let url = Url::parse(url).unwrap();
- let headers = headers
- .iter()
- .map(|(k, v)| (k.to_string(), v.to_string()))
- .collect();
- let result =
- url_to_local_sub_path(&url, headers_content_type(&headers)).unwrap();
- let parts = result.parts.join("/");
- assert_eq!(parts, expected);
- assert_eq!(
- result.parts.iter().any(|p| p.starts_with('#')),
- result.has_hash
- )
- }
- }
-
- #[test]
- fn test_local_global_cache() {
- let temp_dir = TempDir::new();
- let global_cache_path = temp_dir.path().join("global");
- let local_cache_path = temp_dir.path().join("local");
- let global_cache =
- Arc::new(GlobalHttpCache::new(global_cache_path.to_path_buf()));
- let local_cache =
- LocalHttpCache::new(local_cache_path.to_path_buf(), global_cache.clone());
-
- let manifest_file = local_cache_path.join("manifest.json");
- // mapped url
- {
- let url = Url::parse("https://deno.land/x/mod.ts").unwrap();
- let content = "export const test = 5;";
- global_cache
- .set(
- &url,
- HashMap::from([(
- "content-type".to_string(),
- "application/typescript".to_string(),
- )]),
- content.as_bytes(),
- )
- .unwrap();
- let key = local_cache.cache_item_key(&url).unwrap();
- assert_eq!(
- String::from_utf8(local_cache.read_file_bytes(&key).unwrap().unwrap())
- .unwrap(),
- content
- );
- let metadata = local_cache.read_metadata(&key).unwrap().unwrap();
- // won't have any headers because the content-type is derivable from the url
- assert_eq!(metadata.headers, HashMap::new());
- assert_eq!(metadata.url, url.to_string());
- // no manifest file yet
- assert!(!manifest_file.exists());
-
- // now try deleting the global cache and we should still be able to load it
- global_cache_path.remove_dir_all();
- assert_eq!(
- String::from_utf8(local_cache.read_file_bytes(&key).unwrap().unwrap())
- .unwrap(),
- content
- );
- }
-
- // file that's directly mappable to a url
- {
- let content = "export const a = 1;";
- local_cache_path
- .join("deno.land")
- .join("main.js")
- .write(content);
-
- // now we should be able to read this file because it's directly mappable to a url
- let url = Url::parse("https://deno.land/main.js").unwrap();
- let key = local_cache.cache_item_key(&url).unwrap();
- assert_eq!(
- String::from_utf8(local_cache.read_file_bytes(&key).unwrap().unwrap())
- .unwrap(),
- content
- );
- let metadata = local_cache.read_metadata(&key).unwrap().unwrap();
- assert_eq!(metadata.headers, HashMap::new());
- assert_eq!(metadata.url, url.to_string());
- }
-
- // now try a file with a different content-type header
- {
- let url =
- Url::parse("https://deno.land/x/different_content_type.ts").unwrap();
- let content = "export const test = 5;";
- global_cache
- .set(
- &url,
- HashMap::from([(
- "content-type".to_string(),
- "application/javascript".to_string(),
- )]),
- content.as_bytes(),
- )
- .unwrap();
- let key = local_cache.cache_item_key(&url).unwrap();
- assert_eq!(
- String::from_utf8(local_cache.read_file_bytes(&key).unwrap().unwrap())
- .unwrap(),
- content
- );
- let metadata = local_cache.read_metadata(&key).unwrap().unwrap();
- assert_eq!(
- metadata.headers,
- HashMap::from([(
- "content-type".to_string(),
- "application/javascript".to_string(),
- )])
- );
- assert_eq!(metadata.url, url.to_string());
- assert_eq!(
- manifest_file.read_json_value(),
- json!({
- "modules": {
- "https://deno.land/x/different_content_type.ts": {
- "headers": {
- "content-type": "application/javascript"
- }
- }
- }
- })
- );
- // delete the manifest file
- manifest_file.remove_file();
-
- // Now try resolving the key again and the content type should still be application/javascript.
- // This is maintained because we hash the filename when the headers don't match the extension.
- let metadata = local_cache.read_metadata(&key).unwrap().unwrap();
- assert_eq!(
- metadata.headers,
- HashMap::from([(
- "content-type".to_string(),
- "application/javascript".to_string(),
- )])
- );
- }
-
- // reset the local cache
- local_cache_path.remove_dir_all();
- let local_cache =
- LocalHttpCache::new(local_cache_path.to_path_buf(), global_cache.clone());
-
- // now try caching a file with many headers
- {
- let url = Url::parse("https://deno.land/x/my_file.ts").unwrap();
- let content = "export const test = 5;";
- global_cache
- .set(
- &url,
- HashMap::from([
- (
- "content-type".to_string(),
- "application/typescript".to_string(),
- ),
- ("x-typescript-types".to_string(), "./types.d.ts".to_string()),
- ("x-deno-warning".to_string(), "Stop right now.".to_string()),
- (
- "x-other-header".to_string(),
- "Thank you very much.".to_string(),
- ),
- ]),
- content.as_bytes(),
- )
- .unwrap();
- let check_output = |local_cache: &LocalHttpCache| {
- let key = local_cache.cache_item_key(&url).unwrap();
- assert_eq!(
- String::from_utf8(
- local_cache.read_file_bytes(&key).unwrap().unwrap()
- )
- .unwrap(),
- content
- );
- let metadata = local_cache.read_metadata(&key).unwrap().unwrap();
- assert_eq!(
- metadata.headers,
- HashMap::from([
- ("x-typescript-types".to_string(), "./types.d.ts".to_string(),),
- ("x-deno-warning".to_string(), "Stop right now.".to_string(),)
- ])
- );
- assert_eq!(metadata.url, url.to_string());
- assert_eq!(
- manifest_file.read_json_value(),
- json!({
- "modules": {
- "https://deno.land/x/my_file.ts": {
- "headers": {
- "x-deno-warning": "Stop right now.",
- "x-typescript-types": "./types.d.ts"
- }
- }
- }
- })
- );
- };
- check_output(&local_cache);
- // now ensure it's the same when re-creating the cache
- check_output(&LocalHttpCache::new(
- local_cache_path.to_path_buf(),
- global_cache.clone(),
- ));
- }
-
- // reset the local cache
- local_cache_path.remove_dir_all();
- let local_cache =
- LocalHttpCache::new(local_cache_path.to_path_buf(), global_cache.clone());
-
- // try a file that can't be mapped to the file system
- {
- {
- let url =
- Url::parse("https://deno.land/INVALID/Module.ts?dev").unwrap();
- let content = "export const test = 5;";
- global_cache
- .set(&url, HashMap::new(), content.as_bytes())
- .unwrap();
- let key = local_cache.cache_item_key(&url).unwrap();
- assert_eq!(
- String::from_utf8(
- local_cache.read_file_bytes(&key).unwrap().unwrap()
- )
- .unwrap(),
- content
- );
- let metadata = local_cache.read_metadata(&key).unwrap().unwrap();
- // won't have any headers because the content-type is derivable from the url
- assert_eq!(metadata.headers, HashMap::new());
- assert_eq!(metadata.url, url.to_string());
- }
-
- // now try a file in the same directory, but that maps to the local filesystem
- {
- let url = Url::parse("https://deno.land/INVALID/module2.ts").unwrap();
- let content = "export const test = 4;";
- global_cache
- .set(&url, HashMap::new(), content.as_bytes())
- .unwrap();
- let key = local_cache.cache_item_key(&url).unwrap();
- assert_eq!(
- String::from_utf8(
- local_cache.read_file_bytes(&key).unwrap().unwrap()
- )
- .unwrap(),
- content
- );
- assert!(local_cache_path
- .join("deno.land/#invalid_1ee01/module2.ts")
- .exists());
-
- // ensure we can still read this file with a new local cache
- let local_cache = LocalHttpCache::new(
- local_cache_path.to_path_buf(),
- global_cache.clone(),
- );
- assert_eq!(
- String::from_utf8(
- local_cache.read_file_bytes(&key).unwrap().unwrap()
- )
- .unwrap(),
- content
- );
- }
-
- assert_eq!(
- manifest_file.read_json_value(),
- json!({
- "modules": {
- "https://deno.land/INVALID/Module.ts?dev": {
- }
- },
- "folders": {
- "https://deno.land/INVALID/": "deno.land/#invalid_1ee01",
- }
- })
- );
- }
-
- // reset the local cache
- local_cache_path.remove_dir_all();
- let local_cache =
- LocalHttpCache::new(local_cache_path.to_path_buf(), global_cache.clone());
-
- // now try a redirect
- {
- let url = Url::parse("https://deno.land/redirect.ts").unwrap();
- global_cache
- .set(
- &url,
- HashMap::from([("location".to_string(), "./x/mod.ts".to_string())]),
- "Redirecting to other url...".as_bytes(),
- )
- .unwrap();
- let key = local_cache.cache_item_key(&url).unwrap();
- let metadata = local_cache.read_metadata(&key).unwrap().unwrap();
- assert_eq!(
- metadata.headers,
- HashMap::from([("location".to_string(), "./x/mod.ts".to_string())])
- );
- assert_eq!(metadata.url, url.to_string());
- assert_eq!(
- manifest_file.read_json_value(),
- json!({
- "modules": {
- "https://deno.land/redirect.ts": {
- "headers": {
- "location": "./x/mod.ts"
- }
- }
- }
- })
- );
- }
- }
-
- #[test]
- fn test_lsp_local_cache() {
- let temp_dir = TempDir::new();
- let global_cache_path = temp_dir.path().join("global");
- let local_cache_path = temp_dir.path().join("local");
- let global_cache =
- Arc::new(GlobalHttpCache::new(global_cache_path.to_path_buf()));
- let local_cache = LocalLspHttpCache::new(
- local_cache_path.to_path_buf(),
- global_cache.clone(),
- );
-
- // mapped url
- {
- let url = Url::parse("https://deno.land/x/mod.ts").unwrap();
- let content = "export const test = 5;";
- global_cache
- .set(
- &url,
- HashMap::from([(
- "content-type".to_string(),
- "application/typescript".to_string(),
- )]),
- content.as_bytes(),
- )
- .unwrap();
- let key = local_cache.cache_item_key(&url).unwrap();
- assert_eq!(
- String::from_utf8(local_cache.read_file_bytes(&key).unwrap().unwrap())
- .unwrap(),
- content
- );
-
- // check getting the file url works
- let file_url = local_cache.get_file_url(&url);
- let expected = local_cache_path
- .uri_dir()
- .join("deno.land/x/mod.ts")
- .unwrap();
- assert_eq!(file_url, Some(expected));
-
- // get the reverse mapping
- let mapping = local_cache.get_remote_url(
- local_cache_path
- .join("deno.land")
- .join("x")
- .join("mod.ts")
- .as_path(),
- );
- assert_eq!(mapping.as_ref(), Some(&url));
- }
-
- // now try a file with a different content-type header
- {
- let url =
- Url::parse("https://deno.land/x/different_content_type.ts").unwrap();
- let content = "export const test = 5;";
- global_cache
- .set(
- &url,
- HashMap::from([(
- "content-type".to_string(),
- "application/javascript".to_string(),
- )]),
- content.as_bytes(),
- )
- .unwrap();
- let key = local_cache.cache_item_key(&url).unwrap();
- assert_eq!(
- String::from_utf8(local_cache.read_file_bytes(&key).unwrap().unwrap())
- .unwrap(),
- content
- );
-
- let file_url = local_cache.get_file_url(&url).unwrap();
- let path = file_url.to_file_path().unwrap();
- assert!(path.exists());
- let mapping = local_cache.get_remote_url(&path);
- assert_eq!(mapping.as_ref(), Some(&url));
- }
-
- // try http specifiers that can't be mapped to the file system
- {
- let urls = [
- "http://deno.land/INVALID/Module.ts?dev",
- "http://deno.land/INVALID/SubDir/Module.ts?dev",
- ];
- for url in urls {
- let url = Url::parse(url).unwrap();
- let content = "export const test = 5;";
- global_cache
- .set(&url, HashMap::new(), content.as_bytes())
- .unwrap();
- let key = local_cache.cache_item_key(&url).unwrap();
- assert_eq!(
- String::from_utf8(
- local_cache.read_file_bytes(&key).unwrap().unwrap()
- )
- .unwrap(),
- content
- );
-
- let file_url = local_cache.get_file_url(&url).unwrap();
- let path = file_url.to_file_path().unwrap();
- assert!(path.exists());
- let mapping = local_cache.get_remote_url(&path);
- assert_eq!(mapping.as_ref(), Some(&url));
- }
-
- // now try a files in the same and sub directories, that maps to the local filesystem
- let urls = [
- "http://deno.land/INVALID/module2.ts",
- "http://deno.land/INVALID/SubDir/module3.ts",
- "http://deno.land/INVALID/SubDir/sub_dir/module4.ts",
- ];
- for url in urls {
- let url = Url::parse(url).unwrap();
- let content = "export const test = 4;";
- global_cache
- .set(&url, HashMap::new(), content.as_bytes())
- .unwrap();
- let key = local_cache.cache_item_key(&url).unwrap();
- assert_eq!(
- String::from_utf8(
- local_cache.read_file_bytes(&key).unwrap().unwrap()
- )
- .unwrap(),
- content
- );
- let file_url = local_cache.get_file_url(&url).unwrap();
- let path = file_url.to_file_path().unwrap();
- assert!(path.exists());
- let mapping = local_cache.get_remote_url(&path);
- assert_eq!(mapping.as_ref(), Some(&url));
-
- // ensure we can still get this file with a new local cache
- let local_cache = LocalLspHttpCache::new(
- local_cache_path.to_path_buf(),
- global_cache.clone(),
- );
- let file_url = local_cache.get_file_url(&url).unwrap();
- let path = file_url.to_file_path().unwrap();
- assert!(path.exists());
- let mapping = local_cache.get_remote_url(&path);
- assert_eq!(mapping.as_ref(), Some(&url));
- }
- }
- }
-}