From c890f775bbe5b54db8db3efe8783fc09058f2bc4 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Wed, 22 Mar 2023 23:12:48 +0800 Subject: [PATCH] Implemented the last of the FileSystem trait --- Cargo.lock | 2 +- lib/vfs/src/lib.rs | 4 +- lib/vfs/src/webc_volume_fs.rs | 373 +++++++++++++++++++++++++++------- 3 files changed, 304 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17a88197c91..feb4b332ebb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5764,7 +5764,7 @@ dependencies = [ [[package]] name = "webc" version = "5.0.0-rc.5" -source = "git+https://github.com/wasmerio/pirita?branch=compat-patches#8be3e3bc18c9abf0062dd6543ac08f5211327ca3" +source = "git+https://github.com/wasmerio/pirita?branch=compat-patches#e9180ce2467c72139661443f384df105f01da278" dependencies = [ "anyhow", "base64 0.21.0", diff --git a/lib/vfs/src/lib.rs b/lib/vfs/src/lib.rs index 1e735ab2d2b..2722f5a8386 100644 --- a/lib/vfs/src/lib.rs +++ b/lib/vfs/src/lib.rs @@ -502,7 +502,7 @@ impl ReadDir { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct DirEntry { pub path: PathBuf, // weird hack, to fix this we probably need an internal trait object or callbacks or something @@ -532,7 +532,7 @@ impl DirEntry { } #[allow(clippy::len_without_is_empty)] // Clippy thinks it's an iterator. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] // TODO: review this, proper solution would probably use a trait object internally pub struct Metadata { pub ft: FileType, diff --git a/lib/vfs/src/webc_volume_fs.rs b/lib/vfs/src/webc_volume_fs.rs index 79be71f5c90..96eef8c1df1 100644 --- a/lib/vfs/src/webc_volume_fs.rs +++ b/lib/vfs/src/webc_volume_fs.rs @@ -1,11 +1,22 @@ -use std::{convert::TryInto, path::Path}; +use std::{ + convert::{TryFrom, TryInto}, + io::Cursor, + path::{Path, PathBuf}, + pin::Pin, + result::Result, + task::Poll, +}; +use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite}; use webc::{ - compat::{Container, Volume}, - v2::{PathSegment, PathSegments, ToPathSegments}, + compat::{Container, SharedBytes, Volume}, + v2::{PathSegments, ToPathSegments}, }; -use crate::{EmptyFileSystem, FileSystem, FsError, OverlayFileSystem}; +use crate::{ + DirEntry, EmptyFileSystem, FileOpener, FileSystem, FileType, FsError, Metadata, + OpenOptionsConfig, OverlayFileSystem, ReadDir, VirtualFile, +}; #[derive(Debug, Clone)] pub struct WebcVolumeFileSystem { @@ -37,72 +48,264 @@ impl WebcVolumeFileSystem { } impl FileSystem for WebcVolumeFileSystem { - fn read_dir(&self, path: &Path) -> crate::Result { - let _path = normalize(path).map_err(|_| FsError::InvalidInput)?; - // self.volume.read_dir(path) - todo!() + fn read_dir(&self, path: &Path) -> Result { + let meta = self.metadata(path)?; + + if !meta.is_dir() { + return Err(FsError::BaseNotDirectory); + } + + let path = normalize(path)?; + + let mut entries = Vec::new(); + + for (name, meta) in self + .volume() + .read_dir(&path) + .ok_or(FsError::EntryNotFound)? + { + let path = PathBuf::from(path.join(name).to_string()); + entries.push(DirEntry { + path, + metadata: Ok(compat_meta(meta)), + }); + } + + Ok(ReadDir::new(entries)) } - fn create_dir(&self, _path: &Path) -> crate::Result<()> { - Err(FsError::PermissionDenied) + fn create_dir(&self, path: &Path) -> Result<(), FsError> { + // the directory shouldn't exist yet + if self.metadata(path).is_ok() { + return Err(FsError::AlreadyExists); + } + + // it's parent should exist + let parent = path.parent().unwrap_or_else(|| Path::new("/")); + + match self.metadata(parent) { + Ok(parent_meta) if parent_meta.is_dir() => { + // The operation would normally be doable... but we're a readonly + // filesystem + Err(FsError::PermissionDenied) + } + Ok(_) | Err(FsError::EntryNotFound) => Err(FsError::BaseNotDirectory), + Err(other) => Err(other), + } } - fn remove_dir(&self, _path: &Path) -> crate::Result<()> { + fn remove_dir(&self, path: &Path) -> Result<(), FsError> { + // The original directory should exist + let meta = self.metadata(path)?; + + // and it should be a directory + if !meta.is_dir() { + return Err(FsError::BaseNotDirectory); + } + + // but we are a readonly filesystem, so you can't modify anything Err(FsError::PermissionDenied) } - fn rename(&self, _from: &Path, _to: &Path) -> crate::Result<()> { + fn rename(&self, from: &Path, to: &Path) -> Result<(), FsError> { + // The original file should exist + let _ = self.metadata(from)?; + + // we also want to make sure the destination's folder exists, too + let dest_parent = to.parent().unwrap_or_else(|| Path::new("/")); + let parent_meta = self.metadata(dest_parent)?; + if !parent_meta.is_dir() { + return Err(FsError::BaseNotDirectory); + } + + // but we are a readonly filesystem, so you can't modify anything Err(FsError::PermissionDenied) } - fn metadata(&self, _path: &Path) -> crate::Result { - todo!() + fn metadata(&self, path: &Path) -> Result { + let path = normalize(path)?; + + self.volume() + .metadata(path) + .map(compat_meta) + .ok_or(FsError::EntryNotFound) } - fn remove_file(&self, _path: &Path) -> crate::Result<()> { + fn remove_file(&self, path: &Path) -> Result<(), FsError> { + let meta = self.metadata(path)?; + + if !meta.is_file() { + return Err(FsError::NotAFile); + } + Err(FsError::PermissionDenied) } fn new_open_options(&self) -> crate::OpenOptions { - todo!() + crate::OpenOptions::new(self) } } -/// Normalize a [`Path`] into a [`PathSegments`], dealing with things like `..` -/// and skipping `.`'s. -#[tracing::instrument(level = "trace", err)] -fn normalize(path: &Path) -> Result { - if !path.is_absolute() { - return Err(FsError::InvalidInput); - } - - let mut segments: Vec = Vec::new(); +impl FileOpener for WebcVolumeFileSystem { + fn open( + &self, + path: &Path, + conf: &OpenOptionsConfig, + ) -> crate::Result> { + if let Some(parent) = path.parent() { + let parent_meta = self.metadata(parent)?; + if !parent_meta.is_dir() { + return Err(FsError::BaseNotDirectory); + } + } - for component in path.components() { - match component { - std::path::Component::Normal(s) => { - segments.push(s.try_into().map_err(|_| FsError::InvalidInput)?); + match self.volume().metadata(path) { + Some(m) if m.is_file() => {} + Some(_) => return Err(FsError::NotAFile), + None if conf.create() || conf.create_new() => { + // The file would normally be created, but we are a readonly fs. + return Err(FsError::PermissionDenied); } - std::path::Component::CurDir => continue, - std::path::Component::ParentDir => { - // Note: We want /path/to/../../../../../file.txt to normalize - // to /file.txt - let _ = segments.pop(); + None => return Err(FsError::EntryNotFound), + } + + match self.volume().read_file(path) { + Some(bytes) => Ok(Box::new(File(Cursor::new(bytes)))), + None => { + // The metadata() call should guarantee this, so something + // probably went wrong internally + Err(FsError::UnknownError) } - std::path::Component::RootDir | std::path::Component::Prefix(_) => segments.clear(), } } +} + +#[derive(Debug, Clone, PartialEq)] +struct File(Cursor); + +impl VirtualFile for File { + fn last_accessed(&self) -> u64 { + 0 + } + + fn last_modified(&self) -> u64 { + 0 + } + + fn created_time(&self) -> u64 { + 0 + } + + fn size(&self) -> u64 { + self.0.get_ref().len().try_into().unwrap() + } + + fn set_len(&mut self, _new_size: u64) -> crate::Result<()> { + Err(FsError::PermissionDenied) + } + + fn unlink(&mut self) -> crate::Result<()> { + Err(FsError::PermissionDenied) + } + + fn poll_read_ready( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + let bytes_remaining = self.0.get_ref().len() - usize::try_from(self.0.position()).unwrap(); + Poll::Ready(Ok(bytes_remaining)) + } + + fn poll_write_ready( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into())) + } +} + +impl AsyncRead for File { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf) + } +} + +impl AsyncSeek for File { + fn start_seek(mut self: Pin<&mut Self>, position: std::io::SeekFrom) -> std::io::Result<()> { + AsyncSeek::start_seek(Pin::new(&mut self.0), position) + } + + fn poll_complete( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + AsyncSeek::poll_complete(Pin::new(&mut self.0), cx) + } +} + +impl AsyncWrite for File { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + _buf: &[u8], + ) -> Poll> { + Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into())) + } + + fn poll_flush( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into())) + } + + fn poll_shutdown( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into())) + } +} + +fn compat_meta(meta: webc::compat::Metadata) -> Metadata { + match meta { + webc::compat::Metadata::Dir => Metadata { + ft: FileType { + dir: true, + ..Default::default() + }, + ..Default::default() + }, + webc::compat::Metadata::File { length } => Metadata { + ft: FileType { + file: true, + ..Default::default() + }, + len: length.try_into().unwrap(), + ..Default::default() + }, + } +} - segments - .to_path_segments() - .map_err(|_| FsError::InvalidInput) +/// Normalize a [`Path`] into a [`PathSegments`], dealing with things like `..` +/// and skipping `.`'s. +#[tracing::instrument(level = "trace", err)] +fn normalize(path: &Path) -> Result { + // This is all handled by the ToPathSegments impl for &Path + path.to_path_segments().map_err(|_| FsError::InvalidInput) } #[cfg(test)] mod tests { - use crate::{DirEntry, Metadata}; - use super::*; + use crate::DirEntry; + use std::convert::TryFrom; + use tokio::io::AsyncReadExt; + const PYTHON_WEBC: &[u8] = include_bytes!("../../c-api/examples/assets/python-0.1.0.wasmer"); #[test] @@ -127,7 +330,7 @@ mod tests { let paths = [".", "..", "./file.txt", ""]; for path in paths { - assert_eq!(normalize(path.as_ref()), Err(FsError::InvalidInput),); + assert_eq!(normalize(path.as_ref()), Err(FsError::InvalidInput)); } } @@ -137,8 +340,9 @@ mod tests { let fs = WebcVolumeFileSystem::mount_all(&container); - let items = fs.read_dir("/".as_ref()).unwrap(); - panic!("{:?}", items); + // We should now have access to the python directory + let lib_meta = fs.metadata("/lib/python3.6/".as_ref()).unwrap(); + assert!(lib_meta.is_dir()); } #[test] @@ -156,6 +360,32 @@ mod tests { .map(|r| r.unwrap()) .collect(); let expected = vec![ + DirEntry { + path: "/lib/.DS_Store".into(), + metadata: Ok(Metadata { + ft: FileType { + file: true, + ..Default::default() + }, + accessed: 0, + created: 0, + modified: 0, + len: 6148, + }), + }, + DirEntry { + path: "/lib/Parser".into(), + metadata: Ok(Metadata { + ft: FileType { + dir: true, + ..Default::default() + }, + accessed: 0, + created: 0, + modified: 0, + len: 0, + }), + }, DirEntry { path: "/lib/python.wasm".into(), metadata: Ok(crate::Metadata { @@ -166,7 +396,7 @@ mod tests { accessed: 0, created: 0, modified: 0, - len: 1234, + len: 4694941, }), }, DirEntry { @@ -183,23 +413,7 @@ mod tests { }), }, ]; - todo!(); - } - - fn assert_eq_metadata(left: Metadata, right: Metadata) { - let Metadata { - ft, - accessed, - created, - modified, - len, - } = left; - - assert_eq!(ft, right.ft); - assert_eq!(accessed, right.accessed); - assert_eq!(created, right.created); - assert_eq!(modified, right.modified); - assert_eq!(len, right.len); + assert_eq!(entries, expected); } #[test] @@ -218,23 +432,23 @@ mod tests { accessed: 0, created: 0, modified: 0, - len: 1234, + len: 4694941, }; - assert_eq_metadata( + assert_eq!( fs.metadata("/lib/python.wasm".as_ref()).unwrap(), - python_wasm.clone(), + python_wasm, ); - assert_eq_metadata( + assert_eq!( fs.metadata("/../../../../lib/python.wasm".as_ref()) .unwrap(), - python_wasm.clone(), + python_wasm, ); - assert_eq_metadata( + assert_eq!( fs.metadata("/lib/python3.6/../python3.6/../python.wasm".as_ref()) .unwrap(), python_wasm, ); - assert_eq_metadata( + assert_eq!( fs.metadata("/lib/python3.6".as_ref()).unwrap(), crate::Metadata { ft: crate::FileType { @@ -253,8 +467,8 @@ mod tests { ); } - #[test] - fn file_opener() { + #[tokio::test] + async fn file_opener() { let container = Container::from_bytes(PYTHON_WEBC).unwrap(); let volumes = container.volumes(); let volume = volumes["atom"].clone(); @@ -271,7 +485,7 @@ mod tests { ); assert_eq!( fs.new_open_options().read(true).open("/lib").unwrap_err(), - FsError::InvalidInput, + FsError::NotAFile, ); assert_eq!( fs.new_open_options() @@ -280,6 +494,21 @@ mod tests { .unwrap_err(), FsError::EntryNotFound, ); + + // We should be able to actually read the file + let mut f = fs + .new_open_options() + .read(true) + .open("/lib/python.wasm") + .unwrap(); + let mut buffer = Vec::new(); + f.read_to_end(&mut buffer).await.unwrap(); + dbg!(&buffer[..10]); + assert!(buffer.starts_with(b"\0asm")); + assert_eq!( + fs.metadata("/lib/python.wasm".as_ref()).unwrap().len(), + u64::try_from(buffer.len()).unwrap(), + ); } #[test] @@ -343,7 +572,7 @@ mod tests { FsError::BaseNotDirectory, ); assert_eq!( - fs.remove_file("/lib/nested/".as_ref()).unwrap_err(), + fs.create_dir("/lib/nested/".as_ref()).unwrap_err(), FsError::PermissionDenied, ); }