simple_fs/list/
glob.rs

1use crate::{Error, Result, SPath, TOP_MAX_DEPTH};
2use camino::Utf8PathBuf;
3use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
4
5pub const DEFAULT_EXCLUDE_GLOBS: &[&str] = &["**/.git", "**/.DS_Store"];
6
7pub fn get_glob_set(globs: &[&str]) -> Result<GlobSet> {
8	let mut builder = GlobSetBuilder::new();
9
10	for &glob_str in globs {
11		let glob = GlobBuilder::new(glob_str)
12			// NOTE: Important to set to true, otherwise single "*" will pass through "/".
13			.literal_separator(true)
14			.build()
15			.map_err(|e| Error::GlobCantNew {
16				glob: glob_str.to_string(),
17				cause: e,
18			})?;
19		builder.add(glob);
20	}
21
22	let glob_set = builder.build().map_err(|e| Error::GlobSetCantBuild {
23		globs: globs.iter().map(|&v| v.to_string()).collect(),
24		cause: e,
25	})?;
26
27	Ok(glob_set)
28}
29
30pub fn longest_base_path_wild_free(pattern: &SPath) -> SPath {
31	let path = Utf8PathBuf::from(pattern);
32	let mut base_path = Utf8PathBuf::new();
33
34	for component in path.components() {
35		let component_str = component.as_os_str().to_string_lossy();
36		if component_str.contains('*') || component_str.contains('?') {
37			break;
38		}
39		base_path.push(component);
40	}
41
42	SPath::new(base_path)
43}
44
45/// Computes the maximum depth required for a set of glob patterns.
46///
47/// Logic:
48/// 1) If a depth is provided via the argument, it is returned directly.
49/// 2) Otherwise, if any pattern contains "**", returns TOP_MAX_DEPTH.
50/// 3) Else, calculates the maximum folder level from patterns (using the folder count),
51///    regardless if they contain a single "*" or only "/".
52///
53/// Returns at least 1.
54pub fn get_depth(patterns: &[&str], depth: Option<usize>) -> usize {
55	if let Some(user_depth) = depth {
56		return user_depth;
57	}
58	for &g in patterns {
59		if g.contains("**") {
60			return TOP_MAX_DEPTH;
61		}
62	}
63	let mut max_depth = 0;
64	for &g in patterns {
65		let depth_count = g.matches(['\\', '/']).count() + 1;
66		if depth_count > max_depth {
67			max_depth = depth_count;
68		}
69	}
70	max_depth.max(1)
71}
72
73// region:    --- Tests
74
75#[cfg(test)]
76mod tests {
77	use super::*;
78	type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
79
80	#[test]
81	fn test_glob_get_depth_no_depth_simple() -> Result<()> {
82		// -- Setup & Fixtures
83		let test_cases: &[(&[&str], usize)] = &[
84			(&["*/*"], 2),
85			(&["some/path/**/and*/"], TOP_MAX_DEPTH),
86			(&["*"], 1),
87			(&["a/b", "c/d/e/f"], 4),
88			(&[], 1),
89		];
90
91		// -- Exec & Check
92		for &(patterns, expected) in test_cases {
93			// -- Exec: Call get_depth without a provided depth
94			let depth = get_depth(patterns, None);
95			// -- Check: Verify returned depth matches expected value
96			assert_eq!(
97				depth, expected,
98				"For patterns {patterns:?}, expected depth {expected}, got {depth}",
99			);
100		}
101		Ok(())
102	}
103
104	#[test]
105	fn test_glob_get_depth_with_depth_custom() -> Result<()> {
106		// -- Setup & Fixtures
107		let test_cases: &[(&[&str], usize, usize)] = &[
108			(&["*/*"], 5, 5),
109			(&["some/path/**/and*/"], 10, 10),
110			(&["*"], 3, 3),
111			(&["a/b", "c/d/e/f"], 7, 7),
112			(&[], 4, 4),
113		];
114
115		// -- Exec & Check
116		for &(patterns, provided_depth, expected) in test_cases {
117			// -- Exec: Call get_depth with the provided depth value
118			let depth = get_depth(patterns, Some(provided_depth));
119			// -- Check: Verify returned depth equals expected value
120			assert_eq!(
121				depth, expected,
122				"For patterns {patterns:?} with provided depth {provided_depth}, expected depth {expected}, got {depth}",
123			);
124		}
125		Ok(())
126	}
127}
128
129// endregion: --- Tests