1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-24 15:19:26 -05:00
denoland-deno/cli/file_fetcher.rs
2021-09-25 00:35:35 +02:00

1603 lines
48 KiB
Rust

// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
use crate::auth_tokens::AuthTokens;
use crate::colors;
use crate::http_cache::HttpCache;
use crate::http_util::fetch_once;
use crate::http_util::FetchOnceArgs;
use crate::http_util::FetchOnceResult;
use crate::text_encoding;
use crate::version::get_user_agent;
use data_url::DataUrl;
use deno_ast::MediaType;
use deno_core::error::custom_error;
use deno_core::error::generic_error;
use deno_core::error::uri_error;
use deno_core::error::AnyError;
use deno_core::futures;
use deno_core::futures::future::FutureExt;
use deno_core::parking_lot::Mutex;
use deno_core::ModuleSpecifier;
use deno_runtime::deno_fetch::reqwest;
use deno_runtime::deno_web::BlobStore;
use deno_runtime::permissions::Permissions;
use deno_tls::create_http_client;
use deno_tls::rustls::RootCertStore;
use log::debug;
use log::info;
use std::borrow::Borrow;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::future::Future;
use std::io::Read;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
static DENO_AUTH_TOKENS: &str = "DENO_AUTH_TOKENS";
pub const SUPPORTED_SCHEMES: [&str; 5] =
["data", "blob", "file", "http", "https"];
/// A structure representing a source file.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct File {
/// The path to the local version of the source file. For local files this
/// will be the direct path to that file. For remote files, it will be the
/// path to the file in the HTTP cache.
pub local: PathBuf,
/// For remote files, if there was an `X-TypeScript-Type` header, the parsed
/// out value of that header.
pub maybe_types: Option<String>,
/// The resolved media type for the file.
pub media_type: MediaType,
/// The source of the file as a string.
pub source: Arc<String>,
/// The _final_ specifier for the file. The requested specifier and the final
/// specifier maybe different for remote files that have been redirected.
pub specifier: ModuleSpecifier,
pub maybe_headers: Option<HashMap<String, String>>,
}
/// Simple struct implementing in-process caching to prevent multiple
/// fs reads/net fetches for same file.
#[derive(Debug, Clone, Default)]
struct FileCache(Arc<Mutex<HashMap<ModuleSpecifier, File>>>);
impl FileCache {
pub fn get(&self, specifier: &ModuleSpecifier) -> Option<File> {
let cache = self.0.lock();
cache.get(specifier).cloned()
}
pub fn insert(&self, specifier: ModuleSpecifier, file: File) -> Option<File> {
let mut cache = self.0.lock();
cache.insert(specifier, file)
}
}
/// Indicates how cached source files should be handled.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum CacheSetting {
/// Only the cached files should be used. Any files not in the cache will
/// error. This is the equivalent of `--cached-only` in the CLI.
Only,
/// No cached source files should be used, and all files should be reloaded.
/// This is the equivalent of `--reload` in the CLI.
ReloadAll,
/// Only some cached resources should be used. This is the equivalent of
/// `--reload=https://deno.land/std` or
/// `--reload=https://deno.land/std,https://deno.land/x/example`.
ReloadSome(Vec<String>),
/// The cached source files should be used for local modules. This is the
/// default behavior of the CLI.
Use,
}
impl CacheSetting {
/// Returns if the cache should be used for a given specifier.
pub fn should_use(&self, specifier: &ModuleSpecifier) -> bool {
match self {
CacheSetting::ReloadAll => false,
CacheSetting::Use | CacheSetting::Only => true,
CacheSetting::ReloadSome(list) => {
let mut url = specifier.clone();
url.set_fragment(None);
if list.contains(&url.as_str().to_string()) {
return false;
}
url.set_query(None);
let mut path = PathBuf::from(url.as_str());
loop {
if list.contains(&path.to_str().unwrap().to_string()) {
return false;
}
if !path.pop() {
break;
}
}
true
}
}
}
}
/// Fetch a source file from the local file system.
fn fetch_local(specifier: &ModuleSpecifier) -> Result<File, AnyError> {
let local = specifier.to_file_path().map_err(|_| {
uri_error(format!("Invalid file path.\n Specifier: {}", specifier))
})?;
let bytes = fs::read(local.clone())?;
let charset = text_encoding::detect_charset(&bytes).to_string();
let source = strip_shebang(get_source_from_bytes(bytes, Some(charset))?);
let media_type = MediaType::from(specifier);
Ok(File {
local,
maybe_types: None,
media_type,
source: Arc::new(source),
specifier: specifier.clone(),
maybe_headers: None,
})
}
/// Returns the decoded body and content-type of a provided
/// data URL.
pub fn get_source_from_data_url(
specifier: &ModuleSpecifier,
) -> Result<(String, String), AnyError> {
let data_url = DataUrl::process(specifier.as_str())
.map_err(|e| uri_error(format!("{:?}", e)))?;
let mime = data_url.mime_type();
let charset = mime.get_parameter("charset").map(|v| v.to_string());
let (bytes, _) = data_url
.decode_to_vec()
.map_err(|e| uri_error(format!("{:?}", e)))?;
Ok((
strip_shebang(get_source_from_bytes(bytes, charset)?),
format!("{}", mime),
))
}
/// Given a vector of bytes and optionally a charset, decode the bytes to a
/// string.
pub fn get_source_from_bytes(
bytes: Vec<u8>,
maybe_charset: Option<String>,
) -> Result<String, AnyError> {
let source = if let Some(charset) = maybe_charset {
text_encoding::convert_to_utf8(&bytes, &charset)?.to_string()
} else {
String::from_utf8(bytes)?
};
Ok(source)
}
/// Return a validated scheme for a given module specifier.
fn get_validated_scheme(
specifier: &ModuleSpecifier,
) -> Result<String, AnyError> {
let scheme = specifier.scheme();
if !SUPPORTED_SCHEMES.contains(&scheme) {
Err(generic_error(format!(
"Unsupported scheme \"{}\" for module \"{}\". Supported schemes: {:#?}",
scheme, specifier, SUPPORTED_SCHEMES
)))
} else {
Ok(scheme.to_string())
}
}
/// Resolve a media type and optionally the charset from a module specifier and
/// the value of a content type header.
pub fn map_content_type(
specifier: &ModuleSpecifier,
maybe_content_type: Option<String>,
) -> (MediaType, Option<String>) {
if let Some(content_type) = maybe_content_type {
let mut content_types = content_type.split(';');
let content_type = content_types.next().unwrap();
let media_type = MediaType::from_content_type(specifier, content_type);
let charset = content_types
.map(str::trim)
.find_map(|s| s.strip_prefix("charset="))
.map(String::from);
(media_type, charset)
} else {
(MediaType::from(specifier), None)
}
}
/// Remove shebangs from the start of source code strings
fn strip_shebang(mut value: String) -> String {
if value.starts_with("#!") {
if let Some(mid) = value.find('\n') {
let (_, rest) = value.split_at(mid);
value = rest.to_string()
} else {
value.clear()
}
}
value
}
/// A structure for resolving, fetching and caching source files.
#[derive(Debug, Clone)]
pub struct FileFetcher {
auth_tokens: AuthTokens,
allow_remote: bool,
cache: FileCache,
cache_setting: CacheSetting,
http_cache: HttpCache,
http_client: reqwest::Client,
blob_store: BlobStore,
}
impl FileFetcher {
pub fn new(
http_cache: HttpCache,
cache_setting: CacheSetting,
allow_remote: bool,
root_cert_store: Option<RootCertStore>,
blob_store: BlobStore,
unsafely_ignore_certificate_errors: Option<Vec<String>>,
) -> Result<Self, AnyError> {
Ok(Self {
auth_tokens: AuthTokens::new(env::var(DENO_AUTH_TOKENS).ok()),
allow_remote,
cache: Default::default(),
cache_setting,
http_cache,
http_client: create_http_client(
get_user_agent(),
root_cert_store,
None,
None,
unsafely_ignore_certificate_errors,
None,
)?,
blob_store,
})
}
/// Creates a `File` structure for a remote file.
fn build_remote_file(
&self,
specifier: &ModuleSpecifier,
bytes: Vec<u8>,
headers: &HashMap<String, String>,
) -> Result<File, AnyError> {
let local =
self
.http_cache
.get_cache_filename(specifier)
.ok_or_else(|| {
generic_error("Cannot convert specifier to cached filename.")
})?;
let maybe_content_type = headers.get("content-type").cloned();
let (media_type, maybe_charset) =
map_content_type(specifier, maybe_content_type);
let source = strip_shebang(get_source_from_bytes(bytes, maybe_charset)?);
let maybe_types = match media_type {
MediaType::JavaScript | MediaType::Jsx => {
headers.get("x-typescript-types").cloned()
}
_ => None,
};
Ok(File {
local,
maybe_types,
media_type,
source: Arc::new(source),
specifier: specifier.clone(),
maybe_headers: Some(headers.clone()),
})
}
/// Fetch cached remote file.
///
/// This is a recursive operation if source file has redirections.
fn fetch_cached(
&self,
specifier: &ModuleSpecifier,
redirect_limit: i64,
) -> Result<Option<File>, AnyError> {
debug!("FileFetcher::fetch_cached - specifier: {}", specifier);
if redirect_limit < 0 {
return Err(custom_error("Http", "Too many redirects."));
}
let (mut source_file, headers) = match self.http_cache.get(specifier) {
Err(err) => {
if let Some(err) = err.downcast_ref::<std::io::Error>() {
if err.kind() == std::io::ErrorKind::NotFound {
return Ok(None);
}
}
return Err(err);
}
Ok(cache) => cache,
};
if let Some(redirect_to) = headers.get("location") {
let redirect =
deno_core::resolve_import(redirect_to, specifier.as_str())?;
return self.fetch_cached(&redirect, redirect_limit - 1);
}
let mut bytes = Vec::new();
source_file.read_to_end(&mut bytes)?;
let file = self.build_remote_file(specifier, bytes, &headers)?;
Ok(Some(file))
}
/// Convert a data URL into a file, resulting in an error if the URL is
/// invalid.
fn fetch_data_url(
&self,
specifier: &ModuleSpecifier,
) -> Result<File, AnyError> {
debug!("FileFetcher::fetch_data_url() - specifier: {}", specifier);
match self.fetch_cached(specifier, 0) {
Ok(Some(file)) => return Ok(file),
Ok(None) => {}
Err(err) => return Err(err),
}
if self.cache_setting == CacheSetting::Only {
return Err(custom_error(
"NotFound",
format!(
"Specifier not found in cache: \"{}\", --cached-only is specified.",
specifier
),
));
}
let (source, content_type) = get_source_from_data_url(specifier)?;
let (media_type, _) =
map_content_type(specifier, Some(content_type.clone()));
let local =
self
.http_cache
.get_cache_filename(specifier)
.ok_or_else(|| {
generic_error("Cannot convert specifier to cached filename.")
})?;
let mut headers = HashMap::new();
headers.insert("content-type".to_string(), content_type);
self.http_cache.set(specifier, headers, source.as_bytes())?;
Ok(File {
local,
maybe_types: None,
media_type,
source: Arc::new(source),
specifier: specifier.clone(),
maybe_headers: None,
})
}
/// Get a blob URL.
async fn fetch_blob_url(
&self,
specifier: &ModuleSpecifier,
) -> Result<File, AnyError> {
debug!("FileFetcher::fetch_blob_url() - specifier: {}", specifier);
match self.fetch_cached(specifier, 0) {
Ok(Some(file)) => return Ok(file),
Ok(None) => {}
Err(err) => return Err(err),
}
if self.cache_setting == CacheSetting::Only {
return Err(custom_error(
"NotFound",
format!(
"Specifier not found in cache: \"{}\", --cached-only is specified.",
specifier
),
));
}
let blob = {
let blob_store = self.blob_store.borrow();
blob_store
.get_object_url(specifier.clone())?
.ok_or_else(|| {
custom_error(
"NotFound",
format!("Blob URL not found: \"{}\".", specifier),
)
})?
};
let content_type = blob.media_type.clone();
let bytes = blob.read_all().await?;
let (media_type, maybe_charset) =
map_content_type(specifier, Some(content_type.clone()));
let source = strip_shebang(get_source_from_bytes(bytes, maybe_charset)?);
let local =
self
.http_cache
.get_cache_filename(specifier)
.ok_or_else(|| {
generic_error("Cannot convert specifier to cached filename.")
})?;
let mut headers = HashMap::new();
headers.insert("content-type".to_string(), content_type);
self.http_cache.set(specifier, headers, source.as_bytes())?;
Ok(File {
local,
maybe_types: None,
media_type,
source: Arc::new(source),
specifier: specifier.clone(),
maybe_headers: None,
})
}
/// Asynchronously fetch remote source file specified by the URL following
/// redirects.
///
/// **Note** this is a recursive method so it can't be "async", but needs to
/// return a `Pin<Box<..>>`.
fn fetch_remote(
&self,
specifier: &ModuleSpecifier,
permissions: &mut Permissions,
redirect_limit: i64,
) -> Pin<Box<dyn Future<Output = Result<File, AnyError>> + Send>> {
debug!("FileFetcher::fetch_remote() - specifier: {}", specifier);
if redirect_limit < 0 {
return futures::future::err(custom_error("Http", "Too many redirects."))
.boxed();
}
if let Err(err) = permissions.check_specifier(specifier) {
return futures::future::err(err).boxed();
}
if self.cache_setting.should_use(specifier) {
match self.fetch_cached(specifier, redirect_limit) {
Ok(Some(file)) => {
return futures::future::ok(file).boxed();
}
Ok(None) => {}
Err(err) => {
return futures::future::err(err).boxed();
}
}
}
if self.cache_setting == CacheSetting::Only {
return futures::future::err(custom_error(
"NotFound",
format!(
"Specifier not found in cache: \"{}\", --cached-only is specified.",
specifier
),
))
.boxed();
}
info!("{} {}", colors::green("Download"), specifier);
let maybe_etag = match self.http_cache.get(specifier) {
Ok((_, headers)) => headers.get("etag").cloned(),
_ => None,
};
let maybe_auth_token = self.auth_tokens.get(specifier);
let specifier = specifier.clone();
let mut permissions = permissions.clone();
let client = self.http_client.clone();
let file_fetcher = self.clone();
// A single pass of fetch either yields code or yields a redirect.
async move {
match fetch_once(FetchOnceArgs {
client,
url: specifier.clone(),
maybe_etag,
maybe_auth_token,
})
.await?
{
FetchOnceResult::NotModified => {
let file = file_fetcher.fetch_cached(&specifier, 10)?.unwrap();
Ok(file)
}
FetchOnceResult::Redirect(redirect_url, headers) => {
file_fetcher.http_cache.set(&specifier, headers, &[])?;
file_fetcher
.fetch_remote(&redirect_url, &mut permissions, redirect_limit - 1)
.await
}
FetchOnceResult::Code(bytes, headers) => {
file_fetcher
.http_cache
.set(&specifier, headers.clone(), &bytes)?;
let file =
file_fetcher.build_remote_file(&specifier, bytes, &headers)?;
Ok(file)
}
}
}
.boxed()
}
/// Fetch a source file and asynchronously return it.
pub async fn fetch(
&self,
specifier: &ModuleSpecifier,
permissions: &mut Permissions,
) -> Result<File, AnyError> {
debug!("FileFetcher::fetch() - specifier: {}", specifier);
let scheme = get_validated_scheme(specifier)?;
permissions.check_specifier(specifier)?;
if let Some(file) = self.cache.get(specifier) {
Ok(file)
} else if scheme == "file" {
// we do not in memory cache files, as this would prevent files on the
// disk changing effecting things like workers and dynamic imports.
fetch_local(specifier)
} else if scheme == "data" {
let result = self.fetch_data_url(specifier);
if let Ok(file) = &result {
self.cache.insert(specifier.clone(), file.clone());
}
result
} else if scheme == "blob" {
let result = self.fetch_blob_url(specifier).await;
if let Ok(file) = &result {
self.cache.insert(specifier.clone(), file.clone());
}
result
} else if !self.allow_remote {
Err(custom_error(
"NoRemote",
format!("A remote specifier was requested: \"{}\", but --no-remote is specified.", specifier),
))
} else {
let result = self.fetch_remote(specifier, permissions, 10).await;
if let Ok(file) = &result {
self.cache.insert(specifier.clone(), file.clone());
}
result
}
}
/// Get the location of the current HTTP cache associated with the fetcher.
pub fn get_http_cache_location(&self) -> PathBuf {
self.http_cache.location.clone()
}
/// A synchronous way to retrieve a source file, where if the file has already
/// been cached in memory it will be returned, otherwise for local files will
/// be read from disk.
pub fn get_source(&self, specifier: &ModuleSpecifier) -> Option<File> {
let maybe_file = self.cache.get(specifier);
if maybe_file.is_none() {
let is_local = specifier.scheme() == "file";
if is_local {
if let Ok(file) = fetch_local(specifier) {
return Some(file);
}
}
None
} else {
maybe_file
}
}
/// Insert a temporary module into the in memory cache for the file fetcher.
pub fn insert_cached(&self, file: File) -> Option<File> {
self.cache.insert(file.specifier.clone(), file)
}
}
#[cfg(test)]
mod tests {
use super::*;
use deno_core::error::get_custom_error_class;
use deno_core::resolve_url;
use deno_core::resolve_url_or_path;
use deno_runtime::deno_web::Blob;
use deno_runtime::deno_web::InMemoryBlobPart;
use std::rc::Rc;
use tempfile::TempDir;
fn setup(
cache_setting: CacheSetting,
maybe_temp_dir: Option<Rc<TempDir>>,
) -> (FileFetcher, Rc<TempDir>) {
let (file_fetcher, temp_dir, _) =
setup_with_blob_store(cache_setting, maybe_temp_dir);
(file_fetcher, temp_dir)
}
fn setup_with_blob_store(
cache_setting: CacheSetting,
maybe_temp_dir: Option<Rc<TempDir>>,
) -> (FileFetcher, Rc<TempDir>, BlobStore) {
let temp_dir = maybe_temp_dir.unwrap_or_else(|| {
Rc::new(TempDir::new().expect("failed to create temp directory"))
});
let location = temp_dir.path().join("deps");
let blob_store = BlobStore::default();
let file_fetcher = FileFetcher::new(
HttpCache::new(&location),
cache_setting,
true,
None,
blob_store.clone(),
None,
)
.expect("setup failed");
(file_fetcher, temp_dir, blob_store)
}
macro_rules! file_url {
($path:expr) => {
if cfg!(target_os = "windows") {
concat!("file:///C:", $path)
} else {
concat!("file://", $path)
}
};
}
async fn test_fetch(specifier: &ModuleSpecifier) -> (File, FileFetcher) {
let (file_fetcher, _) = setup(CacheSetting::ReloadAll, None);
let result = file_fetcher
.fetch(specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
(result.unwrap(), file_fetcher)
}
async fn test_fetch_remote(
specifier: &ModuleSpecifier,
) -> (File, HashMap<String, String>) {
let _http_server_guard = test_util::http_server();
let (file_fetcher, _) = setup(CacheSetting::ReloadAll, None);
let result: Result<File, AnyError> = file_fetcher
.fetch_remote(specifier, &mut Permissions::allow_all(), 1)
.await;
assert!(result.is_ok());
let (_, headers) = file_fetcher.http_cache.get(specifier).unwrap();
(result.unwrap(), headers)
}
async fn test_fetch_remote_encoded(
fixture: &str,
charset: &str,
expected: &str,
) {
let url_str = format!("http://127.0.0.1:4545/encoding/{}", fixture);
let specifier = resolve_url(&url_str).unwrap();
let (file, headers) = test_fetch_remote(&specifier).await;
assert_eq!(file.source.as_str(), expected);
assert_eq!(file.media_type, MediaType::TypeScript);
assert_eq!(
headers.get("content-type").unwrap(),
&format!("application/typescript;charset={}", charset)
);
}
async fn test_fetch_local_encoded(charset: &str, expected: String) {
let p = test_util::testdata_path().join(format!("encoding/{}.ts", charset));
let specifier = resolve_url_or_path(p.to_str().unwrap()).unwrap();
let (file, _) = test_fetch(&specifier).await;
assert_eq!(file.source.as_str(), expected);
}
#[test]
fn test_get_validated_scheme() {
let fixtures = vec![
("https://deno.land/x/mod.ts", true, "https"),
("http://deno.land/x/mod.ts", true, "http"),
("file:///a/b/c.ts", true, "file"),
("file:///C:/a/b/c.ts", true, "file"),
("data:,some%20text", true, "data"),
("ftp://a/b/c.ts", false, ""),
("mailto:dino@deno.land", false, ""),
];
for (specifier, is_ok, expected) in fixtures {
let specifier = resolve_url_or_path(specifier).unwrap();
let actual = get_validated_scheme(&specifier);
assert_eq!(actual.is_ok(), is_ok);
if is_ok {
assert_eq!(actual.unwrap(), expected);
}
}
}
#[test]
fn test_strip_shebang() {
let value =
"#!/usr/bin/env deno\n\nconsole.log(\"hello deno!\");\n".to_string();
assert_eq!(strip_shebang(value), "\n\nconsole.log(\"hello deno!\");\n");
}
#[test]
fn test_map_content_type() {
let fixtures = vec![
// Extension only
(file_url!("/foo/bar.ts"), None, MediaType::TypeScript, None),
(file_url!("/foo/bar.tsx"), None, MediaType::Tsx, None),
(file_url!("/foo/bar.d.ts"), None, MediaType::Dts, None),
(file_url!("/foo/bar.js"), None, MediaType::JavaScript, None),
(file_url!("/foo/bar.jsx"), None, MediaType::Jsx, None),
(file_url!("/foo/bar.json"), None, MediaType::Json, None),
(file_url!("/foo/bar.wasm"), None, MediaType::Wasm, None),
(file_url!("/foo/bar.cjs"), None, MediaType::JavaScript, None),
(file_url!("/foo/bar.mjs"), None, MediaType::JavaScript, None),
(file_url!("/foo/bar"), None, MediaType::Unknown, None),
// Media type no extension
(
"https://deno.land/x/mod",
Some("application/typescript".to_string()),
MediaType::TypeScript,
None,
),
(
"https://deno.land/x/mod",
Some("text/typescript".to_string()),
MediaType::TypeScript,
None,
),
(
"https://deno.land/x/mod",
Some("video/vnd.dlna.mpeg-tts".to_string()),
MediaType::TypeScript,
None,
),
(
"https://deno.land/x/mod",
Some("video/mp2t".to_string()),
MediaType::TypeScript,
None,
),
(
"https://deno.land/x/mod",
Some("application/x-typescript".to_string()),
MediaType::TypeScript,
None,
),
(
"https://deno.land/x/mod",
Some("application/javascript".to_string()),
MediaType::JavaScript,
None,
),
(
"https://deno.land/x/mod",
Some("text/javascript".to_string()),
MediaType::JavaScript,
None,
),
(
"https://deno.land/x/mod",
Some("application/ecmascript".to_string()),
MediaType::JavaScript,
None,
),
(
"https://deno.land/x/mod",
Some("text/ecmascript".to_string()),
MediaType::JavaScript,
None,
),
(
"https://deno.land/x/mod",
Some("application/x-javascript".to_string()),
MediaType::JavaScript,
None,
),
(
"https://deno.land/x/mod",
Some("application/node".to_string()),
MediaType::JavaScript,
None,
),
(
"https://deno.land/x/mod",
Some("text/jsx".to_string()),
MediaType::Jsx,
None,
),
(
"https://deno.land/x/mod",
Some("text/tsx".to_string()),
MediaType::Tsx,
None,
),
(
"https://deno.land/x/mod",
Some("text/json".to_string()),
MediaType::Json,
None,
),
(
"https://deno.land/x/mod",
Some("text/json; charset=utf-8".to_string()),
MediaType::Json,
Some("utf-8".to_string()),
),
// Extension with media type
(
"https://deno.land/x/mod.ts",
Some("text/plain".to_string()),
MediaType::TypeScript,
None,
),
(
"https://deno.land/x/mod.ts",
Some("foo/bar".to_string()),
MediaType::Unknown,
None,
),
(
"https://deno.land/x/mod.tsx",
Some("application/typescript".to_string()),
MediaType::Tsx,
None,
),
(
"https://deno.land/x/mod.tsx",
Some("application/javascript".to_string()),
MediaType::Tsx,
None,
),
(
"https://deno.land/x/mod.jsx",
Some("application/javascript".to_string()),
MediaType::Jsx,
None,
),
(
"https://deno.land/x/mod.jsx",
Some("application/x-typescript".to_string()),
MediaType::Jsx,
None,
),
(
"https://deno.land/x/mod.d.ts",
Some("application/javascript".to_string()),
MediaType::JavaScript,
None,
),
(
"https://deno.land/x/mod.d.ts",
Some("text/plain".to_string()),
MediaType::Dts,
None,
),
(
"https://deno.land/x/mod.d.ts",
Some("application/x-typescript".to_string()),
MediaType::Dts,
None,
),
];
for (specifier, maybe_content_type, media_type, maybe_charset) in fixtures {
let specifier = resolve_url_or_path(specifier).unwrap();
assert_eq!(
map_content_type(&specifier, maybe_content_type),
(media_type, maybe_charset)
);
}
}
#[tokio::test]
async fn test_insert_cached() {
let (file_fetcher, temp_dir) = setup(CacheSetting::Use, None);
let local = temp_dir.path().join("a.ts");
let specifier =
resolve_url_or_path(local.as_os_str().to_str().unwrap()).unwrap();
let file = File {
local,
maybe_types: None,
media_type: MediaType::TypeScript,
source: Arc::new("some source code".to_string()),
specifier: specifier.clone(),
maybe_headers: None,
};
file_fetcher.insert_cached(file.clone());
let result = file_fetcher
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let result_file = result.unwrap();
assert_eq!(result_file, file);
}
#[tokio::test]
async fn test_get_source() {
let _http_server_guard = test_util::http_server();
let (file_fetcher, _) = setup(CacheSetting::Use, None);
let specifier =
resolve_url("http://localhost:4548/subdir/redirects/redirect1.js")
.unwrap();
let result = file_fetcher
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let maybe_file = file_fetcher.get_source(&specifier);
assert!(maybe_file.is_some());
let file = maybe_file.unwrap();
assert_eq!(file.source.as_str(), "export const redirect = 1;\n");
assert_eq!(
file.specifier,
resolve_url("http://localhost:4545/subdir/redirects/redirect1.js")
.unwrap()
);
}
#[test]
fn test_get_http_cache_location() {
let (file_fetcher, temp_dir) = setup(CacheSetting::Use, None);
let expected = temp_dir.path().join("deps");
let actual = file_fetcher.get_http_cache_location();
assert_eq!(actual, expected);
}
#[tokio::test]
async fn test_fetch_data_url() {
let (file_fetcher, _) = setup(CacheSetting::Use, None);
let specifier = resolve_url("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap();
let result = file_fetcher
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let file = result.unwrap();
assert_eq!(
file.source.as_str(),
"export const a = \"a\";\n\nexport enum A {\n A,\n B,\n C,\n}\n"
);
assert_eq!(file.media_type, MediaType::TypeScript);
assert_eq!(file.maybe_types, None);
assert_eq!(file.specifier, specifier);
}
#[tokio::test]
async fn test_fetch_blob_url() {
let (file_fetcher, _, blob_store) =
setup_with_blob_store(CacheSetting::Use, None);
let bytes =
"export const a = \"a\";\n\nexport enum A {\n A,\n B,\n C,\n}\n"
.as_bytes()
.to_vec();
let specifier = blob_store.insert_object_url(
Blob {
media_type: "application/typescript".to_string(),
parts: vec![Arc::new(InMemoryBlobPart::from(bytes))],
},
None,
);
let result = file_fetcher
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let file = result.unwrap();
assert_eq!(
file.source.as_str(),
"export const a = \"a\";\n\nexport enum A {\n A,\n B,\n C,\n}\n"
);
assert_eq!(file.media_type, MediaType::TypeScript);
assert_eq!(file.maybe_types, None);
assert_eq!(file.specifier, specifier);
}
#[tokio::test]
async fn test_fetch_complex() {
let _http_server_guard = test_util::http_server();
let (file_fetcher, temp_dir) = setup(CacheSetting::Use, None);
let (file_fetcher_01, _) = setup(CacheSetting::Use, Some(temp_dir.clone()));
let (file_fetcher_02, _) = setup(CacheSetting::Use, Some(temp_dir.clone()));
let specifier =
resolve_url_or_path("http://localhost:4545/subdir/mod2.ts").unwrap();
let result = file_fetcher
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let file = result.unwrap();
assert_eq!(
file.source.as_str(),
"export { printHello } from \"./print_hello.ts\";\n"
);
assert_eq!(file.media_type, MediaType::TypeScript);
let cache_filename = file_fetcher
.http_cache
.get_cache_filename(&specifier)
.unwrap();
let mut metadata =
crate::http_cache::Metadata::read(&cache_filename).unwrap();
metadata.headers = HashMap::new();
metadata
.headers
.insert("content-type".to_string(), "text/javascript".to_string());
metadata.write(&cache_filename).unwrap();
let result = file_fetcher_01
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let file = result.unwrap();
assert_eq!(
file.source.as_str(),
"export { printHello } from \"./print_hello.ts\";\n"
);
// This validates that when using the cached value, because we modified
// the value above.
assert_eq!(file.media_type, MediaType::JavaScript);
let (_, headers) = file_fetcher_02.http_cache.get(&specifier).unwrap();
assert_eq!(headers.get("content-type").unwrap(), "text/javascript");
metadata.headers = HashMap::new();
metadata
.headers
.insert("content-type".to_string(), "application/json".to_string());
metadata.write(&cache_filename).unwrap();
let result = file_fetcher_02
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let file = result.unwrap();
assert_eq!(
file.source.as_str(),
"export { printHello } from \"./print_hello.ts\";\n"
);
assert_eq!(file.media_type, MediaType::Json);
// This creates a totally new instance, simulating another Deno process
// invocation and indicates to "cache bust".
let location = temp_dir.path().join("deps");
let file_fetcher = FileFetcher::new(
HttpCache::new(&location),
CacheSetting::ReloadAll,
true,
None,
BlobStore::default(),
None,
)
.expect("setup failed");
let result = file_fetcher
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let file = result.unwrap();
assert_eq!(
file.source.as_str(),
"export { printHello } from \"./print_hello.ts\";\n"
);
assert_eq!(file.media_type, MediaType::TypeScript);
}
#[tokio::test]
async fn test_fetch_uses_cache() {
let _http_server_guard = test_util::http_server();
let temp_dir = TempDir::new()
.expect("could not create temp dir")
.into_path();
let location = temp_dir.join("deps");
let file_fetcher_01 = FileFetcher::new(
HttpCache::new(&location),
CacheSetting::Use,
true,
None,
BlobStore::default(),
None,
)
.expect("could not create file fetcher");
let specifier =
resolve_url("http://localhost:4545/subdir/mismatch_ext.ts").unwrap();
let cache_filename = file_fetcher_01
.http_cache
.get_cache_filename(&specifier)
.unwrap();
let result = file_fetcher_01
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let metadata_filename =
crate::http_cache::Metadata::filename(&cache_filename);
let metadata_file =
fs::File::open(metadata_filename).expect("could not open metadata file");
let metadata_file_metadata = metadata_file.metadata().unwrap();
let metadata_file_modified_01 = metadata_file_metadata.modified().unwrap();
let file_fetcher_02 = FileFetcher::new(
HttpCache::new(&location),
CacheSetting::Use,
true,
None,
BlobStore::default(),
None,
)
.expect("could not create file fetcher");
let result = file_fetcher_02
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let metadata_filename =
crate::http_cache::Metadata::filename(&cache_filename);
let metadata_file =
fs::File::open(metadata_filename).expect("could not open metadata file");
let metadata_file_metadata = metadata_file.metadata().unwrap();
let metadata_file_modified_02 = metadata_file_metadata.modified().unwrap();
assert_eq!(metadata_file_modified_01, metadata_file_modified_02);
// because we converted to a "fixed" directory, we need to cleanup after
// ourselves.
let _ = fs::remove_dir_all(temp_dir);
}
#[tokio::test]
async fn test_fetch_redirected() {
let _http_server_guard = test_util::http_server();
let (file_fetcher, _) = setup(CacheSetting::Use, None);
let specifier =
resolve_url("http://localhost:4546/subdir/redirects/redirect1.js")
.unwrap();
let cached_filename = file_fetcher
.http_cache
.get_cache_filename(&specifier)
.unwrap();
let redirected_specifier =
resolve_url("http://localhost:4545/subdir/redirects/redirect1.js")
.unwrap();
let redirected_cached_filename = file_fetcher
.http_cache
.get_cache_filename(&redirected_specifier)
.unwrap();
let result = file_fetcher
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let file = result.unwrap();
assert_eq!(file.specifier, redirected_specifier);
assert_eq!(
fs::read_to_string(cached_filename).unwrap(),
"",
"redirected files should have empty cached contents"
);
let (_, headers) = file_fetcher
.http_cache
.get(&specifier)
.expect("could not get file");
assert_eq!(
headers.get("location").unwrap(),
"http://localhost:4545/subdir/redirects/redirect1.js"
);
assert_eq!(
fs::read_to_string(redirected_cached_filename).unwrap(),
"export const redirect = 1;\n"
);
let (_, headers) = file_fetcher
.http_cache
.get(&redirected_specifier)
.expect("could not get file");
assert!(headers.get("location").is_none());
}
#[tokio::test]
async fn test_fetch_multiple_redirects() {
let _http_server_guard = test_util::http_server();
let (file_fetcher, _) = setup(CacheSetting::Use, None);
let specifier =
resolve_url("http://localhost:4548/subdir/redirects/redirect1.js")
.unwrap();
let cached_filename = file_fetcher
.http_cache
.get_cache_filename(&specifier)
.unwrap();
let redirected_01_specifier =
resolve_url("http://localhost:4546/subdir/redirects/redirect1.js")
.unwrap();
let redirected_01_cached_filename = file_fetcher
.http_cache
.get_cache_filename(&redirected_01_specifier)
.unwrap();
let redirected_02_specifier =
resolve_url("http://localhost:4545/subdir/redirects/redirect1.js")
.unwrap();
let redirected_02_cached_filename = file_fetcher
.http_cache
.get_cache_filename(&redirected_02_specifier)
.unwrap();
let result = file_fetcher
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let file = result.unwrap();
assert_eq!(file.specifier, redirected_02_specifier);
assert_eq!(
fs::read_to_string(cached_filename).unwrap(),
"",
"redirected files should have empty cached contents"
);
let (_, headers) = file_fetcher
.http_cache
.get(&specifier)
.expect("could not get file");
assert_eq!(
headers.get("location").unwrap(),
"http://localhost:4546/subdir/redirects/redirect1.js"
);
assert_eq!(
fs::read_to_string(redirected_01_cached_filename).unwrap(),
"",
"redirected files should have empty cached contents"
);
let (_, headers) = file_fetcher
.http_cache
.get(&redirected_01_specifier)
.expect("could not get file");
assert_eq!(
headers.get("location").unwrap(),
"http://localhost:4545/subdir/redirects/redirect1.js"
);
assert_eq!(
fs::read_to_string(redirected_02_cached_filename).unwrap(),
"export const redirect = 1;\n"
);
let (_, headers) = file_fetcher
.http_cache
.get(&redirected_02_specifier)
.expect("could not get file");
assert!(headers.get("location").is_none());
}
#[tokio::test]
async fn test_fetch_uses_cache_with_redirects() {
let _http_server_guard = test_util::http_server();
let temp_dir = TempDir::new()
.expect("could not create temp dir")
.into_path();
let location = temp_dir.join("deps");
let file_fetcher_01 = FileFetcher::new(
HttpCache::new(&location),
CacheSetting::Use,
true,
None,
BlobStore::default(),
None,
)
.expect("could not create file fetcher");
let specifier =
resolve_url("http://localhost:4548/subdir/mismatch_ext.ts").unwrap();
let redirected_specifier =
resolve_url("http://localhost:4546/subdir/mismatch_ext.ts").unwrap();
let redirected_cache_filename = file_fetcher_01
.http_cache
.get_cache_filename(&redirected_specifier)
.unwrap();
let result = file_fetcher_01
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let metadata_filename =
crate::http_cache::Metadata::filename(&redirected_cache_filename);
let metadata_file =
fs::File::open(metadata_filename).expect("could not open metadata file");
let metadata_file_metadata = metadata_file.metadata().unwrap();
let metadata_file_modified_01 = metadata_file_metadata.modified().unwrap();
let file_fetcher_02 = FileFetcher::new(
HttpCache::new(&location),
CacheSetting::Use,
true,
None,
BlobStore::default(),
None,
)
.expect("could not create file fetcher");
let result = file_fetcher_02
.fetch(&redirected_specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let metadata_filename =
crate::http_cache::Metadata::filename(&redirected_cache_filename);
let metadata_file =
fs::File::open(metadata_filename).expect("could not open metadata file");
let metadata_file_metadata = metadata_file.metadata().unwrap();
let metadata_file_modified_02 = metadata_file_metadata.modified().unwrap();
assert_eq!(metadata_file_modified_01, metadata_file_modified_02);
// because we converted to a "fixed" directory, we need to cleanup after
// ourselves.
let _ = fs::remove_dir_all(temp_dir);
}
#[tokio::test]
async fn test_fetcher_limits_redirects() {
let _http_server_guard = test_util::http_server();
let (file_fetcher, _) = setup(CacheSetting::Use, None);
let specifier =
resolve_url("http://localhost:4548/subdir/redirects/redirect1.js")
.unwrap();
let result = file_fetcher
.fetch_remote(&specifier, &mut Permissions::allow_all(), 2)
.await;
assert!(result.is_ok());
let result = file_fetcher
.fetch_remote(&specifier, &mut Permissions::allow_all(), 1)
.await;
assert!(result.is_err());
let result = file_fetcher.fetch_cached(&specifier, 2);
assert!(result.is_ok());
let result = file_fetcher.fetch_cached(&specifier, 1);
assert!(result.is_err());
}
#[tokio::test]
async fn test_fetch_same_host_redirect() {
let _http_server_guard = test_util::http_server();
let (file_fetcher, _) = setup(CacheSetting::Use, None);
let specifier = resolve_url(
"http://localhost:4550/REDIRECT/subdir/redirects/redirect1.js",
)
.unwrap();
let cached_filename = file_fetcher
.http_cache
.get_cache_filename(&specifier)
.unwrap();
let redirected_specifier =
resolve_url("http://localhost:4550/subdir/redirects/redirect1.js")
.unwrap();
let redirected_cached_filename = file_fetcher
.http_cache
.get_cache_filename(&redirected_specifier)
.unwrap();
let result = file_fetcher
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let file = result.unwrap();
assert_eq!(file.specifier, redirected_specifier);
assert_eq!(
fs::read_to_string(cached_filename).unwrap(),
"",
"redirected files should have empty cached contents"
);
let (_, headers) = file_fetcher
.http_cache
.get(&specifier)
.expect("could not get file");
assert_eq!(
headers.get("location").unwrap(),
"/subdir/redirects/redirect1.js"
);
assert_eq!(
fs::read_to_string(redirected_cached_filename).unwrap(),
"export const redirect = 1;\n"
);
let (_, headers) = file_fetcher
.http_cache
.get(&redirected_specifier)
.expect("could not get file");
assert!(headers.get("location").is_none());
}
#[tokio::test]
async fn test_fetch_no_remote() {
let _http_server_guard = test_util::http_server();
let temp_dir = TempDir::new().expect("could not create temp dir");
let location = temp_dir.path().join("deps");
let file_fetcher = FileFetcher::new(
HttpCache::new(&location),
CacheSetting::Use,
false,
None,
BlobStore::default(),
None,
)
.expect("could not create file fetcher");
let specifier = resolve_url("http://localhost:4545/002_hello.ts").unwrap();
let result = file_fetcher
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(get_custom_error_class(&err), Some("NoRemote"));
assert_eq!(err.to_string(), "A remote specifier was requested: \"http://localhost:4545/002_hello.ts\", but --no-remote is specified.");
}
#[tokio::test]
async fn test_fetch_cache_only() {
let _http_server_guard = test_util::http_server();
let temp_dir = TempDir::new()
.expect("could not create temp dir")
.into_path();
let location = temp_dir.join("deps");
let file_fetcher_01 = FileFetcher::new(
HttpCache::new(&location),
CacheSetting::Only,
true,
None,
BlobStore::default(),
None,
)
.expect("could not create file fetcher");
let file_fetcher_02 = FileFetcher::new(
HttpCache::new(&location),
CacheSetting::Use,
true,
None,
BlobStore::default(),
None,
)
.expect("could not create file fetcher");
let specifier = resolve_url("http://localhost:4545/002_hello.ts").unwrap();
let result = file_fetcher_01
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(get_custom_error_class(&err), Some("NotFound"));
assert_eq!(err.to_string(), "Specifier not found in cache: \"http://localhost:4545/002_hello.ts\", --cached-only is specified.");
let result = file_fetcher_02
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let result = file_fetcher_01
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
// because we converted to a "fixed" directory, we need to cleanup after
// ourselves.
let _ = fs::remove_dir_all(temp_dir);
}
#[tokio::test]
async fn test_fetch_local_bypasses_file_cache() {
let (file_fetcher, temp_dir) = setup(CacheSetting::Use, None);
let fixture_path = temp_dir.path().join("mod.ts");
let specifier =
resolve_url_or_path(&fixture_path.to_string_lossy()).unwrap();
fs::write(fixture_path.clone(), r#"console.log("hello deno");"#)
.expect("could not write file");
let result = file_fetcher
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let file = result.unwrap();
assert_eq!(file.source.as_str(), r#"console.log("hello deno");"#);
fs::write(fixture_path, r#"console.log("goodbye deno");"#)
.expect("could not write file");
let result = file_fetcher
.fetch(&specifier, &mut Permissions::allow_all())
.await;
assert!(result.is_ok());
let file = result.unwrap();
assert_eq!(file.source.as_str(), r#"console.log("goodbye deno");"#);
}
#[tokio::test]
async fn test_fetch_local_utf_16be() {
let expected = String::from_utf8(
b"\xEF\xBB\xBFconsole.log(\"Hello World\");\x0A".to_vec(),
)
.unwrap();
test_fetch_local_encoded("utf-16be", expected).await;
}
#[tokio::test]
async fn test_fetch_local_utf_16le() {
let expected = String::from_utf8(
b"\xEF\xBB\xBFconsole.log(\"Hello World\");\x0A".to_vec(),
)
.unwrap();
test_fetch_local_encoded("utf-16le", expected).await;
}
#[tokio::test]
async fn test_fetch_local_utf8_with_bom() {
let expected = String::from_utf8(
b"\xEF\xBB\xBFconsole.log(\"Hello World\");\x0A".to_vec(),
)
.unwrap();
test_fetch_local_encoded("utf-8", expected).await;
}
#[tokio::test]
async fn test_fetch_remote_javascript_with_types() {
let specifier =
resolve_url_or_path("http://127.0.0.1:4545/xTypeScriptTypes.js").unwrap();
let (file, _) = test_fetch_remote(&specifier).await;
assert_eq!(
file.maybe_types,
Some("./xTypeScriptTypes.d.ts".to_string())
);
}
#[tokio::test]
async fn test_fetch_remote_jsx_with_types() {
let specifier =
resolve_url_or_path("http://127.0.0.1:4545/xTypeScriptTypes.jsx")
.unwrap();
let (file, _) = test_fetch_remote(&specifier).await;
assert_eq!(file.media_type, MediaType::Jsx,);
assert_eq!(
file.maybe_types,
Some("./xTypeScriptTypes.d.ts".to_string())
);
}
#[tokio::test]
async fn test_fetch_remote_typescript_with_types() {
let specifier =
resolve_url_or_path("http://127.0.0.1:4545/xTypeScriptTypes.ts").unwrap();
let (file, _) = test_fetch_remote(&specifier).await;
assert_eq!(file.maybe_types, None);
}
#[tokio::test]
async fn test_fetch_remote_utf16_le() {
let expected =
std::str::from_utf8(b"\xEF\xBB\xBFconsole.log(\"Hello World\");\x0A")
.unwrap();
test_fetch_remote_encoded("utf-16le.ts", "utf-16le", expected).await;
}
#[tokio::test]
async fn test_fetch_remote_utf16_be() {
let expected =
std::str::from_utf8(b"\xEF\xBB\xBFconsole.log(\"Hello World\");\x0A")
.unwrap();
test_fetch_remote_encoded("utf-16be.ts", "utf-16be", expected).await;
}
#[tokio::test]
async fn test_fetch_remote_window_1255() {
let expected = "console.log(\"\u{5E9}\u{5DC}\u{5D5}\u{5DD} \
\u{5E2}\u{5D5}\u{5DC}\u{5DD}\");\u{A}";
test_fetch_remote_encoded("windows-1255", "windows-1255", expected).await;
}
}