Remove canonicalization functionality from VFS and related components

- Eliminated the `canonicalize` method from the `VfsBackend` trait and its implementations in `InMemoryFs`, `NoopBackend`, and `StdBackend`, simplifying the interface and reducing complexity.
- Updated various parts of the codebase to remove references to canonicalization, including tests and prefetch cache handling, ensuring consistency across the project.
- Adjusted logging and comments to reflect the removal of path normalization logic, enhancing clarity and maintainability.

These changes aim to streamline the VFS implementation by focusing on core functionalities without the overhead of path canonicalization.
This commit is contained in:
Joe 2026-03-01 18:42:41 -06:00
parent 65f10f111c
commit fc8a6e9b9c
21 changed files with 123 additions and 440 deletions

View file

@ -235,30 +235,6 @@ impl VfsBackend for InMemoryFs {
// TODO: We rely on Rojo to prepend cwd to any relative path before storing paths // TODO: We rely on Rojo to prepend cwd to any relative path before storing paths
// in MemoFS. The current implementation will error if no prepended absolute path // in MemoFS. The current implementation will error if no prepended absolute path
// is found. It really only normalizes paths within the provided path's context. // is found. It really only normalizes paths within the provided path's context.
// Example: "/Users/username/project/../other/file.txt" ->
// "/Users/username/other/file.txt"
// Erroneous example: "/Users/../../other/file.txt" -> "/other/file.txt"
// This is not very robust. We should implement proper path normalization here or otherwise
// warn if we are missing context and can not fully canonicalize the path correctly.
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
normalized.pop();
}
std::path::Component::CurDir => {}
_ => normalized.push(component),
}
}
let inner = self.inner.lock().unwrap();
match inner.entries.get(&normalized) {
Some(_) => Ok(normalized),
None => not_found(&normalized),
}
}
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
let inner = self.inner.lock().unwrap(); let inner = self.inner.lock().unwrap();

View file

@ -39,7 +39,6 @@ pub use std_backend::{CriticalErrorHandler, StdBackend, WatcherCriticalError};
/// the initial tree build to avoid stale data during live operation. /// the initial tree build to avoid stale data during live operation.
pub struct PrefetchCache { pub struct PrefetchCache {
pub files: HashMap<PathBuf, Vec<u8>>, pub files: HashMap<PathBuf, Vec<u8>>,
pub canonical: HashMap<PathBuf, PathBuf>,
/// `true` = file, `false` = directory. Paths not in the map fall through /// `true` = file, `false` = directory. Paths not in the map fall through
/// to the backend (e.g. init-file probes for paths that don't exist). /// to the backend (e.g. init-file probes for paths that don't exist).
pub is_file: HashMap<PathBuf, bool>, pub is_file: HashMap<PathBuf, bool>,
@ -93,7 +92,6 @@ pub trait VfsBackend: sealed::Sealed + Send + 'static {
fn metadata(&mut self, path: &Path) -> io::Result<Metadata>; fn metadata(&mut self, path: &Path) -> io::Result<Metadata>;
fn remove_file(&mut self, path: &Path) -> io::Result<()>; fn remove_file(&mut self, path: &Path) -> io::Result<()>;
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>; fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf>;
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>; fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
fn watch(&mut self, path: &Path) -> io::Result<()>; fn watch(&mut self, path: &Path) -> io::Result<()>;
@ -278,18 +276,6 @@ impl VfsInner {
self.backend.metadata(path) self.backend.metadata(path)
} }
fn canonicalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
let path = path.as_ref();
if let Some(cache) = &mut self.prefetch_cache {
if let Some(canonical) = cache.canonical.remove(path) {
return Ok(canonical);
}
}
self.backend.canonicalize(path)
}
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
self.backend.event_receiver() self.backend.event_receiver()
} }
@ -369,9 +355,9 @@ impl Vfs {
/// Load a prefetch cache for fast initial reads. /// Load a prefetch cache for fast initial reads.
/// ///
/// File reads and canonicalize calls will check the cache before hitting /// File reads will check the cache before hitting the backend. Call
/// the backend. Call [`clear_prefetch_cache`] after the initial snapshot /// [`clear_prefetch_cache`] after the initial snapshot build to free
/// build to free memory and ensure live operations get fresh data. /// memory and ensure live operations get fresh data.
pub fn set_prefetch_cache(&self, cache: PrefetchCache) { pub fn set_prefetch_cache(&self, cache: PrefetchCache) {
let mut inner = self.inner.lock().unwrap(); let mut inner = self.inner.lock().unwrap();
inner.prefetch_cache = Some(cache); inner.prefetch_cache = Some(cache);
@ -533,19 +519,6 @@ impl Vfs {
self.inner.lock().unwrap().metadata(path) self.inner.lock().unwrap().metadata(path)
} }
/// Normalize a path via the underlying backend.
///
/// Roughly equivalent to [`std::fs::canonicalize`][std::fs::canonicalize]. Relative paths are
/// resolved against the backend's current working directory (if applicable) and errors are
/// surfaced directly from the backend.
///
/// [std::fs::canonicalize]: https://doc.rust-lang.org/stable/std/fs/fn.canonicalize.html
#[inline]
pub fn canonicalize<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
let path = path.as_ref();
self.inner.lock().unwrap().canonicalize(path)
}
/// Retrieve a handle to the event receiver for this `Vfs`. /// Retrieve a handle to the event receiver for this `Vfs`.
#[inline] #[inline]
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
@ -673,13 +646,6 @@ impl VfsLock<'_> {
self.inner.metadata(path) self.inner.metadata(path)
} }
/// Normalize a path via the underlying backend.
#[inline]
pub fn normalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
let path = path.as_ref();
self.inner.canonicalize(path)
}
/// Retrieve a handle to the event receiver for this `Vfs`. /// Retrieve a handle to the event receiver for this `Vfs`.
#[inline] #[inline]
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
@ -715,74 +681,12 @@ mod test {
); );
} }
/// https://github.com/rojo-rbx/rojo/issues/1200 fn make_prefetch(files: Vec<(&str, &[u8])>) -> PrefetchCache {
#[test]
fn canonicalize_in_memory_success() {
let mut imfs = InMemoryFs::new();
let contents = "Lorem ipsum dolor sit amet.".to_string();
imfs.load_snapshot("/test/file.txt", VfsSnapshot::file(contents.to_string()))
.unwrap();
let vfs = Vfs::new(imfs);
assert_eq!(
vfs.canonicalize("/test/nested/../file.txt").unwrap(),
PathBuf::from("/test/file.txt")
);
assert_eq!(
vfs.read_to_string(vfs.canonicalize("/test/nested/../file.txt").unwrap())
.unwrap()
.to_string(),
contents.to_string()
);
}
#[test]
fn canonicalize_in_memory_missing_errors() {
let imfs = InMemoryFs::new();
let vfs = Vfs::new(imfs);
let err = vfs.canonicalize("test").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[test]
fn canonicalize_std_backend_success() {
let contents = "Lorem ipsum dolor sit amet.".to_string();
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("file.txt");
fs_err::write(&file_path, contents.to_string()).unwrap();
let vfs = Vfs::new(StdBackend::new_for_testing());
let canonicalized = vfs.canonicalize(&file_path).unwrap();
assert_eq!(canonicalized, file_path.canonicalize().unwrap());
assert_eq!(
vfs.read_to_string(&canonicalized).unwrap().to_string(),
contents.to_string()
);
}
#[test]
fn canonicalize_std_backend_missing_errors() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("test");
let vfs = Vfs::new(StdBackend::new_for_testing());
let err = vfs.canonicalize(&file_path).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
fn make_prefetch(files: Vec<(&str, &[u8])>, canonical: Vec<(&str, &str)>) -> PrefetchCache {
PrefetchCache { PrefetchCache {
files: files files: files
.into_iter() .into_iter()
.map(|(k, v)| (PathBuf::from(k), v.to_vec())) .map(|(k, v)| (PathBuf::from(k), v.to_vec()))
.collect(), .collect(),
canonical: canonical
.into_iter()
.map(|(k, v)| (PathBuf::from(k), PathBuf::from(v)))
.collect(),
is_file: HashMap::new(), is_file: HashMap::new(),
children: HashMap::new(), children: HashMap::new(),
} }
@ -792,7 +696,7 @@ mod test {
fn prefetch_cache_read_hit() { fn prefetch_cache_read_hit() {
let imfs = InMemoryFs::new(); let imfs = InMemoryFs::new();
let vfs = Vfs::new(imfs); let vfs = Vfs::new(imfs);
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")], vec![])); vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")]));
assert_eq!(vfs.read("test").unwrap().as_slice(), b"cached"); assert_eq!(vfs.read("test").unwrap().as_slice(), b"cached");
} }
@ -803,7 +707,7 @@ mod test {
imfs.load_snapshot("test", VfsSnapshot::file("backend")) imfs.load_snapshot("test", VfsSnapshot::file("backend"))
.unwrap(); .unwrap();
let vfs = Vfs::new(imfs); let vfs = Vfs::new(imfs);
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")], vec![])); vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")]));
assert_eq!(vfs.read("test").unwrap().as_slice(), b"cached"); assert_eq!(vfs.read("test").unwrap().as_slice(), b"cached");
assert_eq!(vfs.read("test").unwrap().as_slice(), b"backend"); assert_eq!(vfs.read("test").unwrap().as_slice(), b"backend");
@ -815,7 +719,7 @@ mod test {
imfs.load_snapshot("other", VfsSnapshot::file("backend")) imfs.load_snapshot("other", VfsSnapshot::file("backend"))
.unwrap(); .unwrap();
let vfs = Vfs::new(imfs); let vfs = Vfs::new(imfs);
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")], vec![])); vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")]));
assert_eq!(vfs.read("other").unwrap().as_slice(), b"backend"); assert_eq!(vfs.read("other").unwrap().as_slice(), b"backend");
} }
@ -824,7 +728,7 @@ mod test {
fn prefetch_cache_read_to_string_hit() { fn prefetch_cache_read_to_string_hit() {
let imfs = InMemoryFs::new(); let imfs = InMemoryFs::new();
let vfs = Vfs::new(imfs); let vfs = Vfs::new(imfs);
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"hello")], vec![])); vfs.set_prefetch_cache(make_prefetch(vec![("test", b"hello")]));
assert_eq!(vfs.read_to_string("test").unwrap().as_str(), "hello"); assert_eq!(vfs.read_to_string("test").unwrap().as_str(), "hello");
} }
@ -833,7 +737,7 @@ mod test {
fn prefetch_cache_lf_normalized_hit() { fn prefetch_cache_lf_normalized_hit() {
let imfs = InMemoryFs::new(); let imfs = InMemoryFs::new();
let vfs = Vfs::new(imfs); let vfs = Vfs::new(imfs);
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"line1\r\nline2\r\n")], vec![])); vfs.set_prefetch_cache(make_prefetch(vec![("test", b"line1\r\nline2\r\n")]));
assert_eq!( assert_eq!(
vfs.read_to_string_lf_normalized("test").unwrap().as_str(), vfs.read_to_string_lf_normalized("test").unwrap().as_str(),
@ -841,39 +745,13 @@ mod test {
); );
} }
#[test]
fn prefetch_cache_canonicalize_hit() {
let imfs = InMemoryFs::new();
let vfs = Vfs::new(imfs);
vfs.set_prefetch_cache(make_prefetch(vec![], vec![("src/foo", "/abs/src/foo")]));
assert_eq!(
vfs.canonicalize("src/foo").unwrap(),
PathBuf::from("/abs/src/foo")
);
}
#[test]
fn prefetch_cache_canonicalize_miss_falls_through() {
let mut imfs = InMemoryFs::new();
imfs.load_snapshot("/real/file.txt", VfsSnapshot::file("x"))
.unwrap();
let vfs = Vfs::new(imfs);
vfs.set_prefetch_cache(make_prefetch(vec![], vec![("other", "/abs/other")]));
assert_eq!(
vfs.canonicalize("/real/file.txt").unwrap(),
PathBuf::from("/real/file.txt")
);
}
#[test] #[test]
fn prefetch_cache_clear_restores_backend() { fn prefetch_cache_clear_restores_backend() {
let mut imfs = InMemoryFs::new(); let mut imfs = InMemoryFs::new();
imfs.load_snapshot("test", VfsSnapshot::file("backend")) imfs.load_snapshot("test", VfsSnapshot::file("backend"))
.unwrap(); .unwrap();
let vfs = Vfs::new(imfs); let vfs = Vfs::new(imfs);
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")], vec![])); vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")]));
vfs.clear_prefetch_cache(); vfs.clear_prefetch_cache();
assert_eq!(vfs.read("test").unwrap().as_slice(), b"backend"); assert_eq!(vfs.read("test").unwrap().as_slice(), b"backend");
@ -914,45 +792,19 @@ mod test {
fn prefetch_cache_read_to_string_invalid_utf8() { fn prefetch_cache_read_to_string_invalid_utf8() {
let imfs = InMemoryFs::new(); let imfs = InMemoryFs::new();
let vfs = Vfs::new(imfs); let vfs = Vfs::new(imfs);
vfs.set_prefetch_cache(make_prefetch( vfs.set_prefetch_cache(make_prefetch(vec![("test", &[0xFF, 0xFE, 0x00, 0x80])]));
vec![("test", &[0xFF, 0xFE, 0x00, 0x80])],
vec![],
));
let err = vfs.read_to_string("test").unwrap_err(); let err = vfs.read_to_string("test").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData); assert_eq!(err.kind(), io::ErrorKind::InvalidData);
} }
#[test]
fn prefetch_cache_canonicalize_depletion() {
let mut imfs = InMemoryFs::new();
imfs.load_snapshot("/real/path", VfsSnapshot::file("x"))
.unwrap();
let vfs = Vfs::new(imfs);
vfs.set_prefetch_cache(make_prefetch(
vec![],
vec![("/real/path", "/canonical/path")],
));
assert_eq!(
vfs.canonicalize("/real/path").unwrap(),
PathBuf::from("/canonical/path"),
"First call should hit cache"
);
assert_eq!(
vfs.canonicalize("/real/path").unwrap(),
PathBuf::from("/real/path"),
"Second call should fall through to InMemoryFs backend"
);
}
#[test] #[test]
fn prefetch_cache_set_overwrite_previous() { fn prefetch_cache_set_overwrite_previous() {
let imfs = InMemoryFs::new(); let imfs = InMemoryFs::new();
let vfs = Vfs::new(imfs); let vfs = Vfs::new(imfs);
vfs.set_prefetch_cache(make_prefetch(vec![("a", b"first")], vec![])); vfs.set_prefetch_cache(make_prefetch(vec![("a", b"first")]));
vfs.set_prefetch_cache(make_prefetch(vec![("a", b"second")], vec![])); vfs.set_prefetch_cache(make_prefetch(vec![("a", b"second")]));
assert_eq!(vfs.read("a").unwrap().as_slice(), b"second"); assert_eq!(vfs.read("a").unwrap().as_slice(), b"second");
} }
@ -1003,22 +855,17 @@ mod test {
fn prefetch_cache_many_files_std_backend() { fn prefetch_cache_many_files_std_backend() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let mut cache_files = HashMap::new(); let mut cache_files = HashMap::new();
let mut canonical = HashMap::new();
for i in 0..50 { for i in 0..50 {
let path = dir.path().join(format!("f{i}.txt")); let path = dir.path().join(format!("f{i}.txt"));
let content = format!("data_{i}"); let content = format!("data_{i}");
fs_err::write(&path, &content).unwrap(); fs_err::write(&path, &content).unwrap();
cache_files.insert(path.clone(), content.into_bytes()); cache_files.insert(path.clone(), content.into_bytes());
if let Ok(c) = path.canonicalize() {
canonical.insert(path, c);
}
} }
let vfs = Vfs::new(StdBackend::new_for_testing()); let vfs = Vfs::new(StdBackend::new_for_testing());
vfs.set_prefetch_cache(PrefetchCache { vfs.set_prefetch_cache(PrefetchCache {
files: cache_files, files: cache_files,
canonical,
is_file: HashMap::new(), is_file: HashMap::new(),
children: HashMap::new(), children: HashMap::new(),
}); });
@ -1046,7 +893,7 @@ mod test {
imfs.load_snapshot("test", VfsSnapshot::file("original")) imfs.load_snapshot("test", VfsSnapshot::file("original"))
.unwrap(); .unwrap();
let vfs = Vfs::new(imfs); let vfs = Vfs::new(imfs);
vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")], vec![])); vfs.set_prefetch_cache(make_prefetch(vec![("test", b"cached")]));
vfs.write("test", b"written").unwrap(); vfs.write("test", b"written").unwrap();
let data = vfs.read("test").unwrap(); let data = vfs.read("test").unwrap();

View file

@ -1,5 +1,5 @@
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::Path;
use crate::{Metadata, ReadDir, VfsBackend, VfsEvent}; use crate::{Metadata, ReadDir, VfsBackend, VfsEvent};
@ -50,10 +50,6 @@ impl VfsBackend for NoopBackend {
Err(io::Error::other("NoopBackend doesn't do anything")) Err(io::Error::other("NoopBackend doesn't do anything"))
} }
fn canonicalize(&mut self, _path: &Path) -> io::Result<PathBuf> {
Err(io::Error::other("NoopBackend doesn't do anything"))
}
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
crossbeam_channel::never() crossbeam_channel::never()
} }

View file

@ -324,10 +324,6 @@ impl VfsBackend for StdBackend {
}) })
} }
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
fs_err::canonicalize(path)
}
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
self.watcher_receiver.clone() self.watcher_receiver.clone()
} }

View file

@ -99,11 +99,6 @@ impl ChangeProcessor {
// (non-serve commands). never() blocks forever without selecting. // (non-serve commands). never() blocks forever without selecting.
let critical_error_receiver = let critical_error_receiver =
critical_error_receiver.unwrap_or_else(crossbeam_channel::never); critical_error_receiver.unwrap_or_else(crossbeam_channel::never);
// Canonicalize project_root so path comparisons work with the
// \\?\ prefix that std::fs::canonicalize adds on Windows.
let project_root = std::fs::canonicalize(&project_root).unwrap_or(project_root);
let project_file_path =
std::fs::canonicalize(&project_file_path).unwrap_or(project_file_path);
let task = JobThreadContext { let task = JobThreadContext {
tree, tree,
vfs, vfs,
@ -306,24 +301,9 @@ impl JobThreadContext {
None None
} }
/// Canonicalize a path for use as a suppression map key.
fn suppression_key(path: &Path) -> PathBuf { fn suppression_key(path: &Path) -> PathBuf {
if let Ok(canonical) = std::fs::canonicalize(path) {
canonical
} else if let Some(parent) = path.parent() {
if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
if let Some(file_name) = path.file_name() {
canonical_parent.join(file_name)
} else {
path.to_path_buf() path.to_path_buf()
} }
} else {
path.to_path_buf()
}
} else {
path.to_path_buf()
}
}
/// Suppress the next Create/Write VFS event for the given path. /// Suppress the next Create/Write VFS event for the given path.
/// Prevents re-processing a file-watcher event we triggered ourselves. /// Prevents re-processing a file-watcher event we triggered ourselves.
@ -686,23 +666,9 @@ impl JobThreadContext {
_ => None, _ => None,
}; };
if let Some(ref path) = event_path { if let Some(ref path) = event_path {
let canonical = std::fs::canonicalize(path).ok();
let mut suppressed = self.suppressed_paths.lock().unwrap(); let mut suppressed = self.suppressed_paths.lock().unwrap();
// Determine which key matches: try canonical first (most likely let key = path.clone();
// to match what suppress_path stored), then fall back to raw. if let Some(counts) = suppressed.get_mut(key.as_path()) {
let matched_key = canonical
.as_ref()
.filter(|c| suppressed.contains_key(c.as_path()))
.cloned()
.or_else(|| {
if suppressed.contains_key(path) {
Some(path.clone())
} else {
None
}
});
if let Some(key) = matched_key {
if let Some(counts) = suppressed.get_mut(&key) {
let should_suppress = match &event { let should_suppress = match &event {
VfsEvent::Remove(_) if counts.0 > 0 => { VfsEvent::Remove(_) if counts.0 > 0 => {
counts.0 -= 1; counts.0 -= 1;
@ -719,7 +685,6 @@ impl JobThreadContext {
} }
if should_suppress { if should_suppress {
drop(suppressed); drop(suppressed);
// Still commit so VFS stays consistent, but skip patching.
self.vfs self.vfs
.commit_event(&event) .commit_event(&event)
.expect("Error applying VFS change"); .expect("Error applying VFS change");
@ -731,7 +696,6 @@ impl JobThreadContext {
} }
} }
} }
}
// Update the VFS immediately with the event. // Update the VFS immediately with the event.
self.vfs self.vfs
@ -763,16 +727,9 @@ impl JobThreadContext {
// of the tree. Calculate and apply all of these changes. // of the tree. Calculate and apply all of these changes.
let applied_patches = match event { let applied_patches = match event {
VfsEvent::Create(path) | VfsEvent::Write(path) => { VfsEvent::Create(path) | VfsEvent::Write(path) => {
match self.vfs.canonicalize(&path) { if path.exists() {
Ok(canonical_path) => { self.apply_patches(path)
log::info!( } else {
"VFS: canonicalize OK for {} -> {}",
self.display_path(&path),
self.display_path(&canonical_path)
);
self.apply_patches(canonical_path)
}
Err(_) => {
// The path doesn't exist on disk. Two possible causes: // The path doesn't exist on disk. Two possible causes:
// //
// 1. Phantom event from a rename we performed — macOS FSEvents // 1. Phantom event from a rename we performed — macOS FSEvents
@ -780,15 +737,13 @@ impl JobThreadContext {
// suppression exists for this path, consume it and skip. // suppression exists for this path, consume it and skip.
// //
// 2. The file was deleted between the event firing and us // 2. The file was deleted between the event firing and us
// processing it. Fall back to parent-directory canonicalize // processing it. Fall back to parent directory so the tree
// (same strategy as the Remove handler) so the tree can // can reconcile the disappearance.
// reconcile the disappearance.
let consumed = { let consumed = {
let key = Self::suppression_key(&path); let key = Self::suppression_key(&path);
let mut suppressed = self.suppressed_paths.lock().unwrap(); let mut suppressed = self.suppressed_paths.lock().unwrap();
if let Some(counts) = suppressed.get_mut(&key) { if let Some(counts) = suppressed.get_mut(&key) {
if counts.0 > 0 || counts.1 > 0 { if counts.0 > 0 || counts.1 > 0 {
// Drain whichever counter is available
if counts.1 > 0 { if counts.1 > 0 {
counts.1 -= 1; counts.1 -= 1;
} else { } else {
@ -814,13 +769,10 @@ impl JobThreadContext {
); );
Vec::new() Vec::new()
} else { } else {
// No suppression — the file genuinely vanished. Use the
// parent-directory fallback so the tree can reconcile.
let parent = path.parent().unwrap(); let parent = path.parent().unwrap();
let file_name = path.file_name().unwrap(); let file_name = path.file_name().unwrap();
match self.vfs.canonicalize(parent) { if parent.exists() {
Ok(parent_normalized) => { let resolved = parent.join(file_name);
let resolved = parent_normalized.join(file_name);
log::info!( log::info!(
"VFS: Create/Write for vanished file {} — \ "VFS: Create/Write for vanished file {} — \
resolved via parent to {}", resolved via parent to {}",
@ -828,44 +780,30 @@ impl JobThreadContext {
self.display_path(&resolved) self.display_path(&resolved)
); );
self.apply_patches(resolved) self.apply_patches(resolved)
} } else {
Err(err) => {
log::info!( log::info!(
"VFS: Skipping Create/Write for {} — \ "VFS: Skipping Create/Write for {} — \
parent no longer exists: {}", parent no longer exists",
self.display_path(&path), self.display_path(&path),
err
); );
Vec::new() Vec::new()
} }
} }
} }
} }
}
}
VfsEvent::Remove(path) => { VfsEvent::Remove(path) => {
// MemoFS does not track parent removals yet, so we can canonicalize
// the parent path safely and then append the removed path's file name.
// However, if the parent was also deleted (e.g., when deleting a directory
// tree), canonicalize will fail - in that case, just skip this event.
let parent = path.parent().unwrap(); let parent = path.parent().unwrap();
let file_name = path.file_name().unwrap(); if parent.exists() {
match self.vfs.canonicalize(parent) { log::info!("VFS: Remove for {}", self.display_path(&path));
Ok(parent_normalized) => { self.apply_patches(path)
let resolved = parent_normalized.join(file_name); } else {
log::info!("VFS: Remove resolved to {}", self.display_path(&resolved));
self.apply_patches(resolved)
}
Err(err) => {
log::info!( log::info!(
"VFS: Skipping remove event for {} — parent no longer exists: {}", "VFS: Skipping remove event for {} — parent no longer exists",
self.display_path(&path), self.display_path(&path),
err
); );
Vec::new() Vec::new()
} }
} }
}
_ => { _ => {
log::warn!("Unhandled VFS event: {:?}", event); log::warn!("Unhandled VFS event: {:?}", event);
Vec::new() Vec::new()

View file

@ -263,8 +263,7 @@ pub(crate) fn write_sourcemap(
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let tree = session.tree(); let tree = session.tree();
let canonical_project_dir = std::fs::canonicalize(session.root_dir()) let canonical_project_dir = session.root_dir().to_path_buf();
.unwrap_or_else(|_| session.root_dir().to_path_buf());
let root_node = recurse_create_node( let root_node = recurse_create_node(
&tree, &tree,
@ -306,8 +305,7 @@ pub(crate) fn write_sourcemap_from_syncback(
project_dir: &Path, project_dir: &Path,
output: &Path, output: &Path,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let canonical_project_dir = let canonical_project_dir = project_dir.to_path_buf();
std::fs::canonicalize(project_dir).unwrap_or_else(|_| project_dir.to_path_buf());
let root_node = let root_node =
recurse_create_node_from_dom(dom, dom.root_ref(), instance_paths, &canonical_project_dir); recurse_create_node_from_dom(dom, dom.root_ref(), instance_paths, &canonical_project_dir);

View file

@ -130,7 +130,7 @@ pub struct ServeSession {
} }
/// Collect all filesystem paths reachable from the project tree's `$path` /// Collect all filesystem paths reachable from the project tree's `$path`
/// entries, then read file contents and compute canonical paths in parallel. /// entries, then read file contents in parallel.
fn prefetch_project_files(project: &Project) -> io::Result<PrefetchCache> { fn prefetch_project_files(project: &Project) -> io::Result<PrefetchCache> {
use rayon::prelude::*; use rayon::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
@ -153,7 +153,6 @@ fn prefetch_project_files(project: &Project) -> io::Result<PrefetchCache> {
if roots.is_empty() { if roots.is_empty() {
return Ok(PrefetchCache { return Ok(PrefetchCache {
files: HashMap::new(), files: HashMap::new(),
canonical: HashMap::new(),
is_file: HashMap::new(), is_file: HashMap::new(),
children: HashMap::new(), children: HashMap::new(),
}); });
@ -208,24 +207,11 @@ fn prefetch_project_files(project: &Project) -> io::Result<PrefetchCache> {
.collect(); .collect();
let read_elapsed = read_start.elapsed(); let read_elapsed = read_start.elapsed();
let canon_start = Instant::now();
let canonical_data: Vec<_> = entries
.par_iter()
.filter_map(|e| {
let path = e.path().to_path_buf();
std::fs::canonicalize(&path).ok().map(|c| (path, c))
})
.collect();
let canon_elapsed = canon_start.elapsed();
log::debug!( log::debug!(
"Prefetch I/O: read {} files in {:.1?}, canonicalize {} paths in {:.1?}", "Prefetch I/O: read {} files in {:.1?}",
file_data.len(), file_data.len(),
read_elapsed, read_elapsed,
canonical_data.len(),
canon_elapsed,
); );
let mut is_file_map: HashMap<std::path::PathBuf, bool> = HashMap::new(); let mut is_file_map: HashMap<std::path::PathBuf, bool> = HashMap::new();
@ -257,7 +243,6 @@ fn prefetch_project_files(project: &Project) -> io::Result<PrefetchCache> {
Ok(PrefetchCache { Ok(PrefetchCache {
files: file_data.into_iter().collect::<HashMap<_, _>>(), files: file_data.into_iter().collect::<HashMap<_, _>>(),
canonical: canonical_data.into_iter().collect::<HashMap<_, _>>(),
is_file: is_file_map, is_file: is_file_map,
children: children_map, children: children_map,
}) })
@ -287,9 +272,8 @@ impl ServeSession {
match prefetch_project_files(&root_project) { match prefetch_project_files(&root_project) {
Ok(cache) => { Ok(cache) => {
log::debug!( log::debug!(
"Prefetch total: {} files + {} canonical paths in {:.1?}", "Prefetch total: {} files in {:.1?}",
cache.files.len(), cache.files.len(),
cache.canonical.len(),
prefetch_start.elapsed() prefetch_start.elapsed()
); );
vfs.set_prefetch_cache(cache); vfs.set_prefetch_cache(cache);

View file

@ -42,7 +42,7 @@ pub fn snapshot_csv(
.metadata( .metadata(
InstanceMetadata::new() InstanceMetadata::new()
.instigating_source(path) .instigating_source(path)
.relevant_paths(vec![vfs.canonicalize(path)?]), .relevant_paths(vec![path.to_path_buf()]),
); );
AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?; AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?;

View file

@ -85,7 +85,7 @@ pub fn snapshot_dir_no_meta(
children children
}; };
let normalized_path = vfs.canonicalize(path)?; let normalized_path = path.to_path_buf();
let relevant_paths = vec![ let relevant_paths = vec![
normalized_path.clone(), normalized_path.clone(),
// TODO: We shouldn't need to know about Lua existing in this // TODO: We shouldn't need to know about Lua existing in this

View file

@ -32,7 +32,7 @@ pub fn snapshot_json(
.metadata( .metadata(
InstanceMetadata::new() InstanceMetadata::new()
.instigating_source(path) .instigating_source(path)
.relevant_paths(vec![vfs.canonicalize(path)?]) .relevant_paths(vec![path.to_path_buf()])
.context(context), .context(context),
); );

View file

@ -53,7 +53,7 @@ pub fn snapshot_json_model(
snapshot.metadata = snapshot snapshot.metadata = snapshot
.metadata .metadata
.instigating_source(path) .instigating_source(path)
.relevant_paths(vec![vfs.canonicalize(path)?]) .relevant_paths(vec![path.to_path_buf()])
.context(context) .context(context)
.specified_id(id) .specified_id(id)
.schema(schema) .schema(schema)

View file

@ -72,7 +72,7 @@ pub fn snapshot_lua(
.metadata( .metadata(
InstanceMetadata::new() InstanceMetadata::new()
.instigating_source(path) .instigating_source(path)
.relevant_paths(vec![vfs.canonicalize(path)?]) .relevant_paths(vec![path.to_path_buf()])
.context(context), .context(context),
); );

View file

@ -147,10 +147,7 @@ pub fn snapshot_project(
// We SHOULD NOT mark the project file as a relevant path for any // We SHOULD NOT mark the project file as a relevant path for any
// nodes that aren't roots. They'll be updated as part of the project // nodes that aren't roots. They'll be updated as part of the project
// file being updated. // file being updated.
snapshot snapshot.metadata.relevant_paths.push(path.to_path_buf());
.metadata
.relevant_paths
.push(vfs.canonicalize(path)?);
Ok(Some(snapshot)) Ok(Some(snapshot))
} }

View file

@ -30,7 +30,7 @@ pub fn snapshot_rbxm(
.metadata( .metadata(
InstanceMetadata::new() InstanceMetadata::new()
.instigating_source(path) .instigating_source(path)
.relevant_paths(vec![vfs.canonicalize(path)?]) .relevant_paths(vec![path.to_path_buf()])
.context(context), .context(context),
); );

View file

@ -33,7 +33,7 @@ pub fn snapshot_rbxmx(
.metadata( .metadata(
InstanceMetadata::new() InstanceMetadata::new()
.instigating_source(path) .instigating_source(path)
.relevant_paths(vec![vfs.canonicalize(path)?]) .relevant_paths(vec![path.to_path_buf()])
.context(context), .context(context),
); );

View file

@ -33,7 +33,7 @@ pub fn snapshot_toml(
.metadata( .metadata(
InstanceMetadata::new() InstanceMetadata::new()
.instigating_source(path) .instigating_source(path)
.relevant_paths(vec![vfs.canonicalize(path)?]) .relevant_paths(vec![path.to_path_buf()])
.context(context), .context(context),
); );

View file

@ -28,7 +28,7 @@ pub fn snapshot_txt(
.metadata( .metadata(
InstanceMetadata::new() InstanceMetadata::new()
.instigating_source(path) .instigating_source(path)
.relevant_paths(vec![vfs.canonicalize(path)?]) .relevant_paths(vec![path.to_path_buf()])
.context(context), .context(context),
); );

View file

@ -37,7 +37,7 @@ pub fn snapshot_yaml(
.metadata( .metadata(
InstanceMetadata::new() InstanceMetadata::new()
.instigating_source(path) .instigating_source(path)
.relevant_paths(vec![vfs.canonicalize(path)?]) .relevant_paths(vec![path.to_path_buf()])
.context(context), .context(context),
); );

View file

@ -254,15 +254,6 @@ pub fn syncback_loop_with_stats(
let project_path = project.folder_location(); let project_path = project.folder_location();
fn strip_unc_prefix(p: PathBuf) -> PathBuf {
let path_str = p.to_string_lossy();
if path_str.starts_with(r"\\?\") || path_str.starts_with("//?/") {
PathBuf::from(&path_str[4..])
} else {
p
}
}
let phase_timer = std::time::Instant::now(); let phase_timer = std::time::Instant::now();
let existing_paths: HashSet<PathBuf> = if !incremental { let existing_paths: HashSet<PathBuf> = if !incremental {
let mut paths = HashSet::new(); let mut paths = HashSet::new();
@ -407,10 +398,7 @@ pub fn syncback_loop_with_stats(
continue; continue;
} }
} }
let canonical_dir = std::fs::canonicalize(dir) for entry in walkdir::WalkDir::new(dir)
.map(strip_unc_prefix)
.unwrap_or_else(|_| dir.clone());
for entry in walkdir::WalkDir::new(&canonical_dir)
.follow_links(true) .follow_links(true)
.into_iter() .into_iter()
.filter_entry(|e| { .filter_entry(|e| {
@ -661,14 +649,10 @@ pub fn syncback_loop_with_stats(
if !incremental && !existing_paths.is_empty() { if !incremental && !existing_paths.is_empty() {
log::debug!("Clean mode: checking for orphaned files to remove"); log::debug!("Clean mode: checking for orphaned files to remove");
let canonical_project_path = strip_unc_prefix(
std::fs::canonicalize(project_path).unwrap_or_else(|_| project_path.to_path_buf()),
);
let added_paths: HashSet<PathBuf> = fs_snapshot let added_paths: HashSet<PathBuf> = fs_snapshot
.added_paths() .added_paths()
.into_iter() .into_iter()
.map(|p| strip_unc_prefix(canonical_project_path.join(p))) .map(|p| project_path.join(p))
.collect(); .collect();
let mut added_dir_prefixes: HashSet<PathBuf> = HashSet::new(); let mut added_dir_prefixes: HashSet<PathBuf> = HashSet::new();
@ -681,10 +665,7 @@ pub fn syncback_loop_with_stats(
} }
} }
let project_file = strip_unc_prefix( let project_file = project.file_location.clone();
std::fs::canonicalize(&project.file_location)
.unwrap_or_else(|_| project.file_location.clone()),
);
// Collect ALL paths explicitly referenced via $path in the project. // Collect ALL paths explicitly referenced via $path in the project.
// These paths should NOT be removed during orphan cleanup because they // These paths should NOT be removed during orphan cleanup because they
@ -706,12 +687,9 @@ pub fn syncback_loop_with_stats(
) { ) {
if let Some(ref path_node) = node.path { if let Some(ref path_node) = node.path {
let resolved = base_path.join(path_node.path()); let resolved = base_path.join(path_node.path());
let canonical = std::fs::canonicalize(&resolved) protected.insert(resolved.clone());
.map(strip_unc_prefix)
.unwrap_or_else(|_| resolved.clone());
protected.insert(canonical.clone());
if !instance_path.is_empty() { if !instance_path.is_empty() {
mappings.push((canonical, instance_path.to_string())); mappings.push((resolved, instance_path.to_string()));
} }
} }
for (name, child) in &node.children { for (name, child) in &node.children {
@ -881,9 +859,7 @@ pub fn syncback_loop_with_stats(
} }
current_ancestor = Some(old_path); current_ancestor = Some(old_path);
let relative_path = old_path let relative_path = old_path.strip_prefix(project_path).unwrap_or(old_path);
.strip_prefix(&canonical_project_path)
.unwrap_or(old_path);
log::debug!("Removing orphaned path: {}", relative_path.display()); log::debug!("Removing orphaned path: {}", relative_path.display());
if old_path.is_dir() { if old_path.is_dir() {

View file

@ -358,24 +358,9 @@ impl ApiService {
} }
} }
/// Canonicalize a path for use as a suppression map key.
fn suppression_key(path: &Path) -> PathBuf { fn suppression_key(path: &Path) -> PathBuf {
if let Ok(canonical) = std::fs::canonicalize(path) {
canonical
} else if let Some(parent) = path.parent() {
if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
if let Some(file_name) = path.file_name() {
canonical_parent.join(file_name)
} else {
path.to_path_buf() path.to_path_buf()
} }
} else {
path.to_path_buf()
}
} else {
path.to_path_buf()
}
}
/// Suppress the next Create/Write VFS event for the given path. /// Suppress the next Create/Write VFS event for the given path.
fn suppress_path(&self, path: &Path) { fn suppress_path(&self, path: &Path) {

View file

@ -241,22 +241,17 @@ fn parallel_snapshot_with_prefetch_cache() {
use std::collections::HashMap; use std::collections::HashMap;
let mut files = HashMap::new(); let mut files = HashMap::new();
let mut canonical = HashMap::new();
for entry in walkdir::WalkDir::new(root).follow_links(true) { for entry in walkdir::WalkDir::new(root).follow_links(true) {
let entry = entry.unwrap(); let entry = entry.unwrap();
let path = entry.path().to_path_buf(); let path = entry.path().to_path_buf();
if entry.file_type().is_file() { if entry.file_type().is_file() {
files.insert(path.clone(), fs::read(&path).unwrap()); files.insert(path.clone(), fs::read(&path).unwrap());
} }
if let Ok(c) = fs::canonicalize(&path) {
canonical.insert(path, c);
}
} }
let vfs_cached = memofs::Vfs::new_default(); let vfs_cached = memofs::Vfs::new_default();
vfs_cached.set_prefetch_cache(memofs::PrefetchCache { vfs_cached.set_prefetch_cache(memofs::PrefetchCache {
files, files,
canonical,
is_file: std::collections::HashMap::new(), is_file: std::collections::HashMap::new(),
children: std::collections::HashMap::new(), children: std::collections::HashMap::new(),
}); });
@ -336,7 +331,6 @@ fn overlapping_path_roots_no_duplicate_children() {
use std::collections::HashMap; use std::collections::HashMap;
let mut files = HashMap::new(); let mut files = HashMap::new();
let mut canonical = HashMap::new();
let mut is_file = HashMap::new(); let mut is_file = HashMap::new();
let mut children_map: HashMap<std::path::PathBuf, Vec<std::path::PathBuf>> = HashMap::new(); let mut children_map: HashMap<std::path::PathBuf, Vec<std::path::PathBuf>> = HashMap::new();
@ -347,9 +341,6 @@ fn overlapping_path_roots_no_duplicate_children() {
if entry.file_type().is_file() { if entry.file_type().is_file() {
files.insert(path.clone(), fs::read(&path).unwrap()); files.insert(path.clone(), fs::read(&path).unwrap());
} }
if let Ok(c) = fs::canonicalize(&path) {
canonical.insert(path.clone(), c);
}
if entry.depth() > 0 { if entry.depth() > 0 {
if let Some(parent) = entry.path().parent() { if let Some(parent) = entry.path().parent() {
children_map children_map
@ -366,7 +357,6 @@ fn overlapping_path_roots_no_duplicate_children() {
let vfs = memofs::Vfs::new_default(); let vfs = memofs::Vfs::new_default();
vfs.set_prefetch_cache(memofs::PrefetchCache { vfs.set_prefetch_cache(memofs::PrefetchCache {
files, files,
canonical,
is_file, is_file,
children: children_map, children: children_map,
}); });