// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Allow using Arc for this module. #![allow(clippy::disallowed_types)] use std::borrow::Cow; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::io::Error; use std::io::ErrorKind; use std::path::Path; use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; use deno_core::parking_lot::Mutex; use deno_io::fs::File; use deno_io::fs::FsError; use deno_io::fs::FsResult; use deno_io::fs::FsStat; use deno_path_util::normalize_path; use crate::interface::AccessCheckCb; use crate::interface::FsDirEntry; use crate::interface::FsFileType; use crate::FileSystem; use crate::OpenOptions; #[derive(Debug)] enum PathEntry { Dir, File(Vec<u8>), } /// A very basic in-memory file system useful for swapping out in /// the place of a RealFs for testing purposes. /// /// Please develop this out as you need functionality. #[derive(Debug, Default)] pub struct InMemoryFs { entries: Mutex<HashMap<PathBuf, Arc<PathEntry>>>, } impl InMemoryFs { pub fn setup_text_files(&self, files: Vec<(String, String)>) { for (path, text) in files { let path = PathBuf::from(path); self.mkdir_sync(path.parent().unwrap(), true, None).unwrap(); self .write_file_sync( &path, OpenOptions::write(true, false, false, None), None, &text.into_bytes(), ) .unwrap(); } } fn get_entry(&self, path: &Path) -> Option<Arc<PathEntry>> { let path = normalize_path(path); self.entries.lock().get(&path).cloned() } } #[async_trait::async_trait(?Send)] impl FileSystem for InMemoryFs { fn cwd(&self) -> FsResult<PathBuf> { Err(FsError::NotSupported) } fn tmp_dir(&self) -> FsResult<PathBuf> { Err(FsError::NotSupported) } fn chdir(&self, _path: &Path) -> FsResult<()> { Err(FsError::NotSupported) } fn umask(&self, _mask: Option<u32>) -> FsResult<u32> { Err(FsError::NotSupported) } fn open_sync( &self, _path: &Path, _options: OpenOptions, _access_check: Option<AccessCheckCb>, ) -> FsResult<Rc<dyn File>> { Err(FsError::NotSupported) } async fn open_async<'a>( &'a self, path: PathBuf, options: OpenOptions, access_check: Option<AccessCheckCb<'a>>, ) -> FsResult<Rc<dyn File>> { self.open_sync(&path, options, access_check) } fn mkdir_sync( &self, path: &Path, recursive: bool, _mode: Option<u32>, ) -> FsResult<()> { let path = normalize_path(path); if let Some(parent) = path.parent() { let entry = self.entries.lock().get(parent).cloned(); match entry { Some(entry) => match &*entry { PathEntry::File(_) => { return Err(FsError::Io(Error::new( ErrorKind::InvalidInput, "Parent is a file", ))) } PathEntry::Dir => {} }, None => { if recursive { self.mkdir_sync(parent, true, None)?; } else { return Err(FsError::Io(Error::new( ErrorKind::NotFound, "Not found", ))); } } } } let entry = self.entries.lock().get(&path).cloned(); match entry { Some(entry) => match &*entry { PathEntry::File(_) => Err(FsError::Io(Error::new( ErrorKind::InvalidInput, "Is a file", ))), PathEntry::Dir => Ok(()), }, None => { self.entries.lock().insert(path, Arc::new(PathEntry::Dir)); Ok(()) } } } async fn mkdir_async( &self, path: PathBuf, recursive: bool, mode: Option<u32>, ) -> FsResult<()> { self.mkdir_sync(&path, recursive, mode) } fn chmod_sync(&self, _path: &Path, _mode: u32) -> FsResult<()> { Err(FsError::NotSupported) } async fn chmod_async(&self, path: PathBuf, mode: u32) -> FsResult<()> { self.chmod_sync(&path, mode) } fn chown_sync( &self, _path: &Path, _uid: Option<u32>, _gid: Option<u32>, ) -> FsResult<()> { Err(FsError::NotSupported) } async fn chown_async( &self, path: PathBuf, uid: Option<u32>, gid: Option<u32>, ) -> FsResult<()> { self.chown_sync(&path, uid, gid) } fn lchown_sync( &self, _path: &Path, _uid: Option<u32>, _gid: Option<u32>, ) -> FsResult<()> { Err(FsError::NotSupported) } async fn lchown_async( &self, path: PathBuf, uid: Option<u32>, gid: Option<u32>, ) -> FsResult<()> { self.lchown_sync(&path, uid, gid) } fn remove_sync(&self, _path: &Path, _recursive: bool) -> FsResult<()> { Err(FsError::NotSupported) } async fn remove_async(&self, path: PathBuf, recursive: bool) -> FsResult<()> { self.remove_sync(&path, recursive) } fn copy_file_sync(&self, _from: &Path, _to: &Path) -> FsResult<()> { Err(FsError::NotSupported) } async fn copy_file_async(&self, from: PathBuf, to: PathBuf) -> FsResult<()> { self.copy_file_sync(&from, &to) } fn cp_sync(&self, _from: &Path, _to: &Path) -> FsResult<()> { Err(FsError::NotSupported) } async fn cp_async(&self, from: PathBuf, to: PathBuf) -> FsResult<()> { self.cp_sync(&from, &to) } fn stat_sync(&self, path: &Path) -> FsResult<FsStat> { let entry = self.get_entry(path); match entry { Some(entry) => match &*entry { PathEntry::Dir => Ok(FsStat { is_file: false, is_directory: true, is_symlink: false, size: 0, mtime: None, atime: None, birthtime: None, ctime: None, dev: 0, ino: 0, mode: 0, nlink: 0, uid: 0, gid: 0, rdev: 0, blksize: 0, blocks: 0, is_block_device: false, is_char_device: false, is_fifo: false, is_socket: false, }), PathEntry::File(data) => Ok(FsStat { is_file: true, is_directory: false, is_symlink: false, size: data.len() as u64, mtime: None, atime: None, birthtime: None, ctime: None, dev: 0, ino: 0, mode: 0, nlink: 0, uid: 0, gid: 0, rdev: 0, blksize: 0, blocks: 0, is_block_device: false, is_char_device: false, is_fifo: false, is_socket: false, }), }, None => Err(FsError::Io(Error::new(ErrorKind::NotFound, "Not found"))), } } async fn stat_async(&self, path: PathBuf) -> FsResult<FsStat> { self.stat_sync(&path) } fn lstat_sync(&self, _path: &Path) -> FsResult<FsStat> { Err(FsError::NotSupported) } async fn lstat_async(&self, path: PathBuf) -> FsResult<FsStat> { self.lstat_sync(&path) } fn realpath_sync(&self, _path: &Path) -> FsResult<PathBuf> { Err(FsError::NotSupported) } async fn realpath_async(&self, path: PathBuf) -> FsResult<PathBuf> { self.realpath_sync(&path) } fn read_dir_sync(&self, _path: &Path) -> FsResult<Vec<FsDirEntry>> { Err(FsError::NotSupported) } async fn read_dir_async(&self, path: PathBuf) -> FsResult<Vec<FsDirEntry>> { self.read_dir_sync(&path) } fn rename_sync(&self, _oldpath: &Path, _newpath: &Path) -> FsResult<()> { Err(FsError::NotSupported) } async fn rename_async( &self, oldpath: PathBuf, newpath: PathBuf, ) -> FsResult<()> { self.rename_sync(&oldpath, &newpath) } fn link_sync(&self, _oldpath: &Path, _newpath: &Path) -> FsResult<()> { Err(FsError::NotSupported) } async fn link_async( &self, oldpath: PathBuf, newpath: PathBuf, ) -> FsResult<()> { self.link_sync(&oldpath, &newpath) } fn symlink_sync( &self, _oldpath: &Path, _newpath: &Path, _file_type: Option<FsFileType>, ) -> FsResult<()> { Err(FsError::NotSupported) } async fn symlink_async( &self, oldpath: PathBuf, newpath: PathBuf, file_type: Option<FsFileType>, ) -> FsResult<()> { self.symlink_sync(&oldpath, &newpath, file_type) } fn read_link_sync(&self, _path: &Path) -> FsResult<PathBuf> { Err(FsError::NotSupported) } async fn read_link_async(&self, path: PathBuf) -> FsResult<PathBuf> { self.read_link_sync(&path) } fn truncate_sync(&self, _path: &Path, _len: u64) -> FsResult<()> { Err(FsError::NotSupported) } async fn truncate_async(&self, path: PathBuf, len: u64) -> FsResult<()> { self.truncate_sync(&path, len) } fn utime_sync( &self, _path: &Path, _atime_secs: i64, _atime_nanos: u32, _mtime_secs: i64, _mtime_nanos: u32, ) -> FsResult<()> { Err(FsError::NotSupported) } async fn utime_async( &self, path: PathBuf, atime_secs: i64, atime_nanos: u32, mtime_secs: i64, mtime_nanos: u32, ) -> FsResult<()> { self.utime_sync(&path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) } fn lutime_sync( &self, _path: &Path, _atime_secs: i64, _atime_nanos: u32, _mtime_secs: i64, _mtime_nanos: u32, ) -> FsResult<()> { Err(FsError::NotSupported) } async fn lutime_async( &self, path: PathBuf, atime_secs: i64, atime_nanos: u32, mtime_secs: i64, mtime_nanos: u32, ) -> FsResult<()> { self.lutime_sync(&path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) } fn write_file_sync( &self, path: &Path, options: OpenOptions, _access_check: Option<AccessCheckCb>, data: &[u8], ) -> FsResult<()> { let path = normalize_path(path); let has_parent_dir = path .parent() .and_then(|parent| self.get_entry(parent)) .map(|e| matches!(*e, PathEntry::Dir)) .unwrap_or(false); if !has_parent_dir { return Err(FsError::Io(Error::new( ErrorKind::NotFound, "Parent directory does not exist", ))); } let mut entries = self.entries.lock(); let entry = entries.entry(path.clone()); match entry { Entry::Occupied(mut entry) => { if let PathEntry::File(existing_data) = &**entry.get() { if options.create_new { return Err(FsError::Io(Error::new( ErrorKind::AlreadyExists, "File already exists", ))); } if options.append { let mut new_data = existing_data.clone(); new_data.extend_from_slice(data); entry.insert(Arc::new(PathEntry::File(new_data))); } else { entry.insert(Arc::new(PathEntry::File(data.to_vec()))); } Ok(()) } else { Err(FsError::Io(Error::new( ErrorKind::InvalidInput, "Not a file", ))) } } Entry::Vacant(entry) => { entry.insert(Arc::new(PathEntry::File(data.to_vec()))); Ok(()) } } } async fn write_file_async<'a>( &'a self, path: PathBuf, options: OpenOptions, access_check: Option<AccessCheckCb<'a>>, data: Vec<u8>, ) -> FsResult<()> { self.write_file_sync(&path, options, access_check, &data) } fn read_file_sync( &self, path: &Path, _access_check: Option<AccessCheckCb>, ) -> FsResult<Cow<'static, [u8]>> { let entry = self.get_entry(path); match entry { Some(entry) => match &*entry { PathEntry::File(data) => Ok(Cow::Owned(data.clone())), PathEntry::Dir => Err(FsError::Io(Error::new( ErrorKind::InvalidInput, "Is a directory", ))), }, None => Err(FsError::Io(Error::new(ErrorKind::NotFound, "Not found"))), } } async fn read_file_async<'a>( &'a self, path: PathBuf, access_check: Option<AccessCheckCb<'a>>, ) -> FsResult<Cow<'static, [u8]>> { self.read_file_sync(&path, access_check) } }