From 9644c1844a847d5a1bc9b7290625e0bb4d40ec64 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 25 Sep 2025 13:48:08 -0400 Subject: [PATCH] Use custom implementation of canonicalize for WASI --- Cargo.lock | 9 +++++++ crates/fsops/Cargo.toml | 11 ++++++++ crates/fsops/src/lib.rs | 41 +++++++++++++++++++++++++++++ crates/ignore/Cargo.toml | 1 + crates/ignore/src/dir.rs | 4 +-- crates/oxide/Cargo.toml | 1 + crates/oxide/src/glob.rs | 4 +-- crates/oxide/src/paths.rs | 2 +- crates/oxide/src/scanner/mod.rs | 4 +-- crates/oxide/src/scanner/sources.rs | 6 ++--- crates/oxide/tests/scanner.rs | 2 +- 11 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 crates/fsops/Cargo.toml create mode 100644 crates/fsops/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index aaf0b476e4d9..970000ababd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -198,6 +198,13 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "fsops" +version = "0.1.0" +dependencies = [ + "dunce", +] + [[package]] name = "globset" version = "0.4.16" @@ -230,6 +237,7 @@ dependencies = [ "crossbeam-channel", "crossbeam-deque", "dunce", + "fsops", "globset", "log", "memchr", @@ -598,6 +606,7 @@ dependencies = [ "crossbeam", "dunce", "fast-glob", + "fsops", "globwalk", "ignore 0.4.23", "log", diff --git a/crates/fsops/Cargo.toml b/crates/fsops/Cargo.toml new file mode 100644 index 000000000000..1ddf36bd35e4 --- /dev/null +++ b/crates/fsops/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "fsops" +version = "0.1.0" +edition = "2021" + +[lib] +name = "fsops" +bench = false + +[dependencies] +dunce = "1.0.5" diff --git a/crates/fsops/src/lib.rs b/crates/fsops/src/lib.rs new file mode 100644 index 000000000000..41075d58e1c8 --- /dev/null +++ b/crates/fsops/src/lib.rs @@ -0,0 +1,41 @@ +use std::io; +use std::path::{Path, PathBuf}; + +/// https://github.com/oxc-project/oxc-resolver/blob/42a1e3eb50e9a1365c422c41d51f287fe5fb8244/src/file_system.rs#L93-L122 +pub fn canonicalize>(path: P) -> io::Result { + #[cfg(not(target_os = "wasi"))] + { + dunce::canonicalize(path) + } + #[cfg(target_os = "wasi")] + { + canonicalize_wasi(path) + } +} + +#[cfg(target_os = "wasi")] +fn canonicalize_wasi>(path: P) -> io::Result { + use std::fs; + let path = path.as_ref(); + let meta = fs::symlink_metadata(path)?; + if meta.file_type().is_symlink() { + let link = fs::read_link(path)?; + let mut path_buf = path.to_path_buf(); + path_buf.pop(); + for segment in link.iter() { + match segment.to_str() { + Some("..") => { + path_buf.pop(); + } + Some(".") | None => {} + Some(seg) => { + // Need to trim the extra \0 introduces by rust std rust-lang/rust#123727 + path_buf.push(seg.trim_end_matches('\0')); + } + } + } + Ok(path_buf) + } else { + Ok(path.to_path_buf()) + } +} diff --git a/crates/ignore/Cargo.toml b/crates/ignore/Cargo.toml index b8ae1b1bf721..17803f2ec74d 100644 --- a/crates/ignore/Cargo.toml +++ b/crates/ignore/Cargo.toml @@ -26,6 +26,7 @@ memchr = "2.6.3" same-file = "1.0.6" walkdir = "2.4.0" dunce = "1.0.5" +fsops = { path = "../fsops" } [dependencies.regex-automata] version = "0.4.0" diff --git a/crates/ignore/src/dir.rs b/crates/ignore/src/dir.rs index 9bbf1442b382..4149f58ec124 100644 --- a/crates/ignore/src/dir.rs +++ b/crates/ignore/src/dir.rs @@ -176,8 +176,8 @@ impl Ignore { if !self.is_root() { panic!("Ignore::add_parents called on non-root matcher"); } - // CHANGED: Use `dunce::canonicalize` as we use it everywhere else. - let absolute_base = match dunce::canonicalize(path.as_ref()) { + // CHANGED: Use `fsops::canonicalize` as we use it everywhere else. + let absolute_base = match fsops::canonicalize(path.as_ref()) { Ok(path) => Arc::new(path), Err(_) => { // There's not much we can do here, so just return our diff --git a/crates/oxide/Cargo.toml b/crates/oxide/Cargo.toml index 9b800b04994c..69669fe86d09 100644 --- a/crates/oxide/Cargo.toml +++ b/crates/oxide/Cargo.toml @@ -19,6 +19,7 @@ fast-glob = "0.4.3" classification-macros = { path = "../classification-macros" } ignore = { path = "../ignore" } regex = "1.11.1" +fsops = { path = "../fsops" } [dev-dependencies] tempfile = "3.13.0" diff --git a/crates/oxide/src/glob.rs b/crates/oxide/src/glob.rs index 9a6296bcb286..693f3e3c0b04 100644 --- a/crates/oxide/src/glob.rs +++ b/crates/oxide/src/glob.rs @@ -23,7 +23,7 @@ pub fn hoist_static_glob_parts(entries: &Vec, emit_parent_glob: bool) None => base, }; - let base = match dunce::canonicalize(&base) { + let base = match fsops::canonicalize(&base) { Ok(base) => base, Err(err) => { event!(tracing::Level::ERROR, "Failed to resolve glob: {:?}", err); @@ -253,7 +253,7 @@ mod tests { let optimized_sources = optimize_patterns(&sources); let parent_dir = - format!("{}{}", dunce::canonicalize(base).unwrap().display(), "/").replace('\\', "/"); + format!("{}{}", fsops::canonicalize(base).unwrap().display(), "/").replace('\\', "/"); // Remove the temporary directory from the base optimized_sources diff --git a/crates/oxide/src/paths.rs b/crates/oxide/src/paths.rs index ca3b345beba2..a4737dcec2bf 100644 --- a/crates/oxide/src/paths.rs +++ b/crates/oxide/src/paths.rs @@ -60,6 +60,6 @@ impl Path { } pub fn canonicalize(&self) -> io::Result { - Ok(dunce::canonicalize(&self.inner)?.into()) + Ok(fsops::canonicalize(&self.inner)?.into()) } } diff --git a/crates/oxide/src/scanner/mod.rs b/crates/oxide/src/scanner/mod.rs index ec6aea642481..5f8d3f4970e3 100644 --- a/crates/oxide/src/scanner/mod.rs +++ b/crates/oxide/src/scanner/mod.rs @@ -63,7 +63,7 @@ fn init_tracing() { .unwrap_or_else(|_| panic!("Failed to open {file_path}")); let file_path = Path::new(&file_path); - let absolute_file_path = dunce::canonicalize(file_path) + let absolute_file_path = fsops::canonicalize(file_path) .unwrap_or_else(|_| panic!("Failed to canonicalize {file_path:?}")); eprintln!( "{} Writing debug info to: {}\n", @@ -215,7 +215,7 @@ impl Scanner { .into_iter() .filter_map(|changed_content| match changed_content { ChangedContent::File(file, extension) => { - let Ok(file) = dunce::canonicalize(file) else { + let Ok(file) = fsops::canonicalize(file) else { return None; }; Some(ChangedContent::File(file, extension)) diff --git a/crates/oxide/src/scanner/sources.rs b/crates/oxide/src/scanner/sources.rs index f28b16a49865..79068fb01d6c 100644 --- a/crates/oxide/src/scanner/sources.rs +++ b/crates/oxide/src/scanner/sources.rs @@ -102,7 +102,7 @@ impl PublicSourceEntry { /// resolved path. pub fn optimize(&mut self) { // Resolve base path immediately - let Ok(base) = dunce::canonicalize(&self.base) else { + let Ok(base) = fsops::canonicalize(&self.base) else { event!(Level::ERROR, "Failed to resolve base: {:?}", self.base); return; }; @@ -116,7 +116,7 @@ impl PublicSourceEntry { PathBuf::from(&self.base).join(&self.pattern) }; - match dunce::canonicalize(combined_path) { + match fsops::canonicalize(combined_path) { Ok(resolved_path) if resolved_path.is_dir() => { self.base = resolved_path.to_string_lossy().to_string(); self.pattern = "**/*".to_owned(); @@ -144,7 +144,7 @@ impl PublicSourceEntry { Some(static_part) => { // TODO: If the base does not exist on disk, try removing the last slash and try // again. - match dunce::canonicalize(base.join(static_part)) { + match fsops::canonicalize(base.join(static_part)) { Ok(base) => base, Err(err) => { event!(tracing::Level::ERROR, "Failed to resolve glob: {:?}", err); diff --git a/crates/oxide/tests/scanner.rs b/crates/oxide/tests/scanner.rs index 4003ff253048..16dd4433a7da 100644 --- a/crates/oxide/tests/scanner.rs +++ b/crates/oxide/tests/scanner.rs @@ -87,7 +87,7 @@ mod scanner { let candidates = scanner.scan(); let base_dir = - format!("{}{}", dunce::canonicalize(&base).unwrap().display(), "/").replace('\\', "/"); + format!("{}{}", fsops::canonicalize(&base).unwrap().display(), "/").replace('\\', "/"); // Get all scanned files as strings relative to the base directory let mut files = scanner