From 7ade19293cf7319a6938cee5327ba7e85e283f87 Mon Sep 17 00:00:00 2001 From: EgoMoose Date: Sun, 5 Jul 2026 14:26:35 -0400 Subject: [PATCH] Fix windows verbatim paths (#1295) --- CHANGELOG.md | 6 +++++ Cargo.lock | 8 +++++++ Cargo.toml | 1 + crates/memofs/Cargo.toml | 1 + crates/memofs/src/lib.rs | 38 ++++++++++++++++++++++++++++++- crates/memofs/src/std_backend.rs | 2 +- src/cli/sourcemap.rs | 2 +- src/serve_session.rs | 39 +++++++++++++++++++++++++++++++- 8 files changed, 93 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1d007cf..846df589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,12 @@ Making a new release? Simply add the new header with the version and date undern ## Unreleased +* Fixed `$path` values that point outside the project folder failing to match `syncRule`s on Windows, which broke `rojo sourcemap` with a "could not be turned into a Roblox Instance" error. ([#1290]) +* Fixed `rojo serve` silently stopping syncing file changes on Windows when the served project path was a verbatim (`\\?\`) path, because tree paths and file-watcher event paths were canonicalized to different forms. ([#1290]) +* Fixed `rojo sourcemap --absolute` emitting verbatim (`\\?\`) paths on Windows, which broke require types in luau-lsp. ([#1290]) + +[#1290]: https://github.com/rojo-rbx/rojo/pull/1290 + ## [7.7.0] (July 1st, 2026) * `inf` and `nan` values in properties are now synced ([#1176]) diff --git a/Cargo.lock b/Cargo.lock index 93e5fda4..37be6f2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -478,6 +478,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -1316,6 +1322,7 @@ name = "memofs" version = "0.4.0" dependencies = [ "crossbeam-channel", + "dunce", "fs-err", "notify", "serde", @@ -2066,6 +2073,7 @@ dependencies = [ "crossbeam-channel", "csv", "data-encoding", + "dunce", "embed-resource", "env_logger", "float-cmp", diff --git a/Cargo.toml b/Cargo.toml index 06bb4811..975a04e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ backtrace = "0.3.69" bincode = "1.3.3" crossbeam-channel = "0.5.12" csv = "1.3.0" +dunce = "1.0.5" env_logger = "0.9.3" fs-err = "2.11.0" futures = "0.3.30" diff --git a/crates/memofs/Cargo.toml b/crates/memofs/Cargo.toml index f87378e5..c0bfbc75 100644 --- a/crates/memofs/Cargo.toml +++ b/crates/memofs/Cargo.toml @@ -16,6 +16,7 @@ homepage = "https://github.com/rojo-rbx/rojo/tree/master/memofs" [dependencies] crossbeam-channel = "0.5.12" +dunce = "1.0.5" fs-err = "2.11.0" notify = "4.0.17" serde = { version = "1.0.197", features = ["derive"] } diff --git a/crates/memofs/src/lib.rs b/crates/memofs/src/lib.rs index 11acf810..bb6ae5a1 100644 --- a/crates/memofs/src/lib.rs +++ b/crates/memofs/src/lib.rs @@ -644,13 +644,49 @@ mod test { let vfs = Vfs::new(StdBackend::new().unwrap()); let canonicalized = vfs.canonicalize(&file_path).unwrap(); - assert_eq!(canonicalized, file_path.canonicalize().unwrap()); + assert_eq!(canonicalized, dunce::canonicalize(&file_path).unwrap()); assert_eq!( vfs.read_to_string(&canonicalized).unwrap().to_string(), contents.to_string() ); } + #[test] + #[cfg(windows)] + fn canonicalize_std_backend_not_verbatim() { + use std::path::{Component, Prefix}; + + let dir = tempfile::tempdir().unwrap(); + let file_path = dir.path().join("file.txt"); + fs_err::write(&file_path, "hello").unwrap(); + + let vfs = Vfs::new(StdBackend::new().unwrap()); + let canonicalized = vfs.canonicalize(&file_path).unwrap(); + + let is_verbatim = matches!( + canonicalized.components().next(), + Some(Component::Prefix(prefix)) if matches!( + prefix.kind(), + Prefix::Verbatim(_) | Prefix::VerbatimDisk(_) | Prefix::VerbatimUNC(_, _) + ) + ); + assert!( + !is_verbatim, + "expected a non-verbatim path, got {:?}", + canonicalized + ); + + // Joining a relative parent path must preserve the `..` segment. On a + // verbatim path Rust would drop it lexically, which is the root cause + // of the bug. + let joined = canonicalized.join("..").join("sibling"); + assert!( + joined.components().any(|c| c == Component::ParentDir), + "`..` should be preserved when joining onto {:?}", + canonicalized + ); + } + #[test] fn canonicalize_std_backend_missing_errors() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/memofs/src/std_backend.rs b/crates/memofs/src/std_backend.rs index d684bde5..3f0ab2c1 100644 --- a/crates/memofs/src/std_backend.rs +++ b/crates/memofs/src/std_backend.rs @@ -107,7 +107,7 @@ impl VfsBackend for StdBackend { } fn canonicalize(&mut self, path: &Path) -> io::Result { - fs_err::canonicalize(path) + dunce::canonicalize(path) } fn event_receiver(&self) -> crossbeam_channel::Receiver { diff --git a/src/cli/sourcemap.rs b/src/cli/sourcemap.rs index 10fff590..2e4a7ce9 100644 --- a/src/cli/sourcemap.rs +++ b/src/cli/sourcemap.rs @@ -72,7 +72,7 @@ pub struct SourcemapCommand { impl SourcemapCommand { pub fn run(self) -> anyhow::Result<()> { - let project_path = fs_err::canonicalize(resolve_path(&self.project)?)?; + let project_path = dunce::canonicalize(resolve_path(&self.project)?)?; log::trace!("Constructing filesystem with StdBackend"); let vfs = Vfs::new_default()?; diff --git a/src/serve_session.rs b/src/serve_session.rs index e46cdfe9..6cffb0d2 100644 --- a/src/serve_session.rs +++ b/src/serve_session.rs @@ -95,8 +95,9 @@ impl ServeSession { /// currently loaded from the filesystem directly instead of through the /// in-memory filesystem layer. pub fn new>(vfs: Vfs, start_path: P) -> Result { - let start_path = start_path.as_ref(); let start_time = Instant::now(); + let start_path = vfs.canonicalize(start_path.as_ref())?; + let start_path = start_path.as_path(); log::trace!("Starting new ServeSession at path {}", start_path.display()); @@ -240,3 +241,39 @@ pub enum ServeSessionError { source: anyhow::Error, }, } + +#[cfg(test)] +mod test { + use super::*; + + use memofs::StdBackend; + + #[test] + fn tree_is_keyed_by_canonical_paths() { + let dir = tempfile::tempdir().unwrap(); + std::fs::write( + dir.path().join("default.project.json"), + r#"{ "name": "test", "tree": { "$className": "Folder" } }"#, + ) + .unwrap(); + + // `std::fs::canonicalize` yields a verbatim path on Windows. + // On other platforms it simply resolves the path (e.g. symlinks), + // which the session must also handle. + let start_path = std::fs::canonicalize(dir.path()).unwrap(); + + let vfs = Vfs::new(StdBackend::new().unwrap()); + let session = ServeSession::new(vfs, &start_path).unwrap(); + + let project_file = start_path.join("default.project.json"); + let canonical = session.vfs().canonicalize(&project_file).unwrap(); + + assert!( + !session.tree().get_ids_at_path(&canonical).is_empty(), + "project file {} should be tracked in the tree under its canonical \ + path {}, matching what the watcher reports", + project_file.display(), + canonical.display(), + ); + } +}