From fdccb413cd8887fce4e848ea396c50bdbcab11ed Mon Sep 17 00:00:00 2001 From: Nigel Ihza Firdausy <106657905+Ni-zav@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:15:34 +0700 Subject: [PATCH] fix(import): avoid scanning symlink aliases into the library --- crates/engine/src/walk.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/walk.rs b/crates/engine/src/walk.rs index 479c877c..4c35b66b 100644 --- a/crates/engine/src/walk.rs +++ b/crates/engine/src/walk.rs @@ -57,6 +57,8 @@ pub fn files_in(root: &Path, skip: Option<&Path>, limits: Limits, mut keep: impl if let Ok(c) = std::fs::canonicalize(root) { linked.insert(c); } + // Include aliases to the library in the exclusion, not just its lexical path. + let skip_canonical = skip.and_then(|p| std::fs::canonicalize(p).ok()); // (path, depth of the folder it is in, is a folder); popped in path order let mut stack: Vec<(PathBuf, usize, bool)> = vec![(root.to_path_buf(), 0, true)]; while let Some((path, depth, is_dir)) = stack.pop() { @@ -66,7 +68,9 @@ pub fn files_in(root: &Path, skip: Option<&Path>, limits: Limits, mut keep: impl } continue; } - if skip.is_some_and(|s| path == s) { + if skip.is_some_and(|s| path == s) + || skip_canonical.as_ref().is_some_and(|s| std::fs::canonicalize(&path).is_ok_and(|p| p.starts_with(s))) + { continue; } if depth >= limits.max_depth { @@ -165,6 +169,31 @@ mod tests { let _ = std::fs::remove_dir_all(&d); } + /// Folder imports must not revisit the user's own library through symlinks + /// or through an alias to the library's parent. + #[cfg(unix)] + #[test] + fn skipped_library_stays_skipped_through_symlinks_and_ancestors() { + let d = tmp("skip-alias"); + let root = d.join("incoming"); + let library = d.join("library"); + std::fs::create_dir_all(&root).unwrap(); + std::fs::create_dir_all(library.join("Originals")).unwrap(); + std::fs::write(root.join("ordinary.jpg"), b"photo").unwrap(); + std::fs::write(library.join("Originals").join("library-only.jpg"), b"original").unwrap(); + std::os::unix::fs::symlink(&library, root.join("library-link")).unwrap(); + std::os::unix::fs::symlink(&d, root.join("parent-link")).unwrap(); + + let photos = files_in(&root, Some(&library), Limits::default(), |p| p.extension().is_some_and(|e| e == "jpg")); + assert!(photos.files.contains(&root.join("ordinary.jpg"))); + assert!(!photos.files.iter().any(|p| p.ends_with("library-only.jpg")), "{photos:?}"); + + // Scanning a subfolder already inside the excluded library is also forbidden. + let inside = files_in(&library.join("Originals"), Some(&library), Limits::default(), |_| true); + assert!(inside.files.is_empty(), "{inside:?}"); + let _ = std::fs::remove_dir_all(&d); + } + /// A link back up the tree (a loop) is followed at most once: the walk ends. #[cfg(unix)] #[test]