From 2dbf950764cc7fa9972f57bbed11efdf04af96a2 Mon Sep 17 00:00:00 2001 From: "M.Amin Rayej" Date: Tue, 13 Aug 2024 01:53:41 +0330 Subject: [PATCH 01/15] allow nested mounted paths in UnionFilesystem --- lib/virtual-fs/src/union_fs.rs | 180 +++++++++++++++++- tests/integration/cli/src/fixtures.rs | 6 + .../nested-mounted-paths/a/data-a.txt | 0 .../nested-mounted-paths/b/data-b.txt | 0 .../packages/nested-mounted-paths/main.o | Bin 0 -> 48859 bytes .../packages/nested-mounted-paths/out.webc | Bin 0 -> 50802 bytes .../packages/nested-mounted-paths/wasmer.toml | 11 ++ tests/integration/cli/tests/run.rs | 57 +++++- 8 files changed, 245 insertions(+), 9 deletions(-) create mode 100644 tests/integration/cli/tests/packages/nested-mounted-paths/a/data-a.txt create mode 100644 tests/integration/cli/tests/packages/nested-mounted-paths/b/data-b.txt create mode 100644 tests/integration/cli/tests/packages/nested-mounted-paths/main.o create mode 100644 tests/integration/cli/tests/packages/nested-mounted-paths/out.webc create mode 100644 tests/integration/cli/tests/packages/nested-mounted-paths/wasmer.toml diff --git a/lib/virtual-fs/src/union_fs.rs b/lib/virtual-fs/src/union_fs.rs index 4e6eff9f044..72b7bf4e110 100644 --- a/lib/virtual-fs/src/union_fs.rs +++ b/lib/virtual-fs/src/union_fs.rs @@ -158,17 +158,19 @@ impl UnionFileSystem { } fn read_dir_internal(&self, path: &Path) -> Result { - let path = path.to_string_lossy(); + let path_str = path.to_string_lossy(); let mut ret = None; - for (path, mount) in filter_mounts(&self.mounts, path.as_ref()) { - match mount.fs.read_dir(Path::new(path.as_str())) { + for (p, mount) in filter_mounts(&self.mounts, path_str.as_ref()) { + match mount.fs.read_dir(Path::new(p.as_str())) { Ok(dir) => { if ret.is_none() { ret = Some(Vec::new()); } let ret = ret.as_mut().unwrap(); - for sub in dir.flatten() { + for mut sub in dir.flatten() { + let sub_path: PathBuf = sub.path.components().skip(1).collect(); + sub.path = path.join(&sub_path); ret.push(sub); } } @@ -178,6 +180,45 @@ impl UnionFileSystem { } } + // also take into account partial matchings + let mut partial_ret = Vec::new(); + let path = PathBuf::from(path_str.into_owned()); + for mount in &self.mounts { + let mount_path = PathBuf::from(&mount.path); + + if let Ok(postfix) = mount_path.strip_prefix(&path) { + if let Some(_entry) = postfix.components().next() { + if partial_ret + .iter() + .all(|e: &DirEntry| e.path.as_path() != path.join(_entry).as_path()) + { + partial_ret.push(DirEntry { + path: path.join(_entry), + metadata: Ok(Metadata { + ft: FileType::new_dir(), + accessed: 0, + created: 0, + modified: 0, + len: 0, + }), + }) + } + } + } + } + + let ret = if let Some(mut ret) = ret { + ret.extend(partial_ret); + + Some(ret) + } else { + if partial_ret.is_empty() { + None + } else { + Some(partial_ret) + } + }; + match ret { Some(mut ret) => { ret.sort_by(|a, b| match (a.metadata.as_ref(), b.metadata.as_ref()) { @@ -349,6 +390,20 @@ impl FileSystem for UnionFileSystem { } } } + + // if no mount point fully matched, maybe there is a partial matching + let path = path.into_owned(); + for mount in &self.mounts { + if mount.path.starts_with(&path) { + return Ok(Metadata { + ft: FileType::new_dir(), + accessed: 0, + created: 0, + modified: 0, + len: 0, + }); + } + } Err(ret_error) } fn symlink_metadata(&self, path: &Path) -> Result { @@ -373,11 +428,24 @@ impl FileSystem for UnionFileSystem { } } } + + // if no mount point fully matched, maybe there is a partial matching + let path = path.into_owned(); + for mount in &self.mounts { + if mount.path.starts_with(&path) { + return Ok(Metadata { + ft: FileType::new_dir(), + accessed: 0, + created: 0, + modified: 0, + len: 0, + }); + } + } debug!("symlink_metadata: failed={}", ret_error); Err(ret_error) } fn remove_file(&self, path: &Path) -> Result<()> { - println!("remove_file: path={}", path.display()); let mut ret_error = FsError::EntryNotFound; let path = path.to_string_lossy(); for (path, mount) in filter_mounts(&self.mounts, path.as_ref()) { @@ -386,7 +454,6 @@ impl FileSystem for UnionFileSystem { return Ok(ret); } Err(err) => { - println!("returning error {err:?}"); ret_error = err; } } @@ -488,15 +555,16 @@ impl FileOpener for UnionFileSystem { #[cfg(test)] mod tests { - use std::path::Path; + use std::path::{Path, PathBuf}; use tokio::io::AsyncWriteExt; use crate::{mem_fs, ops, FileSystem as FileSystemTrait, FsError, UnionFileSystem}; + use super::{FileOpener, OpenOptionsConfig}; + fn gen_filesystem() -> UnionFileSystem { let mut union = UnionFileSystem::new(); - // fs.mount("/", Box::new(mem_fs::FileSystem::default())); let a = mem_fs::FileSystem::default(); let b = mem_fs::FileSystem::default(); let c = mem_fs::FileSystem::default(); @@ -518,6 +586,102 @@ mod tests { union } + fn gen_nested_filesystem() -> UnionFileSystem { + let mut union = UnionFileSystem::new(); + let a = mem_fs::FileSystem::default(); + a.open( + &PathBuf::from("/data-a.txt"), + &OpenOptionsConfig { + read: true, + write: true, + create_new: false, + create: true, + append: false, + truncate: false, + }, + ) + .unwrap(); + let b = mem_fs::FileSystem::default(); + b.open( + &PathBuf::from("/data-b.txt"), + &OpenOptionsConfig { + read: true, + write: true, + create_new: false, + create: true, + append: false, + truncate: false, + }, + ) + .unwrap(); + + union.mount("mem_fs_1", "/app/a", false, Box::new(a), None); + union.mount("mem_fs_2", "/app/b", false, Box::new(b), None); + + union + } + + #[tokio::test] + async fn test_nested_read_dir() { + let fs = gen_nested_filesystem(); + + let root_contents: Vec = fs + .read_dir(&PathBuf::from("/")) + .unwrap() + .map(|e| e.unwrap().path.to_str().unwrap().to_string()) + .collect(); + assert_eq!(root_contents, vec!["/app"]); + + let app_contents: Vec = fs + .read_dir(&PathBuf::from("/app")) + .unwrap() + .map(|e| e.unwrap().path.to_str().unwrap().to_string()) + .collect(); + assert_eq!(app_contents, vec!["/app/a", "/app/b"]); + + let a_contents: Vec = fs + .read_dir(&PathBuf::from("/app/a")) + .unwrap() + .map(|e| e.unwrap().path.to_str().unwrap().to_string()) + .collect(); + assert_eq!(a_contents, vec!["/app/a/data-a.txt"]); + + let b_contents: Vec = fs + .read_dir(&PathBuf::from("/app/b")) + .unwrap() + .map(|e| e.unwrap().path.to_str().unwrap().to_string()) + .collect(); + assert_eq!(b_contents, vec!["/app/b/data-b.txt"]); + } + + #[tokio::test] + async fn test_nested_metadata() { + let fs = gen_nested_filesystem(); + + assert!(fs.metadata(&PathBuf::from("/")).is_ok()); + assert!(fs.metadata(&PathBuf::from("/app")).is_ok()); + assert!(fs.metadata(&PathBuf::from("/app/a")).is_ok()); + assert!(fs.metadata(&PathBuf::from("/app/b")).is_ok()); + assert!(fs.metadata(&PathBuf::from("/app/a/data-a.txt")).is_ok()); + assert!(fs.metadata(&PathBuf::from("/app/b/data-b.txt")).is_ok()); + } + + #[tokio::test] + async fn test_nested_symlink_metadata() { + let fs = gen_nested_filesystem(); + + assert!(fs.symlink_metadata(&PathBuf::from("/")).is_ok()); + assert!(fs.symlink_metadata(&PathBuf::from("/app")).is_ok()); + assert!(fs.symlink_metadata(&PathBuf::from("/app/a")).is_ok()); + assert!(fs.symlink_metadata(&PathBuf::from("/app/b")).is_ok()); + assert!(fs + .symlink_metadata(&PathBuf::from("/app/a/data-a.txt")) + .is_ok()); + assert!(fs + .symlink_metadata(&PathBuf::from("/app/b/data-b.txt")) + .is_ok()); + } + #[tokio::test] async fn test_new_filesystem() { let fs = gen_filesystem(); diff --git a/tests/integration/cli/src/fixtures.rs b/tests/integration/cli/src/fixtures.rs index f5a438bbb73..193e3c0d483 100644 --- a/tests/integration/cli/src/fixtures.rs +++ b/tests/integration/cli/src/fixtures.rs @@ -8,6 +8,12 @@ pub fn resources() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("resources") } +pub fn packages() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("packages") +} + pub fn php() -> (PathBuf, PathBuf, PathBuf) { let root = Path::new(env!("CARGO_MANIFEST_DIR")); let resources = resources().join("php"); diff --git a/tests/integration/cli/tests/packages/nested-mounted-paths/a/data-a.txt b/tests/integration/cli/tests/packages/nested-mounted-paths/a/data-a.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/integration/cli/tests/packages/nested-mounted-paths/b/data-b.txt b/tests/integration/cli/tests/packages/nested-mounted-paths/b/data-b.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/integration/cli/tests/packages/nested-mounted-paths/main.o b/tests/integration/cli/tests/packages/nested-mounted-paths/main.o new file mode 100644 index 0000000000000000000000000000000000000000..d60e1c8152008e0c6ed7ced318daac183049bb9e GIT binary patch literal 48859 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 literal 0 HcmV?d00001 diff --git a/tests/integration/cli/tests/packages/nested-mounted-paths/wasmer.toml b/tests/integration/cli/tests/packages/nested-mounted-paths/wasmer.toml new file mode 100644 index 00000000000..57a92009a67 --- /dev/null +++ b/tests/integration/cli/tests/packages/nested-mounted-paths/wasmer.toml @@ -0,0 +1,11 @@ +[[module]] +name = "main" +source = "main.o" + +[[command]] +name = "main" +module = "main" + +[fs] +"/app/a" = "a" +"/app/b" = "b" diff --git a/tests/integration/cli/tests/run.rs b/tests/integration/cli/tests/run.rs index 38000ac017b..2da4bb75f5f 100644 --- a/tests/integration/cli/tests/run.rs +++ b/tests/integration/cli/tests/run.rs @@ -15,7 +15,7 @@ use reqwest::{blocking::Client, IntoUrl}; use tempfile::TempDir; use wasmer_integration_tests_cli::{ asset_path, - fixtures::{self, php, resources}, + fixtures::{self, packages, php, resources}, get_wasmer_path, }; @@ -47,6 +47,61 @@ static CACHE_RUST_LOG: Lazy = Lazy::new(|| { .join(",") }); +#[test] +fn nested_mounted_paths() { + let package = packages().join("nested-mounted-paths"); + + let webc = package.join("out.webc"); + + let host_output = Command::new(get_wasmer_path()) + .arg("run") + .arg(package) + .output() + .unwrap(); + let host_stdout = host_output.stdout; + + let webc_output = Command::new(get_wasmer_path()) + .arg("run") + .arg(webc) + .arg(".") + .output() + .unwrap(); + let webc_stdout = webc_output.stdout; + + let expected = "/: +. +.. +.app +.private +app +bin +dev +etc +tmp + +/app: +. +.. +a +b + +/app/a: +. +.. +data-a.txt + +/app/b: +. +.. +data-b.txt +" + .as_bytes() + .to_vec(); + + assert_eq!(&host_stdout, &expected); + assert_eq!(&webc_stdout, &expected); +} + #[test] fn run_python_create_temp_dir_in_subprocess() { let resources = resources().join("python").join("temp-dir-in-child"); From b154f0e226e9e9c5499b75d8fed357f70adf5d20 Mon Sep 17 00:00:00 2001 From: "M.Amin Rayej" Date: Tue, 13 Aug 2024 02:02:41 +0330 Subject: [PATCH 02/15] fix lint --- lib/virtual-fs/src/union_fs.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/virtual-fs/src/union_fs.rs b/lib/virtual-fs/src/union_fs.rs index 72b7bf4e110..2eccd677ab7 100644 --- a/lib/virtual-fs/src/union_fs.rs +++ b/lib/virtual-fs/src/union_fs.rs @@ -211,12 +211,10 @@ impl UnionFileSystem { ret.extend(partial_ret); Some(ret) + } else if partial_ret.is_empty() { + None } else { - if partial_ret.is_empty() { - None - } else { - Some(partial_ret) - } + Some(partial_ret) }; match ret { From c0488580bd87ed462c7d67dae0a4e56f2092bba2 Mon Sep 17 00:00:00 2001 From: "M.Amin Rayej" Date: Tue, 13 Aug 2024 12:26:35 +0330 Subject: [PATCH 03/15] fix path name issue --- lib/virtual-fs/src/union_fs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/virtual-fs/src/union_fs.rs b/lib/virtual-fs/src/union_fs.rs index 2eccd677ab7..0154e47bcd5 100644 --- a/lib/virtual-fs/src/union_fs.rs +++ b/lib/virtual-fs/src/union_fs.rs @@ -170,7 +170,7 @@ impl UnionFileSystem { let ret = ret.as_mut().unwrap(); for mut sub in dir.flatten() { let sub_path: PathBuf = sub.path.components().skip(1).collect(); - sub.path = path.join(&sub_path); + sub.path = PathBuf::from(&mount.path).join(&sub_path); ret.push(sub); } } From e24a9fa50f73ef87235610b7cb3e1e344a38e539 Mon Sep 17 00:00:00 2001 From: "M.Amin Rayej" Date: Tue, 13 Aug 2024 12:27:49 +0330 Subject: [PATCH 04/15] add proxy support to backend-api client --- lib/backend-api/src/client.rs | 37 ++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/lib/backend-api/src/client.rs b/lib/backend-api/src/client.rs index 092a09fb4d6..04b4df5c986 100644 --- a/lib/backend-api/src/client.rs +++ b/lib/backend-api/src/client.rs @@ -6,6 +6,16 @@ use anyhow::{bail, Context as _}; use cynic::{http::CynicReqwestError, GraphQlResponse, Operation}; use url::Url; +pub fn get_proxy() -> Result, anyhow::Error> { + if let Ok(scheme) = std::env::var("http_proxy").or_else(|_| std::env::var("HTTP_PROXY")) { + let proxy = reqwest::Proxy::all(scheme)?; + + Ok(Some(proxy)) + } else { + Ok(None) + } +} + /// API client for the Wasmer API. /// /// Use the queries in [`crate::queries`] to interact with the API. @@ -53,17 +63,22 @@ impl WasmerClient { } pub fn new(graphql_endpoint: Url, user_agent: &str) -> Result { - #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] - let client = reqwest::Client::builder() - .build() - .context("could not construct http client")?; - - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] - let client = reqwest::Client::builder() - .connect_timeout(Duration::from_secs(10)) - .timeout(Duration::from_secs(90)) - .build() - .context("could not construct http client")?; + let builder = { + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + let mut builder = reqwest::ClientBuilder::new(); + + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + let mut builder = reqwest::ClientBuilder::new() + .connect_timeout(Duration::from_secs(10)) + .timeout(Duration::from_secs(90)); + + if let Some(proxy) = get_proxy()? { + builder = builder.proxy(proxy); + } + builder + }; + + let client = builder.build().context("failed to create reqwest client")?; Self::new_with_client(client, graphql_endpoint, user_agent) } From 618044433bfeb5661401c66d41fa2c64615c4452 Mon Sep 17 00:00:00 2001 From: "M.Amin Rayej" Date: Tue, 13 Aug 2024 12:28:22 +0330 Subject: [PATCH 05/15] add proxy support to tests --- lib/wasix/tests/runners.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/wasix/tests/runners.rs b/lib/wasix/tests/runners.rs index 5f1f8826114..b58c41e5998 100644 --- a/lib/wasix/tests/runners.rs +++ b/lib/wasix/tests/runners.rs @@ -12,7 +12,7 @@ use reqwest::Client; use tokio::runtime::Handle; use wasmer::Engine; use wasmer_wasix::{ - http::HttpClient, + http::{reqwest::get_proxy, HttpClient}, runners::Runner, runtime::{ module_cache::{FileSystemCache, ModuleCache, SharedCache}, @@ -229,10 +229,16 @@ async fn download_cached(url: &str) -> bytes::Bytes { } fn client() -> Client { - Client::builder() - .connect_timeout(Duration::from_secs(30)) - .build() - .unwrap() + let builder = { + let mut builder = reqwest::ClientBuilder::new().connect_timeout(Duration::from_secs(30)); + if let Some(proxy) = get_proxy().unwrap() { + builder = builder.proxy(proxy); + } + builder + }; + let client = builder.build().unwrap(); + + client } #[cfg(not(target_os = "windows"))] From 2c2ccc0dfb57787fad78284f4eada8da8eff6ee0 Mon Sep 17 00:00:00 2001 From: "M.Amin Rayej" Date: Tue, 13 Aug 2024 16:24:44 +0330 Subject: [PATCH 06/15] fix issue with Windows path --- lib/virtual-fs/src/union_fs.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/virtual-fs/src/union_fs.rs b/lib/virtual-fs/src/union_fs.rs index 0154e47bcd5..04aae9459f5 100644 --- a/lib/virtual-fs/src/union_fs.rs +++ b/lib/virtual-fs/src/union_fs.rs @@ -630,12 +630,15 @@ mod tests { .collect(); assert_eq!(root_contents, vec!["/app"]); - let app_contents: Vec = fs + let app_contents: Vec = fs .read_dir(&PathBuf::from("/app")) .unwrap() - .map(|e| e.unwrap().path.to_str().unwrap().to_string()) + .map(|e| e.unwrap().path) .collect(); - assert_eq!(app_contents, vec!["/app/a", "/app/b"]); + assert_eq!( + app_contents, + vec![PathBuf::from("/app/a"), PathBuf::from("/app/b")] + ); let a_contents: Vec = fs .read_dir(&PathBuf::from("/app/a")) From 55d66688ae71b8eafca6de75ff97eacf46b6f29f Mon Sep 17 00:00:00 2001 From: "M.Amin Rayej" Date: Tue, 13 Aug 2024 22:23:34 +0330 Subject: [PATCH 07/15] get the proxy scheme from the command line for download --- lib/backend-api/src/client.rs | 39 ++++++++++++++---------- lib/cli/src/commands/package/download.rs | 13 +++++++- lib/cli/src/config/env.rs | 20 ++++++++++++ 3 files changed, 55 insertions(+), 17 deletions(-) diff --git a/lib/backend-api/src/client.rs b/lib/backend-api/src/client.rs index 04b4df5c986..4f87af5b02f 100644 --- a/lib/backend-api/src/client.rs +++ b/lib/backend-api/src/client.rs @@ -6,16 +6,6 @@ use anyhow::{bail, Context as _}; use cynic::{http::CynicReqwestError, GraphQlResponse, Operation}; use url::Url; -pub fn get_proxy() -> Result, anyhow::Error> { - if let Ok(scheme) = std::env::var("http_proxy").or_else(|_| std::env::var("HTTP_PROXY")) { - let proxy = reqwest::Proxy::all(scheme)?; - - Ok(Some(proxy)) - } else { - Ok(None) - } -} - /// API client for the Wasmer API. /// /// Use the queries in [`crate::queries`] to interact with the API. @@ -62,20 +52,21 @@ impl WasmerClient { }) } - pub fn new(graphql_endpoint: Url, user_agent: &str) -> Result { + pub fn new_with_proxy( + graphql_endpoint: Url, + user_agent: &str, + proxy: reqwest::Proxy, + ) -> Result { let builder = { #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] let mut builder = reqwest::ClientBuilder::new(); #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] - let mut builder = reqwest::ClientBuilder::new() + let builder = reqwest::ClientBuilder::new() .connect_timeout(Duration::from_secs(10)) .timeout(Duration::from_secs(90)); - if let Some(proxy) = get_proxy()? { - builder = builder.proxy(proxy); - } - builder + builder.proxy(proxy) }; let client = builder.build().context("failed to create reqwest client")?; @@ -83,6 +74,22 @@ impl WasmerClient { Self::new_with_client(client, graphql_endpoint, user_agent) } + pub fn new(graphql_endpoint: Url, user_agent: &str) -> Result { + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + let client = reqwest::Client::builder() + .build() + .context("could not construct http client")?; + + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + let client = reqwest::Client::builder() + .connect_timeout(Duration::from_secs(10)) + .timeout(Duration::from_secs(90)) + .build() + .context("could not construct http client")?; + + Self::new_with_client(client, graphql_endpoint, user_agent) + } + pub fn with_auth_token(mut self, auth_token: String) -> Self { self.auth_token = Some(auth_token); self diff --git a/lib/cli/src/commands/package/download.rs b/lib/cli/src/commands/package/download.rs index c4a79e24611..bc168fae95e 100644 --- a/lib/cli/src/commands/package/download.rs +++ b/lib/cli/src/commands/package/download.rs @@ -28,6 +28,10 @@ pub struct PackageDownload { #[clap(long)] pub quiet: bool, + /// proxy to use for downloading + #[clap(long)] + pub proxy: Option, + /// The package to download. package: PackageSource, } @@ -97,7 +101,13 @@ impl PackageDownload { // caveat: client_unauthennticated will use a token if provided, it // just won't fail if none is present. So, _unauthenticated() can actually // produce an authenticated client. - let client = self.env.client_unauthennticated()?; + let client = if let Some(proxy) = &self.proxy { + let proxy = reqwest::Proxy::all(proxy)?; + + self.env.client_unauthennticated_with_proxy(proxy)? + } else { + self.env.client_unauthennticated()? + }; let version = id.version_or_default().to_string(); let version = if version == "*" { @@ -299,6 +309,7 @@ mod tests { out_path: Some(out_path.clone()), package: "wasmer/hello@0.1.0".parse().unwrap(), quiet: true, + proxy: None, }; cmd.execute().unwrap(); diff --git a/lib/cli/src/config/env.rs b/lib/cli/src/config/env.rs index 0a3fbf9d1fc..496962c14f7 100644 --- a/lib/cli/src/config/env.rs +++ b/lib/cli/src/config/env.rs @@ -138,6 +138,26 @@ impl WasmerEnv { Ok(client) } + pub fn client_unauthennticated_with_proxy( + &self, + proxy: reqwest::Proxy, + ) -> Result { + let registry_url = self.registry_endpoint()?; + let client = wasmer_api::WasmerClient::new_with_proxy( + registry_url, + &DEFAULT_WASMER_CLI_USER_AGENT, + proxy, + )?; + + let client = if let Some(token) = self.token() { + client.with_auth_token(token) + } else { + client + }; + + Ok(client) + } + pub fn client(&self) -> Result { let client = self.client_unauthennticated()?; if client.auth_token().is_none() { From f57661c115b293b0eb067d2a574511ff9b86d2dd Mon Sep 17 00:00:00 2001 From: "M.Amin Rayej" Date: Tue, 13 Aug 2024 22:25:18 +0330 Subject: [PATCH 08/15] add the source code for the nested mounted paths integration test --- .../packages/nested-mounted-paths/main.c | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/integration/cli/tests/packages/nested-mounted-paths/main.c diff --git a/tests/integration/cli/tests/packages/nested-mounted-paths/main.c b/tests/integration/cli/tests/packages/nested-mounted-paths/main.c new file mode 100644 index 00000000000..46854e25f8a --- /dev/null +++ b/tests/integration/cli/tests/packages/nested-mounted-paths/main.c @@ -0,0 +1,83 @@ +#include +#include +#include +#include +#include +#include +#include + +int main() +{ + DIR *dir; + struct dirent *entry; + + printf("/:\n"); + dir = opendir("/"); + + if (dir != NULL) + { + while ((entry = readdir(dir)) != NULL) + { + printf("%s\n", entry->d_name); + } + closedir(dir); + } + else + { + perror("opendir"); + return 1; + } + + printf("\n/app:\n"); + dir = opendir("/app"); + + if (dir != NULL) + { + while ((entry = readdir(dir)) != NULL) + { + printf("%s\n", entry->d_name); + } + closedir(dir); + } + else + { + perror("opendir"); + return 1; + } + + printf("\n/app/a:\n"); + dir = opendir("/app/a"); + + if (dir != NULL) + { + while ((entry = readdir(dir)) != NULL) + { + printf("%s\n", entry->d_name); + } + closedir(dir); + } + else + { + perror("opendir"); + return 1; + } + + printf("\n/app/b:\n"); + dir = opendir("/app/b"); + + if (dir != NULL) + { + while ((entry = readdir(dir)) != NULL) + { + printf("%s\n", entry->d_name); + } + closedir(dir); + } + else + { + perror("opendir"); + return 1; + } + + return 0; +} \ No newline at end of file From 8f7723398400453aa5f1dd25fd4bd4e5c4a672db Mon Sep 17 00:00:00 2001 From: "M.Amin Rayej" Date: Fri, 16 Aug 2024 05:11:30 +0330 Subject: [PATCH 09/15] implement nested mounted paths via nested union fs --- lib/virtual-fs/src/lib.rs | 2 +- lib/virtual-fs/src/union_fs.rs | 830 ++++++++---------- .../package_loader/load_package_tree.rs | 4 +- tests/lib/wast/src/wasi_wast.rs | 48 +- 4 files changed, 396 insertions(+), 488 deletions(-) diff --git a/lib/virtual-fs/src/lib.rs b/lib/virtual-fs/src/lib.rs index 186470974e5..e294e274513 100644 --- a/lib/virtual-fs/src/lib.rs +++ b/lib/virtual-fs/src/lib.rs @@ -580,7 +580,7 @@ impl From for io::Error { #[derive(Debug)] pub struct ReadDir { // TODO: to do this properly we need some kind of callback to the core FS abstraction - data: Vec, + pub(crate) data: Vec, index: usize, } diff --git a/lib/virtual-fs/src/union_fs.rs b/lib/virtual-fs/src/union_fs.rs index 04aae9459f5..9db12862f68 100644 --- a/lib/virtual-fs/src/union_fs.rs +++ b/lib/virtual-fs/src/union_fs.rs @@ -4,88 +4,100 @@ use crate::*; -use std::{ - path::Path, - sync::{Arc, Mutex, Weak}, -}; +use std::path::Path; -use tracing::{debug, trace}; +#[derive(Debug)] +pub enum UnionOrFs { + Union(Box), + FS(Box), +} + +impl UnionOrFs { + pub fn fs(&self) -> &dyn FileSystem { + match self { + UnionOrFs::Union(union) => union, + UnionOrFs::FS(fs) => fs, + } + } + + pub fn is_union(&self) -> bool { + match self { + UnionOrFs::Union(_) => true, + UnionOrFs::FS(_) => false, + } + } +} + +impl FileSystem for UnionOrFs { + fn readlink(&self, path: &Path) -> Result { + self.fs().readlink(path) + } + + fn read_dir(&self, path: &Path) -> Result { + self.fs().read_dir(path) + } + + fn create_dir(&self, path: &Path) -> Result<()> { + self.fs().create_dir(path) + } + + fn remove_dir(&self, path: &Path) -> Result<()> { + self.fs().remove_dir(path) + } + + fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<()>> { + self.fs().rename(from, to) + } + + fn metadata(&self, path: &Path) -> Result { + self.fs().metadata(path) + } + + fn symlink_metadata(&self, path: &Path) -> Result { + self.fs().symlink_metadata(path) + } -pub type TempHolding = Arc>>>>; + fn remove_file(&self, path: &Path) -> Result<()> { + self.fs().remove_file(path) + } + + fn new_open_options(&self) -> OpenOptions { + self.fs().new_open_options() + } +} #[derive(Debug)] pub struct MountPoint { - pub path: String, + pub path: PathBuf, pub name: String, - pub fs: Option>>, - pub weak_fs: Weak>, - pub temp_holding: TempHolding, + pub fs: UnionOrFs, pub should_sanitize: bool, pub new_path: Option, } -impl Clone for MountPoint { - fn clone(&self) -> Self { - Self { - path: self.path.clone(), - name: self.name.clone(), - fs: None, - weak_fs: self.weak_fs.clone(), - temp_holding: self.temp_holding.clone(), - should_sanitize: self.should_sanitize, - new_path: self.new_path.clone(), - } - } -} - impl MountPoint { - pub fn fs(&self) -> Option>> { - match &self.fs { - Some(a) => Some(a.clone()), - None => self.weak_fs.upgrade(), - } + pub fn fs(&self) -> &UnionOrFs { + &self.fs } - /// Tries to recover the internal `Weak` to a `Arc` - fn solidify(&mut self) { - if self.fs.is_none() { - self.fs = self.weak_fs.upgrade(); - } - { - let mut guard = self.temp_holding.lock().unwrap(); - let fs = guard.take(); - if self.fs.is_none() { - self.fs = fs; - } - } + pub fn fs_mut(&mut self) -> &mut UnionOrFs { + &mut self.fs } - /// Returns a strong-referenced copy of the internal `Arc` - fn strong(&self) -> Option { - self.fs().map(|fs| StrongMountPoint { + pub fn mount_point_ref(&self) -> MountPointRef<'_> { + MountPointRef { path: self.path.clone(), name: self.name.clone(), - fs, + fs: &self.fs, should_sanitize: self.should_sanitize, new_path: self.new_path.clone(), - }) + } } } -/// A `strong` mount point holds a strong `Arc` reference to the filesystem -/// mounted at path `path`. -#[derive(Debug)] -pub struct StrongMountPoint { - pub path: String, - pub name: String, - pub fs: Arc>, - pub should_sanitize: bool, - pub new_path: Option, -} - /// Allows different filesystems of different types /// to be mounted at various mount points -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default)] pub struct UnionFileSystem { pub mounts: Vec, } @@ -101,42 +113,112 @@ impl UnionFileSystem { } impl UnionFileSystem { + fn find_mount(&self, path: PathBuf) -> Option<(PathBuf, PathBuf, &UnionOrFs)> { + let mut components = path.components().collect::>(); + + if let Some(c) = components.first().copied() { + components.remove(0); + + let sub_path = components.into_iter().collect::(); + + if let Some(mount) = self + .mounts + .iter() + .find(|m| m.path.as_os_str() == c.as_os_str()) + { + if sub_path.components().next().is_none() { + let sub_path = if mount.fs.is_union() { + sub_path + } else { + PathBuf::from("/") + }; + + return Some((PathBuf::from(c.as_os_str()), sub_path, &mount.fs)); + } + match &mount.fs { + UnionOrFs::Union(union) => { + return union.find_mount(sub_path).map(|(prefix, path, fs)| { + let prefix = PathBuf::from(c.as_os_str()).join(prefix); + + (prefix, path, fs) + }); + } + UnionOrFs::FS(_) => { + return Some(( + PathBuf::from(c.as_os_str()), + PathBuf::from("/").join(sub_path), + &mount.fs, + )); + } + } + } + } + + None + } + pub fn mount( &mut self, - name: &str, - path: &str, + name: String, + path: PathBuf, should_sanitize: bool, fs: Box, - new_path: Option<&str>, + new_path: Option, ) { - self.unmount(path); - let mut path = path.to_string(); - if !path.starts_with('/') { - path.insert(0, '/'); - } - if !path.ends_with('/') { - path += "/"; - } - let new_path = new_path.map(|new_path| { - let mut new_path = new_path.to_string(); - if !new_path.ends_with('/') { - new_path += "/"; + let mut components = path.components().collect::>(); + if let Some(c) = components.first().copied() { + components.remove(0); + + let sub_path = components.into_iter().collect::(); + + if let Some(mount) = self + .mounts + .iter_mut() + .find(|m| m.path.as_os_str() == c.as_os_str()) + { + match mount.fs_mut() { + UnionOrFs::Union(union) => { + union.mount( + name, + sub_path, + should_sanitize, + fs, + new_path.clone(), // TODO: what to do with new_path + ) + } + UnionOrFs::FS(_) => { + println!("path: {path:?} is already mounted"); + } + } + } else { + let fs = if sub_path.components().next().is_none() { + UnionOrFs::FS(fs) + } else { + let mut union = UnionFileSystem::new(); + union.mount( + name.clone(), + sub_path, + should_sanitize, + fs, + new_path.clone(), + ); + + UnionOrFs::Union(Box::new(union)) + }; + + let mount = MountPoint { + path: PathBuf::from(c.as_os_str()), + name, + fs, + should_sanitize, + new_path, + }; + + self.mounts.push(mount); } - new_path - }); - let fs = Arc::new(fs); - - let mount = MountPoint { - path, - name: name.to_string(), - fs: None, - weak_fs: Arc::downgrade(&fs), - temp_holding: Arc::new(Mutex::new(Some(fs.clone()))), - should_sanitize, - new_path, - }; - - self.mounts.push(mount); + } else { + println!("empty path"); + } } pub fn unmount(&mut self, path: &str) { @@ -153,47 +235,33 @@ impl UnionFileSystem { path2 = (path2[..(path2.len() - 1)]).to_string(); } - self.mounts - .retain(|mount| mount.path != path2 && mount.path != path3); + self.mounts.retain(|mount| { + mount.path.to_str().unwrap() != path2 && mount.path.to_str().unwrap() != path3 + }); } +} - fn read_dir_internal(&self, path: &Path) -> Result { - let path_str = path.to_string_lossy(); - - let mut ret = None; - for (p, mount) in filter_mounts(&self.mounts, path_str.as_ref()) { - match mount.fs.read_dir(Path::new(p.as_str())) { - Ok(dir) => { - if ret.is_none() { - ret = Some(Vec::new()); - } - let ret = ret.as_mut().unwrap(); - for mut sub in dir.flatten() { - let sub_path: PathBuf = sub.path.components().skip(1).collect(); - sub.path = PathBuf::from(&mount.path).join(&sub_path); - ret.push(sub); - } - } - Err(err) => { - debug!("failed to read dir - {}", err); - } +impl FileSystem for UnionFileSystem { + fn readlink(&self, path: &Path) -> Result { + if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { + match fs { + UnionOrFs::Union(_) => Err(FsError::NotAFile), + UnionOrFs::FS(fs) => fs.readlink(&path), } + } else { + Err(FsError::EntryNotFound) } + } - // also take into account partial matchings - let mut partial_ret = Vec::new(); - let path = PathBuf::from(path_str.into_owned()); - for mount in &self.mounts { - let mount_path = PathBuf::from(&mount.path); - - if let Ok(postfix) = mount_path.strip_prefix(&path) { - if let Some(_entry) = postfix.components().next() { - if partial_ret + fn read_dir(&self, path: &Path) -> Result { + if let Some((prefix, path, fs)) = self.find_mount(path.to_owned()) { + match fs { + UnionOrFs::Union(union) => { + let entries = union + .mounts .iter() - .all(|e: &DirEntry| e.path.as_path() != path.join(_entry).as_path()) - { - partial_ret.push(DirEntry { - path: path.join(_entry), + .map(|m| DirEntry { + path: prefix.join(m.path.clone()), metadata: Ok(Metadata { ft: FileType::new_dir(), accessed: 0, @@ -202,311 +270,118 @@ impl UnionFileSystem { len: 0, }), }) - } - } - } - } + .collect::>(); - let ret = if let Some(mut ret) = ret { - ret.extend(partial_ret); - - Some(ret) - } else if partial_ret.is_empty() { - None - } else { - Some(partial_ret) - }; - - match ret { - Some(mut ret) => { - ret.sort_by(|a, b| match (a.metadata.as_ref(), b.metadata.as_ref()) { - (Ok(a), Ok(b)) => a.modified.cmp(&b.modified), - _ => std::cmp::Ordering::Equal, - }); - Ok(ReadDir::new(ret)) - } - None => Err(FsError::EntryNotFound), - } - } - - /// Deletes all mount points that do not have `sanitize` set in the options - pub fn sanitize(mut self) -> Self { - self.solidify(); - self.mounts.retain(|mount| !mount.should_sanitize); - self - } - - /// Tries to recover the internal `Weak` to a `Arc` - pub fn solidify(&mut self) { - for mount in self.mounts.iter_mut() { - mount.solidify(); - } - } -} - -impl FileSystem for UnionFileSystem { - fn readlink(&self, path: &Path) -> Result { - debug!("readlink: path={}", path.display()); - let mut ret_error = FsError::EntryNotFound; - let path = path.to_string_lossy(); - for (path_inner, mount) in filter_mounts(&self.mounts, path.as_ref()) { - match mount.fs.readlink(Path::new(path_inner.as_str())) { - Ok(ret) => { - return Ok(ret); + Ok(ReadDir::new(entries)) } - Err(err) => { - // This fixes a bug when attempting to create the directory /usr when it does not exist - // on the x86 version of memfs - // TODO: patch virtual-fs and remove - if let FsError::NotAFile = &err { - ret_error = FsError::EntryNotFound; - } else { - debug!("readlink failed: (path={}) - {}", path, err); - ret_error = err; + UnionOrFs::FS(fs) => { + let mut entries = fs.read_dir(&path)?; + + for entry in &mut entries.data { + let path: PathBuf = entry.path.components().skip(1).collect(); + entry.path = PathBuf::from(&prefix).join(path); } + + Ok(entries) } } + } else { + Err(FsError::EntryNotFound) } - debug!("readlink: failed={}", ret_error); - Err(ret_error) } - fn read_dir(&self, path: &Path) -> Result { - debug!("read_dir: path={}", path.display()); - self.read_dir_internal(path) - } fn create_dir(&self, path: &Path) -> Result<()> { - debug!("create_dir: path={}", path.display()); - if path.parent().is_none() { - return Err(FsError::BaseNotDirectory); - } - if self.read_dir_internal(path).is_ok() { - //return Err(FsError::AlreadyExists); - return Ok(()); - } - - let path = path.to_string_lossy(); - let mut ret_error = FsError::EntryNotFound; - for (path, mount) in filter_mounts(&self.mounts, path.as_ref()) { - match mount.fs.create_dir(Path::new(path.as_str())) { - Ok(ret) => { - return Ok(ret); - } - Err(err) => { - ret_error = err; - } + if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { + match fs { + UnionOrFs::Union(_) => Err(FsError::AlreadyExists), + UnionOrFs::FS(fs) => fs.create_dir(&path), } + } else { + Err(FsError::EntryNotFound) } - Err(ret_error) } fn remove_dir(&self, path: &Path) -> Result<()> { - debug!("remove_dir: path={}", path.display()); - if path.parent().is_none() { - return Err(FsError::BaseNotDirectory); - } - // https://github.com/rust-lang/rust/issues/86442 - // DirectoryNotEmpty is not implemented consistently - if path.is_dir() && self.read_dir(path).map(|s| !s.is_empty()).unwrap_or(false) { - return Err(FsError::DirectoryNotEmpty); - } - let mut ret_error = FsError::EntryNotFound; - let path = path.to_string_lossy(); - for (path, mount) in filter_mounts(&self.mounts, path.as_ref()) { - match mount.fs.remove_dir(Path::new(path.as_str())) { - Ok(ret) => { - return Ok(ret); - } - Err(err) => { - ret_error = err; - } + if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { + match fs { + UnionOrFs::Union(_) => Err(FsError::PermissionDenied), + UnionOrFs::FS(fs) => fs.remove_dir(&path), } + } else { + Err(FsError::EntryNotFound) } - Err(ret_error) } fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<()>> { - Box::pin(async { - println!("rename: from={} to={}", from.display(), to.display()); - if from.parent().is_none() { - return Err(FsError::BaseNotDirectory); - } - if to.parent().is_none() { - return Err(FsError::BaseNotDirectory); - } - let mut ret_error = FsError::EntryNotFound; - let from = from.to_string_lossy(); - let to = to.to_string_lossy(); - #[cfg(target_os = "windows")] - let to = to.replace('\\', "/"); - for (path, mount) in filter_mounts(&self.mounts, from.as_ref()) { - let mut to = if to.starts_with(mount.path.as_str()) { - (to[mount.path.len()..]).to_string() - } else { - ret_error = FsError::UnknownError; - continue; - }; - if !to.starts_with('/') { - to = format!("/{}", to); - } - match mount - .fs - .rename(Path::new(&path), Path::new(to.as_str())) - .await - { - Ok(ret) => { - trace!("rename ok"); - return Ok(ret); - } - Err(err) => { - trace!("rename error (from={}, to={}) - {}", from, to, err); - ret_error = err; + Box::pin(async move { + if let Some((prefix, path, fs)) = self.find_mount(from.to_owned()) { + match fs { + UnionOrFs::Union(_) => Err(FsError::PermissionDenied), + UnionOrFs::FS(fs) => { + let to = to.strip_prefix(prefix).map_err(|_| FsError::InvalidInput)?; + + let to = PathBuf::from("/").join(to); + + fs.rename(&path, &to).await } } + } else { + Err(FsError::EntryNotFound) } - trace!("rename failed - {}", ret_error); - Err(ret_error) }) } fn metadata(&self, path: &Path) -> Result { - debug!("metadata: path={}", path.display()); - let mut ret_error = FsError::EntryNotFound; - let path = path.to_string_lossy(); - for (path, mount) in filter_mounts(&self.mounts, path.as_ref()) { - match mount.fs.metadata(Path::new(path.as_str())) { - Ok(ret) => { - return Ok(ret); - } - Err(err) => { - // This fixes a bug when attempting to create the directory /usr when it does not exist - // on the x86 version of memfs - // TODO: patch virtual-fs and remove - if let FsError::NotAFile = &err { - ret_error = FsError::EntryNotFound; - } else { - debug!("metadata failed: (path={}) - {}", path, err); - ret_error = err; - } - } - } - } - - // if no mount point fully matched, maybe there is a partial matching - let path = path.into_owned(); - for mount in &self.mounts { - if mount.path.starts_with(&path) { - return Ok(Metadata { + if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { + match fs { + UnionOrFs::Union(_) => Ok(Metadata { ft: FileType::new_dir(), accessed: 0, created: 0, modified: 0, len: 0, - }); + }), + UnionOrFs::FS(fs) => fs.metadata(&path), } + } else { + Err(FsError::EntryNotFound) } - Err(ret_error) } fn symlink_metadata(&self, path: &Path) -> Result { - debug!("symlink_metadata: path={}", path.display()); - let mut ret_error = FsError::EntryNotFound; - let path = path.to_string_lossy(); - for (path_inner, mount) in filter_mounts(&self.mounts, path.as_ref()) { - match mount.fs.symlink_metadata(Path::new(path_inner.as_str())) { - Ok(ret) => { - return Ok(ret); - } - Err(err) => { - // This fixes a bug when attempting to create the directory /usr when it does not exist - // on the x86 version of memfs - // TODO: patch virtual-fs and remove - if let FsError::NotAFile = &err { - ret_error = FsError::EntryNotFound; - } else { - debug!("metadata failed: (path={}) - {}", path, err); - ret_error = err; - } - } - } - } - - // if no mount point fully matched, maybe there is a partial matching - let path = path.into_owned(); - for mount in &self.mounts { - if mount.path.starts_with(&path) { - return Ok(Metadata { + if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { + match fs { + UnionOrFs::Union(_) => Ok(Metadata { ft: FileType::new_dir(), accessed: 0, created: 0, modified: 0, len: 0, - }); + }), + UnionOrFs::FS(fs) => fs.symlink_metadata(&path), } + } else { + Err(FsError::EntryNotFound) } - debug!("symlink_metadata: failed={}", ret_error); - Err(ret_error) } fn remove_file(&self, path: &Path) -> Result<()> { - let mut ret_error = FsError::EntryNotFound; - let path = path.to_string_lossy(); - for (path, mount) in filter_mounts(&self.mounts, path.as_ref()) { - match mount.fs.remove_file(Path::new(path.as_str())) { - Ok(ret) => { - return Ok(ret); - } - Err(err) => { - ret_error = err; - } + if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { + match fs { + UnionOrFs::Union(_) => Err(FsError::NotAFile), + UnionOrFs::FS(fs) => fs.remove_file(&path), } + } else { + Err(FsError::EntryNotFound) } - Err(ret_error) } fn new_open_options(&self) -> OpenOptions { OpenOptions::new(self) } } -fn filter_mounts( - mounts: &[MountPoint], - target: &str, -) -> impl Iterator { - // On Windows, Path might use \ instead of /, wich mill messup the matching of mount points - #[cfg(target_os = "windows")] - let target = target.replace('\\', "/"); - - let mut biggest_path = 0usize; - let mut ret = Vec::new(); - for mount in mounts.iter().rev() { - let mut test_mount_path1 = mount.path.clone(); - if !test_mount_path1.ends_with('/') { - test_mount_path1.push('/'); - } - - let mut test_mount_path2 = mount.path.clone(); - if test_mount_path2.ends_with('/') { - test_mount_path2 = test_mount_path2[..(test_mount_path2.len() - 1)].to_string(); - } - - if target == test_mount_path1 || target == test_mount_path2 { - if let Some(mount) = mount.strong() { - biggest_path = biggest_path.max(mount.path.len()); - let mut path = "/".to_string(); - if let Some(ref np) = mount.new_path { - path = np.to_string(); - } - ret.push((path, mount)); - } - } else if target.starts_with(test_mount_path1.as_str()) { - if let Some(mount) = mount.strong() { - biggest_path = biggest_path.max(mount.path.len()); - let path = &target[test_mount_path2.len()..]; - let mut path = path.to_string(); - if let Some(ref np) = mount.new_path { - path = format!("{}{}", np, &path[1..]); - } - ret.push((path, mount)); - } - } - } - ret.retain(|(_, b)| b.path.len() >= biggest_path); - ret.into_iter() +#[derive(Debug)] +pub struct MountPointRef<'a> { + pub path: PathBuf, + pub name: String, + pub fs: &'a dyn FileSystem, + pub should_sanitize: bool, + pub new_path: Option, } impl FileOpener for UnionFileSystem { @@ -515,39 +390,19 @@ impl FileOpener for UnionFileSystem { path: &Path, conf: &OpenOptionsConfig, ) -> Result> { - debug!("open: path={}", path.display()); - let mut ret_err = FsError::EntryNotFound; - let path = path.to_string_lossy(); - - if conf.create() || conf.create_new() { - for (path, mount) in filter_mounts(&self.mounts, path.as_ref()) { - if let Ok(mut ret) = mount - .fs + let parent = path.parent().unwrap(); + let file_name = path.file_name().unwrap(); + if let Some((_, path, fs)) = self.find_mount(parent.to_owned()) { + match fs { + UnionOrFs::Union(_) => Err(FsError::PermissionDenied), + UnionOrFs::FS(fs) => fs .new_open_options() - .truncate(conf.truncate()) - .append(conf.append()) - .read(conf.read()) - .write(conf.write()) - .open(path) - { - if conf.create_new() { - ret.unlink().ok(); - continue; - } - return Ok(ret); - } - } - } - for (path, mount) in filter_mounts(&self.mounts, path.as_ref()) { - match mount.fs.new_open_options().options(conf.clone()).open(path) { - Ok(ret) => return Ok(ret), - Err(err) if ret_err == FsError::EntryNotFound => { - ret_err = err; - } - _ => {} + .options(conf.clone()) + .open(path.join(file_name)), } + } else { + Err(FsError::EntryNotFound) } - Err(ret_err) } } @@ -557,7 +412,7 @@ mod tests { use tokio::io::AsyncWriteExt; - use crate::{mem_fs, ops, FileSystem as FileSystemTrait, FsError, UnionFileSystem}; + use crate::{mem_fs, FileSystem as FileSystemTrait, FsError, UnionFileSystem}; use super::{FileOpener, OpenOptionsConfig}; @@ -572,14 +427,62 @@ mod tests { let g = mem_fs::FileSystem::default(); let h = mem_fs::FileSystem::default(); - union.mount("mem_fs_1", "/test_new_filesystem", false, Box::new(a), None); - union.mount("mem_fs_2", "/test_create_dir", false, Box::new(b), None); - union.mount("mem_fs_3", "/test_remove_dir", false, Box::new(c), None); - union.mount("mem_fs_4", "/test_rename", false, Box::new(d), None); - union.mount("mem_fs_5", "/test_metadata", false, Box::new(e), None); - union.mount("mem_fs_6", "/test_remove_file", false, Box::new(f), None); - union.mount("mem_fs_6", "/test_readdir", false, Box::new(g), None); - union.mount("mem_fs_6", "/test_canonicalize", false, Box::new(h), None); + union.mount( + "mem_fs_1".to_string(), + PathBuf::from("/test_new_filesystem"), + false, + Box::new(a), + None, + ); + union.mount( + "mem_fs_2".to_string(), + PathBuf::from("/test_create_dir"), + false, + Box::new(b), + None, + ); + union.mount( + "mem_fs_3".to_string(), + PathBuf::from("/test_remove_dir"), + false, + Box::new(c), + None, + ); + union.mount( + "mem_fs_4".to_string(), + PathBuf::from("/test_rename"), + false, + Box::new(d), + None, + ); + union.mount( + "mem_fs_5".to_string(), + PathBuf::from("/test_metadata"), + false, + Box::new(e), + None, + ); + union.mount( + "mem_fs_6".to_string(), + PathBuf::from("/test_remove_file"), + false, + Box::new(f), + None, + ); + union.mount( + "mem_fs_6".to_string(), + PathBuf::from("/test_readdir"), + false, + Box::new(g), + None, + ); + union.mount( + "mem_fs_6".to_string(), + PathBuf::from("/test_canonicalize"), + false, + Box::new(h), + None, + ); union } @@ -613,8 +516,20 @@ mod tests { ) .unwrap(); - union.mount("mem_fs_1", "/app/a", false, Box::new(a), None); - union.mount("mem_fs_2", "/app/b", false, Box::new(b), None); + union.mount( + "mem_fs_1".to_string(), + PathBuf::from("/app/a"), + false, + Box::new(a), + None, + ); + union.mount( + "mem_fs_2".to_string(), + PathBuf::from("/app/b"), + false, + Box::new(b), + None, + ); union } @@ -707,16 +622,14 @@ mod tests { assert_eq!( fs.create_dir(Path::new("/")), - Err(FsError::BaseNotDirectory), - "creating a directory that has no parent", + Err(FsError::AlreadyExists), + "root should already exist", ); - let _ = fs_extra::remove_items(&["/test_create_dir"]); - assert_eq!( fs.create_dir(Path::new("/test_create_dir")), - Ok(()), - "creating a directory", + Err(FsError::AlreadyExists), + "/test_create_dir already mounted and exists", ); assert_eq!( @@ -762,12 +675,10 @@ mod tests { async fn test_remove_dir() { let fs = gen_filesystem(); - let _ = fs_extra::remove_items(&["/test_remove_dir"]); - assert_eq!( fs.remove_dir(Path::new("/")), - Err(FsError::BaseNotDirectory), - "removing a directory that has no parent", + Err(FsError::PermissionDenied), + "cannot remove the root directory", ); assert_eq!( @@ -778,8 +689,8 @@ mod tests { assert_eq!( fs.create_dir(Path::new("/test_remove_dir")), - Ok(()), - "creating a directory", + Err(FsError::AlreadyExists), + "mount point already exists", ); assert_eq!( @@ -821,8 +732,6 @@ mod tests { !read_dir_names(&fs, "/test_remove_dir").contains(&"foo".to_string()), "the foo directory still exists" ); - - let _ = fs_extra::remove_items(&["/test_remove_dir"]); } fn read_dir_names(fs: &dyn crate::FileSystem, path: &str) -> Vec { @@ -836,20 +745,21 @@ mod tests { async fn test_rename() { let fs = gen_filesystem(); - let _ = fs_extra::remove_items(&["./test_rename"]); - assert_eq!( fs.rename(Path::new("/"), Path::new("/bar")).await, - Err(FsError::BaseNotDirectory), + Err(FsError::PermissionDenied), "renaming a directory that has no parent", ); assert_eq!( fs.rename(Path::new("/foo"), Path::new("/")).await, - Err(FsError::BaseNotDirectory), + Err(FsError::EntryNotFound), "renaming to a directory that has no parent", ); - assert_eq!(fs.create_dir(Path::new("/test_rename")), Ok(())); + assert_eq!( + fs.create_dir(Path::new("/test_rename")), + Err(FsError::AlreadyExists) + ); assert_eq!(fs.create_dir(Path::new("/test_rename/foo")), Ok(())); assert_eq!(fs.create_dir(Path::new("/test_rename/foo/qux")), Ok(())); @@ -1001,15 +911,11 @@ mod tests { let fs = gen_filesystem(); - let _ = fs_extra::remove_items(&["/test_metadata"]); - - assert_eq!(fs.create_dir(Path::new("/test_metadata")), Ok(())); - let root_metadata = fs.metadata(Path::new("/test_metadata")).unwrap(); assert!(root_metadata.ft.dir); - assert!(root_metadata.accessed == root_metadata.created); - assert!(root_metadata.modified == root_metadata.created); + assert_eq!(root_metadata.accessed, root_metadata.created); + assert_eq!(root_metadata.modified, root_metadata.created); assert!(root_metadata.modified > 0); assert_eq!(fs.create_dir(Path::new("/test_metadata/foo")), Ok(())); @@ -1053,10 +959,6 @@ mod tests { async fn test_remove_file() { let fs = gen_filesystem(); - let _ = fs_extra::remove_items(&["./test_remove_file"]); - - assert!(fs.create_dir(Path::new("/test_remove_file")).is_ok()); - assert!( fs.new_open_options() .write(true) @@ -1259,34 +1161,4 @@ mod tests { let _ = fs_extra::remove_items(&["./test_canonicalize"]); } */ - - #[tokio::test] - #[ignore = "Not yet supported. See https://github.com/wasmerio/wasmer/issues/3678"] - async fn mount_to_overlapping_directories() { - let top_level = mem_fs::FileSystem::default(); - ops::touch(&top_level, "/file.txt").unwrap(); - let nested = mem_fs::FileSystem::default(); - ops::touch(&nested, "/another-file.txt").unwrap(); - - let mut fs = UnionFileSystem::default(); - fs.mount( - "top-level", - "/", - false, - Box::new(top_level), - Some("/top-level"), - ); - fs.mount( - "nested", - "/", - false, - Box::new(nested), - Some("/top-level/nested"), - ); - - assert!(ops::is_dir(&fs, "/top-level")); - assert!(ops::is_file(&fs, "/top-level/file.txt")); - assert!(ops::is_dir(&fs, "/top-level/nested")); - assert!(ops::is_file(&fs, "/top-level/nested/another-file.txt")); - } } diff --git a/lib/wasix/src/runtime/package_loader/load_package_tree.rs b/lib/wasix/src/runtime/package_loader/load_package_tree.rs index f635b2b35c6..825f374433c 100644 --- a/lib/wasix/src/runtime/package_loader/load_package_tree.rs +++ b/lib/wasix/src/runtime/package_loader/load_package_tree.rs @@ -397,8 +397,8 @@ fn filesystem_v3( let webc_vol = WebcVolumeFileSystem::new(volume.clone()); union_fs.mount( - volume_name, - mount_path.to_str().unwrap(), + volume_name.clone(), + mount_path.clone(), false, Box::new(webc_vol), None, diff --git a/tests/lib/wast/src/wasi_wast.rs b/tests/lib/wast/src/wasi_wast.rs index 0eba68a2470..1ebbc02abbe 100644 --- a/tests/lib/wast/src/wasi_wast.rs +++ b/tests/lib/wast/src/wasi_wast.rs @@ -248,12 +248,48 @@ impl<'a> WasiTest<'a> { let mut union = union_fs::UnionFileSystem::new(); - union.mount("mem_fs", "/test_fs", false, Box::new(a), None); - union.mount("mem_fs_2", "/snapshot1", false, Box::new(b), None); - union.mount("mem_fs_3", "/tests", false, Box::new(c), None); - union.mount("mem_fs_4", "/nightly_2022_10_18", false, Box::new(d), None); - union.mount("mem_fs_5", "/unstable", false, Box::new(e), None); - union.mount("mem_fs_6", "/.tmp_wasmer_wast_0", false, Box::new(f), None); + union.mount( + "mem_fs".to_string(), + PathBuf::from("/test_fs"), + false, + Box::new(a), + None, + ); + union.mount( + "mem_fs_2".to_string(), + PathBuf::from("/snapshot1"), + false, + Box::new(b), + None, + ); + union.mount( + "mem_fs_3".to_string(), + PathBuf::from("/tests"), + false, + Box::new(c), + None, + ); + union.mount( + "mem_fs_4".to_string(), + PathBuf::from("/nightly_2022_10_18"), + false, + Box::new(d), + None, + ); + union.mount( + "mem_fs_5".to_string(), + PathBuf::from("/unstable"), + false, + Box::new(e), + None, + ); + union.mount( + "mem_fs_6".to_string(), + PathBuf::from("/.tmp_wasmer_wast_0"), + false, + Box::new(f), + None, + ); Box::new(union) } From bc78f5a19f9b682fe8ea1276c1183d09f997d518 Mon Sep 17 00:00:00 2001 From: "M.Amin Rayej" Date: Sun, 18 Aug 2024 21:19:53 +0330 Subject: [PATCH 10/15] fix some tests --- lib/virtual-fs/src/union_fs.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/virtual-fs/src/union_fs.rs b/lib/virtual-fs/src/union_fs.rs index 9db12862f68..03b21f66b26 100644 --- a/lib/virtual-fs/src/union_fs.rs +++ b/lib/virtual-fs/src/union_fs.rs @@ -293,8 +293,15 @@ impl FileSystem for UnionFileSystem { fn create_dir(&self, path: &Path) -> Result<()> { if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { match fs { - UnionOrFs::Union(_) => Err(FsError::AlreadyExists), - UnionOrFs::FS(fs) => fs.create_dir(&path), + // TODO: These should return EEXIST instead of OK, but our wast test system + // is not robust enough for this. Needs a rewrite for this to work. + UnionOrFs::Union(_) => Ok(()), + UnionOrFs::FS(fs) => { + if path.as_os_str() == "/" { + return Ok(()); + } + fs.create_dir(&path) + } } } else { Err(FsError::EntryNotFound) From 06af69421cd1645e3fb7a236260db7cac96a1ee6 Mon Sep 17 00:00:00 2001 From: "M.Amin Rayej" Date: Sun, 18 Aug 2024 21:25:14 +0330 Subject: [PATCH 11/15] adjust union fs --- lib/virtual-fs/src/union_fs.rs | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/lib/virtual-fs/src/union_fs.rs b/lib/virtual-fs/src/union_fs.rs index 03b21f66b26..2957d75af77 100644 --- a/lib/virtual-fs/src/union_fs.rs +++ b/lib/virtual-fs/src/union_fs.rs @@ -627,17 +627,9 @@ mod tests { async fn test_create_dir() { let fs = gen_filesystem(); - assert_eq!( - fs.create_dir(Path::new("/")), - Err(FsError::AlreadyExists), - "root should already exist", - ); + assert_eq!(fs.create_dir(Path::new("/")), Ok(()),); - assert_eq!( - fs.create_dir(Path::new("/test_create_dir")), - Err(FsError::AlreadyExists), - "/test_create_dir already mounted and exists", - ); + assert_eq!(fs.create_dir(Path::new("/test_create_dir")), Ok(())); assert_eq!( fs.create_dir(Path::new("/test_create_dir/foo")), @@ -694,11 +686,7 @@ mod tests { "cannot remove a directory that doesn't exist", ); - assert_eq!( - fs.create_dir(Path::new("/test_remove_dir")), - Err(FsError::AlreadyExists), - "mount point already exists", - ); + assert_eq!(fs.create_dir(Path::new("/test_remove_dir")), Ok(())); assert_eq!( fs.create_dir(Path::new("/test_remove_dir/foo")), @@ -763,10 +751,7 @@ mod tests { "renaming to a directory that has no parent", ); - assert_eq!( - fs.create_dir(Path::new("/test_rename")), - Err(FsError::AlreadyExists) - ); + assert_eq!(fs.create_dir(Path::new("/test_rename")), Ok(()),); assert_eq!(fs.create_dir(Path::new("/test_rename/foo")), Ok(())); assert_eq!(fs.create_dir(Path::new("/test_rename/foo/qux")), Ok(())); From 91f4e63632e3c0a5ae190202085384505b056405 Mon Sep 17 00:00:00 2001 From: "M.Amin Rayej" Date: Sun, 18 Aug 2024 21:48:17 +0330 Subject: [PATCH 12/15] fix windows paths --- lib/virtual-fs/src/union_fs.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/virtual-fs/src/union_fs.rs b/lib/virtual-fs/src/union_fs.rs index 2957d75af77..d23f6459a73 100644 --- a/lib/virtual-fs/src/union_fs.rs +++ b/lib/virtual-fs/src/union_fs.rs @@ -545,12 +545,12 @@ mod tests { async fn test_nested_read_dir() { let fs = gen_nested_filesystem(); - let root_contents: Vec = fs + let root_contents: Vec = fs .read_dir(&PathBuf::from("/")) .unwrap() - .map(|e| e.unwrap().path.to_str().unwrap().to_string()) + .map(|e| e.unwrap().path.clone()) .collect(); - assert_eq!(root_contents, vec!["/app"]); + assert_eq!(root_contents, vec![PathBuf::from("/app")]); let app_contents: Vec = fs .read_dir(&PathBuf::from("/app")) @@ -562,19 +562,19 @@ mod tests { vec![PathBuf::from("/app/a"), PathBuf::from("/app/b")] ); - let a_contents: Vec = fs + let a_contents: Vec = fs .read_dir(&PathBuf::from("/app/a")) .unwrap() - .map(|e| e.unwrap().path.to_str().unwrap().to_string()) + .map(|e| e.unwrap().path.clone()) .collect(); - assert_eq!(a_contents, vec!["/app/a/data-a.txt"]); + assert_eq!(a_contents, vec![PathBuf::from("/app/a/data-a.txt")]); - let b_contents: Vec = fs + let b_contents: Vec = fs .read_dir(&PathBuf::from("/app/b")) .unwrap() - .map(|e| e.unwrap().path.to_str().unwrap().to_string()) + .map(|e| e.unwrap().path) .collect(); - assert_eq!(b_contents, vec!["/app/b/data-b.txt"]); + assert_eq!(b_contents, vec![PathBuf::from("/app/b/data-b.txt")]); } #[tokio::test] From 2acc9cd0703ded3561deda1be40735327187ec86 Mon Sep 17 00:00:00 2001 From: "M.Amin Rayej" Date: Tue, 20 Aug 2024 16:51:13 +0330 Subject: [PATCH 13/15] add mount support to Filesystem --- lib/virtual-fs/src/arc_fs.rs | 4 + lib/virtual-fs/src/empty_fs.rs | 4 + lib/virtual-fs/src/host_fs.rs | 4 + lib/virtual-fs/src/lib.rs | 10 + lib/virtual-fs/src/mem_fs/filesystem.rs | 7 +- lib/virtual-fs/src/overlay_fs.rs | 4 + lib/virtual-fs/src/passthru_fs.rs | 4 + lib/virtual-fs/src/static_fs.rs | 4 + lib/virtual-fs/src/tmp_fs.rs | 13 +- lib/virtual-fs/src/trace_fs.rs | 7 +- lib/virtual-fs/src/union_fs.rs | 579 +++++++----------- lib/virtual-fs/src/webc_volume_fs.rs | 9 + lib/wasix/src/fs/mod.rs | 10 + lib/wasix/src/runners/wasi_common.rs | 16 +- .../package_loader/load_package_tree.rs | 20 +- 15 files changed, 324 insertions(+), 371 deletions(-) diff --git a/lib/virtual-fs/src/arc_fs.rs b/lib/virtual-fs/src/arc_fs.rs index 605f814ddc8..ca8d3d52753 100644 --- a/lib/virtual-fs/src/arc_fs.rs +++ b/lib/virtual-fs/src/arc_fs.rs @@ -53,4 +53,8 @@ impl FileSystem for ArcFileSystem { fn new_open_options(&self) -> OpenOptions { self.fs.new_open_options() } + + fn mount(&self, name: String, path: &Path, fs: Box) -> Result<()> { + self.fs.mount(name, path, fs) + } } diff --git a/lib/virtual-fs/src/empty_fs.rs b/lib/virtual-fs/src/empty_fs.rs index 6745c337453..318fc3aee8c 100644 --- a/lib/virtual-fs/src/empty_fs.rs +++ b/lib/virtual-fs/src/empty_fs.rs @@ -67,6 +67,10 @@ impl FileSystem for EmptyFileSystem { fn new_open_options(&self) -> OpenOptions { OpenOptions::new(self) } + + fn mount(&self, name: String, path: &Path, fs: Box) -> Result<()> { + Err(FsError::Unsupported) + } } impl FileOpener for EmptyFileSystem { diff --git a/lib/virtual-fs/src/host_fs.rs b/lib/virtual-fs/src/host_fs.rs index cde3eba76cd..33958392cb1 100644 --- a/lib/virtual-fs/src/host_fs.rs +++ b/lib/virtual-fs/src/host_fs.rs @@ -235,6 +235,10 @@ impl crate::FileSystem for FileSystem { .and_then(TryInto::try_into) .map_err(Into::into) } + + fn mount(&self, _name: String, _path: &Path, _fs: Box) -> Result<()> { + Err(FsError::Unsupported) + } } impl TryInto for std::fs::Metadata { diff --git a/lib/virtual-fs/src/lib.rs b/lib/virtual-fs/src/lib.rs index e294e274513..4b5a95097ee 100644 --- a/lib/virtual-fs/src/lib.rs +++ b/lib/virtual-fs/src/lib.rs @@ -98,6 +98,8 @@ pub trait FileSystem: fmt::Debug + Send + Sync + 'static + Upcastable { fn remove_file(&self, path: &Path) -> Result<()>; fn new_open_options(&self) -> OpenOptions; + + fn mount(&self, name: String, path: &Path, fs: Box) -> Result<()>; } impl dyn FileSystem + 'static { @@ -152,6 +154,10 @@ where fn new_open_options(&self) -> OpenOptions { (**self).new_open_options() } + + fn mount(&self, name: String, path: &Path, fs: Box) -> Result<()> { + (**self).mount(name, path, fs) + } } pub trait FileOpener { @@ -510,6 +516,9 @@ pub enum FsError { /// Some other unhandled error. If you see this, it's probably a bug. #[error("unknown error found")] UnknownError, + /// Operation is not supported on this filesystem + #[error("unsupported")] + Unsupported, } impl From for FsError { @@ -570,6 +579,7 @@ impl From for io::Error { FsError::DirectoryNotEmpty => io::ErrorKind::Other, FsError::UnknownError => io::ErrorKind::Other, FsError::StorageFull => io::ErrorKind::Other, + FsError::Unsupported => io::ErrorKind::Unsupported, // NOTE: Add this once the "io_error_more" Rust feature is stabilized // FsError::StorageFull => io::ErrorKind::StorageFull, }; diff --git a/lib/virtual-fs/src/mem_fs/filesystem.rs b/lib/virtual-fs/src/mem_fs/filesystem.rs index 0358125e8d5..54954d466b2 100644 --- a/lib/virtual-fs/src/mem_fs/filesystem.rs +++ b/lib/virtual-fs/src/mem_fs/filesystem.rs @@ -3,7 +3,7 @@ use self::offloaded_file::OffloadBackingStore; use super::*; -use crate::{DirEntry, FileSystem as _, FileType, FsError, Metadata, OpenOptions, ReadDir, Result}; +use crate::{DirEntry, FileType, FsError, Metadata, OpenOptions, ReadDir, Result}; use futures::future::{BoxFuture, Either}; use slab::Slab; use std::collections::VecDeque; @@ -680,6 +680,11 @@ impl crate::FileSystem for FileSystem { fn new_open_options(&self) -> OpenOptions { OpenOptions::new(self) } + + fn mount(&self, _name: String, path: &Path, fs: Box) -> Result<()> { + let fs: Arc = Arc::new(fs); + self.mount(path.to_owned(), &fs, PathBuf::from("/")) + } } impl fmt::Debug for FileSystem { diff --git a/lib/virtual-fs/src/overlay_fs.rs b/lib/virtual-fs/src/overlay_fs.rs index a29511b80c5..17bab76c522 100644 --- a/lib/virtual-fs/src/overlay_fs.rs +++ b/lib/virtual-fs/src/overlay_fs.rs @@ -414,6 +414,10 @@ where fn new_open_options(&self) -> OpenOptions<'_> { OpenOptions::new(self) } + + fn mount(&self, _name: String, _path: &Path, _fs: Box) -> Result<(), FsError> { + Err(FsError::Unsupported) + } } impl FileOpener for OverlayFileSystem diff --git a/lib/virtual-fs/src/passthru_fs.rs b/lib/virtual-fs/src/passthru_fs.rs index 0d138a90075..3e1580bd596 100644 --- a/lib/virtual-fs/src/passthru_fs.rs +++ b/lib/virtual-fs/src/passthru_fs.rs @@ -53,6 +53,10 @@ impl FileSystem for PassthruFileSystem { fn new_open_options(&self) -> OpenOptions { self.fs.new_open_options() } + + fn mount(&self, _name: String, _path: &Path, _fs: Box) -> Result<()> { + Err(FsError::Unsupported) + } } #[cfg(test)] diff --git a/lib/virtual-fs/src/static_fs.rs b/lib/virtual-fs/src/static_fs.rs index 9a45702a4a6..aff7ac3cd0e 100644 --- a/lib/virtual-fs/src/static_fs.rs +++ b/lib/virtual-fs/src/static_fs.rs @@ -382,6 +382,10 @@ impl FileSystem for StaticFileSystem { self.memory.symlink_metadata(Path::new(&path)) } } + + fn mount(&self, _name: String, _path: &Path, _fs: Box) -> Result<(), FsError> { + Err(FsError::Unsupported) + } } fn normalizes_path(path: &Path) -> String { diff --git a/lib/virtual-fs/src/tmp_fs.rs b/lib/virtual-fs/src/tmp_fs.rs index 0f0667f402c..5541ee0d838 100644 --- a/lib/virtual-fs/src/tmp_fs.rs +++ b/lib/virtual-fs/src/tmp_fs.rs @@ -8,8 +8,8 @@ use std::{ }; use crate::{ - limiter::DynFsMemoryLimiter, mem_fs, BoxFuture, FileSystem, Metadata, OpenOptions, ReadDir, - Result, + limiter::DynFsMemoryLimiter, mem_fs, BoxFuture, FileSystem, Metadata, OpenOptions, + ReadDir, Result, }; #[derive(Debug, Default, Clone)] @@ -96,4 +96,13 @@ impl FileSystem for TmpFileSystem { fn new_open_options(&self) -> OpenOptions { self.fs.new_open_options() } + + fn mount( + &self, + name: String, + path: &Path, + fs: Box, + ) -> Result<()> { + FileSystem::mount(&self.fs, name, path, fs) + } } diff --git a/lib/virtual-fs/src/trace_fs.rs b/lib/virtual-fs/src/trace_fs.rs index 44aa5bbbbc5..0a54549b912 100644 --- a/lib/virtual-fs/src/trace_fs.rs +++ b/lib/virtual-fs/src/trace_fs.rs @@ -1,5 +1,5 @@ use std::{ - path::PathBuf, + path::{Path, PathBuf}, pin::Pin, task::{Context, Poll}, }; @@ -87,6 +87,11 @@ where fn new_open_options(&self) -> crate::OpenOptions { crate::OpenOptions::new(self) } + + #[tracing::instrument(level = "trace", skip(self))] + fn mount(&self, name: String, path: &Path, fs: Box) -> crate::Result<()> { + self.0.mount(name, path, fs) + } } impl FileOpener for TraceFileSystem diff --git a/lib/virtual-fs/src/union_fs.rs b/lib/virtual-fs/src/union_fs.rs index d23f6459a73..641eba4ae24 100644 --- a/lib/virtual-fs/src/union_fs.rs +++ b/lib/virtual-fs/src/union_fs.rs @@ -4,84 +4,22 @@ use crate::*; -use std::path::Path; - -#[derive(Debug)] -pub enum UnionOrFs { - Union(Box), - FS(Box), -} - -impl UnionOrFs { - pub fn fs(&self) -> &dyn FileSystem { - match self { - UnionOrFs::Union(union) => union, - UnionOrFs::FS(fs) => fs, - } - } - - pub fn is_union(&self) -> bool { - match self { - UnionOrFs::Union(_) => true, - UnionOrFs::FS(_) => false, - } - } -} - -impl FileSystem for UnionOrFs { - fn readlink(&self, path: &Path) -> Result { - self.fs().readlink(path) - } - - fn read_dir(&self, path: &Path) -> Result { - self.fs().read_dir(path) - } - - fn create_dir(&self, path: &Path) -> Result<()> { - self.fs().create_dir(path) - } - - fn remove_dir(&self, path: &Path) -> Result<()> { - self.fs().remove_dir(path) - } - - fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<()>> { - self.fs().rename(from, to) - } - - fn metadata(&self, path: &Path) -> Result { - self.fs().metadata(path) - } - - fn symlink_metadata(&self, path: &Path) -> Result { - self.fs().symlink_metadata(path) - } - - fn remove_file(&self, path: &Path) -> Result<()> { - self.fs().remove_file(path) - } - - fn new_open_options(&self) -> OpenOptions { - self.fs().new_open_options() - } -} +use std::{ + collections::HashMap, + path::Path, + sync::{Arc, RwLock}, +}; #[derive(Debug)] pub struct MountPoint { pub path: PathBuf, pub name: String, - pub fs: UnionOrFs, - pub should_sanitize: bool, - pub new_path: Option, + pub fs: Arc>, } impl MountPoint { - pub fn fs(&self) -> &UnionOrFs { - &self.fs - } - - pub fn fs_mut(&mut self) -> &mut UnionOrFs { - &mut self.fs + pub fn fs(&self) -> &Box { + self.fs.as_ref() } pub fn mount_point_ref(&self) -> MountPointRef<'_> { @@ -89,8 +27,6 @@ impl MountPoint { path: self.path.clone(), name: self.name.clone(), fs: &self.fs, - should_sanitize: self.should_sanitize, - new_path: self.new_path.clone(), } } } @@ -99,7 +35,7 @@ impl MountPoint { /// to be mounted at various mount points #[derive(Debug, Default)] pub struct UnionFileSystem { - pub mounts: Vec, + pub mounts: RwLock>, } impl UnionFileSystem { @@ -108,12 +44,15 @@ impl UnionFileSystem { } pub fn clear(&mut self) { - self.mounts.clear(); + self.mounts.write().unwrap().clear(); } } impl UnionFileSystem { - fn find_mount(&self, path: PathBuf) -> Option<(PathBuf, PathBuf, &UnionOrFs)> { + fn find_mount( + &self, + path: PathBuf, + ) -> Option<(PathBuf, PathBuf, Arc>)> { let mut components = path.components().collect::>(); if let Some(c) = components.first().copied() { @@ -123,256 +62,135 @@ impl UnionFileSystem { if let Some(mount) = self .mounts - .iter() - .find(|m| m.path.as_os_str() == c.as_os_str()) + .read() + .unwrap() + .get(&PathBuf::from(c.as_os_str())) { - if sub_path.components().next().is_none() { - let sub_path = if mount.fs.is_union() { - sub_path - } else { - PathBuf::from("/") - }; - - return Some((PathBuf::from(c.as_os_str()), sub_path, &mount.fs)); - } - match &mount.fs { - UnionOrFs::Union(union) => { - return union.find_mount(sub_path).map(|(prefix, path, fs)| { - let prefix = PathBuf::from(c.as_os_str()).join(prefix); - - (prefix, path, fs) - }); - } - UnionOrFs::FS(_) => { - return Some(( - PathBuf::from(c.as_os_str()), - PathBuf::from("/").join(sub_path), - &mount.fs, - )); - } - } + return Some(( + PathBuf::from(c.as_os_str()), + PathBuf::from("/").join(sub_path), + mount.fs.clone(), + )); } } None } - - pub fn mount( - &mut self, - name: String, - path: PathBuf, - should_sanitize: bool, - fs: Box, - new_path: Option, - ) { - let mut components = path.components().collect::>(); - if let Some(c) = components.first().copied() { - components.remove(0); - - let sub_path = components.into_iter().collect::(); - - if let Some(mount) = self - .mounts - .iter_mut() - .find(|m| m.path.as_os_str() == c.as_os_str()) - { - match mount.fs_mut() { - UnionOrFs::Union(union) => { - union.mount( - name, - sub_path, - should_sanitize, - fs, - new_path.clone(), // TODO: what to do with new_path - ) - } - UnionOrFs::FS(_) => { - println!("path: {path:?} is already mounted"); - } - } - } else { - let fs = if sub_path.components().next().is_none() { - UnionOrFs::FS(fs) - } else { - let mut union = UnionFileSystem::new(); - union.mount( - name.clone(), - sub_path, - should_sanitize, - fs, - new_path.clone(), - ); - - UnionOrFs::Union(Box::new(union)) - }; - - let mount = MountPoint { - path: PathBuf::from(c.as_os_str()), - name, - fs, - should_sanitize, - new_path, - }; - - self.mounts.push(mount); - } - } else { - println!("empty path"); - } - } - - pub fn unmount(&mut self, path: &str) { - let path1 = path.to_string(); - let mut path2 = path1; - if !path2.starts_with('/') { - path2.insert(0, '/'); - } - let mut path3 = path2.clone(); - if !path3.ends_with('/') { - path3.push('/') - } - if path2.ends_with('/') { - path2 = (path2[..(path2.len() - 1)]).to_string(); - } - - self.mounts.retain(|mount| { - mount.path.to_str().unwrap() != path2 && mount.path.to_str().unwrap() != path3 - }); - } } impl FileSystem for UnionFileSystem { fn readlink(&self, path: &Path) -> Result { - if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { - match fs { - UnionOrFs::Union(_) => Err(FsError::NotAFile), - UnionOrFs::FS(fs) => fs.readlink(&path), - } + if path == PathBuf::from("/") { + return Err(FsError::NotAFile); + } else if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { + fs.readlink(&path) } else { Err(FsError::EntryNotFound) } } fn read_dir(&self, path: &Path) -> Result { - if let Some((prefix, path, fs)) = self.find_mount(path.to_owned()) { - match fs { - UnionOrFs::Union(union) => { - let entries = union - .mounts - .iter() - .map(|m| DirEntry { - path: prefix.join(m.path.clone()), - metadata: Ok(Metadata { - ft: FileType::new_dir(), - accessed: 0, - created: 0, - modified: 0, - len: 0, - }), - }) - .collect::>(); - - Ok(ReadDir::new(entries)) - } - UnionOrFs::FS(fs) => { - let mut entries = fs.read_dir(&path)?; - - for entry in &mut entries.data { - let path: PathBuf = entry.path.components().skip(1).collect(); - entry.path = PathBuf::from(&prefix).join(path); - } - - Ok(entries) - } + if path == PathBuf::from("/") { + let entries = self + .mounts + .read() + .unwrap() + .keys() + .map(|p| DirEntry { + path: PathBuf::from("/").join(p), + metadata: Ok(Metadata { + ft: FileType::new_dir(), + accessed: 0, + created: 0, + modified: 0, + len: 0, + }), + }) + .collect::>(); + + Ok(ReadDir::new(entries)) + } else if let Some((prefix, path, fs)) = self.find_mount(path.to_owned()) { + let mut entries = fs.read_dir(&path)?; + + for entry in &mut entries.data { + let path: PathBuf = entry.path.components().skip(1).collect(); + entry.path = PathBuf::from(&prefix).join(path); } + + Ok(entries) } else { Err(FsError::EntryNotFound) } } fn create_dir(&self, path: &Path) -> Result<()> { - if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { - match fs { - // TODO: These should return EEXIST instead of OK, but our wast test system - // is not robust enough for this. Needs a rewrite for this to work. - UnionOrFs::Union(_) => Ok(()), - UnionOrFs::FS(fs) => { - if path.as_os_str() == "/" { - return Ok(()); - } - fs.create_dir(&path) - } - } + if path == PathBuf::from("/") { + Ok(()) + } else if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { + fs.create_dir(&path) } else { Err(FsError::EntryNotFound) } } fn remove_dir(&self, path: &Path) -> Result<()> { - if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { - match fs { - UnionOrFs::Union(_) => Err(FsError::PermissionDenied), - UnionOrFs::FS(fs) => fs.remove_dir(&path), - } + if path == PathBuf::from("/") { + Err(FsError::PermissionDenied) + } else if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { + fs.remove_dir(&path) } else { Err(FsError::EntryNotFound) } } fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<()>> { Box::pin(async move { - if let Some((prefix, path, fs)) = self.find_mount(from.to_owned()) { - match fs { - UnionOrFs::Union(_) => Err(FsError::PermissionDenied), - UnionOrFs::FS(fs) => { - let to = to.strip_prefix(prefix).map_err(|_| FsError::InvalidInput)?; + if from == PathBuf::from("/") { + Err(FsError::PermissionDenied) + } else if let Some((prefix, path, fs)) = self.find_mount(from.to_owned()) { + let to = to.strip_prefix(prefix).map_err(|_| FsError::InvalidInput)?; - let to = PathBuf::from("/").join(to); + let to = PathBuf::from("/").join(to); - fs.rename(&path, &to).await - } - } + fs.rename(&path, &to).await } else { Err(FsError::EntryNotFound) } }) } fn metadata(&self, path: &Path) -> Result { - if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { - match fs { - UnionOrFs::Union(_) => Ok(Metadata { - ft: FileType::new_dir(), - accessed: 0, - created: 0, - modified: 0, - len: 0, - }), - UnionOrFs::FS(fs) => fs.metadata(&path), - } + if path == PathBuf::from("/") { + Ok(Metadata { + ft: FileType::new_dir(), + accessed: 0, + created: 0, + modified: 0, + len: 0, + }) + } else if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { + fs.metadata(&path) } else { Err(FsError::EntryNotFound) } } fn symlink_metadata(&self, path: &Path) -> Result { - if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { - match fs { - UnionOrFs::Union(_) => Ok(Metadata { - ft: FileType::new_dir(), - accessed: 0, - created: 0, - modified: 0, - len: 0, - }), - UnionOrFs::FS(fs) => fs.symlink_metadata(&path), - } + if path == PathBuf::from("/") { + Ok(Metadata { + ft: FileType::new_dir(), + accessed: 0, + created: 0, + modified: 0, + len: 0, + }) + } else if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { + fs.symlink_metadata(&path) } else { Err(FsError::EntryNotFound) } } fn remove_file(&self, path: &Path) -> Result<()> { - if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { - match fs { - UnionOrFs::Union(_) => Err(FsError::NotAFile), - UnionOrFs::FS(fs) => fs.remove_file(&path), - } + if path == PathBuf::from("/") { + Err(FsError::NotAFile) + } else if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { + fs.remove_file(&path) } else { Err(FsError::EntryNotFound) } @@ -380,6 +198,55 @@ impl FileSystem for UnionFileSystem { fn new_open_options(&self) -> OpenOptions { OpenOptions::new(self) } + + fn mount( + &self, + name: String, + path: &Path, + fs: Box, + ) -> Result<()> { + let mut components = path.components().collect::>(); + if let Some(c) = components.first().copied() { + components.remove(0); + + let sub_path = components.into_iter().collect::(); + + if let Some(mount) = self + .mounts + .read() + .unwrap() + .get(&PathBuf::from(c.as_os_str())) + { + return mount.fs.mount(name, sub_path.as_path(), fs); + } else { + let fs = if sub_path.components().next().is_none() { + fs + } else { + let union = UnionFileSystem::new(); + union.mount(name.clone(), sub_path.as_path(), fs)?; + + Box::new(union) + }; + + let fs = Arc::new(fs); + + let mount = MountPoint { + path: PathBuf::from(c.as_os_str()), + name, + fs, + }; + + self.mounts + .write() + .unwrap() + .insert(PathBuf::from(c.as_os_str()), mount); + } + } else { + println!("empty path"); + } + + Ok(()) + } } #[derive(Debug)] @@ -387,8 +254,6 @@ pub struct MountPointRef<'a> { pub path: PathBuf, pub name: String, pub fs: &'a dyn FileSystem, - pub should_sanitize: bool, - pub new_path: Option, } impl FileOpener for UnionFileSystem { @@ -397,18 +262,18 @@ impl FileOpener for UnionFileSystem { path: &Path, conf: &OpenOptionsConfig, ) -> Result> { - let parent = path.parent().unwrap(); - let file_name = path.file_name().unwrap(); - if let Some((_, path, fs)) = self.find_mount(parent.to_owned()) { - match fs { - UnionOrFs::Union(_) => Err(FsError::PermissionDenied), - UnionOrFs::FS(fs) => fs - .new_open_options() + if path == PathBuf::from("/") { + return Err(FsError::NotAFile); + } else { + let parent = path.parent().unwrap(); + let file_name = path.file_name().unwrap(); + if let Some((_, path, fs)) = self.find_mount(parent.to_owned()) { + fs.new_open_options() .options(conf.clone()) - .open(path.join(file_name)), + .open(path.join(file_name)) + } else { + Err(FsError::EntryNotFound) } - } else { - Err(FsError::EntryNotFound) } } } @@ -424,7 +289,7 @@ mod tests { use super::{FileOpener, OpenOptionsConfig}; fn gen_filesystem() -> UnionFileSystem { - let mut union = UnionFileSystem::new(); + let union = UnionFileSystem::new(); let a = mem_fs::FileSystem::default(); let b = mem_fs::FileSystem::default(); let c = mem_fs::FileSystem::default(); @@ -434,68 +299,68 @@ mod tests { let g = mem_fs::FileSystem::default(); let h = mem_fs::FileSystem::default(); - union.mount( - "mem_fs_1".to_string(), - PathBuf::from("/test_new_filesystem"), - false, - Box::new(a), - None, - ); - union.mount( - "mem_fs_2".to_string(), - PathBuf::from("/test_create_dir"), - false, - Box::new(b), - None, - ); - union.mount( - "mem_fs_3".to_string(), - PathBuf::from("/test_remove_dir"), - false, - Box::new(c), - None, - ); - union.mount( - "mem_fs_4".to_string(), - PathBuf::from("/test_rename"), - false, - Box::new(d), - None, - ); - union.mount( - "mem_fs_5".to_string(), - PathBuf::from("/test_metadata"), - false, - Box::new(e), - None, - ); - union.mount( - "mem_fs_6".to_string(), - PathBuf::from("/test_remove_file"), - false, - Box::new(f), - None, - ); - union.mount( - "mem_fs_6".to_string(), - PathBuf::from("/test_readdir"), - false, - Box::new(g), - None, - ); - union.mount( - "mem_fs_6".to_string(), - PathBuf::from("/test_canonicalize"), - false, - Box::new(h), - None, - ); + union + .mount( + "mem_fs_1".to_string(), + PathBuf::from("/test_new_filesystem").as_path(), + Box::new(a), + ) + .unwrap(); + union + .mount( + "mem_fs_2".to_string(), + PathBuf::from("/test_create_dir").as_path(), + Box::new(b), + ) + .unwrap(); + union + .mount( + "mem_fs_3".to_string(), + PathBuf::from("/test_remove_dir").as_path(), + Box::new(c), + ) + .unwrap(); + union + .mount( + "mem_fs_4".to_string(), + PathBuf::from("/test_rename").as_path(), + Box::new(d), + ) + .unwrap(); + union + .mount( + "mem_fs_5".to_string(), + PathBuf::from("/test_metadata").as_path(), + Box::new(e), + ) + .unwrap(); + union + .mount( + "mem_fs_6".to_string(), + PathBuf::from("/test_remove_file").as_path(), + Box::new(f), + ) + .unwrap(); + union + .mount( + "mem_fs_6".to_string(), + PathBuf::from("/test_readdir").as_path(), + Box::new(g), + ) + .unwrap(); + union + .mount( + "mem_fs_6".to_string(), + PathBuf::from("/test_canonicalize").as_path(), + Box::new(h), + ) + .unwrap(); union } fn gen_nested_filesystem() -> UnionFileSystem { - let mut union = UnionFileSystem::new(); + let union = UnionFileSystem::new(); let a = mem_fs::FileSystem::default(); a.open( &PathBuf::from("/data-a.txt"), @@ -523,20 +388,20 @@ mod tests { ) .unwrap(); - union.mount( - "mem_fs_1".to_string(), - PathBuf::from("/app/a"), - false, - Box::new(a), - None, - ); - union.mount( - "mem_fs_2".to_string(), - PathBuf::from("/app/b"), - false, - Box::new(b), - None, - ); + union + .mount( + "mem_fs_1".to_string(), + PathBuf::from("/app/a").as_path(), + Box::new(a), + ) + .unwrap(); + union + .mount( + "mem_fs_2".to_string(), + PathBuf::from("/app/b").as_path(), + Box::new(b), + ) + .unwrap(); union } diff --git a/lib/virtual-fs/src/webc_volume_fs.rs b/lib/virtual-fs/src/webc_volume_fs.rs index 7ce0638590e..745a0570e39 100644 --- a/lib/virtual-fs/src/webc_volume_fs.rs +++ b/lib/virtual-fs/src/webc_volume_fs.rs @@ -155,6 +155,15 @@ impl FileSystem for WebcVolumeFileSystem { fn new_open_options(&self) -> crate::OpenOptions { crate::OpenOptions::new(self) } + + fn mount( + &self, + _name: String, + _path: &Path, + _fs: Box, + ) -> Result<(), FsError> { + Err(FsError::Unsupported) + } } impl FileOpener for WebcVolumeFileSystem { diff --git a/lib/wasix/src/fs/mod.rs b/lib/wasix/src/fs/mod.rs index a1fe86e05d0..35b20d10ff5 100644 --- a/lib/wasix/src/fs/mod.rs +++ b/lib/wasix/src/fs/mod.rs @@ -376,6 +376,12 @@ impl FileSystem for WasiFsRoot { WasiFsRoot::Backing(fs) => fs.new_open_options(), } } + fn mount(&self, name: String, path: &Path, fs: Box) -> virtual_fs::Result<()> { + match self { + WasiFsRoot::Sandbox(f) => f.mount(name, path, fs), + WasiFsRoot::Backing(f) => f.mount(name, path, fs), + } + } } /// Merge the contents of one filesystem into another. @@ -2154,6 +2160,9 @@ impl FileSystem for FallbackFileSystem { fn new_open_options(&self) -> virtual_fs::OpenOptions { Self::fail(); } + fn mount(&self, _name: String, _path: &Path, _fs: Box) -> virtual_fs::Result<()> { + Self::fail() + } } pub fn virtual_file_type_to_wasi_file_type(file_type: virtual_fs::FileType) -> Filetype { @@ -2222,5 +2231,6 @@ pub fn fs_error_into_wasi_err(fs_error: FsError) -> Errno { FsError::DirectoryNotEmpty => Errno::Notempty, FsError::StorageFull => Errno::Overflow, FsError::Lock | FsError::UnknownError => Errno::Io, + FsError::Unsupported => Errno::Notsup, } } diff --git a/lib/wasix/src/runners/wasi_common.rs b/lib/wasix/src/runners/wasi_common.rs index d1302b11718..61cd3aeadc1 100644 --- a/lib/wasix/src/runners/wasi_common.rs +++ b/lib/wasix/src/runners/wasi_common.rs @@ -166,8 +166,7 @@ fn build_directory_mappings( })?; } - root_fs - .mount(guest_path.clone(), fs, "/".into()) + TmpFileSystem::mount(&root_fs, guest_path.clone(), fs, "/".into()) .with_context(|| format!("Unable to mount \"{}\"", guest_path.display()))?; } } @@ -348,6 +347,19 @@ impl virtual_fs::FileSystem for RelativeOrAbsolutePathHack { fn new_open_options(&self) -> virtual_fs::OpenOptions { virtual_fs::OpenOptions::new(self) } + + fn mount( + &self, + name: String, + path: &Path, + fs: Box, + ) -> virtual_fs::Result<()> { + let name_ref = &name; + let f_ref = &Arc::new(fs); + self.execute(path, move |f, p| { + f.mount(name_ref.clone(), p, Box::new(f_ref.clone())) + }) + } } impl virtual_fs::FileOpener for RelativeOrAbsolutePathHack { diff --git a/lib/wasix/src/runtime/package_loader/load_package_tree.rs b/lib/wasix/src/runtime/package_loader/load_package_tree.rs index 825f374433c..4ccf27bae09 100644 --- a/lib/wasix/src/runtime/package_loader/load_package_tree.rs +++ b/lib/wasix/src/runtime/package_loader/load_package_tree.rs @@ -363,7 +363,7 @@ fn filesystem_v3( let mut mountings: Vec<_> = pkg.filesystem.iter().collect(); mountings.sort_by_key(|m| std::cmp::Reverse(m.mount_path.as_path())); - let mut union_fs = UnionFileSystem::new(); + let union_fs = UnionFileSystem::new(); for ResolvedFileSystemMapping { mount_path, @@ -396,13 +396,7 @@ fn filesystem_v3( })?; let webc_vol = WebcVolumeFileSystem::new(volume.clone()); - union_fs.mount( - volume_name.clone(), - mount_path.clone(), - false, - Box::new(webc_vol), - None, - ); + union_fs.mount(volume_name.clone(), mount_path, Box::new(webc_vol))?; } let fs = OverlayFileSystem::new(virtual_fs::EmptyFileSystem::default(), [union_fs]); @@ -590,6 +584,16 @@ where fn new_open_options(&self) -> virtual_fs::OpenOptions { virtual_fs::OpenOptions::new(self) } + + fn mount( + &self, + name: String, + path: &Path, + fs: Box, + ) -> virtual_fs::Result<()> { + let path = self.path(path)?; + self.inner.mount(name, path.as_path(), fs) + } } impl virtual_fs::FileOpener for MappedPathFileSystem From 650f4387ad0647b8f38cf5b5881ef3c1a39eaa37 Mon Sep 17 00:00:00 2001 From: "M.Amin Rayej" Date: Tue, 20 Aug 2024 18:19:32 +0330 Subject: [PATCH 14/15] fix tests --- lib/virtual-fs/src/arc_fs.rs | 7 +- lib/virtual-fs/src/empty_fs.rs | 7 +- lib/virtual-fs/src/host_fs.rs | 7 +- lib/virtual-fs/src/lib.rs | 12 ++- lib/virtual-fs/src/mem_fs/filesystem.rs | 7 +- lib/virtual-fs/src/overlay_fs.rs | 7 +- lib/virtual-fs/src/passthru_fs.rs | 7 +- lib/virtual-fs/src/static_fs.rs | 7 +- lib/virtual-fs/src/tmp_fs.rs | 4 +- lib/virtual-fs/src/trace_fs.rs | 7 +- lib/virtual-fs/src/union_fs.rs | 127 ++++++++++++++++-------- lib/wasix/src/fs/mod.rs | 14 ++- lib/wasix/src/runners/wasi_common.rs | 2 +- tests/lib/wast/src/wasi_wast.rs | 38 +++---- 14 files changed, 172 insertions(+), 81 deletions(-) diff --git a/lib/virtual-fs/src/arc_fs.rs b/lib/virtual-fs/src/arc_fs.rs index ca8d3d52753..4646226cfe2 100644 --- a/lib/virtual-fs/src/arc_fs.rs +++ b/lib/virtual-fs/src/arc_fs.rs @@ -54,7 +54,12 @@ impl FileSystem for ArcFileSystem { self.fs.new_open_options() } - fn mount(&self, name: String, path: &Path, fs: Box) -> Result<()> { + fn mount( + &self, + name: String, + path: &Path, + fs: Box, + ) -> Result<()> { self.fs.mount(name, path, fs) } } diff --git a/lib/virtual-fs/src/empty_fs.rs b/lib/virtual-fs/src/empty_fs.rs index 318fc3aee8c..5c6fd956803 100644 --- a/lib/virtual-fs/src/empty_fs.rs +++ b/lib/virtual-fs/src/empty_fs.rs @@ -68,7 +68,12 @@ impl FileSystem for EmptyFileSystem { OpenOptions::new(self) } - fn mount(&self, name: String, path: &Path, fs: Box) -> Result<()> { + fn mount( + &self, + name: String, + path: &Path, + fs: Box, + ) -> Result<()> { Err(FsError::Unsupported) } } diff --git a/lib/virtual-fs/src/host_fs.rs b/lib/virtual-fs/src/host_fs.rs index 33958392cb1..199ee9fb502 100644 --- a/lib/virtual-fs/src/host_fs.rs +++ b/lib/virtual-fs/src/host_fs.rs @@ -236,7 +236,12 @@ impl crate::FileSystem for FileSystem { .map_err(Into::into) } - fn mount(&self, _name: String, _path: &Path, _fs: Box) -> Result<()> { + fn mount( + &self, + _name: String, + _path: &Path, + _fs: Box, + ) -> Result<()> { Err(FsError::Unsupported) } } diff --git a/lib/virtual-fs/src/lib.rs b/lib/virtual-fs/src/lib.rs index 4b5a95097ee..17043287f55 100644 --- a/lib/virtual-fs/src/lib.rs +++ b/lib/virtual-fs/src/lib.rs @@ -99,7 +99,8 @@ pub trait FileSystem: fmt::Debug + Send + Sync + 'static + Upcastable { fn new_open_options(&self) -> OpenOptions; - fn mount(&self, name: String, path: &Path, fs: Box) -> Result<()>; + fn mount(&self, name: String, path: &Path, fs: Box) + -> Result<()>; } impl dyn FileSystem + 'static { @@ -154,8 +155,13 @@ where fn new_open_options(&self) -> OpenOptions { (**self).new_open_options() } - - fn mount(&self, name: String, path: &Path, fs: Box) -> Result<()> { + + fn mount( + &self, + name: String, + path: &Path, + fs: Box, + ) -> Result<()> { (**self).mount(name, path, fs) } } diff --git a/lib/virtual-fs/src/mem_fs/filesystem.rs b/lib/virtual-fs/src/mem_fs/filesystem.rs index 54954d466b2..65b34ce542f 100644 --- a/lib/virtual-fs/src/mem_fs/filesystem.rs +++ b/lib/virtual-fs/src/mem_fs/filesystem.rs @@ -681,7 +681,12 @@ impl crate::FileSystem for FileSystem { OpenOptions::new(self) } - fn mount(&self, _name: String, path: &Path, fs: Box) -> Result<()> { + fn mount( + &self, + _name: String, + path: &Path, + fs: Box, + ) -> Result<()> { let fs: Arc = Arc::new(fs); self.mount(path.to_owned(), &fs, PathBuf::from("/")) } diff --git a/lib/virtual-fs/src/overlay_fs.rs b/lib/virtual-fs/src/overlay_fs.rs index 17bab76c522..90c1803ece6 100644 --- a/lib/virtual-fs/src/overlay_fs.rs +++ b/lib/virtual-fs/src/overlay_fs.rs @@ -415,7 +415,12 @@ where OpenOptions::new(self) } - fn mount(&self, _name: String, _path: &Path, _fs: Box) -> Result<(), FsError> { + fn mount( + &self, + _name: String, + _path: &Path, + _fs: Box, + ) -> Result<(), FsError> { Err(FsError::Unsupported) } } diff --git a/lib/virtual-fs/src/passthru_fs.rs b/lib/virtual-fs/src/passthru_fs.rs index 3e1580bd596..0164d6c9e86 100644 --- a/lib/virtual-fs/src/passthru_fs.rs +++ b/lib/virtual-fs/src/passthru_fs.rs @@ -54,7 +54,12 @@ impl FileSystem for PassthruFileSystem { self.fs.new_open_options() } - fn mount(&self, _name: String, _path: &Path, _fs: Box) -> Result<()> { + fn mount( + &self, + _name: String, + _path: &Path, + _fs: Box, + ) -> Result<()> { Err(FsError::Unsupported) } } diff --git a/lib/virtual-fs/src/static_fs.rs b/lib/virtual-fs/src/static_fs.rs index aff7ac3cd0e..10e303edc0c 100644 --- a/lib/virtual-fs/src/static_fs.rs +++ b/lib/virtual-fs/src/static_fs.rs @@ -383,7 +383,12 @@ impl FileSystem for StaticFileSystem { } } - fn mount(&self, _name: String, _path: &Path, _fs: Box) -> Result<(), FsError> { + fn mount( + &self, + _name: String, + _path: &Path, + _fs: Box, + ) -> Result<(), FsError> { Err(FsError::Unsupported) } } diff --git a/lib/virtual-fs/src/tmp_fs.rs b/lib/virtual-fs/src/tmp_fs.rs index 5541ee0d838..f16036bf29a 100644 --- a/lib/virtual-fs/src/tmp_fs.rs +++ b/lib/virtual-fs/src/tmp_fs.rs @@ -8,8 +8,8 @@ use std::{ }; use crate::{ - limiter::DynFsMemoryLimiter, mem_fs, BoxFuture, FileSystem, Metadata, OpenOptions, - ReadDir, Result, + limiter::DynFsMemoryLimiter, mem_fs, BoxFuture, FileSystem, Metadata, OpenOptions, ReadDir, + Result, }; #[derive(Debug, Default, Clone)] diff --git a/lib/virtual-fs/src/trace_fs.rs b/lib/virtual-fs/src/trace_fs.rs index 0a54549b912..ca132bd233c 100644 --- a/lib/virtual-fs/src/trace_fs.rs +++ b/lib/virtual-fs/src/trace_fs.rs @@ -89,7 +89,12 @@ where } #[tracing::instrument(level = "trace", skip(self))] - fn mount(&self, name: String, path: &Path, fs: Box) -> crate::Result<()> { + fn mount( + &self, + name: String, + path: &Path, + fs: Box, + ) -> crate::Result<()> { self.0.mount(name, path, fs) } } diff --git a/lib/virtual-fs/src/union_fs.rs b/lib/virtual-fs/src/union_fs.rs index 641eba4ae24..7426eccd31d 100644 --- a/lib/virtual-fs/src/union_fs.rs +++ b/lib/virtual-fs/src/union_fs.rs @@ -18,7 +18,7 @@ pub struct MountPoint { } impl MountPoint { - pub fn fs(&self) -> &Box { + pub fn fs(&self) -> &(dyn FileSystem + Send + Sync) { self.fs.as_ref() } @@ -46,9 +46,26 @@ impl UnionFileSystem { pub fn clear(&mut self) { self.mounts.write().unwrap().clear(); } + + fn is_root(&self) -> bool { + let map = self.mounts.read().unwrap(); + + map.len() == 1 && map.contains_key(&PathBuf::from("/")) + } + + fn prepare_path(&self, path: &Path) -> PathBuf { + if self.is_root() { + path.to_owned() + } else { + path.strip_prefix(PathBuf::from("/")) + .unwrap_or(path) + .to_owned() + } + } } impl UnionFileSystem { + #[allow(clippy::type_complexity)] fn find_mount( &self, path: PathBuf, @@ -80,8 +97,10 @@ impl UnionFileSystem { impl FileSystem for UnionFileSystem { fn readlink(&self, path: &Path) -> Result { - if path == PathBuf::from("/") { - return Err(FsError::NotAFile); + let path = self.prepare_path(path); + + if path.as_os_str().is_empty() { + Err(FsError::NotAFile) } else if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { fs.readlink(&path) } else { @@ -90,7 +109,9 @@ impl FileSystem for UnionFileSystem { } fn read_dir(&self, path: &Path) -> Result { - if path == PathBuf::from("/") { + let path = self.prepare_path(path); + + if path.as_os_str().is_empty() { let entries = self .mounts .read() @@ -114,7 +135,7 @@ impl FileSystem for UnionFileSystem { for entry in &mut entries.data { let path: PathBuf = entry.path.components().skip(1).collect(); - entry.path = PathBuf::from(&prefix).join(path); + entry.path = PathBuf::from("/").join(PathBuf::from(&prefix).join(path)); } Ok(entries) @@ -124,16 +145,28 @@ impl FileSystem for UnionFileSystem { } fn create_dir(&self, path: &Path) -> Result<()> { - if path == PathBuf::from("/") { + let path = self.prepare_path(path); + + if path.as_os_str().is_empty() { Ok(()) } else if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { - fs.create_dir(&path) + let result = fs.create_dir(&path); + + if let Err(e) = result { + if e == FsError::AlreadyExists { + return Ok(()); + } + } + + result } else { Err(FsError::EntryNotFound) } } fn remove_dir(&self, path: &Path) -> Result<()> { - if path == PathBuf::from("/") { + let path = self.prepare_path(path); + + if path.as_os_str().is_empty() { Err(FsError::PermissionDenied) } else if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { fs.remove_dir(&path) @@ -143,7 +176,10 @@ impl FileSystem for UnionFileSystem { } fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<()>> { Box::pin(async move { - if from == PathBuf::from("/") { + let from = self.prepare_path(from); + let to = self.prepare_path(to); + + if from.as_os_str().is_empty() { Err(FsError::PermissionDenied) } else if let Some((prefix, path, fs)) = self.find_mount(from.to_owned()) { let to = to.strip_prefix(prefix).map_err(|_| FsError::InvalidInput)?; @@ -157,7 +193,9 @@ impl FileSystem for UnionFileSystem { }) } fn metadata(&self, path: &Path) -> Result { - if path == PathBuf::from("/") { + let path = self.prepare_path(path); + + if path.as_os_str().is_empty() { Ok(Metadata { ft: FileType::new_dir(), accessed: 0, @@ -172,7 +210,9 @@ impl FileSystem for UnionFileSystem { } } fn symlink_metadata(&self, path: &Path) -> Result { - if path == PathBuf::from("/") { + let path = self.prepare_path(path); + + if path.as_os_str().is_empty() { Ok(Metadata { ft: FileType::new_dir(), accessed: 0, @@ -187,7 +227,9 @@ impl FileSystem for UnionFileSystem { } } fn remove_file(&self, path: &Path) -> Result<()> { - if path == PathBuf::from("/") { + let path = self.prepare_path(path); + + if path.as_os_str().is_empty() { Err(FsError::NotAFile) } else if let Some((_, path, fs)) = self.find_mount(path.to_owned()) { fs.remove_file(&path) @@ -218,31 +260,31 @@ impl FileSystem for UnionFileSystem { .get(&PathBuf::from(c.as_os_str())) { return mount.fs.mount(name, sub_path.as_path(), fs); - } else { - let fs = if sub_path.components().next().is_none() { - fs - } else { - let union = UnionFileSystem::new(); - union.mount(name.clone(), sub_path.as_path(), fs)?; - - Box::new(union) - }; - - let fs = Arc::new(fs); - - let mount = MountPoint { - path: PathBuf::from(c.as_os_str()), - name, - fs, - }; - - self.mounts - .write() - .unwrap() - .insert(PathBuf::from(c.as_os_str()), mount); } + + let fs = if sub_path.components().next().is_none() { + fs + } else { + let union = UnionFileSystem::new(); + union.mount(name.clone(), sub_path.as_path(), fs)?; + + Box::new(union) + }; + + let fs = Arc::new(fs); + + let mount = MountPoint { + path: PathBuf::from(c.as_os_str()), + name, + fs, + }; + + self.mounts + .write() + .unwrap() + .insert(PathBuf::from(c.as_os_str()), mount); } else { - println!("empty path"); + return Err(FsError::EntryNotFound); } Ok(()) @@ -262,8 +304,10 @@ impl FileOpener for UnionFileSystem { path: &Path, conf: &OpenOptionsConfig, ) -> Result> { - if path == PathBuf::from("/") { - return Err(FsError::NotAFile); + let path = self.prepare_path(path); + + if path.as_os_str().is_empty() { + Err(FsError::NotAFile) } else { let parent = path.parent().unwrap(); let file_name = path.file_name().unwrap(); @@ -280,7 +324,10 @@ impl FileOpener for UnionFileSystem { #[cfg(test)] mod tests { - use std::path::{Path, PathBuf}; + use std::{ + collections::HashSet, + path::{Path, PathBuf}, + }; use tokio::io::AsyncWriteExt; @@ -417,14 +464,14 @@ mod tests { .collect(); assert_eq!(root_contents, vec![PathBuf::from("/app")]); - let app_contents: Vec = fs + let app_contents: HashSet = fs .read_dir(&PathBuf::from("/app")) .unwrap() .map(|e| e.unwrap().path) .collect(); assert_eq!( app_contents, - vec![PathBuf::from("/app/a"), PathBuf::from("/app/b")] + HashSet::from_iter([PathBuf::from("/app/a"), PathBuf::from("/app/b")].into_iter()) ); let a_contents: Vec = fs diff --git a/lib/wasix/src/fs/mod.rs b/lib/wasix/src/fs/mod.rs index 35b20d10ff5..9ab56c8d7ff 100644 --- a/lib/wasix/src/fs/mod.rs +++ b/lib/wasix/src/fs/mod.rs @@ -376,7 +376,12 @@ impl FileSystem for WasiFsRoot { WasiFsRoot::Backing(fs) => fs.new_open_options(), } } - fn mount(&self, name: String, path: &Path, fs: Box) -> virtual_fs::Result<()> { + fn mount( + &self, + name: String, + path: &Path, + fs: Box, + ) -> virtual_fs::Result<()> { match self { WasiFsRoot::Sandbox(f) => f.mount(name, path, fs), WasiFsRoot::Backing(f) => f.mount(name, path, fs), @@ -2160,7 +2165,12 @@ impl FileSystem for FallbackFileSystem { fn new_open_options(&self) -> virtual_fs::OpenOptions { Self::fail(); } - fn mount(&self, _name: String, _path: &Path, _fs: Box) -> virtual_fs::Result<()> { + fn mount( + &self, + _name: String, + _path: &Path, + _fs: Box, + ) -> virtual_fs::Result<()> { Self::fail() } } diff --git a/lib/wasix/src/runners/wasi_common.rs b/lib/wasix/src/runners/wasi_common.rs index 61cd3aeadc1..82b2135f98a 100644 --- a/lib/wasix/src/runners/wasi_common.rs +++ b/lib/wasix/src/runners/wasi_common.rs @@ -166,7 +166,7 @@ fn build_directory_mappings( })?; } - TmpFileSystem::mount(&root_fs, guest_path.clone(), fs, "/".into()) + TmpFileSystem::mount(root_fs, guest_path.clone(), fs, "/".into()) .with_context(|| format!("Unable to mount \"{}\"", guest_path.display()))?; } } diff --git a/tests/lib/wast/src/wasi_wast.rs b/tests/lib/wast/src/wasi_wast.rs index 1ebbc02abbe..4491fbc82d4 100644 --- a/tests/lib/wast/src/wasi_wast.rs +++ b/tests/lib/wast/src/wasi_wast.rs @@ -246,50 +246,38 @@ impl<'a> WasiTest<'a> { let e = mem_fs::FileSystem::default(); let f = mem_fs::FileSystem::default(); - let mut union = union_fs::UnionFileSystem::new(); + let union = union_fs::UnionFileSystem::new(); union.mount( "mem_fs".to_string(), - PathBuf::from("/test_fs"), - false, + PathBuf::from("/test_fs").as_ref(), Box::new(a), - None, - ); + )?; union.mount( "mem_fs_2".to_string(), - PathBuf::from("/snapshot1"), - false, + PathBuf::from("/snapshot1").as_ref(), Box::new(b), - None, - ); + )?; union.mount( "mem_fs_3".to_string(), - PathBuf::from("/tests"), - false, + PathBuf::from("/tests").as_ref(), Box::new(c), - None, - ); + )?; union.mount( "mem_fs_4".to_string(), - PathBuf::from("/nightly_2022_10_18"), - false, + PathBuf::from("/nightly_2022_10_18").as_ref(), Box::new(d), - None, - ); + )?; union.mount( "mem_fs_5".to_string(), - PathBuf::from("/unstable"), - false, + PathBuf::from("/unstable").as_ref(), Box::new(e), - None, - ); + )?; union.mount( "mem_fs_6".to_string(), - PathBuf::from("/.tmp_wasmer_wast_0"), - false, + PathBuf::from("/.tmp_wasmer_wast_0").as_ref(), Box::new(f), - None, - ); + )?; Box::new(union) } From c3e998690c3068e277c4214e014bf2d5d3701e72 Mon Sep 17 00:00:00 2001 From: "M.Amin Rayej" Date: Tue, 20 Aug 2024 18:40:36 +0330 Subject: [PATCH 15/15] use dashmap --- Cargo.lock | 21 ++++++++++++++--- Cargo.toml | 1 + lib/virtual-fs/Cargo.toml | 1 + lib/virtual-fs/src/union_fs.rs | 43 ++++++++++------------------------ lib/vm/Cargo.toml | 2 +- lib/wasix/Cargo.toml | 2 +- 6 files changed, 34 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0139620867..2e25d324452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1252,6 +1252,20 @@ dependencies = [ "parking_lot_core 0.9.10", ] +[[package]] +name = "dashmap" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.20", + "hashbrown 0.14.5", + "lock_api 0.4.12", + "once_cell", + "parking_lot_core 0.9.10", +] + [[package]] name = "data-encoding" version = "2.6.0" @@ -4706,7 +4720,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" dependencies = [ - "dashmap", + "dashmap 5.5.3", "futures 0.3.30", "lazy_static", "log 0.4.22", @@ -6013,6 +6027,7 @@ dependencies = [ "anyhow", "async-trait", "bytes 1.6.1", + "dashmap 6.0.1", "derivative", "dunce", "filetime", @@ -7080,7 +7095,7 @@ dependencies = [ "cfg-if 1.0.0", "corosensei", "crossbeam-queue 0.3.11", - "dashmap", + "dashmap 6.0.1", "derivative", "enum-iterator", "fnv", @@ -7114,7 +7129,7 @@ dependencies = [ "cfg-if 1.0.0", "chrono", "cooked-waker", - "dashmap", + "dashmap 6.0.1", "derivative", "futures 0.3.30", "getrandom", diff --git a/Cargo.toml b/Cargo.toml index 324c5008164..023e1038115 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,7 @@ edge-schema = { version = "=0.1.0" } shared-buffer = "0.1.4" # Third-party crates +dashmap = "6.0.1" http = "1.0.0" hyper = "1" reqwest = { version = "0.12.0", default-features = false } diff --git a/lib/virtual-fs/Cargo.toml b/lib/virtual-fs/Cargo.toml index f6db418f880..20923cf9515 100644 --- a/lib/virtual-fs/Cargo.toml +++ b/lib/virtual-fs/Cargo.toml @@ -10,6 +10,7 @@ repository.workspace = true rust-version.workspace = true [dependencies] +dashmap.workspace = true dunce = "1.0.4" anyhow = { version = "1.0.66", optional = true } async-trait = { version = "^0.1" } diff --git a/lib/virtual-fs/src/union_fs.rs b/lib/virtual-fs/src/union_fs.rs index 7426eccd31d..88d56ad061c 100644 --- a/lib/virtual-fs/src/union_fs.rs +++ b/lib/virtual-fs/src/union_fs.rs @@ -2,13 +2,11 @@ //! its not as simple as TmpFs. not currently used but was used by //! the previoulsy implementation of Deploy - now using TmpFs +use dashmap::DashMap; + use crate::*; -use std::{ - collections::HashMap, - path::Path, - sync::{Arc, RwLock}, -}; +use std::{path::Path, sync::Arc}; #[derive(Debug)] pub struct MountPoint { @@ -35,7 +33,7 @@ impl MountPoint { /// to be mounted at various mount points #[derive(Debug, Default)] pub struct UnionFileSystem { - pub mounts: RwLock>, + pub mounts: DashMap, } impl UnionFileSystem { @@ -44,13 +42,11 @@ impl UnionFileSystem { } pub fn clear(&mut self) { - self.mounts.write().unwrap().clear(); + self.mounts.clear(); } fn is_root(&self) -> bool { - let map = self.mounts.read().unwrap(); - - map.len() == 1 && map.contains_key(&PathBuf::from("/")) + self.mounts.len() == 1 && self.mounts.contains_key(&PathBuf::from("/")) } fn prepare_path(&self, path: &Path) -> PathBuf { @@ -77,12 +73,7 @@ impl UnionFileSystem { let sub_path = components.into_iter().collect::(); - if let Some(mount) = self - .mounts - .read() - .unwrap() - .get(&PathBuf::from(c.as_os_str())) - { + if let Some(mount) = self.mounts.get(&PathBuf::from(c.as_os_str())) { return Some(( PathBuf::from(c.as_os_str()), PathBuf::from("/").join(sub_path), @@ -114,11 +105,9 @@ impl FileSystem for UnionFileSystem { if path.as_os_str().is_empty() { let entries = self .mounts - .read() - .unwrap() - .keys() - .map(|p| DirEntry { - path: PathBuf::from("/").join(p), + .iter() + .map(|i| DirEntry { + path: PathBuf::from("/").join(i.key()), metadata: Ok(Metadata { ft: FileType::new_dir(), accessed: 0, @@ -253,12 +242,7 @@ impl FileSystem for UnionFileSystem { let sub_path = components.into_iter().collect::(); - if let Some(mount) = self - .mounts - .read() - .unwrap() - .get(&PathBuf::from(c.as_os_str())) - { + if let Some(mount) = self.mounts.get(&PathBuf::from(c.as_os_str())) { return mount.fs.mount(name, sub_path.as_path(), fs); } @@ -279,10 +263,7 @@ impl FileSystem for UnionFileSystem { fs, }; - self.mounts - .write() - .unwrap() - .insert(PathBuf::from(c.as_os_str()), mount); + self.mounts.insert(PathBuf::from(c.as_os_str()), mount); } else { return Err(FsError::EntryNotFound); } diff --git a/lib/vm/Cargo.toml b/lib/vm/Cargo.toml index a89893b7e40..2239d5db2f7 100644 --- a/lib/vm/Cargo.toml +++ b/lib/vm/Cargo.toml @@ -14,6 +14,7 @@ version.workspace = true [dependencies] memoffset.workspace = true +dashmap.workspace = true wasmer-types = { path = "../types", version = "=4.3.5" } libc.workspace = true indexmap = { version = "1.6" } @@ -28,7 +29,6 @@ lazy_static = "1.4.0" region = { version = "3.0.2" } corosensei = { version = "0.1.2" } derivative = { version = "^2" } -dashmap = { version = "5.4" } fnv = "1.0.3" # - Optional shared dependencies. tracing = { version = "0.1", optional = true } diff --git a/lib/wasix/Cargo.toml b/lib/wasix/Cargo.toml index 3a5f3e0dbce..6fb82e1b962 100644 --- a/lib/wasix/Cargo.toml +++ b/lib/wasix/Cargo.toml @@ -24,6 +24,7 @@ wasmer-emscripten = { path = "../emscripten", version = "=4.3.5", optional = tru wasmer-config = { version = "0.5.0", path = "../config" } http.workspace = true +dashmap.workspace = true xxhash-rust = { version = "0.8.8", features = ["xxh64"] } rusty_pool = { version = "0.7.0", optional = true } cfg-if = "1.0" @@ -65,7 +66,6 @@ heapless = "0.7.16" once_cell = "1.17.0" pin-project = "1.0.12" semver = "1.0.17" -dashmap = "5.4.0" tempfile = "3.6.0" num_enum = "0.5.7" # Used by the WCGI runner