// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2021 The Gleam contributors use super::*; use std::ops::Deref; use std::{ cell::RefCell, collections::{HashMap, HashSet}, rc::Rc, time::Duration, }; use camino::{Utf8Path, Utf8PathBuf}; /// An in memory sharable collection of pretend files that can be used in place /// of a real file system. It is a shared reference to a set of buffer than can /// be cheaply cloned, all resulting copies pointing to the same internal /// buffers. /// /// Useful in tests and in environments like the browser where there is no file /// system. /// /// Not thread safe. The compiler is single threaded, so that's OK. /// /// Only supports absolute paths. The root directory ("/") is always guaranteed /// to exist. /// #[derive(Clone, Debug, PartialEq, Eq)] pub struct InMemoryFileSystem { files: Rc>>, } impl Default for InMemoryFileSystem { fn default() -> Self { let mut files = HashMap::new(); // Ensure root directory always exists. let _ = files.insert(Utf8PathBuf::from("/"), InMemoryFile::directory()); Self { files: Rc::new(RefCell::new(files)), } } } impl InMemoryFileSystem { pub fn new() -> Self { Self::default() } pub fn reset(&self) { self.files.deref().borrow_mut().clear(); } /// Returns the contents of each file, excluding directories. /// /// # Panics /// /// Panics if this is not the only reference to the underlying files. /// pub fn into_contents(self) -> HashMap { Rc::try_unwrap(self.files) .expect("InMemoryFileSystem::into_files called on a clone") .into_inner() .into_iter() .filter_map(|(path, file)| file.into_content().map(|content| (path, content))) .collect() } /// All files currently in the filesystem (directories are not included). pub fn files(&self) -> Vec { self.files .borrow() .iter() .filter(|(_, f)| !f.is_directory()) .map(|(path, _)| path) .cloned() .collect() } #[cfg(test)] /// Set the modification time of a file. /// /// Panics if the file does not exist. /// pub fn set_modification_time(&self, path: &Utf8Path, time: SystemTime) { self.files .deref() .borrow_mut() .get_mut(path) .unwrap() .modification_time = time; } pub fn try_set_modification_time( &self, path: &Utf8Path, time: SystemTime, ) -> Result<(), Error> { self.files .deref() .borrow_mut() .get_mut(path) .ok_or_else(|| Error::FileIo { kind: FileKind::File, action: FileIoAction::Open, path: path.to_path_buf(), err: None, })? .modification_time = time; Ok(()) } } impl FileSystemWriter for InMemoryFileSystem { fn delete_directory(&self, path: &Utf8Path) -> Result<(), Error> { let mut files = self.files.deref().borrow_mut(); if files.get(path).is_some_and(|f| !f.is_directory()) { return Err(Error::FileIo { kind: FileKind::Directory, action: FileIoAction::Delete, path: path.to_path_buf(), err: None, }); } let root = Utf8Path::new("/"); if path != root { // Ensure the root path always exists. // Deleting other files is fine. let _ = files.remove(path); } // Remove any files in the directory while let Some(file) = files .keys() .find(|file| file.as_path() != root && file.starts_with(path)) { let file = file.clone(); let _ = files.remove(&file); } Ok(()) } fn copy(&self, from: &Utf8Path, to: &Utf8Path) -> Result<(), Error> { self.write_bytes(to, &self.read_bytes(from)?) } fn copy_dir(&self, _: &Utf8Path, _: &Utf8Path) -> Result<(), Error> { panic!("unimplemented") // TODO } fn mkdir(&self, path: &Utf8Path) -> Result<(), Error> { // Traverse ancestors from parent to root. // Create each missing ancestor. for ancestor in path.ancestors() { if ancestor == "" { // Ignore the final ancestor of a relative path. continue; } // Ensure we don't overwrite an existing file. // We can ignore existing directories though. let mut files = self.files.deref().borrow_mut(); if files.get(ancestor).is_some_and(|f| !f.is_directory()) { return Err(Error::FileIo { kind: FileKind::Directory, action: FileIoAction::Create, path: ancestor.to_path_buf(), err: None, }); } let dir = InMemoryFile::directory(); _ = files.insert(ancestor.to_path_buf(), dir); } Ok(()) } fn hardlink(&self, left: &Utf8Path, right: &Utf8Path) -> Result<(), Error> { let mut files = self.files.deref().borrow_mut(); let Some(file) = files.get(left).cloned() else { return Err(Error::FileIo { kind: FileKind::File, action: FileIoAction::ReadMetadata, path: left.to_path_buf(), err: None, }); }; let _ = files.insert(right.into(), file); Ok(()) } fn symlink_dir(&self, _: &Utf8Path, _: &Utf8Path) -> Result<(), Error> { panic!("unimplemented") // TODO } fn delete_file(&self, path: &Utf8Path) -> Result<(), Error> { let mut files = self.files.deref().borrow_mut(); if files.get(path).is_some_and(|f| f.is_directory()) { return Err(Error::FileIo { kind: FileKind::File, action: FileIoAction::Delete, path: path.to_path_buf(), err: None, }); } let _ = files.remove(path); Ok(()) } fn write(&self, path: &Utf8Path, content: &str) -> Result<(), Error> { self.write_bytes(path, content.as_bytes()) } fn write_bytes(&self, path: &Utf8Path, content: &[u8]) -> Result<(), Error> { // Ensure directories exist if let Some(parent) = path.parent() { self.mkdir(parent)?; } let mut file = InMemoryFile::default(); _ = io::Write::write(&mut file, content).expect("channel buffer write"); _ = self .files .deref() .borrow_mut() .insert(path.to_path_buf(), file); Ok(()) } fn exists(&self, path: &Utf8Path) -> bool { self.files.deref().borrow().contains_key(path) } } impl FileSystemReader for InMemoryFileSystem { fn canonicalise(&self, path: &Utf8Path) -> Result { Ok(path.to_path_buf()) } fn read(&self, path: &Utf8Path) -> Result { let path = path.to_path_buf(); let files = self.files.deref().borrow(); let buffer = files .get(&path) .and_then(|file| file.node.as_file_buffer()) .ok_or_else(|| Error::FileIo { kind: FileKind::File, action: FileIoAction::Open, path: path.clone(), err: None, })?; let bytes = buffer.borrow(); let unicode = String::from_utf8(bytes.clone()).map_err(|err| Error::FileIo { kind: FileKind::File, action: FileIoAction::Read, path: path.clone(), err: Some(err.to_string()), })?; Ok(unicode) } fn read_bytes(&self, path: &Utf8Path) -> Result, Error> { let path = path.to_path_buf(); let files = self.files.deref().borrow(); let buffer = files .get(&path) .and_then(|file| file.node.as_file_buffer()) .ok_or_else(|| Error::FileIo { kind: FileKind::File, action: FileIoAction::Open, path: path.clone(), err: None, })?; let bytes = buffer.borrow().clone(); Ok(bytes) } fn is_file(&self, path: &Utf8Path) -> bool { self.files .deref() .borrow() .get(path) .is_some_and(|file| !file.is_directory()) } fn is_directory(&self, path: &Utf8Path) -> bool { self.files .deref() .borrow() .get(path) .is_some_and(|file| file.is_directory()) } fn reader(&self, _path: &Utf8Path) -> Result { unreachable!("Memory reader unimplemented") } fn read_dir(&self, path: &Utf8Path) -> Result { let read_dir = ReadDir::from_iter( self.files .deref() .borrow() .keys() .map(|file_path| file_path.to_path_buf()) .filter(|file_path| file_path.parent().is_some_and(|parent| path == parent)) .map(DirEntry::from_pathbuf) .map(Ok), ); Ok(read_dir) } fn modification_time(&self, path: &Utf8Path) -> Result { let files = self.files.deref().borrow(); let file = files.get(path).ok_or_else(|| Error::FileIo { kind: FileKind::File, action: FileIoAction::ReadMetadata, path: path.to_path_buf(), err: None, })?; Ok(file.modification_time) } fn is_same_file(&self, _left: &Utf8Path, _right: &Utf8Path) -> Result { unreachable!("is_same_file unimplemented") } } /// The representation of a file or directory in the in-memory filesystem. /// /// Stores a file's buffer of contents. /// #[derive(Debug, Clone, PartialEq, Eq)] pub enum InMemoryFileNode { File(Rc>>), Directory, } impl InMemoryFileNode { /// Returns this file's file buffer if this isn't a directory. fn as_file_buffer(&self) -> Option<&Rc>>> { match self { Self::File(buffer) => Some(buffer), Self::Directory => None, } } /// Returns this file's file buffer if this isn't a directory. fn into_file_buffer(self) -> Option>>> { match self { Self::File(buffer) => Some(buffer), Self::Directory => None, } } } /// An in memory sharable that can be used in place of a real file. It is a /// shared reference to a buffer than can be cheaply cloned, all resulting copies /// pointing to the same internal buffer. /// /// Useful in tests and in environments like the browser where there is no file /// system. /// /// This struct holds common properties of different types of filesystem nodes /// (files and directories). The `node` field contains the file's content /// buffer, if this is not a directory. /// /// Not thread safe. The compiler is single threaded, so that's OK. /// #[derive(Debug, Clone, PartialEq, Eq)] pub struct InMemoryFile { node: InMemoryFileNode, modification_time: SystemTime, } impl InMemoryFile { /// Creates a directory. pub fn directory() -> Self { Self { node: InMemoryFileNode::Directory, ..Self::default() } } /// Checks whether this is a directory's entry. pub fn is_directory(&self) -> bool { matches!(self.node, InMemoryFileNode::Directory) } /// Returns this file's contents if this is not a directory. /// /// # Panics /// /// Panics if this is not the only reference to the underlying files. /// pub fn into_content(self) -> Option { let buffer = self.node.into_file_buffer()?; let contents = Rc::try_unwrap(buffer) .expect("InMemoryFile::into_content called with multiple references") .into_inner(); // All null bytes are usually from when a binary file is empty, and // aren't particularly useful as text, so we treat them as binary. if contents.iter().all(|byte| *byte == 0) { return Some(Content::Binary(contents)); } match String::from_utf8(contents) { Ok(s) => Some(Content::Text(s)), Err(e) => Some(Content::Binary(e.into_bytes())), } } } impl Default for InMemoryFile { fn default() -> Self { Self { node: InMemoryFileNode::File(Rc::new(RefCell::new(vec![]))), // We use a fixed time here so that the tests are deterministic. In // future we may want to inject this in some fashion. modification_time: SystemTime::UNIX_EPOCH + Duration::from_secs(663112800), } } } impl io::Write for InMemoryFile { fn write(&mut self, buf: &[u8]) -> io::Result { let Some(buffer) = self.node.as_file_buffer() else { // Not a file return Err(io::Error::from(io::ErrorKind::NotFound)); }; let mut reference = (*buffer).borrow_mut(); reference.write(buf) } fn flush(&mut self) -> io::Result<()> { let Some(buffer) = self.node.as_file_buffer() else { // Not a file return Err(io::Error::from(io::ErrorKind::NotFound)); }; let mut reference = (*buffer).borrow_mut(); reference.flush() } } impl CommandExecutor for InMemoryFileSystem { fn exec(&self, _command: Command) -> Result { Ok(0) // Always succeed. } } impl BeamCompilerIO for InMemoryFileSystem { fn compile_beam( &self, _out: &Utf8Path, _lib: &Utf8Path, _modules: &HashSet, _stdio: Stdio, ) -> Result, Error> { Ok(Vec::new()) // Always succeed. } } #[test] fn test_empty_in_memory_fs_has_root() { let imfs = InMemoryFileSystem::new(); assert!(imfs.exists(Utf8Path::new("/"))); } #[test] fn test_cannot_remove_root_from_in_memory_fs() -> Result<(), Error> { let imfs = InMemoryFileSystem::new(); imfs.write(&Utf8PathBuf::from("/a/b/c.txt"), "a")?; imfs.delete_directory(Utf8Path::new("/"))?; assert!(!imfs.exists(Utf8Path::new("/a/b/c.txt"))); assert!(imfs.exists(Utf8Path::new("/"))); Ok(()) } #[test] fn test_files() -> Result<(), Error> { let imfs = InMemoryFileSystem::new(); imfs.write(&Utf8PathBuf::from("/a/b/c.txt"), "a")?; imfs.write(&Utf8PathBuf::from("/d/e.txt"), "a")?; let mut files = imfs.files(); // Sort for test determinism due to hash map usage. files.sort_unstable(); assert_eq!( vec![ Utf8PathBuf::from("/a/b/c.txt"), Utf8PathBuf::from("/d/e.txt"), ], files ); Ok(()) } #[test] fn test_in_memory_dir_walking() -> Result<(), Error> { use itertools::Itertools; let imfs = InMemoryFileSystem::new(); imfs.write(&Utf8PathBuf::from("/a/b/a.txt"), "a")?; imfs.write(&Utf8PathBuf::from("/a/b/b.txt"), "a")?; imfs.write(&Utf8PathBuf::from("/a/b/c.txt"), "a")?; imfs.write(&Utf8PathBuf::from("/b/d.txt"), "a")?; imfs.write(&Utf8PathBuf::from("/a/c/e.txt"), "a")?; imfs.write(&Utf8PathBuf::from("/a/c/d/f.txt"), "a")?; imfs.write(&Utf8PathBuf::from("/a/g.txt"), "a")?; imfs.write(&Utf8PathBuf::from("/h.txt"), "a")?; imfs.mkdir(&Utf8PathBuf::from("/a/e"))?; let mut walked_entries: Vec = DirWalker::new(Utf8PathBuf::from("/a/")) .into_file_iter(&imfs) .map_ok(Utf8PathBuf::into_string) .try_collect()?; // Keep test deterministic due to hash map usage walked_entries.sort_unstable(); assert_eq!( vec![ "/a/b/a.txt".to_owned(), "/a/b/b.txt".to_owned(), "/a/b/c.txt".to_owned(), "/a/c/d/f.txt".to_owned(), "/a/c/e.txt".to_owned(), "/a/g.txt".to_owned(), ], walked_entries, ); Ok(()) }