mirror of
https://github.com/vrc-get/vrc-get.git
synced 2026-06-21 09:58:08 +00:00
chore: use the real file system for testing
This commit is contained in:
parent
2d080204ac
commit
1bf81a3b6a
10 changed files with 55 additions and 849 deletions
|
|
@ -52,6 +52,9 @@ windows = { version = "0.61", features = ["Win32_System_Threading", "Win32_Secur
|
|||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
plist = { version = "1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
|
||||
[features]
|
||||
default = ["rustls"]
|
||||
native-tls = ["reqwest/native-tls-vendored"]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use common::*;
|
||||
use futures::executor::block_on;
|
||||
use std::collections::HashSet;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
|
@ -1200,8 +1199,7 @@ fn no_temp_folder_after_add() {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let env_vfs = VirtualFileSystem::new();
|
||||
let env = VirtualEnvironment::new(env_vfs);
|
||||
let env = VirtualInstaller::new();
|
||||
|
||||
let resolve = project
|
||||
.remove_request(&["com.vrchat.avatars"])
|
||||
|
|
@ -1232,6 +1230,7 @@ fn no_temp_folder_after_add() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "No suitable way to lock a file"]
|
||||
fn locked_in_package_folder() {
|
||||
block_on(async {
|
||||
let mut project = VirtualProjectBuilder::new()
|
||||
|
|
@ -1251,14 +1250,9 @@ fn locked_in_package_folder() {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
project
|
||||
.io()
|
||||
.deny_deletion("Packages/com.vrchat.avatars/content.txt".as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
//project.io().lock("Packages/com.vrchat.avatars/content.txt".as_ref()).await.unwrap();
|
||||
|
||||
let env_vfs = VirtualFileSystem::new();
|
||||
let env = VirtualEnvironment::new(env_vfs);
|
||||
let env = VirtualInstaller::new();
|
||||
|
||||
let resolve = project
|
||||
.remove_request(&["com.vrchat.avatars"])
|
||||
|
|
|
|||
|
|
@ -4,13 +4,11 @@
|
|||
|
||||
mod package_collection;
|
||||
mod virtual_environment;
|
||||
mod virtual_file_system;
|
||||
mod virtual_project_builder;
|
||||
|
||||
pub use package_collection::PackageCollection;
|
||||
pub use package_collection::PackageCollectionBuilder;
|
||||
pub use virtual_environment::VirtualEnvironment;
|
||||
pub use virtual_file_system::VirtualFileSystem;
|
||||
pub use virtual_environment::VirtualInstaller;
|
||||
pub use virtual_project_builder::VirtualProjectBuilder;
|
||||
|
||||
use vrc_get_vpm::PackageInfo;
|
||||
|
|
@ -95,3 +93,10 @@ pub fn assert_installing_to_dependencies_only(
|
|||
.expect("not installing to dependencies");
|
||||
assert_eq!(base_range, &DependencyRange::version(version));
|
||||
}
|
||||
|
||||
pub fn block_on<F: Future>(f: F) -> F::Output {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(f)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::common::{PackageCollection, VirtualFileSystem};
|
||||
use crate::common::PackageCollection;
|
||||
use indexmap::IndexMap;
|
||||
use serde_json::json;
|
||||
use std::future::Future;
|
||||
|
|
@ -12,17 +12,15 @@ use vrc_get_vpm::{
|
|||
AbortCheck, HttpClient, PackageInfo, PackageInstaller, PackageManifest, UnityProject,
|
||||
};
|
||||
|
||||
pub struct VirtualEnvironment {
|
||||
vfs: VirtualFileSystem,
|
||||
}
|
||||
pub struct VirtualInstaller {}
|
||||
|
||||
impl VirtualEnvironment {
|
||||
pub fn new(vfs: VirtualFileSystem) -> Self {
|
||||
Self { vfs }
|
||||
impl VirtualInstaller {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageInstaller for VirtualEnvironment {
|
||||
impl PackageInstaller for VirtualInstaller {
|
||||
fn install_package(
|
||||
&self,
|
||||
_: &impl ProjectIo,
|
||||
|
|
|
|||
|
|
@ -1,772 +0,0 @@
|
|||
use futures::Stream;
|
||||
use indexmap::IndexMap;
|
||||
use indexmap::map::Entry as IndexEntry;
|
||||
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<V, E>(kind: ErrorKind, error: E) -> io::Result<V>
|
||||
where
|
||||
E: Into<Box<dyn error::Error + Send + Sync>>,
|
||||
{
|
||||
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<Vec<&'a OsStr>> {
|
||||
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<Option<(Vec<&'a OsStr>, &'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(())
|
||||
}
|
||||
|
||||
fn write_sync(
|
||||
&self,
|
||||
path: &Path,
|
||||
content: &[u8],
|
||||
) -> impl Future<Output = io::Result<()>> + Send {
|
||||
self.write(path, content)
|
||||
}
|
||||
|
||||
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<Metadata> {
|
||||
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<Self::ReadDirStream> {
|
||||
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<Self::FileStream> {
|
||||
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<Self::FileStream> {
|
||||
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<Self::FileStream> {
|
||||
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")]
|
||||
type MutexGuard = ();
|
||||
|
||||
#[cfg(feature = "vrc-get-litedb")]
|
||||
async fn new_mutex(&self, _: &OsStr) -> io::Result<Self::MutexGuard> {
|
||||
err(ErrorKind::Unsupported, "shared mutex")
|
||||
}
|
||||
|
||||
#[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<FileEntry> {
|
||||
match self {
|
||||
FileSystemEntry::File(e) => Ok(e),
|
||||
FileSystemEntry::Directory(_) => err(IS_DIRECTORY, "is a directory"),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_directory(self) -> io::Result<DirectoryEntry> {
|
||||
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<Mutex<DirectoryContent>>,
|
||||
}
|
||||
|
||||
struct DirectoryContent {
|
||||
content: IndexMap<OsString, Option<FileSystemEntry>>,
|
||||
}
|
||||
|
||||
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<FileSystemEntry>>) -> 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<FileSystemEntry>>);
|
||||
|
||||
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<FileSystemEntry>>),
|
||||
Vacant(indexmap::map::VacantEntry<'a, OsString, Option<FileSystemEntry>>),
|
||||
}
|
||||
|
||||
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<FileSystemEntry> {
|
||||
self.backed
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(name)
|
||||
.cloned()
|
||||
.ok_or_else(|| io::Error::new(ErrorKind::NotFound, "file not found"))
|
||||
}
|
||||
|
||||
async fn get_folder(&self, path: &[&OsStr]) -> io::Result<DirectoryEntry> {
|
||||
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<DirectoryEntry> {
|
||||
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<FileEntry> {
|
||||
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<FileEntry> {
|
||||
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<DirectoryEntry> {
|
||||
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<DirectoryEntry> {
|
||||
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<Mutex<FileContent>>,
|
||||
}
|
||||
|
||||
struct FileContent {
|
||||
content: Vec<u8>,
|
||||
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<DirEntry>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, _: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
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<FileType> {
|
||||
Ok(self.metadata.file_type())
|
||||
}
|
||||
|
||||
async fn metadata(&self) -> io::Result<Metadata> {
|
||||
Ok(self.metadata.clone())
|
||||
}
|
||||
}
|
||||
|
||||
mod file_stream {
|
||||
use crate::common::virtual_file_system::FileContent;
|
||||
use futures::{AsyncRead, AsyncSeek, AsyncWrite};
|
||||
use std::future::Future;
|
||||
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<Mutex<FileContent>>,
|
||||
position: usize,
|
||||
}
|
||||
|
||||
impl FileStream {
|
||||
pub(super) fn new(content: Arc<Mutex<FileContent>>) -> Self {
|
||||
Self {
|
||||
content,
|
||||
position: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncSeek for FileStream {
|
||||
fn poll_seek(
|
||||
mut self: Pin<&mut Self>,
|
||||
_: &mut Context<'_>,
|
||||
pos: SeekFrom,
|
||||
) -> Poll<io::Result<u64>> {
|
||||
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<io::Result<usize>> {
|
||||
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<io::Result<usize>> {
|
||||
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<io::Result<()>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl vrc_get_vpm::io::FileStream for FileStream {}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::common::VirtualFileSystem;
|
||||
use indexmap::IndexMap;
|
||||
use serde_json::json;
|
||||
use vrc_get_vpm::io::IoTrait;
|
||||
use std::path::{Path, PathBuf};
|
||||
use vrc_get_vpm::io::{DefaultProjectIo, IoTrait};
|
||||
use vrc_get_vpm::unity_project::pending_project_changes::Remove;
|
||||
use vrc_get_vpm::version::{Version, VersionRange};
|
||||
use vrc_get_vpm::{PackageManifest, UnityProject};
|
||||
|
|
@ -88,7 +88,21 @@ impl VirtualProjectBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub async fn build(&self) -> std::io::Result<UnityProject<VirtualFileSystem>> {
|
||||
#[track_caller]
|
||||
pub fn build(&self) -> impl Future<Output = std::io::Result<UnityProject<DefaultProjectIo>>> {
|
||||
let project_path = Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!(
|
||||
"test_projects/{}_L{}",
|
||||
env!("CARGO_CRATE_NAME"),
|
||||
std::panic::Location::caller().line()
|
||||
));
|
||||
|
||||
self.build_impl(project_path)
|
||||
}
|
||||
|
||||
async fn build_impl(
|
||||
&self,
|
||||
project_path: PathBuf,
|
||||
) -> std::io::Result<UnityProject<DefaultProjectIo>> {
|
||||
let vpm_manifest = {
|
||||
let mut dependencies = serde_json::Map::new();
|
||||
for (dependency, version) in &self.dependencies {
|
||||
|
|
@ -116,15 +130,22 @@ impl VirtualProjectBuilder {
|
|||
})
|
||||
};
|
||||
|
||||
let fs = VirtualFileSystem::new();
|
||||
fs.add_file(
|
||||
"Packages/vpm-manifest.json".as_ref(),
|
||||
match tokio::fs::remove_dir_all(&project_path).await {
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Ok(()) => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
tokio::fs::create_dir_all(&project_path.join("Packages")).await?;
|
||||
tokio::fs::write(
|
||||
project_path.join("Packages/vpm-manifest.json"),
|
||||
vpm_manifest.to_string().as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
fs.add_file(
|
||||
"ProjectSettings/ProjectVersion.txt".as_ref(),
|
||||
tokio::fs::create_dir_all(&project_path.join("ProjectSettings")).await?;
|
||||
tokio::fs::write(
|
||||
project_path.join("ProjectSettings/ProjectVersion.txt"),
|
||||
format!(
|
||||
"m_EditorVersion: {version}\n\
|
||||
m_EditorVersionWithRevision: {version} ({revision})\n\
|
||||
|
|
@ -137,13 +158,14 @@ impl VirtualProjectBuilder {
|
|||
.await?;
|
||||
|
||||
for (name, contents) in &self.files {
|
||||
fs.add_file(name.as_ref(), contents.as_bytes()).await?;
|
||||
tokio::fs::create_dir_all(&project_path.join(name).parent().unwrap()).await?;
|
||||
tokio::fs::write(project_path.join(name), contents).await?;
|
||||
}
|
||||
|
||||
for name in &self.directories {
|
||||
fs.create_dir_all(name.as_ref()).await?;
|
||||
tokio::fs::create_dir_all(project_path.join(name)).await?;
|
||||
}
|
||||
|
||||
UnityProject::load(fs).await
|
||||
UnityProject::load(DefaultProjectIo::new(project_path.into())).await
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use crate::common::VirtualProjectBuilder;
|
||||
use futures::executor::block_on;
|
||||
use crate::common::*;
|
||||
use vrc_get_vpm::version::{ReleaseType, UnityVersion, Version};
|
||||
|
||||
mod common;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use crate::common::*;
|
||||
use futures::executor::block_on;
|
||||
use vrc_get_vpm::unity_project::pending_project_changes::RemoveReason;
|
||||
use vrc_get_vpm::version::Version;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use crate::common::*;
|
||||
use futures::executor::block_on;
|
||||
use vrc_get_vpm::PackageManifest;
|
||||
use vrc_get_vpm::version::Version;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
//! This file contains tests for the virtual file system.
|
||||
|
||||
use futures::AsyncReadExt;
|
||||
use vrc_get_vpm::io::IoTrait;
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
futures::executor::block_on(async {
|
||||
let vfs = common::VirtualFileSystem::new();
|
||||
let content = b"";
|
||||
vfs.add_file("test".as_ref(), content).await.unwrap();
|
||||
|
||||
let mut read = Vec::new();
|
||||
vfs.open("test".as_ref())
|
||||
.await
|
||||
.unwrap()
|
||||
.read_to_end(&mut read)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(read, content);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_dir_contains_forbidden() {
|
||||
futures::executor::block_on(async {
|
||||
let vfs = common::VirtualFileSystem::new();
|
||||
let content = b"";
|
||||
vfs.add_file("test-dir/forbidden-file".as_ref(), content)
|
||||
.await
|
||||
.unwrap();
|
||||
vfs.deny_deletion("test-dir/forbidden-file".as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
vfs.remove_dir_all("test-dir".as_ref()).await.unwrap_err();
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue