// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use std::borrow::Cow; use std::cell::RefCell; 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::ops::Range; use std::path::Path; use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; use deno_core::anyhow::anyhow; 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_core::ResourceHandleFd; 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 thiserror::Error; use crate::util; use crate::util::display::DisplayTreeNode; use crate::util::fs::canonicalize_path; #[derive(Debug, Copy, Clone)] pub enum VfsFileSubDataKind { /// Raw bytes of the file. Raw, /// Bytes to use for module loading. For example, for TypeScript /// files this will be the transpiled JavaScript source. ModuleGraph, } #[derive(Error, Debug)] #[error( "Failed to strip prefix '{}' from '{}'", root_path.display(), target.display() )] pub struct StripRootError { root_path: PathBuf, target: PathBuf, } #[derive(Debug)] pub struct VfsBuilder { root_path: PathBuf, root_dir: VirtualDirectory, files: Vec>, current_offset: u64, file_offsets: HashMap, } impl VfsBuilder { pub fn new(root_path: PathBuf) -> Result { let root_path = canonicalize_path(&root_path) .with_context(|| format!("Canonicalizing {}", root_path.display()))?; log::debug!("Building vfs with root '{}'", root_path.display()); Ok(Self { root_dir: VirtualDirectory { name: root_path .file_stem() .map(|s| s.to_string_lossy().into_owned()) .unwrap_or("root".to_string()), entries: Vec::new(), }, root_path, files: Vec::new(), current_offset: 0, file_offsets: Default::default(), }) } pub fn set_new_root_path( &mut self, root_path: PathBuf, ) -> Result<(), AnyError> { let root_path = canonicalize_path(&root_path)?; self.root_path = root_path; self.root_dir = VirtualDirectory { name: self .root_path .file_stem() .map(|s| s.to_string_lossy().into_owned()) .unwrap_or("root".to_string()), entries: vec![VfsEntry::Dir(VirtualDirectory { name: std::mem::take(&mut self.root_dir.name), entries: std::mem::take(&mut self.root_dir.entries), })], }; Ok(()) } pub fn with_root_dir( &mut self, with_root: impl FnOnce(&mut VirtualDirectory) -> R, ) -> R { with_root(&mut self.root_dir) } pub fn add_dir_recursive(&mut self, path: &Path) -> Result<(), AnyError> { let target_path = canonicalize_path(path)?; if path != target_path { self.add_symlink(path, &target_path)?; } self.add_dir_recursive_internal(&target_path) } fn add_dir_recursive_internal( &mut self, path: &Path, ) -> Result<(), AnyError> { self.add_dir(path)?; let read_dir = std::fs::read_dir(path) .with_context(|| format!("Reading {}", path.display()))?; let mut dir_entries = read_dir.into_iter().collect::, _>>()?; dir_entries.sort_by_cached_key(|entry| entry.file_name()); // determinism for entry in dir_entries { let file_type = entry.file_type()?; let path = entry.path(); if file_type.is_dir() { self.add_dir_recursive_internal(&path)?; } else if file_type.is_file() { self.add_file_at_path_not_symlink(&path)?; } else if file_type.is_symlink() { match util::fs::canonicalize_path(&path) { Ok(target) => { if let Err(StripRootError { .. }) = self.add_symlink(&path, &target) { if target.is_file() { // this may change behavior, so warn the user about it log::warn!( "{} Symlink target is outside '{}'. Inlining symlink at '{}' to '{}' as file.", crate::colors::yellow("Warning"), self.root_path.display(), path.display(), target.display(), ); // inline the symlink and make the target file let file_bytes = std::fs::read(&target) .with_context(|| format!("Reading {}", path.display()))?; self.add_file_with_data_inner( &path, file_bytes, VfsFileSubDataKind::Raw, )?; } else { log::warn!( "{} Symlink target is outside '{}'. Excluding symlink at '{}' with target '{}'.", crate::colors::yellow("Warning"), self.root_path.display(), path.display(), target.display(), ); } } } Err(err) => { log::warn!( "{} Failed resolving symlink. Ignoring.\n Path: {}\n Message: {:#}", crate::colors::yellow("Warning"), path.display(), err ); } } } } Ok(()) } fn add_dir( &mut self, path: &Path, ) -> Result<&mut VirtualDirectory, StripRootError> { log::debug!("Ensuring directory '{}'", path.display()); let path = self.path_relative_root(path)?; 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!(), }; } Ok(current_dir) } pub fn add_file_at_path(&mut self, path: &Path) -> Result<(), AnyError> { let target_path = canonicalize_path(path)?; if target_path != path { self.add_symlink(path, &target_path)?; } self.add_file_at_path_not_symlink(&target_path) } fn add_file_at_path_not_symlink( &mut self, path: &Path, ) -> Result<(), AnyError> { let file_bytes = std::fs::read(path) .with_context(|| format!("Reading {}", path.display()))?; self.add_file_with_data_inner(path, file_bytes, VfsFileSubDataKind::Raw) } pub fn add_file_with_data( &mut self, path: &Path, data: Vec, sub_data_kind: VfsFileSubDataKind, ) -> Result<(), AnyError> { let target_path = canonicalize_path(path)?; if target_path != path { self.add_symlink(path, &target_path)?; } self.add_file_with_data_inner(&target_path, data, sub_data_kind) } fn add_file_with_data_inner( &mut self, path: &Path, data: Vec, sub_data_kind: VfsFileSubDataKind, ) -> Result<(), AnyError> { log::debug!("Adding file '{}'", path.display()); 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 offset_and_len = OffsetWithLength { offset, len: data.len() as u64, }; match dir.entries.binary_search_by(|e| e.name().cmp(&name)) { Ok(index) => { let entry = &mut dir.entries[index]; match entry { VfsEntry::File(virtual_file) => match sub_data_kind { VfsFileSubDataKind::Raw => { virtual_file.offset = offset_and_len; } VfsFileSubDataKind::ModuleGraph => { virtual_file.module_graph_offset = offset_and_len; } }, VfsEntry::Dir(_) | VfsEntry::Symlink(_) => unreachable!(), } } Err(insert_index) => { dir.entries.insert( insert_index, VfsEntry::File(VirtualFile { name: name.to_string(), offset: offset_and_len, module_graph_offset: offset_and_len, }), ); } } // new file, update the list of files if self.current_offset == offset { self.files.push(data); self.current_offset += offset_and_len.len; } Ok(()) } fn add_symlink( &mut self, path: &Path, target: &Path, ) -> Result<(), StripRootError> { log::debug!( "Adding symlink '{}' to '{}'", path.display(), target.display() ); let relative_target = self.path_relative_root(target)?; let relative_path = match self.path_relative_root(path) { Ok(path) => path, Err(StripRootError { .. }) => { // ignore if the original path is outside the root directory return Ok(()); } }; if relative_target == relative_path { // it's the same, ignore return Ok(()); } 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(_) => Ok(()), // previously inserted Err(insert_index) => { dir.entries.insert( insert_index, VfsEntry::Symlink(VirtualSymlink { name: name.to_string(), dest_parts: relative_target .components() .map(|c| c.as_os_str().to_string_lossy().to_string()) .collect::>(), }), ); Ok(()) } } } pub fn into_dir_and_files(self) -> (VirtualDirectory, Vec>) { (self.root_dir, self.files) } fn path_relative_root(&self, path: &Path) -> Result { match path.strip_prefix(&self.root_path) { Ok(p) => Ok(p.to_path_buf()), Err(_) => Err(StripRootError { root_path: self.root_path.clone(), target: path.to_path_buf(), }), } } } pub fn output_vfs(builder: &VfsBuilder, executable_name: &str) { if !log::log_enabled!(log::Level::Info) { return; // no need to compute if won't output } if builder.root_dir.entries.is_empty() { return; // nothing to output } let mut text = String::new(); let display_tree = vfs_as_display_tree(builder, executable_name); display_tree.print(&mut text).unwrap(); // unwrap ok because it's writing to a string log::info!( "\n{}\n", deno_terminal::colors::bold("Embedded File System") ); log::info!("{}\n", text.trim()); } fn vfs_as_display_tree( builder: &VfsBuilder, executable_name: &str, ) -> DisplayTreeNode { enum EntryOutput<'a> { All, Subset(Vec>), File, Symlink(&'a [String]), } impl<'a> EntryOutput<'a> { pub fn as_display_tree(&self, name: String) -> DisplayTreeNode { DisplayTreeNode { text: match self { EntryOutput::All | EntryOutput::Subset(_) | EntryOutput::File => name, EntryOutput::Symlink(parts) => { format!("{} --> {}", name, parts.join("/")) } }, children: match self { EntryOutput::All => vec![DisplayTreeNode::from_text("*".to_string())], EntryOutput::Subset(vec) => vec .iter() .map(|e| e.output.as_display_tree(e.name.to_string())) .collect(), EntryOutput::File | EntryOutput::Symlink(_) => vec![], }, } } } pub struct DirEntryOutput<'a> { name: &'a str, output: EntryOutput<'a>, } fn include_all_entries<'a>( dir: &Path, vfs_dir: &'a VirtualDirectory, ) -> EntryOutput<'a> { EntryOutput::Subset( vfs_dir .entries .iter() .map(|entry| DirEntryOutput { name: entry.name(), output: analyze_entry(&dir.join(entry.name()), entry), }) .collect(), ) } fn analyze_entry<'a>(path: &Path, entry: &'a VfsEntry) -> EntryOutput<'a> { match entry { VfsEntry::Dir(virtual_directory) => analyze_dir(path, virtual_directory), VfsEntry::File(_) => EntryOutput::File, VfsEntry::Symlink(virtual_symlink) => { EntryOutput::Symlink(&virtual_symlink.dest_parts) } } } fn analyze_dir<'a>( dir: &Path, vfs_dir: &'a VirtualDirectory, ) -> EntryOutput<'a> { let real_entry_count = std::fs::read_dir(dir) .ok() .map(|entries| entries.flat_map(|e| e.ok()).count()) .unwrap_or(0); if real_entry_count == vfs_dir.entries.len() { let children = vfs_dir .entries .iter() .map(|entry| DirEntryOutput { name: entry.name(), output: analyze_entry(&dir.join(entry.name()), entry), }) .collect::>(); if children .iter() .all(|c| !matches!(c.output, EntryOutput::Subset(_))) { EntryOutput::All } else { EntryOutput::Subset(children) } } else { include_all_entries(dir, vfs_dir) } } // always include all the entries for the root directory, otherwise the // user might not have context about what's being shown let output = include_all_entries(&builder.root_path, &builder.root_dir); output .as_display_tree(deno_terminal::colors::italic(executable_name).to_string()) } #[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, ctime: None, blksize: 0, size: 0, dev: 0, ino: 0, mode: 0, nlink: 0, uid: 0, gid: 0, rdev: 0, blocks: 0, is_block_device: false, is_char_device: false, is_fifo: false, is_socket: false, }, VfsEntryRef::File(file) => FsStat { is_directory: false, is_file: true, is_symlink: false, atime: None, birthtime: None, mtime: None, ctime: None, blksize: 0, size: file.offset.len, dev: 0, ino: 0, mode: 0, nlink: 0, uid: 0, gid: 0, rdev: 0, blocks: 0, is_block_device: false, is_char_device: false, is_fifo: false, is_socket: false, }, VfsEntryRef::Symlink(_) => FsStat { is_directory: false, is_file: false, is_symlink: true, atime: None, birthtime: None, mtime: None, ctime: None, blksize: 0, size: 0, dev: 0, ino: 0, mode: 0, nlink: 0, uid: 0, gid: 0, rdev: 0, blocks: 0, is_block_device: false, is_char_device: false, is_fifo: false, is_socket: false, }, } } } // 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 { #[serde(rename = "n")] pub name: String, // should be sorted by name #[serde(rename = "e")] pub entries: Vec, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct OffsetWithLength { #[serde(rename = "o")] pub offset: u64, #[serde(rename = "l")] pub len: u64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VirtualFile { #[serde(rename = "n")] pub name: String, #[serde(rename = "o")] pub offset: OffsetWithLength, /// Offset file to use for module loading when it differs from the /// raw file. Often this will be the same offset as above for data /// such as JavaScript files, but for TypeScript files the `offset` /// will be the original raw bytes when included as an asset and this /// offset will be to the transpiled JavaScript source. #[serde(rename = "m")] pub module_graph_offset: OffsetWithLength, } #[derive(Debug, Serialize, Deserialize)] pub struct VirtualSymlink { #[serde(rename = "n")] pub name: String, #[serde(rename = "p")] pub dest_parts: Vec, } 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, ) -> 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, ) -> 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)) } } struct FileBackedVfsFile { file: VirtualFile, pos: RefCell, vfs: Arc, } impl FileBackedVfsFile { fn seek(&self, pos: SeekFrom) -> FsResult { match pos { SeekFrom::Start(pos) => { *self.pos.borrow_mut() = pos; Ok(pos) } SeekFrom::End(offset) => { if offset < 0 && -offset as u64 > self.file.offset.len { let msg = "An attempt was made to move the file pointer before the beginning of the file."; Err( std::io::Error::new(std::io::ErrorKind::PermissionDenied, msg) .into(), ) } else { let mut current_pos = self.pos.borrow_mut(); *current_pos = if offset >= 0 { self.file.offset.len - (offset as u64) } else { self.file.offset.len + (-offset as u64) }; Ok(*current_pos) } } SeekFrom::Current(offset) => { let mut current_pos = self.pos.borrow_mut(); 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 { let read_pos = { let mut pos = self.pos.borrow_mut(); let read_pos = *pos; // advance the position due to the read *pos = std::cmp::min(self.file.offset.len, *pos + buf.len() as u64); read_pos }; self .vfs .read_file(&self.file, read_pos, buf) .map_err(|err| err.into()) } fn read_to_end(&self) -> FsResult> { let read_pos = { let mut pos = self.pos.borrow_mut(); let read_pos = *pos; // todo(dsherret): should this always set it to the end of the file? if *pos < self.file.offset.len { // advance the position due to the read *pos = self.file.offset.len; } read_pos }; if read_pos > self.file.offset.len { return Ok(Cow::Borrowed(&[])); } if read_pos == 0 { Ok( self .vfs .read_file_all(&self.file, VfsFileSubDataKind::Raw)?, ) } else { let size = (self.file.offset.len - read_pos) as usize; let mut buf = vec![0; size]; self.vfs.read_file(&self.file, read_pos, &mut buf)?; Ok(Cow::Owned(buf)) } } } #[async_trait::async_trait(?Send)] impl deno_io::fs::File for FileBackedVfsFile { fn read_sync(self: Rc, buf: &mut [u8]) -> FsResult { self.read_to_buf(buf) } async fn read_byob( self: Rc, mut buf: BufMutView, ) -> FsResult<(usize, BufMutView)> { // this is fast, no need to spawn a task let nread = self.read_to_buf(&mut buf)?; Ok((nread, buf)) } fn write_sync(self: Rc, _buf: &[u8]) -> FsResult { Err(FsError::NotSupported) } async fn write( self: Rc, _buf: BufView, ) -> FsResult { Err(FsError::NotSupported) } fn write_all_sync(self: Rc, _buf: &[u8]) -> FsResult<()> { Err(FsError::NotSupported) } async fn write_all(self: Rc, _buf: BufView) -> FsResult<()> { Err(FsError::NotSupported) } fn read_all_sync(self: Rc) -> FsResult> { self.read_to_end() } async fn read_all_async(self: Rc) -> FsResult> { // this is fast, no need to spawn a task self.read_to_end() } fn chmod_sync(self: Rc, _pathmode: u32) -> FsResult<()> { Err(FsError::NotSupported) } async fn chmod_async(self: Rc, _mode: u32) -> FsResult<()> { Err(FsError::NotSupported) } fn seek_sync(self: Rc, pos: SeekFrom) -> FsResult { self.seek(pos) } async fn seek_async(self: Rc, pos: SeekFrom) -> FsResult { self.seek(pos) } fn datasync_sync(self: Rc) -> FsResult<()> { Err(FsError::NotSupported) } async fn datasync_async(self: Rc) -> FsResult<()> { Err(FsError::NotSupported) } fn sync_sync(self: Rc) -> FsResult<()> { Err(FsError::NotSupported) } async fn sync_async(self: Rc) -> FsResult<()> { Err(FsError::NotSupported) } fn stat_sync(self: Rc) -> FsResult { Err(FsError::NotSupported) } async fn stat_async(self: Rc) -> FsResult { Err(FsError::NotSupported) } fn lock_sync(self: Rc, _exclusive: bool) -> FsResult<()> { Err(FsError::NotSupported) } async fn lock_async(self: Rc, _exclusive: bool) -> FsResult<()> { Err(FsError::NotSupported) } fn unlock_sync(self: Rc) -> FsResult<()> { Err(FsError::NotSupported) } async fn unlock_async(self: Rc) -> FsResult<()> { Err(FsError::NotSupported) } fn truncate_sync(self: Rc, _len: u64) -> FsResult<()> { Err(FsError::NotSupported) } async fn truncate_async(self: Rc, _len: u64) -> FsResult<()> { Err(FsError::NotSupported) } fn utime_sync( self: Rc, _atime_secs: i64, _atime_nanos: u32, _mtime_secs: i64, _mtime_nanos: u32, ) -> FsResult<()> { Err(FsError::NotSupported) } async fn utime_async( self: Rc, _atime_secs: i64, _atime_nanos: u32, _mtime_secs: i64, _mtime_nanos: u32, ) -> FsResult<()> { Err(FsError::NotSupported) } // lower level functionality fn as_stdio(self: Rc) -> FsResult { Err(FsError::NotSupported) } fn backing_fd(self: Rc) -> Option { None } fn try_clone_inner(self: Rc) -> FsResult> { Ok(self) } } #[derive(Debug)] pub struct FileBackedVfs { vfs_data: Cow<'static, [u8]>, fs_root: VfsRoot, } impl FileBackedVfs { pub fn new(data: Cow<'static, [u8]>, fs_root: VfsRoot) -> Self { Self { vfs_data: data, 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, path: &Path, ) -> std::io::Result> { 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> { 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 { 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 { let (_, entry) = self.fs_root.find_entry_no_follow(path)?; Ok(entry.as_fs_stat()) } pub fn stat(&self, path: &Path) -> std::io::Result { let (_, entry) = self.fs_root.find_entry(path)?; Ok(entry.as_fs_stat()) } pub fn canonicalize(&self, path: &Path) -> std::io::Result { let (path, _) = self.fs_root.find_entry(path)?; Ok(path) } pub fn read_file_all( &self, file: &VirtualFile, sub_data_kind: VfsFileSubDataKind, ) -> std::io::Result> { let read_len = match sub_data_kind { VfsFileSubDataKind::Raw => file.offset.len, VfsFileSubDataKind::ModuleGraph => file.module_graph_offset.len, }; let read_range = self.get_read_range(file, sub_data_kind, 0, read_len)?; match &self.vfs_data { Cow::Borrowed(data) => Ok(Cow::Borrowed(&data[read_range])), Cow::Owned(data) => Ok(Cow::Owned(data[read_range].to_vec())), } } pub fn read_file( &self, file: &VirtualFile, pos: u64, buf: &mut [u8], ) -> std::io::Result { let read_range = self.get_read_range( file, VfsFileSubDataKind::Raw, pos, buf.len() as u64, )?; let read_len = read_range.len(); buf[..read_len].copy_from_slice(&self.vfs_data[read_range]); Ok(read_len) } fn get_read_range( &self, file: &VirtualFile, sub_data_kind: VfsFileSubDataKind, pos: u64, len: u64, ) -> std::io::Result> { let file_offset_and_len = match sub_data_kind { VfsFileSubDataKind::Raw => file.offset, VfsFileSubDataKind::ModuleGraph => file.module_graph_offset, }; if pos > file_offset_and_len.len { return Err(std::io::Error::new( std::io::ErrorKind::UnexpectedEof, "unexpected EOF", )); } let file_offset = self.fs_root.start_file_offset + file_offset_and_len.offset; let start = file_offset + pos; let end = file_offset + std::cmp::min(pos + len, file_offset_and_len.len); Ok(start as usize..end as usize) } 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 console_static_text::ansi::strip_ansi_codes; use std::io::Write; use test_util::TempDir; use super::*; #[track_caller] fn read_file(vfs: &FileBackedVfs, path: &Path) -> String { let file = vfs.file_entry(path).unwrap(); String::from_utf8( vfs .read_file_all(file, VfsFileSubDataKind::Raw) .unwrap() .into_owned(), ) .unwrap() } #[test] fn builds_and_uses_virtual_fs() { let temp_dir = TempDir::new(); // we canonicalize the temp directory because the vfs builder // will canonicalize the root path let src_path = temp_dir.path().canonicalize().join("src"); src_path.create_dir_all(); let src_path = src_path.to_path_buf(); let mut builder = VfsBuilder::new(src_path.clone()).unwrap(); builder .add_file_with_data_inner( &src_path.join("a.txt"), "data".into(), VfsFileSubDataKind::Raw, ) .unwrap(); builder .add_file_with_data_inner( &src_path.join("b.txt"), "data".into(), VfsFileSubDataKind::Raw, ) .unwrap(); assert_eq!(builder.files.len(), 1); // because duplicate data builder .add_file_with_data_inner( &src_path.join("c.txt"), "c".into(), VfsFileSubDataKind::Raw, ) .unwrap(); builder .add_file_with_data_inner( &src_path.join("sub_dir").join("d.txt"), "d".into(), VfsFileSubDataKind::Raw, ) .unwrap(); builder .add_file_with_data_inner( &src_path.join("e.txt"), "e".into(), VfsFileSubDataKind::Raw, ) .unwrap(); builder .add_symlink( &src_path.join("sub_dir").join("e.txt"), &src_path.join("e.txt"), ) .unwrap(); // 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(); let temp_dir_path = temp_dir.path().canonicalize(); 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").as_path(), temp_dir_path.join("src/sub_dir_link").as_path(), ) .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").to_path_buf(); let mut builder = VfsBuilder::new(src_path.clone()).unwrap(); 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 dest_path = temp_dir.path().join("dest"); let data = std::fs::read(&virtual_fs_file).unwrap(); ( dest_path.to_path_buf(), FileBackedVfs::new( Cow::Owned(data), VfsRoot { dir: root_dir, root_path: dest_path.to_path_buf(), start_file_offset: 0, }, ), ) } #[test] fn circular_symlink() { let temp_dir = TempDir::new(); let src_path = temp_dir.path().canonicalize().join("src"); src_path.create_dir_all(); let src_path = src_path.to_path_buf(); let mut builder = VfsBuilder::new(src_path.clone()).unwrap(); builder .add_symlink(&src_path.join("a.txt"), &src_path.join("b.txt")) .unwrap(); builder .add_symlink(&src_path.join("b.txt"), &src_path.join("c.txt")) .unwrap(); builder .add_symlink(&src_path.join("c.txt"), &src_path.join("a.txt")) .unwrap(); 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().canonicalize(); let mut builder = VfsBuilder::new(temp_path.to_path_buf()).unwrap(); builder .add_file_with_data_inner( temp_path.join("a.txt").as_path(), "0123456789".to_string().into_bytes(), VfsFileSubDataKind::Raw, ) .unwrap(); 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"); } #[test] fn test_vfs_as_display_tree() { let temp_dir = TempDir::new(); temp_dir.write("root.txt", ""); temp_dir.create_dir_all("a"); temp_dir.write("a/a.txt", ""); temp_dir.write("a/b.txt", ""); temp_dir.create_dir_all("b"); temp_dir.write("b/a.txt", ""); temp_dir.write("b/b.txt", ""); temp_dir.create_dir_all("c"); temp_dir.write("c/a.txt", "contents"); temp_dir.symlink_file("c/a.txt", "c/b.txt"); assert_eq!(temp_dir.read_to_string("c/b.txt"), "contents"); // ensure the symlink works let mut vfs_builder = VfsBuilder::new(temp_dir.path().to_path_buf()).unwrap(); // full dir vfs_builder .add_dir_recursive(temp_dir.path().join("a").as_path()) .unwrap(); // part of the dir vfs_builder .add_file_at_path(temp_dir.path().join("b/a.txt").as_path()) .unwrap(); // symlink vfs_builder .add_dir_recursive(temp_dir.path().join("c").as_path()) .unwrap(); temp_dir.write("c/c.txt", ""); // write an extra file so it shows the whole directory let node = vfs_as_display_tree(&vfs_builder, "executable"); let mut text = String::new(); node.print(&mut text).unwrap(); assert_eq!( strip_ansi_codes(&text), r#"executable ├─┬ a │ └── * ├─┬ b │ └── a.txt └─┬ c ├── a.txt └── b.txt --> c/a.txt "# ); } }