use futures::Stream; use indexmap::map::Entry as IndexEntry; use indexmap::IndexMap; use std::ffi::{OsStr, OsString}; use std::future::Future; use std::io::ErrorKind; use std::path::{Component, Path, PathBuf}; use std::pin::Pin; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; use std::{error, io}; use vrc_get_vpm::io::{EnvironmentIo, ExitStatus, FileType, IoTrait, Metadata, ProjectIo}; pub(crate) use file_stream::*; const NOTA_DIRECTORY: ErrorKind = ErrorKind::Other; // NotADirectory is unstable const IS_DIRECTORY: ErrorKind = ErrorKind::Other; // IsADirectory is unstable fn err(kind: ErrorKind, error: E) -> io::Result where E: Into>, { Err(io::Error::new(kind, error)) } /// The virtual file system is a TraitIo implementation for testing. /// /// This struct implements All EnvironmentIo and ProjectIo methods. pub struct VirtualFileSystem { root: DirectoryEntry, } impl VirtualFileSystem { pub fn new() -> Self { Self { root: DirectoryEntry::new(), } } pub async fn add_file(&self, path: &Path, content: &[u8]) -> io::Result<()> { let Some((dir_path, last)) = self.resolve2(path)? else { return err(IS_DIRECTORY, "is directory"); }; self.root .create_dir_all(&dir_path) .await? .create_file(last, true) .await? .set_content(content) .await; Ok(()) } pub async fn deny_deletion(&self, path: &Path) -> io::Result<()> { let Some((dir_path, last)) = self.resolve2(path)? else { return err(IS_DIRECTORY, "is directory"); }; self.root .get_folder(&dir_path) .await? .get(last) .await? .as_file()? .deny_deletion(); Ok(()) } } impl VirtualFileSystem { fn resolve<'a>(&self, path: &'a Path) -> io::Result> { let mut result = Vec::new(); for x in path.components() { match x { Component::Prefix(_) | Component::RootDir => { panic!("absolute path") } Component::CurDir => continue, Component::ParentDir => { if result.pop().is_none() { panic!("accessing parent folder") } } Component::Normal(component) => result.push(component), } } Ok(result) } fn resolve2<'a>(&self, path: &'a Path) -> io::Result, &'a OsStr)>> { let mut resolved = self.resolve(path)?; if resolved.is_empty() { Ok(None) } else { let last = resolved.remove(resolved.len() - 1); Ok(Some((resolved, last))) } } } impl vrc_get_vpm::io::IoTrait for VirtualFileSystem { async fn create_dir_all(&self, path: &Path) -> io::Result<()> { self.root.create_dir_all(&self.resolve(path)?).await?; Ok(()) } async fn write(&self, path: &Path, content: &[u8]) -> io::Result<()> { let Some((dir_path, last)) = self.resolve2(path)? else { return err(IS_DIRECTORY, "is directory"); }; let file = self .root .get_folder(&dir_path) .await? .create_file(last, false) .await?; file.set_content(content).await; Ok(()) } async fn remove_file(&self, path: &Path) -> io::Result<()> { let Some((dir_path, last)) = self.resolve2(path)? else { return err(IS_DIRECTORY, "is directory"); }; self.root .get_folder(&dir_path) .await? .remove_file(last) .await?; Ok(()) } async fn remove_dir(&self, path: &Path) -> io::Result<()> { let Some((dir_path, last)) = self.resolve2(path)? else { return err(ErrorKind::PermissionDenied, "removing root"); }; self.root .get_folder(&dir_path) .await? .remove_dir_all(last) .await?; Ok(()) } async fn remove_dir_all(&self, path: &Path) -> io::Result<()> { let Some((dir_path, last)) = self.resolve2(path)? else { return err(ErrorKind::PermissionDenied, "removing root"); }; self.root .get_folder(&dir_path) .await? .remove_dir_all(last) .await?; Ok(()) } async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> { let Some((from_dir, from_last)) = self.resolve2(from)? else { return err(ErrorKind::PermissionDenied, "moving root"); }; let Some((to_dir, to_last)) = self.resolve2(to)? else { return err(ErrorKind::PermissionDenied, "moving to root"); }; let from_dir = self.root.get_folder(&from_dir).await?; let to_dir = self.root.get_folder(&to_dir).await?; let mut from_dir = from_dir.backed.lock().unwrap(); let from_entry = match from_dir.entry(from_last.to_os_string()) { Entry::Occupied(e) => e, Entry::Vacant(_) => return err(ErrorKind::NotFound, "file not found"), }; let original = from_entry.shift_remove(); drop(from_dir); let mut to_dir = to_dir.backed.lock().unwrap(); let to_entry = match to_dir.entry(to_last.to_os_string()) { Entry::Occupied(_) => return err(ErrorKind::AlreadyExists, "file exists"), Entry::Vacant(e) => e, }; to_entry.insert(original); drop(to_dir); Ok(()) } async fn metadata(&self, path: &Path) -> io::Result { let Some((dir_path, last)) = self.resolve2(path)? else { return Ok(Metadata::dir()); }; Ok(self .root .get_folder(&dir_path) .await? .get(last) .await? .metadata()) } type DirEntry = DirEntry; type ReadDirStream = ReadDirStream; async fn read_dir(&self, path: &Path) -> io::Result { let root = self.root.get_folder(&self.resolve(path)?).await?; Ok(ReadDirStream::new(root)) } type FileStream = FileStream; async fn create_new(&self, path: &Path) -> io::Result { let Some((dir_path, last)) = self.resolve2(path)? else { return err(IS_DIRECTORY, "is directory"); }; Ok(FileStream::new( self.root .get_folder(&dir_path) .await? .create_file(last, true) .await? .content .clone(), )) } async fn create(&self, path: &Path) -> io::Result { let Some((dir_path, last)) = self.resolve2(path)? else { return err(IS_DIRECTORY, "is directory"); }; Ok(FileStream::new( self.root .get_folder(&dir_path) .await? .create_file(last, false) .await? .content .clone(), )) } async fn open(&self, path: &Path) -> io::Result { let Some((dir_path, last)) = self.resolve2(path)? else { return err(IS_DIRECTORY, "is directory"); }; Ok(FileStream::new( self.root .get_folder(&dir_path) .await? .get(last) .await? .into_file()? .content .clone(), )) } } impl EnvironmentIo for VirtualFileSystem { fn resolve(&self, path: &Path) -> PathBuf { self.resolve(path) .expect("unexpected full path") .iter() .collect() } #[cfg(feature = "vrc-get-litedb")] fn connect_lite_db(&self) -> io::Result { err(ErrorKind::Unsupported, "lite db") } #[cfg(feature = "experimental-project-management")] type ProjectIo = VirtualFileSystem; #[cfg(feature = "experimental-project-management")] fn new_project_io(&self, _: &Path) -> Self::ProjectIo { panic!("not implemented") // TODO: implement } } impl ProjectIo for VirtualFileSystem {} #[derive(Clone)] enum FileSystemEntry { File(FileEntry), Directory(DirectoryEntry), } impl FileSystemEntry { fn metadata(&self) -> Metadata { match self { FileSystemEntry::File(_) => Metadata::file(), FileSystemEntry::Directory(_) => Metadata::dir(), } } fn into_file(self) -> io::Result { match self { FileSystemEntry::File(e) => Ok(e), FileSystemEntry::Directory(_) => err(IS_DIRECTORY, "is a directory"), } } fn into_directory(self) -> io::Result { match self { FileSystemEntry::File(_) => err(NOTA_DIRECTORY, "is a file"), FileSystemEntry::Directory(e) => Ok(e), } } fn as_file(&self) -> io::Result<&FileEntry> { match self { FileSystemEntry::File(e) => Ok(e), FileSystemEntry::Directory(_) => err(IS_DIRECTORY, "is a directory"), } } fn as_directory(&self) -> io::Result<&DirectoryEntry> { match self { FileSystemEntry::File(_) => err(NOTA_DIRECTORY, "is a file"), FileSystemEntry::Directory(e) => Ok(e), } } } #[derive(Clone)] struct DirectoryEntry { backed: Arc>, } struct DirectoryContent { content: IndexMap>, } impl DirectoryContent { fn new() -> Self { Self { content: IndexMap::new(), } } fn is_empty(&self) -> bool { self.content.is_empty() || self.content.values().all(Option::is_none) } fn get(&self, name: &OsStr) -> Option<&FileSystemEntry> { self.content.get(name)?.as_ref() } fn entry(&mut self, name: OsString) -> Entry { Entry::new(self.content.entry(name)) } fn last(&self) -> Option<(&OsString, &FileSystemEntry)> { self.content .iter() .rev() .find_map(|(k, v)| v.as_ref().map(|v| (k, v))) } fn remove_last(&mut self) { if let Some(slot) = self.content.values_mut().rev().find(|v| v.is_some()) { *slot = None; } } fn get_index_internal(&self, index: usize) -> Option<(&OsString, Option<&FileSystemEntry>)> { self.content.get_index(index).map(|(k, v)| (k, v.as_ref())) } } enum Entry<'a> { Occupied(OccupiedEntry<'a>), Vacant(VacantEntry<'a>), } impl<'a> Entry<'a> { fn new(entry: IndexEntry<'a, OsString, Option>) -> Self { match entry { IndexEntry::Occupied(e) => match e.get() { Some(_) => Entry::Occupied(OccupiedEntry(e)), None => Entry::Vacant(VacantEntry::Occupied(e)), }, IndexEntry::Vacant(e) => Entry::Vacant(VacantEntry::Vacant(e)), } } } struct OccupiedEntry<'a>(indexmap::map::OccupiedEntry<'a, OsString, Option>); impl<'a> OccupiedEntry<'a> { fn into_mut(self) -> &'a mut FileSystemEntry { self.0.into_mut().as_mut().unwrap() } fn shift_remove(self) -> FileSystemEntry { self.0.into_mut().take().unwrap() } fn get_mut(&mut self) -> &mut FileSystemEntry { self.0.get_mut().as_mut().unwrap() } } enum VacantEntry<'a> { Occupied(indexmap::map::OccupiedEntry<'a, OsString, Option>), Vacant(indexmap::map::VacantEntry<'a, OsString, Option>), } impl<'a> VacantEntry<'a> { fn insert(self, entry: FileSystemEntry) -> &'a mut FileSystemEntry { match self { VacantEntry::Occupied(e) => e.into_mut().insert(entry), VacantEntry::Vacant(e) => e.insert(Some(entry)).as_mut().unwrap(), } } } impl DirectoryEntry { fn new() -> Self { Self { backed: Arc::new(Mutex::new(DirectoryContent::new())), } } async fn get(&self, name: &OsStr) -> io::Result { self.backed .lock() .unwrap() .get(name) .map(Clone::clone) .ok_or_else(|| io::Error::new(ErrorKind::NotFound, "file not found")) } async fn get_folder(&self, path: &[&OsStr]) -> io::Result { let mut current = self.clone(); for component in path { current = current.get(component).await?.into_directory()?; } Ok(current) } async fn create_dir_all(&self, path: &[&OsStr]) -> io::Result { let mut current = self.clone(); for component in path { let mut locked = current.backed.lock().unwrap(); let next = match locked.entry(component.to_os_string()) { Entry::Occupied(e) => e.into_mut().as_directory()?.clone(), Entry::Vacant(e) => e .insert(FileSystemEntry::Directory(DirectoryEntry::new())) .as_directory() .unwrap() .clone(), }; drop(locked); current = next; } Ok(current) } async fn create_file(&self, name: &OsStr, new: bool) -> io::Result { let mut backed = self.backed.lock().unwrap(); match backed.entry(name.to_os_string()) { Entry::Occupied(e) => match e.into_mut() { FileSystemEntry::File(_) if new => err(ErrorKind::AlreadyExists, "file exists"), FileSystemEntry::File(entry) => Ok(entry.clone()), FileSystemEntry::Directory(_) => err(IS_DIRECTORY, "directory exists"), }, Entry::Vacant(e) => { let FileSystemEntry::File(e) = e.insert(FileSystemEntry::File(FileEntry::new())) else { unreachable!() }; Ok(e.clone()) } } } async fn remove_file(&self, name: &OsStr) -> io::Result { let mut backed = self.backed.lock().unwrap(); match backed.entry(name.to_os_string()) { Entry::Occupied(mut e) => { let file = e.get_mut().as_file()?; if !file.can_remove() { return err(ErrorKind::PermissionDenied, "file is locked"); } Ok(e.shift_remove().into_file().unwrap()) } Entry::Vacant(_) => err(ErrorKind::NotFound, "file not found"), } } async fn remove_dir(&self, name: &OsStr) -> io::Result { let mut backed = self.backed.lock().unwrap(); match backed.entry(name.to_os_string()) { Entry::Occupied(mut e) => { let as_dir = e.get_mut().as_directory()?; if !as_dir.backed.lock().unwrap().is_empty() { return err(ErrorKind::Other, "directory not empty"); // DirectoryNotEmpty is unstable } Ok(e.shift_remove().into_directory().unwrap()) } Entry::Vacant(_) => err(ErrorKind::NotFound, "file not found"), } } async fn remove_dir_all(&self, name: &OsStr) -> io::Result { let mut backed = self.backed.lock().unwrap(); match backed.entry(name.to_os_string()) { Entry::Occupied(mut e) => { e.get_mut().as_directory()?.clear()?; Ok(e.shift_remove().into_directory().unwrap()) } Entry::Vacant(_) => err(ErrorKind::NotFound, "file not found"), } } fn clear(&self) -> io::Result<()> { let mut backed = self.backed.lock().unwrap(); while let Some((_, child)) = backed.last() { match child { FileSystemEntry::File(f) => { if !f.can_remove() { return err(ErrorKind::PermissionDenied, "file is locked"); } } FileSystemEntry::Directory(d) => d.clear()?, } backed.remove_last(); } Ok(()) } } #[derive(Clone)] struct FileEntry { content: Arc>, } struct FileContent { content: Vec, locked: bool, } impl FileContent { fn new() -> Self { Self { content: Vec::new(), locked: false, } } } impl FileEntry { fn new() -> Self { Self { content: Arc::new(Mutex::new(FileContent::new())), } } pub(crate) async fn set_content(&self, content: &[u8]) { self.content.lock().unwrap().content = content.to_vec(); } pub(crate) fn deny_deletion(&self) { self.content.lock().unwrap().locked = true; } pub(crate) fn can_remove(&self) -> bool { !self.content.lock().unwrap().locked } } pub struct ReadDirStream { index: usize, dir: DirectoryEntry, } impl ReadDirStream { fn new(dir: DirectoryEntry) -> Self { Self { index: 0, dir } } } impl Stream for ReadDirStream { type Item = io::Result; fn poll_next(mut self: Pin<&mut Self>, _: &mut Context) -> Poll> { let locked = self.dir.backed.lock().unwrap(); let mut index = self.index; loop { let Some((name, entry)) = locked.get_index_internal(index) else { drop(locked); self.index = index; return Poll::Ready(None); }; index += 1; let Some(entry) = entry else { continue; }; let entry = DirEntry::new(name, entry.metadata()); drop(locked); self.index = index; return Poll::Ready(Some(Ok(entry))); } } } pub struct DirEntry { name: OsString, metadata: Metadata, } impl DirEntry { fn new(name: &OsStr, metadata: Metadata) -> Self { Self { name: name.to_os_string(), metadata, } } } impl vrc_get_vpm::io::DirEntry for DirEntry { fn file_name(&self) -> OsString { self.name.clone() } async fn file_type(&self) -> io::Result { Ok(self.metadata.file_type()) } async fn metadata(&self) -> io::Result { Ok(self.metadata.clone()) } } mod file_stream { use crate::common::virtual_file_system::FileContent; use futures::{AsyncRead, AsyncSeek, AsyncWrite}; use std::io; use std::io::{ErrorKind, SeekFrom}; use std::pin::Pin; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll}; pub struct FileStream { content: Arc>, position: usize, } impl FileStream { pub(super) fn new(content: Arc>) -> Self { Self { content, position: 0, } } } impl AsyncSeek for FileStream { fn poll_seek( mut self: Pin<&mut Self>, _: &mut Context<'_>, pos: SeekFrom, ) -> Poll> { match pos { SeekFrom::Start(position) => { self.position = position .try_into() .map_err(|_| io::Error::new(ErrorKind::InvalidInput, "invalid position"))?; Poll::Ready(Ok(self.position as u64)) } SeekFrom::Current(offset) => { let offset = offset .try_into() .map_err(|_| io::Error::new(ErrorKind::InvalidInput, "invalid position"))?; self.position = self.position.checked_add_signed(offset).ok_or_else(|| { io::Error::new(ErrorKind::InvalidInput, "invalid position") })?; Poll::Ready(Ok(self.position as u64)) } SeekFrom::End(offset) => { let offset = offset .try_into() .map_err(|_| io::Error::new(ErrorKind::InvalidInput, "invalid position"))?; let lock = self.content.clone(); let guard = lock.lock().unwrap(); self.position = guard .content .len() .checked_add_signed(offset) .ok_or_else(|| { io::Error::new(ErrorKind::InvalidInput, "invalid position") })?; Poll::Ready(Ok(self.position as u64)) } } } } impl AsyncRead for FileStream { fn poll_read( mut self: Pin<&mut Self>, _: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { let lock = self.content.clone(); let guard = lock.lock().unwrap(); let len = guard.content.len(); let remaining = len - self.position; let to_copy = buf.len().min(remaining); buf[..to_copy].copy_from_slice(&guard.content[self.position..][..to_copy]); self.position += to_copy; Poll::Ready(Ok(to_copy)) } } impl AsyncWrite for FileStream { fn poll_write( self: Pin<&mut Self>, _: &mut Context<'_>, buf: &[u8], ) -> Poll> { let lock = self.content.clone(); let mut guard = lock.lock().unwrap(); let new_len = self.position + buf.len(); if new_len > guard.content.len() { guard.content.resize(new_len, 0); } guard.content[self.position..][..buf.len()].copy_from_slice(buf); Poll::Ready(Ok(buf.len())) } fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } } }