summaryrefslogtreecommitdiff
path: root/cli/standalone/virtual_fs.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/standalone/virtual_fs.rs')
-rw-r--r--cli/standalone/virtual_fs.rs983
1 files changed, 983 insertions, 0 deletions
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");
+ }
+}