diff options
Diffstat (limited to 'cli/standalone')
-rw-r--r-- | cli/standalone/binary.rs | 314 | ||||
-rw-r--r-- | cli/standalone/file_system.rs | 337 | ||||
-rw-r--r-- | cli/standalone/mod.rs | 255 | ||||
-rw-r--r-- | cli/standalone/virtual_fs.rs | 983 |
4 files changed, 1804 insertions, 85 deletions
diff --git a/cli/standalone/binary.rs b/cli/standalone/binary.rs index 51d8db79e..9ccb39e54 100644 --- a/cli/standalone/binary.rs +++ b/cli/standalone/binary.rs @@ -1,10 +1,13 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +use std::collections::BTreeMap; +use std::env::current_exe; use std::io::Read; use std::io::Seek; use std::io::SeekFrom; use std::io::Write; use std::path::Path; +use std::path::PathBuf; use deno_ast::ModuleSpecifier; use deno_core::anyhow::Context; @@ -14,22 +17,112 @@ use deno_core::futures::AsyncReadExt; use deno_core::futures::AsyncSeekExt; use deno_core::serde_json; use deno_core::url::Url; +use deno_npm::registry::PackageDepNpmSchemeValueParseError; +use deno_npm::resolution::SerializedNpmResolutionSnapshot; use deno_runtime::permissions::PermissionsOptions; +use deno_semver::npm::NpmPackageReq; +use deno_semver::npm::NpmVersionReqSpecifierParseError; use log::Level; use serde::Deserialize; use serde::Serialize; +use crate::args::package_json::PackageJsonDepValueParseError; +use crate::args::package_json::PackageJsonDeps; use crate::args::CaData; use crate::args::CliOptions; use crate::args::CompileFlags; +use crate::args::PackageJsonDepsProvider; use crate::cache::DenoDir; use crate::file_fetcher::FileFetcher; use crate::http_util::HttpClient; +use crate::npm::CliNpmRegistryApi; +use crate::npm::CliNpmResolver; +use crate::npm::NpmCache; +use crate::npm::NpmResolution; use crate::util::progress_bar::ProgressBar; use crate::util::progress_bar::ProgressBarStyle; +use super::virtual_fs::FileBackedVfs; +use super::virtual_fs::VfsBuilder; +use super::virtual_fs::VfsRoot; +use super::virtual_fs::VirtualDirectory; + const MAGIC_TRAILER: &[u8; 8] = b"d3n0l4nd"; +#[derive(Serialize, Deserialize)] +enum SerializablePackageJsonDepValueParseError { + SchemeValue(String), + Specifier(String), + Unsupported { scheme: String }, +} + +impl SerializablePackageJsonDepValueParseError { + pub fn from_err(err: PackageJsonDepValueParseError) -> Self { + match err { + PackageJsonDepValueParseError::SchemeValue(err) => { + Self::SchemeValue(err.value) + } + PackageJsonDepValueParseError::Specifier(err) => { + Self::Specifier(err.source.to_string()) + } + PackageJsonDepValueParseError::Unsupported { scheme } => { + Self::Unsupported { scheme } + } + } + } + + pub fn into_err(self) -> PackageJsonDepValueParseError { + match self { + SerializablePackageJsonDepValueParseError::SchemeValue(value) => { + PackageJsonDepValueParseError::SchemeValue( + PackageDepNpmSchemeValueParseError { value }, + ) + } + SerializablePackageJsonDepValueParseError::Specifier(source) => { + PackageJsonDepValueParseError::Specifier( + NpmVersionReqSpecifierParseError { + source: monch::ParseErrorFailureError::new(source), + }, + ) + } + SerializablePackageJsonDepValueParseError::Unsupported { scheme } => { + PackageJsonDepValueParseError::Unsupported { scheme } + } + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct SerializablePackageJsonDeps( + BTreeMap< + String, + Result<NpmPackageReq, SerializablePackageJsonDepValueParseError>, + >, +); + +impl SerializablePackageJsonDeps { + pub fn from_deps(deps: PackageJsonDeps) -> Self { + Self( + deps + .into_iter() + .map(|(name, req)| { + let res = + req.map_err(SerializablePackageJsonDepValueParseError::from_err); + (name, res) + }) + .collect(), + ) + } + + pub fn into_deps(self) -> PackageJsonDeps { + self + .0 + .into_iter() + .map(|(name, res)| (name, res.map_err(|err| err.into_err()))) + .collect() + } +} + #[derive(Deserialize, Serialize)] pub struct Metadata { pub argv: Vec<String>, @@ -44,27 +137,74 @@ pub struct Metadata { pub unsafely_ignore_certificate_errors: Option<Vec<String>>, pub maybe_import_map: Option<(Url, String)>, pub entrypoint: ModuleSpecifier, + /// Whether this uses a node_modules directory (true) or the global cache (false). + pub node_modules_dir: bool, + pub npm_snapshot: Option<SerializedNpmResolutionSnapshot>, + pub package_json_deps: Option<SerializablePackageJsonDeps>, +} + +pub fn load_npm_vfs(root_dir_path: PathBuf) -> Result<FileBackedVfs, AnyError> { + let file_path = current_exe().unwrap(); + let mut file = std::fs::File::open(file_path)?; + file.seek(SeekFrom::End(-(TRAILER_SIZE as i64)))?; + let mut trailer = [0; TRAILER_SIZE]; + file.read_exact(&mut trailer)?; + let trailer = Trailer::parse(&trailer)?.unwrap(); + file.seek(SeekFrom::Start(trailer.npm_vfs_pos))?; + let mut vfs_data = vec![0; trailer.npm_vfs_len() as usize]; + file.read_exact(&mut vfs_data)?; + let mut dir: VirtualDirectory = serde_json::from_slice(&vfs_data)?; + + // align the name of the directory with the root dir + dir.name = root_dir_path + .file_name() + .unwrap() + .to_string_lossy() + .to_string(); + + let fs_root = VfsRoot { + dir, + root_path: root_dir_path, + start_file_offset: trailer.npm_files_pos, + }; + Ok(FileBackedVfs::new(file, fs_root)) } -pub fn write_binary_bytes( +fn write_binary_bytes( writer: &mut impl Write, original_bin: Vec<u8>, metadata: &Metadata, eszip: eszip::EszipV2, + npm_vfs: Option<&VirtualDirectory>, + npm_files: &Vec<Vec<u8>>, ) -> Result<(), AnyError> { let metadata = serde_json::to_string(metadata)?.as_bytes().to_vec(); + let npm_vfs = serde_json::to_string(&npm_vfs)?.as_bytes().to_vec(); let eszip_archive = eszip.into_bytes(); - let eszip_pos = original_bin.len(); - let metadata_pos = eszip_pos + eszip_archive.len(); - let mut trailer = MAGIC_TRAILER.to_vec(); - trailer.write_all(&eszip_pos.to_be_bytes())?; - trailer.write_all(&metadata_pos.to_be_bytes())?; - writer.write_all(&original_bin)?; writer.write_all(&eszip_archive)?; writer.write_all(&metadata)?; - writer.write_all(&trailer)?; + writer.write_all(&npm_vfs)?; + for file in npm_files { + writer.write_all(file)?; + } + + // write the trailer, which includes the positions + // of the data blocks in the file + writer.write_all(&{ + let eszip_pos = original_bin.len() as u64; + let metadata_pos = eszip_pos + (eszip_archive.len() as u64); + let npm_vfs_pos = metadata_pos + (metadata.len() as u64); + let npm_files_pos = npm_vfs_pos + (npm_vfs.len() as u64); + Trailer { + eszip_pos, + metadata_pos, + npm_vfs_pos, + npm_files_pos, + } + .as_bytes() + })?; Ok(()) } @@ -73,12 +213,15 @@ pub fn is_standalone_binary(exe_path: &Path) -> bool { let Ok(mut output_file) = std::fs::File::open(exe_path) else { return false; }; - if output_file.seek(SeekFrom::End(-24)).is_err() { + if output_file + .seek(SeekFrom::End(-(TRAILER_SIZE as i64))) + .is_err() + { // This seek may fail because the file is too small to possibly be // `deno compile` output. return false; } - let mut trailer = [0; 24]; + let mut trailer = [0; TRAILER_SIZE]; if output_file.read_exact(&mut trailer).is_err() { return false; }; @@ -88,13 +231,9 @@ pub fn is_standalone_binary(exe_path: &Path) -> bool { /// This function will try to run this binary as a standalone binary /// produced by `deno compile`. It determines if this is a standalone -/// binary by checking for the magic trailer string `d3n0l4nd` at EOF-24 (8 bytes * 3). -/// The magic trailer is followed by: -/// - a u64 pointer to the JS bundle embedded in the binary -/// - a u64 pointer to JSON metadata (serialized flags) embedded in the binary -/// These are dereferenced, and the bundle is executed under the configuration -/// specified by the metadata. If no magic trailer is present, this function -/// exits with `Ok(None)`. +/// binary by skipping over the trailer width at the end of the file, +/// then checking for the magic trailer string `d3n0l4nd`. If found, +/// the bundle is executed. If not, this function exits with `Ok(None)`. pub async fn extract_standalone( exe_path: &Path, cli_args: Vec<String>, @@ -104,21 +243,17 @@ pub async fn extract_standalone( let mut bufreader = deno_core::futures::io::BufReader::new(AllowStdIo::new(file)); - let trailer_pos = bufreader.seek(SeekFrom::End(-24)).await?; - let mut trailer = [0; 24]; + let _trailer_pos = bufreader + .seek(SeekFrom::End(-(TRAILER_SIZE as i64))) + .await?; + let mut trailer = [0; TRAILER_SIZE]; bufreader.read_exact(&mut trailer).await?; - let (magic_trailer, rest) = trailer.split_at(8); - if magic_trailer != MAGIC_TRAILER { - return Ok(None); - } - - let (eszip_archive_pos, rest) = rest.split_at(8); - let metadata_pos = rest; - let eszip_archive_pos = u64_from_bytes(eszip_archive_pos)?; - let metadata_pos = u64_from_bytes(metadata_pos)?; - let metadata_len = trailer_pos - metadata_pos; + let trailer = match Trailer::parse(&trailer)? { + None => return Ok(None), + Some(trailer) => trailer, + }; - bufreader.seek(SeekFrom::Start(eszip_archive_pos)).await?; + bufreader.seek(SeekFrom::Start(trailer.eszip_pos)).await?; let (eszip, loader) = eszip::EszipV2::parse(bufreader) .await @@ -126,12 +261,14 @@ pub async fn extract_standalone( let mut bufreader = loader.await.context("Failed to parse eszip archive")?; - bufreader.seek(SeekFrom::Start(metadata_pos)).await?; + bufreader + .seek(SeekFrom::Start(trailer.metadata_pos)) + .await?; let mut metadata = String::new(); bufreader - .take(metadata_len) + .take(trailer.metadata_len()) .read_to_string(&mut metadata) .await .context("Failed to read metadata from the current executable")?; @@ -142,6 +279,57 @@ pub async fn extract_standalone( Ok(Some((metadata, eszip))) } +const TRAILER_SIZE: usize = std::mem::size_of::<Trailer>() + 8; // 8 bytes for the magic trailer string + +struct Trailer { + eszip_pos: u64, + metadata_pos: u64, + npm_vfs_pos: u64, + npm_files_pos: u64, +} + +impl Trailer { + pub fn parse(trailer: &[u8]) -> Result<Option<Trailer>, AnyError> { + let (magic_trailer, rest) = trailer.split_at(8); + if magic_trailer != MAGIC_TRAILER { + return Ok(None); + } + + let (eszip_archive_pos, rest) = rest.split_at(8); + let (metadata_pos, rest) = rest.split_at(8); + let (npm_vfs_pos, npm_files_pos) = rest.split_at(8); + let eszip_archive_pos = u64_from_bytes(eszip_archive_pos)?; + let metadata_pos = u64_from_bytes(metadata_pos)?; + let npm_vfs_pos = u64_from_bytes(npm_vfs_pos)?; + let npm_files_pos = u64_from_bytes(npm_files_pos)?; + Ok(Some(Trailer { + eszip_pos: eszip_archive_pos, + metadata_pos, + npm_vfs_pos, + npm_files_pos, + })) + } + + pub fn metadata_len(&self) -> u64 { + self.npm_vfs_pos - self.metadata_pos + } + + pub fn npm_vfs_len(&self) -> u64 { + self.npm_files_pos - self.npm_vfs_pos + } + + pub fn as_bytes(&self) -> Vec<u8> { + let mut trailer = MAGIC_TRAILER.to_vec(); + trailer.write_all(&self.eszip_pos.to_be_bytes()).unwrap(); + trailer.write_all(&self.metadata_pos.to_be_bytes()).unwrap(); + trailer.write_all(&self.npm_vfs_pos.to_be_bytes()).unwrap(); + trailer + .write_all(&self.npm_files_pos.to_be_bytes()) + .unwrap(); + trailer + } +} + fn u64_from_bytes(arr: &[u8]) -> Result<u64, AnyError> { let fixed_arr: &[u8; 8] = arr .try_into() @@ -153,18 +341,34 @@ pub struct DenoCompileBinaryWriter<'a> { file_fetcher: &'a FileFetcher, client: &'a HttpClient, deno_dir: &'a DenoDir, + npm_api: &'a CliNpmRegistryApi, + npm_cache: &'a NpmCache, + npm_resolver: &'a CliNpmResolver, + resolution: &'a NpmResolution, + package_json_deps_provider: &'a PackageJsonDepsProvider, } impl<'a> DenoCompileBinaryWriter<'a> { + #[allow(clippy::too_many_arguments)] pub fn new( file_fetcher: &'a FileFetcher, client: &'a HttpClient, deno_dir: &'a DenoDir, + npm_api: &'a CliNpmRegistryApi, + npm_cache: &'a NpmCache, + npm_resolver: &'a CliNpmResolver, + resolution: &'a NpmResolution, + package_json_deps_provider: &'a PackageJsonDepsProvider, ) -> Self { Self { file_fetcher, client, deno_dir, + npm_api, + npm_cache, + npm_resolver, + resolution, + package_json_deps_provider, } } @@ -284,6 +488,14 @@ impl<'a> DenoCompileBinaryWriter<'a> { .resolve_import_map(self.file_fetcher) .await? .map(|import_map| (import_map.base_url().clone(), import_map.to_json())); + let (npm_snapshot, npm_vfs, npm_files) = if self.resolution.has_packages() { + let (root_dir, files) = self.build_vfs()?.into_dir_and_files(); + let snapshot = self.resolution.serialized_snapshot(); + (Some(snapshot), Some(root_dir), files) + } else { + (None, None, Vec::new()) + }; + let metadata = Metadata { argv: compile_flags.args.clone(), unstable: cli_options.unstable(), @@ -299,8 +511,44 @@ impl<'a> DenoCompileBinaryWriter<'a> { ca_data, entrypoint: entrypoint.clone(), maybe_import_map, + node_modules_dir: self.npm_resolver.node_modules_path().is_some(), + npm_snapshot, + package_json_deps: self + .package_json_deps_provider + .deps() + .map(|deps| SerializablePackageJsonDeps::from_deps(deps.clone())), }; - write_binary_bytes(writer, original_bin, &metadata, eszip) + write_binary_bytes( + writer, + original_bin, + &metadata, + eszip, + npm_vfs.as_ref(), + &npm_files, + ) + } + + fn build_vfs(&self) -> Result<VfsBuilder, AnyError> { + if let Some(node_modules_path) = self.npm_resolver.node_modules_path() { + let mut builder = VfsBuilder::new(node_modules_path.clone()); + builder.add_dir_recursive(&node_modules_path)?; + Ok(builder) + } else { + // DO NOT include the user's registry url as it may contain credentials, + // but also don't make this dependent on the registry url + let registry_url = self.npm_api.base_url(); + let root_path = self.npm_cache.registry_folder(registry_url); + let mut builder = VfsBuilder::new(root_path); + for package in self.resolution.all_packages() { + let folder = self + .npm_resolver + .resolve_pkg_folder_from_pkg_id(&package.pkg_id)?; + builder.add_dir_recursive(&folder)?; + } + // overwrite the root directory's name to obscure the user's registry url + builder.set_root_dir_name("node_modules".to_string()); + Ok(builder) + } } } diff --git a/cli/standalone/file_system.rs b/cli/standalone/file_system.rs new file mode 100644 index 000000000..f0891f71d --- /dev/null +++ b/cli/standalone/file_system.rs @@ -0,0 +1,337 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use std::path::Path; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; + +use deno_runtime::deno_fs::FileSystem; +use deno_runtime::deno_fs::FsDirEntry; +use deno_runtime::deno_fs::FsFileType; +use deno_runtime::deno_fs::OpenOptions; +use deno_runtime::deno_fs::RealFs; +use deno_runtime::deno_io::fs::File; +use deno_runtime::deno_io::fs::FsError; +use deno_runtime::deno_io::fs::FsResult; +use deno_runtime::deno_io::fs::FsStat; + +use super::virtual_fs::FileBackedVfs; + +#[derive(Debug, Clone)] +pub struct DenoCompileFileSystem(Arc<FileBackedVfs>); + +impl DenoCompileFileSystem { + pub fn new(vfs: FileBackedVfs) -> Self { + Self(Arc::new(vfs)) + } + + fn error_if_in_vfs(&self, path: &Path) -> FsResult<()> { + if self.0.is_path_within(path) { + Err(FsError::NotSupported) + } else { + Ok(()) + } + } + + fn copy_to_real_path(&self, oldpath: &Path, newpath: &Path) -> FsResult<()> { + let old_file = self.0.file_entry(oldpath)?; + let old_file_bytes = self.0.read_file_all(old_file)?; + RealFs.write_file_sync( + newpath, + OpenOptions { + read: false, + write: true, + create: true, + truncate: true, + append: false, + create_new: false, + mode: None, + }, + &old_file_bytes, + ) + } +} + +#[async_trait::async_trait(?Send)] +impl FileSystem for DenoCompileFileSystem { + fn cwd(&self) -> FsResult<PathBuf> { + RealFs.cwd() + } + + fn tmp_dir(&self) -> FsResult<PathBuf> { + RealFs.tmp_dir() + } + + fn chdir(&self, path: &Path) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.chdir(path) + } + + fn umask(&self, mask: Option<u32>) -> FsResult<u32> { + RealFs.umask(mask) + } + + fn open_sync( + &self, + path: &Path, + options: OpenOptions, + ) -> FsResult<Rc<dyn File>> { + if self.0.is_path_within(path) { + Ok(self.0.open_file(path)?) + } else { + RealFs.open_sync(path, options) + } + } + async fn open_async( + &self, + path: PathBuf, + options: OpenOptions, + ) -> FsResult<Rc<dyn File>> { + if self.0.is_path_within(&path) { + Ok(self.0.open_file(&path)?) + } else { + RealFs.open_async(path, options).await + } + } + + fn mkdir_sync( + &self, + path: &Path, + recursive: bool, + mode: u32, + ) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.mkdir_sync(path, recursive, mode) + } + async fn mkdir_async( + &self, + path: PathBuf, + recursive: bool, + mode: u32, + ) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs.mkdir_async(path, recursive, mode).await + } + + fn chmod_sync(&self, path: &Path, mode: u32) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.chmod_sync(path, mode) + } + async fn chmod_async(&self, path: PathBuf, mode: u32) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs.chmod_async(path, mode).await + } + + fn chown_sync( + &self, + path: &Path, + uid: Option<u32>, + gid: Option<u32>, + ) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.chown_sync(path, uid, gid) + } + async fn chown_async( + &self, + path: PathBuf, + uid: Option<u32>, + gid: Option<u32>, + ) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs.chown_async(path, uid, gid).await + } + + fn remove_sync(&self, path: &Path, recursive: bool) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.remove_sync(path, recursive) + } + async fn remove_async(&self, path: PathBuf, recursive: bool) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs.remove_async(path, recursive).await + } + + fn copy_file_sync(&self, oldpath: &Path, newpath: &Path) -> FsResult<()> { + self.error_if_in_vfs(newpath)?; + if self.0.is_path_within(oldpath) { + self.copy_to_real_path(oldpath, newpath) + } else { + RealFs.copy_file_sync(oldpath, newpath) + } + } + async fn copy_file_async( + &self, + oldpath: PathBuf, + newpath: PathBuf, + ) -> FsResult<()> { + self.error_if_in_vfs(&newpath)?; + if self.0.is_path_within(&oldpath) { + let fs = self.clone(); + tokio::task::spawn_blocking(move || { + fs.copy_to_real_path(&oldpath, &newpath) + }) + .await? + } else { + RealFs.copy_file_async(oldpath, newpath).await + } + } + + fn stat_sync(&self, path: &Path) -> FsResult<FsStat> { + if self.0.is_path_within(path) { + Ok(self.0.stat(path)?) + } else { + RealFs.stat_sync(path) + } + } + async fn stat_async(&self, path: PathBuf) -> FsResult<FsStat> { + if self.0.is_path_within(&path) { + Ok(self.0.stat(&path)?) + } else { + RealFs.stat_async(path).await + } + } + + fn lstat_sync(&self, path: &Path) -> FsResult<FsStat> { + if self.0.is_path_within(path) { + Ok(self.0.lstat(path)?) + } else { + RealFs.lstat_sync(path) + } + } + async fn lstat_async(&self, path: PathBuf) -> FsResult<FsStat> { + if self.0.is_path_within(&path) { + Ok(self.0.lstat(&path)?) + } else { + RealFs.lstat_async(path).await + } + } + + fn realpath_sync(&self, path: &Path) -> FsResult<PathBuf> { + if self.0.is_path_within(path) { + Ok(self.0.canonicalize(path)?) + } else { + RealFs.realpath_sync(path) + } + } + async fn realpath_async(&self, path: PathBuf) -> FsResult<PathBuf> { + if self.0.is_path_within(&path) { + Ok(self.0.canonicalize(&path)?) + } else { + RealFs.realpath_async(path).await + } + } + + fn read_dir_sync(&self, path: &Path) -> FsResult<Vec<FsDirEntry>> { + if self.0.is_path_within(path) { + Ok(self.0.read_dir(path)?) + } else { + RealFs.read_dir_sync(path) + } + } + async fn read_dir_async(&self, path: PathBuf) -> FsResult<Vec<FsDirEntry>> { + if self.0.is_path_within(&path) { + Ok(self.0.read_dir(&path)?) + } else { + RealFs.read_dir_async(path).await + } + } + + fn rename_sync(&self, oldpath: &Path, newpath: &Path) -> FsResult<()> { + self.error_if_in_vfs(oldpath)?; + self.error_if_in_vfs(newpath)?; + RealFs.rename_sync(oldpath, newpath) + } + async fn rename_async( + &self, + oldpath: PathBuf, + newpath: PathBuf, + ) -> FsResult<()> { + self.error_if_in_vfs(&oldpath)?; + self.error_if_in_vfs(&newpath)?; + RealFs.rename_async(oldpath, newpath).await + } + + fn link_sync(&self, oldpath: &Path, newpath: &Path) -> FsResult<()> { + self.error_if_in_vfs(oldpath)?; + self.error_if_in_vfs(newpath)?; + RealFs.link_sync(oldpath, newpath) + } + async fn link_async( + &self, + oldpath: PathBuf, + newpath: PathBuf, + ) -> FsResult<()> { + self.error_if_in_vfs(&oldpath)?; + self.error_if_in_vfs(&newpath)?; + RealFs.link_async(oldpath, newpath).await + } + + fn symlink_sync( + &self, + oldpath: &Path, + newpath: &Path, + file_type: Option<FsFileType>, + ) -> FsResult<()> { + self.error_if_in_vfs(oldpath)?; + self.error_if_in_vfs(newpath)?; + RealFs.symlink_sync(oldpath, newpath, file_type) + } + async fn symlink_async( + &self, + oldpath: PathBuf, + newpath: PathBuf, + file_type: Option<FsFileType>, + ) -> FsResult<()> { + self.error_if_in_vfs(&oldpath)?; + self.error_if_in_vfs(&newpath)?; + RealFs.symlink_async(oldpath, newpath, file_type).await + } + + fn read_link_sync(&self, path: &Path) -> FsResult<PathBuf> { + if self.0.is_path_within(path) { + Ok(self.0.read_link(path)?) + } else { + RealFs.read_link_sync(path) + } + } + async fn read_link_async(&self, path: PathBuf) -> FsResult<PathBuf> { + if self.0.is_path_within(&path) { + Ok(self.0.read_link(&path)?) + } else { + RealFs.read_link_async(path).await + } + } + + fn truncate_sync(&self, path: &Path, len: u64) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.truncate_sync(path, len) + } + async fn truncate_async(&self, path: PathBuf, len: u64) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs.truncate_async(path, len).await + } + + fn utime_sync( + &self, + path: &Path, + atime_secs: i64, + atime_nanos: u32, + mtime_secs: i64, + mtime_nanos: u32, + ) -> FsResult<()> { + self.error_if_in_vfs(path)?; + RealFs.utime_sync(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) + } + async fn utime_async( + &self, + path: PathBuf, + atime_secs: i64, + atime_nanos: u32, + mtime_secs: i64, + mtime_nanos: u32, + ) -> FsResult<()> { + self.error_if_in_vfs(&path)?; + RealFs + .utime_async(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) + .await + } +} diff --git a/cli/standalone/mod.rs b/cli/standalone/mod.rs index e00ab8ab2..db2743be8 100644 --- a/cli/standalone/mod.rs +++ b/cli/standalone/mod.rs @@ -1,17 +1,25 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use crate::args::get_root_cert_store; +use crate::args::npm_pkg_req_ref_to_binary_command; use crate::args::CaData; use crate::args::CacheSetting; +use crate::args::PackageJsonDepsProvider; use crate::args::StorageKeyResolver; +use crate::cache::Caches; use crate::cache::DenoDir; +use crate::cache::NodeAnalysisCache; use crate::file_fetcher::get_source_from_data_url; use crate::http_util::HttpClient; +use crate::module_loader::CjsResolutionStore; +use crate::module_loader::NpmModuleLoader; +use crate::node::CliCjsEsmCodeAnalyzer; use crate::npm::create_npm_fs_resolver; use crate::npm::CliNpmRegistryApi; use crate::npm::CliNpmResolver; use crate::npm::NpmCache; use crate::npm::NpmResolution; +use crate::resolver::MappedSpecifierResolver; use crate::util::progress_bar::ProgressBar; use crate::util::progress_bar::ProgressBarStyle; use crate::util::v8::construct_v8_flags; @@ -19,7 +27,7 @@ use crate::worker::CliMainWorkerFactory; use crate::worker::CliMainWorkerOptions; use crate::worker::HasNodeSpecifierChecker; use crate::worker::ModuleLoaderFactory; -use crate::CliGraphResolver; +use deno_ast::MediaType; use deno_core::anyhow::Context; use deno_core::error::type_error; use deno_core::error::AnyError; @@ -29,31 +37,44 @@ use deno_core::ModuleLoader; use deno_core::ModuleSpecifier; use deno_core::ModuleType; use deno_core::ResolutionKind; -use deno_graph::source::Resolver; use deno_runtime::deno_fs; +use deno_runtime::deno_node; +use deno_runtime::deno_node::analyze::NodeCodeTranslator; use deno_runtime::deno_node::NodeResolver; use deno_runtime::deno_tls::rustls::RootCertStore; use deno_runtime::deno_tls::RootCertStoreProvider; use deno_runtime::deno_web::BlobStore; use deno_runtime::permissions::Permissions; use deno_runtime::permissions::PermissionsContainer; +use deno_semver::npm::NpmPackageReqReference; use import_map::parse_from_json; use std::pin::Pin; use std::rc::Rc; use std::sync::Arc; mod binary; +mod file_system; +mod virtual_fs; pub use binary::extract_standalone; pub use binary::is_standalone_binary; pub use binary::DenoCompileBinaryWriter; +use self::binary::load_npm_vfs; use self::binary::Metadata; +use self::file_system::DenoCompileFileSystem; + +struct SharedModuleLoaderState { + eszip: eszip::EszipV2, + mapped_specifier_resolver: MappedSpecifierResolver, + npm_module_loader: Arc<NpmModuleLoader>, +} #[derive(Clone)] struct EmbeddedModuleLoader { - eszip: Arc<eszip::EszipV2>, - maybe_import_map_resolver: Option<Arc<CliGraphResolver>>, + shared: Arc<SharedModuleLoaderState>, + root_permissions: PermissionsContainer, + dynamic_permissions: PermissionsContainer, } impl ModuleLoader for EmbeddedModuleLoader { @@ -61,10 +82,10 @@ impl ModuleLoader for EmbeddedModuleLoader { &self, specifier: &str, referrer: &str, - _kind: ResolutionKind, + kind: ResolutionKind, ) -> Result<ModuleSpecifier, AnyError> { // Try to follow redirects when resolving. - let referrer = match self.eszip.get_module(referrer) { + let referrer = match self.shared.eszip.get_module(referrer) { Some(eszip::Module { ref specifier, .. }) => { ModuleSpecifier::parse(specifier)? } @@ -74,27 +95,93 @@ impl ModuleLoader for EmbeddedModuleLoader { } }; - self - .maybe_import_map_resolver + let permissions = if matches!(kind, ResolutionKind::DynamicImport) { + &self.dynamic_permissions + } else { + &self.root_permissions + }; + + if let Some(result) = self + .shared + .npm_module_loader + .resolve_if_in_npm_package(specifier, &referrer, permissions) + { + return result; + } + + let maybe_mapped = self + .shared + .mapped_specifier_resolver + .resolve(specifier, &referrer)? + .into_specifier(); + + // npm specifier + let specifier_text = maybe_mapped .as_ref() - .map(|r| r.resolve(specifier, &referrer)) - .unwrap_or_else(|| { - deno_core::resolve_import(specifier, referrer.as_str()) - .map_err(|err| err.into()) - }) + .map(|r| r.as_str()) + .unwrap_or(specifier); + if let Ok(reference) = NpmPackageReqReference::from_str(specifier_text) { + return self + .shared + .npm_module_loader + .resolve_req_reference(&reference, permissions); + } + + // Built-in Node modules + if let Some(module_name) = specifier_text.strip_prefix("node:") { + return deno_node::resolve_builtin_node_module(module_name); + } + + match maybe_mapped { + Some(resolved) => Ok(resolved), + None => deno_core::resolve_import(specifier, referrer.as_str()) + .map_err(|err| err.into()), + } } fn load( &self, module_specifier: &ModuleSpecifier, - _maybe_referrer: Option<&ModuleSpecifier>, - _is_dynamic: bool, + maybe_referrer: Option<&ModuleSpecifier>, + is_dynamic: bool, ) -> Pin<Box<deno_core::ModuleSourceFuture>> { let is_data_uri = get_source_from_data_url(module_specifier).ok(); + let permissions = if is_dynamic { + &self.dynamic_permissions + } else { + &self.root_permissions + }; + + if let Some(result) = + self.shared.npm_module_loader.load_sync_if_in_npm_package( + module_specifier, + maybe_referrer, + permissions, + ) + { + return match result { + Ok(code_source) => Box::pin(deno_core::futures::future::ready(Ok( + deno_core::ModuleSource::new_with_redirect( + match code_source.media_type { + MediaType::Json => ModuleType::Json, + _ => ModuleType::JavaScript, + }, + code_source.code, + module_specifier, + &code_source.found_url, + ), + ))), + Err(err) => Box::pin(deno_core::futures::future::ready(Err(err))), + }; + } + let module = self + .shared .eszip .get_module(module_specifier.as_str()) - .ok_or_else(|| type_error("Module not found")); + .ok_or_else(|| { + type_error(format!("Module not found: {}", module_specifier)) + }); // TODO(mmastrac): This clone can probably be removed in the future if ModuleSpecifier is no longer a full-fledged URL let module_specifier = module_specifier.clone(); @@ -128,24 +215,32 @@ impl ModuleLoader for EmbeddedModuleLoader { } struct StandaloneModuleLoaderFactory { - loader: EmbeddedModuleLoader, + shared: Arc<SharedModuleLoaderState>, } impl ModuleLoaderFactory for StandaloneModuleLoaderFactory { fn create_for_main( &self, - _root_permissions: PermissionsContainer, - _dynamic_permissions: PermissionsContainer, + root_permissions: PermissionsContainer, + dynamic_permissions: PermissionsContainer, ) -> Rc<dyn ModuleLoader> { - Rc::new(self.loader.clone()) + Rc::new(EmbeddedModuleLoader { + shared: self.shared.clone(), + root_permissions, + dynamic_permissions, + }) } fn create_for_worker( &self, - _root_permissions: PermissionsContainer, - _dynamic_permissions: PermissionsContainer, + root_permissions: PermissionsContainer, + dynamic_permissions: PermissionsContainer, ) -> Rc<dyn ModuleLoader> { - Rc::new(self.loader.clone()) + Rc::new(EmbeddedModuleLoader { + shared: self.shared.clone(), + root_permissions, + dynamic_permissions, + }) } fn create_source_map_getter( @@ -183,6 +278,9 @@ pub async fn run( metadata: Metadata, ) -> Result<(), AnyError> { let main_module = &metadata.entrypoint; + let current_exe_path = std::env::current_exe().unwrap(); + let current_exe_name = + current_exe_path.file_name().unwrap().to_string_lossy(); let dir = DenoDir::new(None)?; let root_cert_store_provider = Arc::new(StandaloneRootCertStoreProvider { ca_stores: metadata.ca_stores, @@ -194,9 +292,14 @@ pub async fn run( Some(root_cert_store_provider.clone()), metadata.unsafely_ignore_certificate_errors.clone(), )); - let npm_registry_url = CliNpmRegistryApi::default_url().to_owned(); + // use a dummy npm registry url + let npm_registry_url = ModuleSpecifier::parse("https://localhost/").unwrap(); + let root_path = std::env::temp_dir() + .join(format!("deno-compile-{}", current_exe_name)) + .join("node_modules"); + let npm_cache = Arc::new(NpmCache::new( - dir.npm_folder_path(), + root_path.clone(), CacheSetting::Use, http_client.clone(), progress_bar.clone(), @@ -207,44 +310,92 @@ pub async fn run( http_client.clone(), progress_bar.clone(), )); - let fs = Arc::new(deno_fs::RealFs); - let npm_resolution = - Arc::new(NpmResolution::from_serialized(npm_api.clone(), None, None)); + let (fs, node_modules_path, snapshot) = if let Some(snapshot) = + metadata.npm_snapshot + { + let vfs_root_dir_path = if metadata.node_modules_dir { + root_path + } else { + npm_cache.registry_folder(&npm_registry_url) + }; + let vfs = + load_npm_vfs(vfs_root_dir_path).context("Failed to load npm vfs.")?; + let node_modules_path = if metadata.node_modules_dir { + Some(vfs.root().to_path_buf()) + } else { + None + }; + ( + Arc::new(DenoCompileFileSystem::new(vfs)) as Arc<dyn deno_fs::FileSystem>, + node_modules_path, + Some(snapshot.into_valid()?), + ) + } else { + ( + Arc::new(deno_fs::RealFs) as Arc<dyn deno_fs::FileSystem>, + None, + None, + ) + }; + let npm_resolution = Arc::new(NpmResolution::from_serialized( + npm_api.clone(), + snapshot, + None, + )); + let has_node_modules_dir = node_modules_path.is_some(); let npm_fs_resolver = create_npm_fs_resolver( fs.clone(), npm_cache, &progress_bar, npm_registry_url, npm_resolution.clone(), - None, + node_modules_path, ); let npm_resolver = Arc::new(CliNpmResolver::new( + fs.clone(), npm_resolution.clone(), npm_fs_resolver, None, )); let node_resolver = Arc::new(NodeResolver::new(fs.clone(), npm_resolver.clone())); + let cjs_resolutions = Arc::new(CjsResolutionStore::default()); + let cache_db = Caches::new(dir.clone()); + let node_analysis_cache = NodeAnalysisCache::new(cache_db.node_analysis_db()); + let cjs_esm_code_analyzer = CliCjsEsmCodeAnalyzer::new(node_analysis_cache); + let node_code_translator = Arc::new(NodeCodeTranslator::new( + cjs_esm_code_analyzer, + fs.clone(), + node_resolver.clone(), + npm_resolver.clone(), + )); + let package_json_deps_provider = Arc::new(PackageJsonDepsProvider::new( + metadata + .package_json_deps + .map(|serialized| serialized.into_deps()), + )); + let maybe_import_map = metadata.maybe_import_map.map(|(base, source)| { + Arc::new(parse_from_json(&base, &source).unwrap().import_map) + }); let module_loader_factory = StandaloneModuleLoaderFactory { - loader: EmbeddedModuleLoader { - eszip: Arc::new(eszip), - maybe_import_map_resolver: metadata.maybe_import_map.map( - |(base, source)| { - Arc::new(CliGraphResolver::new( - None, - Some(Arc::new( - parse_from_json(&base, &source).unwrap().import_map, - )), - false, - npm_api.clone(), - npm_resolution.clone(), - Default::default(), - )) - }, + shared: Arc::new(SharedModuleLoaderState { + eszip, + mapped_specifier_resolver: MappedSpecifierResolver::new( + maybe_import_map.clone(), + package_json_deps_provider.clone(), ), - }, + npm_module_loader: Arc::new(NpmModuleLoader::new( + cjs_resolutions, + node_code_translator, + fs.clone(), + node_resolver.clone(), + )), + }), }; + let permissions = PermissionsContainer::new(Permissions::from_options( + &metadata.permissions, + )?); let worker_factory = CliMainWorkerFactory::new( StorageKeyResolver::empty(), npm_resolver.clone(), @@ -260,14 +411,17 @@ pub async fn run( debug: false, coverage_dir: None, enable_testing_features: false, - has_node_modules_dir: false, + has_node_modules_dir, inspect_brk: false, inspect_wait: false, is_inspecting: false, - is_npm_main: false, + is_npm_main: main_module.scheme() == "npm", location: metadata.location, - // todo(dsherret): support a binary command being compiled - maybe_binary_npm_command_name: None, + maybe_binary_npm_command_name: NpmPackageReqReference::from_specifier( + main_module, + ) + .ok() + .map(|req_ref| npm_pkg_req_ref_to_binary_command(&req_ref)), origin_data_folder_path: None, seed: metadata.seed, unsafely_ignore_certificate_errors: metadata @@ -278,9 +432,6 @@ pub async fn run( v8_set_flags(construct_v8_flags(&metadata.v8_flags, vec![])); - let permissions = PermissionsContainer::new(Permissions::from_options( - &metadata.permissions, - )?); let mut worker = worker_factory .create_main_worker(main_module.clone(), permissions) .await?; diff --git a/cli/standalone/virtual_fs.rs b/cli/standalone/virtual_fs.rs new file mode 100644 index 000000000..9c0601bcc --- /dev/null +++ b/cli/standalone/virtual_fs.rs @@ -0,0 +1,983 @@ +// 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::fs::File; +use std::io::Read; +use std::io::Seek; +use std::io::SeekFrom; +use std::path::Path; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; + +use deno_core::anyhow::Context; +use deno_core::error::AnyError; +use deno_core::parking_lot::Mutex; +use deno_core::BufMutView; +use deno_core::BufView; +use deno_runtime::deno_fs::FsDirEntry; +use deno_runtime::deno_io; +use deno_runtime::deno_io::fs::FsError; +use deno_runtime::deno_io::fs::FsResult; +use deno_runtime::deno_io::fs::FsStat; +use serde::Deserialize; +use serde::Serialize; + +use crate::util; + +pub struct VfsBuilder { + root_path: PathBuf, + root_dir: VirtualDirectory, + files: Vec<Vec<u8>>, + current_offset: u64, + file_offsets: HashMap<String, u64>, +} + +impl VfsBuilder { + pub fn new(root_path: PathBuf) -> Self { + Self { + root_dir: VirtualDirectory { + name: root_path + .file_stem() + .unwrap() + .to_string_lossy() + .into_owned(), + entries: Vec::new(), + }, + root_path, + files: Vec::new(), + current_offset: 0, + file_offsets: Default::default(), + } + } + + pub fn set_root_dir_name(&mut self, name: String) { + self.root_dir.name = name; + } + + pub fn add_dir_recursive(&mut self, path: &Path) -> Result<(), AnyError> { + self.add_dir(path); + let read_dir = std::fs::read_dir(path) + .with_context(|| format!("Reading {}", path.display()))?; + + for entry in read_dir { + let entry = entry?; + let file_type = entry.file_type()?; + let path = entry.path(); + + if file_type.is_dir() { + self.add_dir_recursive(&path)?; + } else if file_type.is_file() { + let file_bytes = std::fs::read(&path) + .with_context(|| format!("Reading {}", path.display()))?; + self.add_file(&path, file_bytes); + } else if file_type.is_symlink() { + let target = std::fs::read_link(&path) + .with_context(|| format!("Reading symlink {}", path.display()))?; + self.add_symlink(&path, &target); + } + } + + Ok(()) + } + + pub fn add_dir(&mut self, path: &Path) -> &mut VirtualDirectory { + let path = path.strip_prefix(&self.root_path).unwrap(); + let mut current_dir = &mut self.root_dir; + + for component in path.components() { + let name = component.as_os_str().to_string_lossy(); + let index = match current_dir + .entries + .binary_search_by(|e| e.name().cmp(&name)) + { + Ok(index) => index, + Err(insert_index) => { + current_dir.entries.insert( + insert_index, + VfsEntry::Dir(VirtualDirectory { + name: name.to_string(), + entries: Vec::new(), + }), + ); + insert_index + } + }; + match &mut current_dir.entries[index] { + VfsEntry::Dir(dir) => { + current_dir = dir; + } + _ => unreachable!(), + }; + } + + current_dir + } + + pub fn add_file(&mut self, path: &Path, data: Vec<u8>) { + let checksum = util::checksum::gen(&[&data]); + let offset = if let Some(offset) = self.file_offsets.get(&checksum) { + // duplicate file, reuse an old offset + *offset + } else { + self.file_offsets.insert(checksum, self.current_offset); + self.current_offset + }; + + let dir = self.add_dir(path.parent().unwrap()); + let name = path.file_name().unwrap().to_string_lossy(); + let data_len = data.len(); + match dir.entries.binary_search_by(|e| e.name().cmp(&name)) { + Ok(_) => unreachable!(), + Err(insert_index) => { + dir.entries.insert( + insert_index, + VfsEntry::File(VirtualFile { + name: name.to_string(), + offset, + len: data.len() as u64, + }), + ); + } + } + + // new file, update the list of files + if self.current_offset == offset { + self.files.push(data); + self.current_offset += data_len as u64; + } + } + + pub fn add_symlink(&mut self, path: &Path, target: &Path) { + let dest = target.strip_prefix(&self.root_path).unwrap().to_path_buf(); + let dir = self.add_dir(path.parent().unwrap()); + let name = path.file_name().unwrap().to_string_lossy(); + match dir.entries.binary_search_by(|e| e.name().cmp(&name)) { + Ok(_) => unreachable!(), + Err(insert_index) => { + dir.entries.insert( + insert_index, + VfsEntry::Symlink(VirtualSymlink { + name: name.to_string(), + dest_parts: dest + .components() + .map(|c| c.as_os_str().to_string_lossy().to_string()) + .collect::<Vec<_>>(), + }), + ); + } + } + } + + pub fn into_dir_and_files(self) -> (VirtualDirectory, Vec<Vec<u8>>) { + (self.root_dir, self.files) + } +} + +#[derive(Debug)] +enum VfsEntryRef<'a> { + Dir(&'a VirtualDirectory), + File(&'a VirtualFile), + Symlink(&'a VirtualSymlink), +} + +impl<'a> VfsEntryRef<'a> { + pub fn as_fs_stat(&self) -> FsStat { + match self { + VfsEntryRef::Dir(_) => FsStat { + is_directory: true, + is_file: false, + is_symlink: false, + atime: None, + birthtime: None, + mtime: None, + blksize: 0, + size: 0, + dev: 0, + ino: 0, + mode: 0, + nlink: 0, + uid: 0, + gid: 0, + rdev: 0, + blocks: 0, + }, + VfsEntryRef::File(file) => FsStat { + is_directory: false, + is_file: true, + is_symlink: false, + atime: None, + birthtime: None, + mtime: None, + blksize: 0, + size: file.len, + dev: 0, + ino: 0, + mode: 0, + nlink: 0, + uid: 0, + gid: 0, + rdev: 0, + blocks: 0, + }, + VfsEntryRef::Symlink(_) => FsStat { + is_directory: false, + is_file: false, + is_symlink: true, + atime: None, + birthtime: None, + mtime: None, + blksize: 0, + size: 0, + dev: 0, + ino: 0, + mode: 0, + nlink: 0, + uid: 0, + gid: 0, + rdev: 0, + blocks: 0, + }, + } + } +} + +// todo(dsherret): we should store this more efficiently in the binary +#[derive(Debug, Serialize, Deserialize)] +pub enum VfsEntry { + Dir(VirtualDirectory), + File(VirtualFile), + Symlink(VirtualSymlink), +} + +impl VfsEntry { + pub fn name(&self) -> &str { + match self { + VfsEntry::Dir(dir) => &dir.name, + VfsEntry::File(file) => &file.name, + VfsEntry::Symlink(symlink) => &symlink.name, + } + } + + fn as_ref(&self) -> VfsEntryRef { + match self { + VfsEntry::Dir(dir) => VfsEntryRef::Dir(dir), + VfsEntry::File(file) => VfsEntryRef::File(file), + VfsEntry::Symlink(symlink) => VfsEntryRef::Symlink(symlink), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VirtualDirectory { + pub name: String, + // should be sorted by name + pub entries: Vec<VfsEntry>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VirtualFile { + pub name: String, + pub offset: u64, + pub len: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VirtualSymlink { + pub name: String, + pub dest_parts: Vec<String>, +} + +impl VirtualSymlink { + pub fn resolve_dest_from_root(&self, root: &Path) -> PathBuf { + let mut dest = root.to_path_buf(); + for part in &self.dest_parts { + dest.push(part); + } + dest + } +} + +#[derive(Debug)] +pub struct VfsRoot { + pub dir: VirtualDirectory, + pub root_path: PathBuf, + pub start_file_offset: u64, +} + +impl VfsRoot { + fn find_entry<'a>( + &'a self, + path: &Path, + ) -> std::io::Result<(PathBuf, VfsEntryRef<'a>)> { + self.find_entry_inner(path, &mut HashSet::new()) + } + + fn find_entry_inner<'a>( + &'a self, + path: &Path, + seen: &mut HashSet<PathBuf>, + ) -> std::io::Result<(PathBuf, VfsEntryRef<'a>)> { + let mut path = Cow::Borrowed(path); + loop { + let (resolved_path, entry) = + self.find_entry_no_follow_inner(&path, seen)?; + match entry { + VfsEntryRef::Symlink(symlink) => { + if !seen.insert(path.to_path_buf()) { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "circular symlinks", + )); + } + path = Cow::Owned(symlink.resolve_dest_from_root(&self.root_path)); + } + _ => { + return Ok((resolved_path, entry)); + } + } + } + } + + fn find_entry_no_follow( + &self, + path: &Path, + ) -> std::io::Result<(PathBuf, VfsEntryRef)> { + self.find_entry_no_follow_inner(path, &mut HashSet::new()) + } + + fn find_entry_no_follow_inner<'a>( + &'a self, + path: &Path, + seen: &mut HashSet<PathBuf>, + ) -> std::io::Result<(PathBuf, VfsEntryRef<'a>)> { + let relative_path = match path.strip_prefix(&self.root_path) { + Ok(p) => p, + Err(_) => { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "path not found", + )); + } + }; + let mut final_path = self.root_path.clone(); + let mut current_entry = VfsEntryRef::Dir(&self.dir); + for component in relative_path.components() { + let component = component.as_os_str().to_string_lossy(); + let current_dir = match current_entry { + VfsEntryRef::Dir(dir) => { + final_path.push(component.as_ref()); + dir + } + VfsEntryRef::Symlink(symlink) => { + let dest = symlink.resolve_dest_from_root(&self.root_path); + let (resolved_path, entry) = self.find_entry_inner(&dest, seen)?; + final_path = resolved_path; // overwrite with the new resolved path + match entry { + VfsEntryRef::Dir(dir) => { + final_path.push(component.as_ref()); + dir + } + _ => { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "path not found", + )); + } + } + } + _ => { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "path not found", + )); + } + }; + match current_dir + .entries + .binary_search_by(|e| e.name().cmp(&component)) + { + Ok(index) => { + current_entry = current_dir.entries[index].as_ref(); + } + Err(_) => { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "path not found", + )); + } + } + } + + Ok((final_path, current_entry)) + } +} + +#[derive(Clone)] +struct FileBackedVfsFile { + file: VirtualFile, + pos: Arc<Mutex<u64>>, + vfs: Arc<FileBackedVfs>, +} + +impl FileBackedVfsFile { + fn seek(&self, pos: SeekFrom) -> FsResult<u64> { + match pos { + SeekFrom::Start(pos) => { + *self.pos.lock() = pos; + Ok(pos) + } + SeekFrom::End(offset) => { + if offset < 0 && -offset as u64 > self.file.len { + Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "An attempt was made to move the file pointer before the beginning of the file.").into()) + } else { + let mut current_pos = self.pos.lock(); + *current_pos = if offset >= 0 { + self.file.len - (offset as u64) + } else { + self.file.len + (-offset as u64) + }; + Ok(*current_pos) + } + } + SeekFrom::Current(offset) => { + let mut current_pos = self.pos.lock(); + if offset >= 0 { + *current_pos += offset as u64; + } else if -offset as u64 > *current_pos { + return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "An attempt was made to move the file pointer before the beginning of the file.").into()); + } else { + *current_pos -= -offset as u64; + } + Ok(*current_pos) + } + } + } + + fn read_to_buf(&self, buf: &mut [u8]) -> FsResult<usize> { + let pos = { + let mut pos = self.pos.lock(); + let read_pos = *pos; + // advance the position due to the read + *pos = std::cmp::min(self.file.len, *pos + buf.len() as u64); + read_pos + }; + self + .vfs + .read_file(&self.file, pos, buf) + .map_err(|err| err.into()) + } + + fn read_to_end(&self) -> FsResult<Vec<u8>> { + let pos = { + let mut pos = self.pos.lock(); + let read_pos = *pos; + // todo(dsherret): should this always set it to the end of the file? + if *pos < self.file.len { + // advance the position due to the read + *pos = self.file.len; + } + read_pos + }; + if pos > self.file.len { + return Ok(Vec::new()); + } + let size = (self.file.len - pos) as usize; + let mut buf = vec![0; size]; + self.vfs.read_file(&self.file, pos, &mut buf)?; + Ok(buf) + } +} + +#[async_trait::async_trait(?Send)] +impl deno_io::fs::File for FileBackedVfsFile { + fn read_sync(self: Rc<Self>, buf: &mut [u8]) -> FsResult<usize> { + self.read_to_buf(buf) + } + async fn read_byob( + self: Rc<Self>, + mut buf: BufMutView, + ) -> FsResult<(usize, BufMutView)> { + let inner = (*self).clone(); + tokio::task::spawn(async move { + let nread = inner.read_to_buf(&mut buf)?; + Ok((nread, buf)) + }) + .await? + } + + fn write_sync(self: Rc<Self>, _buf: &[u8]) -> FsResult<usize> { + Err(FsError::NotSupported) + } + async fn write( + self: Rc<Self>, + _buf: BufView, + ) -> FsResult<deno_core::WriteOutcome> { + Err(FsError::NotSupported) + } + + fn write_all_sync(self: Rc<Self>, _buf: &[u8]) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn write_all(self: Rc<Self>, _buf: BufView) -> FsResult<()> { + Err(FsError::NotSupported) + } + + fn read_all_sync(self: Rc<Self>) -> FsResult<Vec<u8>> { + self.read_to_end() + } + async fn read_all_async(self: Rc<Self>) -> FsResult<Vec<u8>> { + let inner = (*self).clone(); + tokio::task::spawn_blocking(move || inner.read_to_end()).await? + } + + fn chmod_sync(self: Rc<Self>, _pathmode: u32) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn chmod_async(self: Rc<Self>, _mode: u32) -> FsResult<()> { + Err(FsError::NotSupported) + } + + fn seek_sync(self: Rc<Self>, pos: SeekFrom) -> FsResult<u64> { + self.seek(pos) + } + async fn seek_async(self: Rc<Self>, pos: SeekFrom) -> FsResult<u64> { + self.seek(pos) + } + + fn datasync_sync(self: Rc<Self>) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn datasync_async(self: Rc<Self>) -> FsResult<()> { + Err(FsError::NotSupported) + } + + fn sync_sync(self: Rc<Self>) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn sync_async(self: Rc<Self>) -> FsResult<()> { + Err(FsError::NotSupported) + } + + fn stat_sync(self: Rc<Self>) -> FsResult<FsStat> { + Err(FsError::NotSupported) + } + async fn stat_async(self: Rc<Self>) -> FsResult<FsStat> { + Err(FsError::NotSupported) + } + + fn lock_sync(self: Rc<Self>, _exclusive: bool) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn lock_async(self: Rc<Self>, _exclusive: bool) -> FsResult<()> { + Err(FsError::NotSupported) + } + + fn unlock_sync(self: Rc<Self>) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn unlock_async(self: Rc<Self>) -> FsResult<()> { + Err(FsError::NotSupported) + } + + fn truncate_sync(self: Rc<Self>, _len: u64) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn truncate_async(self: Rc<Self>, _len: u64) -> FsResult<()> { + Err(FsError::NotSupported) + } + + fn utime_sync( + self: Rc<Self>, + _atime_secs: i64, + _atime_nanos: u32, + _mtime_secs: i64, + _mtime_nanos: u32, + ) -> FsResult<()> { + Err(FsError::NotSupported) + } + async fn utime_async( + self: Rc<Self>, + _atime_secs: i64, + _atime_nanos: u32, + _mtime_secs: i64, + _mtime_nanos: u32, + ) -> FsResult<()> { + Err(FsError::NotSupported) + } + + // lower level functionality + fn as_stdio(self: Rc<Self>) -> FsResult<std::process::Stdio> { + Err(FsError::NotSupported) + } + #[cfg(unix)] + fn backing_fd(self: Rc<Self>) -> Option<std::os::unix::prelude::RawFd> { + None + } + #[cfg(windows)] + fn backing_fd(self: Rc<Self>) -> Option<std::os::windows::io::RawHandle> { + None + } + fn try_clone_inner(self: Rc<Self>) -> FsResult<Rc<dyn deno_io::fs::File>> { + Ok(self) + } +} + +#[derive(Debug)] +pub struct FileBackedVfs { + file: Mutex<File>, + fs_root: VfsRoot, +} + +impl FileBackedVfs { + pub fn new(file: File, fs_root: VfsRoot) -> Self { + Self { + file: Mutex::new(file), + fs_root, + } + } + + pub fn root(&self) -> &Path { + &self.fs_root.root_path + } + + pub fn is_path_within(&self, path: &Path) -> bool { + path.starts_with(&self.fs_root.root_path) + } + + pub fn open_file( + self: &Arc<Self>, + path: &Path, + ) -> std::io::Result<Rc<dyn deno_io::fs::File>> { + let file = self.file_entry(path)?; + Ok(Rc::new(FileBackedVfsFile { + file: file.clone(), + vfs: self.clone(), + pos: Default::default(), + })) + } + + pub fn read_dir(&self, path: &Path) -> std::io::Result<Vec<FsDirEntry>> { + let dir = self.dir_entry(path)?; + Ok( + dir + .entries + .iter() + .map(|entry| FsDirEntry { + name: entry.name().to_string(), + is_file: matches!(entry, VfsEntry::File(_)), + is_directory: matches!(entry, VfsEntry::Dir(_)), + is_symlink: matches!(entry, VfsEntry::Symlink(_)), + }) + .collect(), + ) + } + + pub fn read_link(&self, path: &Path) -> std::io::Result<PathBuf> { + let (_, entry) = self.fs_root.find_entry_no_follow(path)?; + match entry { + VfsEntryRef::Symlink(symlink) => { + Ok(symlink.resolve_dest_from_root(&self.fs_root.root_path)) + } + VfsEntryRef::Dir(_) | VfsEntryRef::File(_) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "not a symlink", + )), + } + } + + pub fn lstat(&self, path: &Path) -> std::io::Result<FsStat> { + let (_, entry) = self.fs_root.find_entry_no_follow(path)?; + Ok(entry.as_fs_stat()) + } + + pub fn stat(&self, path: &Path) -> std::io::Result<FsStat> { + let (_, entry) = self.fs_root.find_entry(path)?; + Ok(entry.as_fs_stat()) + } + + pub fn canonicalize(&self, path: &Path) -> std::io::Result<PathBuf> { + let (path, _) = self.fs_root.find_entry(path)?; + Ok(path) + } + + pub fn read_file_all(&self, file: &VirtualFile) -> std::io::Result<Vec<u8>> { + let mut buf = vec![0; file.len as usize]; + self.read_file(file, 0, &mut buf)?; + Ok(buf) + } + + pub fn read_file( + &self, + file: &VirtualFile, + pos: u64, + buf: &mut [u8], + ) -> std::io::Result<usize> { + let mut fs_file = self.file.lock(); + fs_file.seek(SeekFrom::Start( + self.fs_root.start_file_offset + file.offset + pos, + ))?; + fs_file.read(buf) + } + + pub fn dir_entry(&self, path: &Path) -> std::io::Result<&VirtualDirectory> { + let (_, entry) = self.fs_root.find_entry(path)?; + match entry { + VfsEntryRef::Dir(dir) => Ok(dir), + VfsEntryRef::Symlink(_) => unreachable!(), + VfsEntryRef::File(_) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "path is a file", + )), + } + } + + pub fn file_entry(&self, path: &Path) -> std::io::Result<&VirtualFile> { + let (_, entry) = self.fs_root.find_entry(path)?; + match entry { + VfsEntryRef::Dir(_) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "path is a directory", + )), + VfsEntryRef::Symlink(_) => unreachable!(), + VfsEntryRef::File(file) => Ok(file), + } + } +} + +#[cfg(test)] +mod test { + use std::io::Write; + use test_util::TempDir; + + use super::*; + + fn read_file(vfs: &FileBackedVfs, path: &Path) -> String { + let file = vfs.file_entry(path).unwrap(); + String::from_utf8(vfs.read_file_all(file).unwrap()).unwrap() + } + + #[test] + fn builds_and_uses_virtual_fs() { + let temp_dir = TempDir::new(); + let src_path = temp_dir.path().join("src"); + let mut builder = VfsBuilder::new(src_path.clone()); + builder.add_file(&src_path.join("a.txt"), "data".into()); + builder.add_file(&src_path.join("b.txt"), "data".into()); + assert_eq!(builder.files.len(), 1); // because duplicate data + builder.add_file(&src_path.join("c.txt"), "c".into()); + builder.add_file(&src_path.join("sub_dir").join("d.txt"), "d".into()); + builder.add_file(&src_path.join("e.txt"), "e".into()); + builder.add_symlink( + &src_path.join("sub_dir").join("e.txt"), + &src_path.join("e.txt"), + ); + + // get the virtual fs + let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir); + + assert_eq!(read_file(&virtual_fs, &dest_path.join("a.txt")), "data"); + assert_eq!(read_file(&virtual_fs, &dest_path.join("b.txt")), "data"); + + // attempt reading a symlink + assert_eq!( + read_file(&virtual_fs, &dest_path.join("sub_dir").join("e.txt")), + "e", + ); + + // canonicalize symlink + assert_eq!( + virtual_fs + .canonicalize(&dest_path.join("sub_dir").join("e.txt")) + .unwrap(), + dest_path.join("e.txt"), + ); + + // metadata + assert!( + virtual_fs + .lstat(&dest_path.join("sub_dir").join("e.txt")) + .unwrap() + .is_symlink + ); + assert!( + virtual_fs + .stat(&dest_path.join("sub_dir").join("e.txt")) + .unwrap() + .is_file + ); + assert!( + virtual_fs + .stat(&dest_path.join("sub_dir")) + .unwrap() + .is_directory, + ); + assert!(virtual_fs.stat(&dest_path.join("e.txt")).unwrap().is_file,); + } + + #[test] + fn test_include_dir_recursive() { + let temp_dir = TempDir::new(); + temp_dir.create_dir_all("src/nested/sub_dir"); + temp_dir.write("src/a.txt", "data"); + temp_dir.write("src/b.txt", "data"); + util::fs::symlink_dir( + &temp_dir.path().join("src/nested/sub_dir"), + &temp_dir.path().join("src/sub_dir_link"), + ) + .unwrap(); + temp_dir.write("src/nested/sub_dir/c.txt", "c"); + + // build and create the virtual fs + let src_path = temp_dir.path().join("src"); + let mut builder = VfsBuilder::new(src_path.clone()); + builder.add_dir_recursive(&src_path).unwrap(); + let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir); + + assert_eq!(read_file(&virtual_fs, &dest_path.join("a.txt")), "data",); + assert_eq!(read_file(&virtual_fs, &dest_path.join("b.txt")), "data",); + + assert_eq!( + read_file( + &virtual_fs, + &dest_path.join("nested").join("sub_dir").join("c.txt") + ), + "c", + ); + assert_eq!( + read_file(&virtual_fs, &dest_path.join("sub_dir_link").join("c.txt")), + "c", + ); + assert!( + virtual_fs + .lstat(&dest_path.join("sub_dir_link")) + .unwrap() + .is_symlink + ); + + assert_eq!( + virtual_fs + .canonicalize(&dest_path.join("sub_dir_link").join("c.txt")) + .unwrap(), + dest_path.join("nested").join("sub_dir").join("c.txt"), + ); + } + + fn into_virtual_fs( + builder: VfsBuilder, + temp_dir: &TempDir, + ) -> (PathBuf, FileBackedVfs) { + let virtual_fs_file = temp_dir.path().join("virtual_fs"); + let (root_dir, files) = builder.into_dir_and_files(); + { + let mut file = std::fs::File::create(&virtual_fs_file).unwrap(); + for file_data in &files { + file.write_all(file_data).unwrap(); + } + } + let file = std::fs::File::open(&virtual_fs_file).unwrap(); + let dest_path = temp_dir.path().join("dest"); + ( + dest_path.clone(), + FileBackedVfs::new( + file, + VfsRoot { + dir: root_dir, + root_path: dest_path, + start_file_offset: 0, + }, + ), + ) + } + + #[test] + fn circular_symlink() { + let temp_dir = TempDir::new(); + let src_path = temp_dir.path().join("src"); + let mut builder = VfsBuilder::new(src_path.clone()); + builder.add_symlink(&src_path.join("a.txt"), &src_path.join("b.txt")); + builder.add_symlink(&src_path.join("b.txt"), &src_path.join("c.txt")); + builder.add_symlink(&src_path.join("c.txt"), &src_path.join("a.txt")); + let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir); + assert_eq!( + virtual_fs + .file_entry(&dest_path.join("a.txt")) + .err() + .unwrap() + .to_string(), + "circular symlinks", + ); + assert_eq!( + virtual_fs.read_link(&dest_path.join("a.txt")).unwrap(), + dest_path.join("b.txt") + ); + assert_eq!( + virtual_fs.read_link(&dest_path.join("b.txt")).unwrap(), + dest_path.join("c.txt") + ); + } + + #[tokio::test] + async fn test_open_file() { + let temp_dir = TempDir::new(); + let temp_path = temp_dir.path(); + let mut builder = VfsBuilder::new(temp_path.to_path_buf()); + builder.add_file( + &temp_path.join("a.txt"), + "0123456789".to_string().into_bytes(), + ); + let (dest_path, virtual_fs) = into_virtual_fs(builder, &temp_dir); + let virtual_fs = Arc::new(virtual_fs); + let file = virtual_fs.open_file(&dest_path.join("a.txt")).unwrap(); + file.clone().seek_sync(SeekFrom::Current(2)).unwrap(); + let mut buf = vec![0; 2]; + file.clone().read_sync(&mut buf).unwrap(); + assert_eq!(buf, b"23"); + file.clone().read_sync(&mut buf).unwrap(); + assert_eq!(buf, b"45"); + file.clone().seek_sync(SeekFrom::Current(-4)).unwrap(); + file.clone().read_sync(&mut buf).unwrap(); + assert_eq!(buf, b"23"); + file.clone().seek_sync(SeekFrom::Start(2)).unwrap(); + file.clone().read_sync(&mut buf).unwrap(); + assert_eq!(buf, b"23"); + file.clone().seek_sync(SeekFrom::End(2)).unwrap(); + file.clone().read_sync(&mut buf).unwrap(); + assert_eq!(buf, b"89"); + file.clone().seek_sync(SeekFrom::Current(-8)).unwrap(); + file.clone().read_sync(&mut buf).unwrap(); + assert_eq!(buf, b"23"); + assert_eq!( + file + .clone() + .seek_sync(SeekFrom::Current(-5)) + .err() + .unwrap() + .into_io_error() + .to_string(), + "An attempt was made to move the file pointer before the beginning of the file." + ); + // go beyond the file length, then back + file.clone().seek_sync(SeekFrom::Current(40)).unwrap(); + file.clone().seek_sync(SeekFrom::Current(-38)).unwrap(); + let read_buf = file.clone().read(2).await.unwrap(); + assert_eq!(read_buf.to_vec(), b"67"); + file.clone().seek_sync(SeekFrom::Current(-2)).unwrap(); + + // read to the end of the file + let all_buf = file.clone().read_all_sync().unwrap(); + assert_eq!(all_buf.to_vec(), b"6789"); + file.clone().seek_sync(SeekFrom::Current(-9)).unwrap(); + + // try try_clone_inner and read_all_async + let all_buf = file + .try_clone_inner() + .unwrap() + .read_all_async() + .await + .unwrap(); + assert_eq!(all_buf.to_vec(), b"123456789"); + } +} |