diff --git a/lib/vfs/src/overlay_fs.rs b/lib/vfs/src/overlay_fs.rs index 1a6050a69..989d78889 100644 --- a/lib/vfs/src/overlay_fs.rs +++ b/lib/vfs/src/overlay_fs.rs @@ -1,24 +1,17 @@ use std::{fmt::Debug, path::Path}; use crate::{ - FileOpener, FileSystems, FsError, Metadata, OpenOptions, OpenOptionsConfig, ReadDir, - VirtualFile, + FileOpener, FileSystem, FileSystemExt, FileSystems, FsError, Metadata, OpenOptions, + OpenOptionsConfig, ReadDir, VirtualFile, }; -/// A primary read-write filesystem and chain of read-only secondary filesystems -/// that are overlayed on top of each other. +/// A primary filesystem and chain of secondary filesystems that are overlayed +/// on top of each other. /// /// # Precedence /// -/// The general rule is that mutating operations (e.g. -/// [`crate::FileSystem::remove_dir()`] or [`FileOpener::open()`] with -/// [`OpenOptions::write()`] set) will only be executed against the "primary" -/// filesystem. +/// The [`OverlayFileSystem`] will execute operations based on precedence. /// -/// For operations which don't make modifications (e.g. [`FileOpener::open()`] -/// in read-only mode), [`FileSystem`] will first check the primary filesystem, -/// and if that fails it will iterate through the secondary filesystems until -/// either one of them succeeds or there are no more filesystems. /// /// Most importantly, this means earlier filesystems can shadow files and /// directories that have a lower precedence. @@ -36,7 +29,7 @@ use crate::{ /// use wasmer_vfs::{ /// mem_fs::FileSystem as MemFS, /// host_fs::FileSystem as HostFS, -/// overlay_fs::FileSystem as OverlayFS, +/// overlay_fs::FileSystem, /// }; /// let fs = OverlayFS::new(MemFS::default(), [HostFS]); /// @@ -50,24 +43,24 @@ use crate::{ /// /// A more complex example is #[derive(Clone, PartialEq, Eq)] -pub struct FileSystem +pub struct OverlayFileSystem where - P: crate::FileSystem, + P: FileSystem, S: for<'a> FileSystems<'a>, { primary: P, secondaries: S, } -impl FileSystem +impl OverlayFileSystem where - P: crate::FileSystem, + P: FileSystem, S: for<'a> FileSystems<'a>, { - /// Create a new [`FileSystem`] using a primary [`crate::FileSystem`] and - /// a chain of read-only secondary [`FileSystems`]. + /// Create a new [`FileSystem`] using a primary [`crate::FileSystem`] and a + /// chain of secondary [`FileSystems`]. pub fn new(primary: P, secondaries: S) -> Self { - FileSystem { + OverlayFileSystem { primary, secondaries, } @@ -94,15 +87,35 @@ where } /// Iterate over all filesystems in order of precedence. - pub fn iter(&self) -> impl Iterator + '_ { - std::iter::once(self.primary() as &dyn crate::FileSystem) + pub fn iter(&self) -> impl Iterator + '_ { + std::iter::once(self.primary() as &dyn FileSystem) .chain(self.secondaries().iter_filesystems()) } + + /// Try to apply an operation to each [`FileSystem`] in order of precedence. + /// + /// This uses [`should_continue()`] to determine whether an error is fatal + /// and needs to be returned to the caller, or whether we should try the + /// next [`FileSystem`] in the chain. + fn for_each(&self, mut func: F) -> Result + where + F: FnMut(&dyn FileSystem) -> Result, + { + for fs in self.iter() { + match func(fs) { + Ok(result) => return Ok(result), + Err(e) if should_continue(e) => continue, + Err(other) => return Err(other), + } + } + + Err(FsError::EntryNotFound) + } } -impl crate::FileSystem for FileSystem +impl FileSystem for OverlayFileSystem where - P: crate::FileSystem, + P: FileSystem, S: for<'a> crate::FileSystems<'a> + Send + Sync, { fn read_dir(&self, path: &Path) -> Result { @@ -135,82 +148,24 @@ where } } - fn create_dir(&self, _path: &Path) -> Result<(), FsError> { - todo!() + fn create_dir(&self, path: &Path) -> Result<(), FsError> { + self.for_each(|fs| fs.create_dir(path)) } fn remove_dir(&self, path: &Path) -> Result<(), FsError> { - match self.primary.remove_dir(path) { - Ok(_) => return Ok(()), - Err(e) if should_continue(e) => { - // It's not in the primary filesystem, so we'll check the - // secondaries to see whether we need to return a permission - // error or a not found error. - } - Err(other) => return Err(other), - } - - for fs in self.secondaries().iter_filesystems() { - match fs.metadata(path) { - Ok(m) if m.is_dir() => { - return Err(FsError::PermissionDenied); - } - Ok(_) => return Err(FsError::BaseNotDirectory), - Err(e) if should_continue(e) => continue, - Err(e) => return Err(e), - } - } - - Err(FsError::BaseNotDirectory) + self.for_each(|fs| fs.remove_dir(path)) } fn rename(&self, from: &Path, to: &Path) -> Result<(), FsError> { - match self.primary.rename(from, to) { - Ok(_) => return Ok(()), - Err(e) if should_continue(e) => {} - Err(e) => return Err(e), - } - - for fs in self.secondaries().iter_filesystems() { - if fs.metadata(from).is_ok() { - return Err(FsError::PermissionDenied); - } - } - - Err(FsError::EntryNotFound) + self.for_each(|fs| fs.rename(from, to)) } fn metadata(&self, path: &Path) -> Result { - for fs in self.iter() { - match fs.metadata(path) { - Ok(meta) => return Ok(meta), - Err(e) if should_continue(e) => continue, - Err(e) => return Err(e), - } - } - - Err(FsError::EntryNotFound) + self.for_each(|fs| fs.metadata(path)) } fn remove_file(&self, path: &Path) -> Result<(), FsError> { - match self.primary.remove_file(path) { - Ok(_) => return Ok(()), - Err(e) if should_continue(e) => {} - Err(e) => return Err(e), - } - - for fs in self.secondaries.iter_filesystems() { - match fs.metadata(path) { - Ok(meta) if meta.is_file() => { - return Err(FsError::PermissionDenied); - } - Ok(_) => return Err(FsError::NotAFile), - Err(FsError::EntryNotFound) => {} - Err(e) => return Err(e), - } - } - - Err(FsError::EntryNotFound) + self.for_each(|fs| fs.remove_file(path)) } fn new_open_options(&self) -> OpenOptions<'_> { @@ -218,47 +173,48 @@ where } } -impl FileOpener for FileSystem +impl FileOpener for OverlayFileSystem where - P: crate::FileSystem, - S: for<'a> FileSystems<'a>, + P: FileSystem, + S: for<'a> FileSystems<'a> + Send + Sync, { fn open( &self, path: &Path, conf: &OpenOptionsConfig, ) -> Result, FsError> { - // First, try the primary filesystem - match self - .primary - .new_open_options() - .options(conf.clone()) - .open(path) - { - Ok(f) => return Ok(f), - Err(e) if should_continue(e) => {} - Err(e) => return Err(e), - }; + // FIXME: There is probably a smarter way to do this without the extra + // FileSystem::metadata() calls or the risk of TOCTOU issues. - if conf.would_mutate() { - return Err(FsError::PermissionDenied); - } + if (conf.create || conf.create_new) && !self.exists(path) { + if let Some(parent) = path.parent() { + // As a special case, we want to direct all newly created files + // to the primary filesystem so it just *looks* like they are + // created alongside secondary filesystems. + let would_normally_be_created_on_a_secondary_fs = self + .secondaries + .iter_filesystems() + .into_iter() + .any(|fs| fs.exists(parent)); - for fs in self.secondaries.iter_filesystems() { - match fs.new_open_options().options(conf.clone()).open(path) { - Ok(f) => return Ok(f), - Err(e) if should_continue(e) => continue, - Err(e) => return Err(e), + if would_normally_be_created_on_a_secondary_fs { + self.primary.create_dir_all(parent)?; + return self + .primary + .new_open_options() + .options(conf.clone()) + .open(path); + } } } - Err(FsError::EntryNotFound) + self.for_each(|fs| fs.new_open_options().options(conf.clone()).open(path)) } } -impl Debug for FileSystem +impl Debug for OverlayFileSystem where - P: crate::FileSystem, + P: FileSystem, S: for<'a> FileSystems<'a>, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -302,13 +258,13 @@ mod tests { #[test] fn can_be_used_as_an_object() { fn _box_with_memfs( - fs: FileSystem>, + fs: OverlayFileSystem>, ) -> Box { Box::new(fs) } fn _arc( - fs: FileSystem, Vec>>, + fs: OverlayFileSystem, Vec>>, ) -> Arc { Arc::new(fs) } @@ -335,7 +291,7 @@ mod tests { .unwrap(); secondary.create_dir(third).unwrap(); - let overlay = FileSystem::new(primary, [secondary]); + let overlay = OverlayFileSystem::new(primary, [secondary]); // Delete a folder on the primary filesystem overlay.remove_dir(first).unwrap(); @@ -376,7 +332,7 @@ mod tests { .await .unwrap(); - let fs = FileSystem::new(primary, [secondary]); + let fs = OverlayFileSystem::new(primary, [secondary]); // Any new files will be created on the primary fs let _ = fs @@ -420,7 +376,7 @@ mod tests { .touch("/secondary/overlayed.txt") .unwrap(); - let fs = FileSystem::new(primary, [secondary, secondary_overlayed]); + let fs = OverlayFileSystem::new(primary, [secondary, secondary_overlayed]); let paths: Vec<_> = fs.walk("/").map(|entry| entry.path()).collect(); assert_eq!(