diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index c6550fd5..96d54fb6 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -100,6 +100,7 @@ enum-map = { workspace = true } enum-map-derive = "0.17.0" enumset = { version = "1.1.5", features = ["serde"] } gethostname = "0.5.0" +globset = "0.4.15" humantime = "2.1.0" itertools = "0.13.0" quick_cache = "0.6.9" @@ -124,7 +125,6 @@ xattr = "1" anyhow = { workspace = true } expect-test = "1.5.0" flate2 = "1.0.35" -globset = "0.4.15" insta = { version = "1.41.1", features = ["redactions", "ron"] } mockall = "0.13" pretty_assertions = "1.4.1" diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index e0717a39..0236827e 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -115,6 +115,7 @@ pub(crate) mod progress; /// Structs which are saved in JSON or binary format in the repository pub mod repofile; pub(crate) mod repository; +pub mod util; /// Virtual File System support - allows to act on the repository like on a file system pub mod vfs; diff --git a/crates/core/src/util.rs b/crates/core/src/util.rs new file mode 100644 index 00000000..74791af8 --- /dev/null +++ b/crates/core/src/util.rs @@ -0,0 +1,29 @@ +use globset::GlobMatcher; + +/// Extend `globset::GlobMatcher` to allow mathing on unix paths (on every platform) +pub trait GlobMatcherExt { + /// Match on unix paths, i.e. paths which are available as `&[u8]` + fn is_unix_match(&self, path: impl AsRef<[u8]>) -> bool; +} + +impl GlobMatcherExt for GlobMatcher { + // This is a hacky implementation, espeically for windows where we convert lossily + // into an utf8 string and match on the windows path given by that string. + // Note: `GlobMatcher` internally converts into a `&[u8]` to perform the matching + // TODO: Use https://github.com/BurntSushi/ripgrep/pull/2955 once it is available. + #[cfg(not(windows))] + fn is_unix_match(&self, path: impl AsRef<[u8]>) -> bool { + use std::{ffi::OsStr, os::unix::ffi::OsStrExt, path::PathBuf}; + + let path = PathBuf::from(OsStr::from_bytes(path.as_ref())); + self.is_match(&path) + } + #[cfg(windows)] + fn is_unix_match(&self, path: impl AsRef<[u8]>) -> bool { + use std::{ffi::OsStr, path::Path}; + + let string: &str = &String::from_utf8_lossy(path.as_ref()); + let path = Path::new(OsStr::new(string)); + self.is_match(path) + } +} diff --git a/crates/core/tests/integration/find.rs b/crates/core/tests/integration/find.rs index b9fb0fbd..abb24df2 100644 --- a/crates/core/tests/integration/find.rs +++ b/crates/core/tests/integration/find.rs @@ -1,10 +1,7 @@ // don't warn about try_from paths conversion on unix #![allow(clippy::unnecessary_fallible_conversions)] -use std::{ - path::{Path, PathBuf}, - str::FromStr, -}; +use std::{path::PathBuf, str::FromStr}; use anyhow::Result; use globset::Glob; @@ -13,6 +10,7 @@ use rstest::rstest; use rustic_core::{ repofile::{Node, SnapshotFile}, + util::GlobMatcherExt, BackupOptions, FindMatches, FindNode, }; use typed_path::UnixPath; @@ -39,9 +37,8 @@ fn test_find(tar_gz_testdata: Result, set_up_repo: Result) assert_with_win("find-nodes-not-found", not_found); // test non-existing match let glob = Glob::new("not_existing")?.compile_matcher(); - let not_found = repo.find_matching_nodes(vec![snapshot.tree], &|path, _| { - glob.is_match(PathBuf::try_from(path).unwrap()) - })?; + let not_found = + repo.find_matching_nodes(vec![snapshot.tree], &|path, _| glob.is_unix_match(path))?; assert_debug_snapshot!("find-matching-nodes-not-found", not_found); // test existing path @@ -51,10 +48,7 @@ fn test_find(tar_gz_testdata: Result, set_up_repo: Result) // test existing match let glob = Glob::new("testfile")?.compile_matcher(); let match_func = |path: &UnixPath, _: &Node| { - glob.is_match(PathBuf::try_from(path).unwrap()) - || path - .file_name() - .is_some_and(|f| glob.is_match(Path::new(&String::from_utf8(f.to_vec()).unwrap()))) + glob.is_unix_match(path) || path.file_name().is_some_and(|f| glob.is_unix_match(f)) }; let FindMatches { paths, matches, .. } = repo.find_matching_nodes(vec![snapshot.tree], &match_func)?; @@ -62,10 +56,7 @@ fn test_find(tar_gz_testdata: Result, set_up_repo: Result) // test existing match let glob = Glob::new("testfile*")?.compile_matcher(); let match_func = |path: &UnixPath, _: &Node| { - glob.is_match(PathBuf::try_from(path).unwrap()) - || path - .file_name() - .is_some_and(|f| glob.is_match(Path::new(&String::from_utf8(f.to_vec()).unwrap()))) + glob.is_unix_match(path) || path.file_name().is_some_and(|f| glob.is_unix_match(f)) }; let FindMatches { paths, matches, .. } = repo.find_matching_nodes(vec![snapshot.tree], &match_func)?; diff --git a/examples/find/examples/find.rs b/examples/find/examples/find.rs index df9b48da..b5c1854d 100644 --- a/examples/find/examples/find.rs +++ b/examples/find/examples/find.rs @@ -1,9 +1,9 @@ //! `ls` example use globset::Glob; use rustic_backend::BackendOptions; -use rustic_core::{FindMatches, Repository, RepositoryOptions}; +use rustic_core::{util::GlobMatcherExt, FindMatches, Repository, RepositoryOptions}; use simplelog::{Config, LevelFilter, SimpleLogger}; -use std::{error::Error, path::PathBuf}; +use std::error::Error; // don't warn about try_from paths conversion on unix #[allow(clippy::unnecessary_fallible_conversions)] @@ -32,9 +32,7 @@ fn main() -> Result<(), Box> { paths, nodes, matches, - } = repo.find_matching_nodes(tree_ids, &|path, _| { - glob.is_match(PathBuf::try_from(path).unwrap()) - })?; + } = repo.find_matching_nodes(tree_ids, &|path, _| glob.is_unix_match(path))?; for (snap, matches) in snapshots.iter().zip(matches) { println!("results in {snap:?}"); for (path_idx, node_idx) in matches {