From c8585f1d72581339b2ec67baadc0f268580e2bdc Mon Sep 17 00:00:00 2001 From: AI Tokens Service Date: Tue, 14 Jul 2026 23:11:35 +0300 Subject: [PATCH 1/2] fix: normalize path separators for cross-platform keyword matching Normalize both stored paths and query keywords to use forward slashes before matching in filter_by_keywords. This ensures that on Windows: - cd bar/meow matches ~\foo\bar\meow - cd bar\meow matches ~/foo/bar/meow Unlike PR #1222 which only normalizes the path, this normalizes both sides of the match. Normalization is unconditional (harmless no-op on Linux/macOS) enabling cross-platform test execution. Added 7 new test cases for backslash path matching. Closes #1217 --- src/db/stream.rs | 42 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/src/db/stream.rs b/src/db/stream.rs index 24c84e0..8a4ac2f 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -83,11 +83,18 @@ impl<'a> Stream<'a> { None => return true, }; - let path = util::to_lowercase(path); + // Normalize both path and keywords to use forward slashes so that + // backslash-separated paths (common on Windows) match forward-slash + // queries and vice versa. + let path = util::to_lowercase(path).replace('\\', "/"); + let kw_last = util::to_lowercase(keywords_last).replace('\\', "/"); + let keywords_norm: Vec = + keywords.iter().map(|k| util::to_lowercase(k).replace('\\', "/")).collect(); + let mut path = path.as_str(); - match path.rfind(keywords_last) { + match path.rfind(kw_last.as_str()) { Some(idx) => { - if path[idx + keywords_last.len()..].contains(path::is_separator) { + if path[idx + kw_last.len()..].contains(path::is_separator) { return false; } path = &path[..idx]; @@ -95,8 +102,8 @@ impl<'a> Stream<'a> { None => return false, } - for keyword in keywords.iter().rev() { - match path.rfind(keyword) { + for keyword in keywords_norm.iter().rev() { + match path.rfind(keyword.as_str()) { Some(idx) => path = &path[..idx], None => return false, } @@ -208,4 +215,29 @@ mod tests { let stream = Stream::new(db, options); assert_eq!(is_match, stream.filter_by_keywords(path)); } + + // Forward slash matching on Windows-style backslash paths. + // These tests run on ALL platforms because the normalization is + // unconditional — on Linux/macOS it's a harmless no-op. + #[rstest] + // Single keyword with forward slash query against backslash path + #[case(&["bar/meow"], r"~\foo\bar\meow", true)] + // Multiple keywords with forward slash query + #[case(&["bar", "meow"], r"~\foo\bar\meow", true)] + // Backslash query against forward-slash path (reverse direction) + #[case(&[r"bar\meow"], "/foo/bar/meow", true)] + // Mixed separators in query + #[case(&[r"bar/meow"], r"~\foo\bar\meow", true)] + // Full path with forward slashes + #[case(&[r"~\foo\bar\meow"], r"~\foo\bar\meow", true)] + // Forward slash query should not match partial component + #[case(&["bar/mee"], r"~\foo\bar\meow", false)] + // Single partial keyword — should NOT match since bar is not last component + #[case(&["bar"], r"~\foo\bar\meow", false)] + fn query_backslash_paths(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) { + let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false); + let options = StreamOptions::new(0).with_keywords(keywords.iter()); + let stream = Stream::new(db, options); + assert_eq!(is_match, stream.filter_by_keywords(path)); + } } From eb3f2f39d251bbd99cd9ea58eb69e3c7abcc380d Mon Sep 17 00:00:00 2001 From: AI Tokens Service Date: Tue, 14 Jul 2026 23:27:44 +0300 Subject: [PATCH 2/2] style: fix rustfmt multiline fn signature rustfmt requires splitting long function signatures across multiple lines. This was the only CI failure. --- src/db/stream.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/db/stream.rs b/src/db/stream.rs index 8a4ac2f..0b76023 100644 --- a/src/db/stream.rs +++ b/src/db/stream.rs @@ -234,7 +234,11 @@ mod tests { #[case(&["bar/mee"], r"~\foo\bar\meow", false)] // Single partial keyword — should NOT match since bar is not last component #[case(&["bar"], r"~\foo\bar\meow", false)] - fn query_backslash_paths(#[case] keywords: &[&str], #[case] path: &str, #[case] is_match: bool) { + fn query_backslash_paths( + #[case] keywords: &[&str], + #[case] path: &str, + #[case] is_match: bool, + ) { let db = &mut Database::new(PathBuf::new(), Vec::new(), |_| Vec::new(), false); let options = StreamOptions::new(0).with_keywords(keywords.iter()); let stream = Stream::new(db, options);