1use regex::Regex;
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use std::sync::Mutex;
11
12lazy_static::lazy_static! {
13 static ref PATTERN_CACHE: Mutex<HashMap<String, Regex>> = Mutex::new(HashMap::new());
15}
16
17pub fn translate_pattern(pattern: &str) -> String {
28 let mut regex_pattern = String::new();
29 let mut i = 0;
30 let chars: Vec<char> = pattern.chars().collect();
31 let n = chars.len();
32
33 while i < n {
34 let c = chars[i];
35 match c {
36 '*' => {
37 if i + 1 < n && chars[i + 1] == '*' {
38 regex_pattern.push_str(".*");
43 i += 2;
44 } else {
45 regex_pattern.push_str("[^/]*");
47 i += 1;
48 }
49 }
50 '?' => {
51 regex_pattern.push_str("[^/]");
53 i += 1;
54 }
55 '[' => {
56 let mut j = i + 1;
60 if j < n && chars[j] == '!' {
61 j += 1;
62 }
63 if j < n && chars[j] == ']' {
64 j += 1;
65 }
66 while j < n && chars[j] != ']' {
67 j += 1;
68 }
69 if j >= n {
70 regex_pattern.push_str("\\[");
72 i += 1;
73 } else {
74 let body: String = chars[i + 1..j].iter().collect();
81 let mut stuff = body.replace('\\', "\\\\");
82 if let Some(rest) = stuff.strip_prefix('!') {
83 stuff = format!("^/{rest}");
84 } else if stuff.starts_with('^') {
85 stuff.insert(0, '\\');
86 }
87
88 regex_pattern.push('[');
89 regex_pattern.push_str(&stuff);
90 regex_pattern.push(']');
91 i = j + 1;
92 }
93 }
94 _ => {
95 match c {
97 '\\' | '.' | '^' | '$' | '+' | '{' | '}' | '|' | '(' | ')' => {
98 regex_pattern.push('\\');
99 regex_pattern.push(c);
100 }
101 _ => {
102 regex_pattern.push(c);
103 }
104 }
105 i += 1;
106 }
107 }
108 }
109
110 format!("^{}$", regex_pattern)
112}
113
114pub fn compile_pattern(pattern: &str) -> Result<Regex, regex::Error> {
116 let mut cache = PATTERN_CACHE.lock().unwrap();
117
118 if let Some(regex) = cache.get(pattern) {
119 return Ok(regex.clone());
120 }
121
122 let regex_pattern = translate_pattern(pattern);
123 let regex = Regex::new(®ex_pattern)?;
124 cache.insert(pattern.to_string(), regex.clone());
125
126 Ok(regex)
127}
128
129pub fn pattern_match(name: &str, pattern: &str) -> Result<bool, regex::Error> {
131 let regex = compile_pattern(pattern)?;
132 Ok(regex.is_match(name))
133}
134
135pub fn pattern_filter(names: &[String], pattern: &str) -> Result<Vec<String>, regex::Error> {
137 let regex = compile_pattern(pattern)?;
138 Ok(names
139 .iter()
140 .filter(|name| regex.is_match(name))
141 .cloned()
142 .collect())
143}
144
145pub fn normalize_path(path: &Path) -> String {
148 path.to_string_lossy().replace('\\', "/")
149}
150
151pub fn get_matching_files<P: AsRef<Path>>(
159 dirname: P,
160 include_patterns: &[String],
161 exclude_patterns: &[String],
162) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
163 let dirname = dirname.as_ref().canonicalize()?;
164 let include_patterns = if include_patterns.is_empty() {
165 vec!["**".to_string()]
166 } else {
167 include_patterns.to_vec()
168 };
169
170 let mut include_regexes = Vec::new();
172 for pattern in &include_patterns {
173 include_regexes.push(compile_pattern(pattern)?);
174 }
175
176 let mut exclude_regexes = Vec::new();
177 for pattern in exclude_patterns {
178 exclude_regexes.push(compile_pattern(pattern)?);
179 }
180
181 let mut matched_files = Vec::new();
182
183 fn walk_dir(
185 dir: &Path,
186 base_dir: &Path,
187 include_regexes: &[Regex],
188 exclude_regexes: &[Regex],
189 matched_files: &mut Vec<PathBuf>,
190 ) -> Result<(), Box<dyn std::error::Error>> {
191 if !dir.is_dir() {
192 return Ok(());
193 }
194
195 for entry in std::fs::read_dir(dir)? {
196 let entry = entry?;
197 let path = entry.path();
198
199 if path.is_dir() {
200 let relative_path = path.strip_prefix(base_dir)?;
205 let normalized_path = normalize_path(relative_path);
206
207 let excluded = exclude_regexes
208 .iter()
209 .any(|regex| regex.is_match(&normalized_path));
210
211 if !excluded {
212 walk_dir(
213 &path,
214 base_dir,
215 include_regexes,
216 exclude_regexes,
217 matched_files,
218 )?;
219 }
220 } else if path.is_file() {
221 let relative_path = path.strip_prefix(base_dir)?;
223 let normalized_path = normalize_path(relative_path);
224
225 let included = include_regexes
227 .iter()
228 .any(|regex| regex.is_match(&normalized_path));
229
230 if included {
231 let excluded = exclude_regexes
233 .iter()
234 .any(|regex| regex.is_match(&normalized_path));
235
236 if !excluded {
237 matched_files.push(path);
238 }
239 }
240 }
241 }
242
243 Ok(())
244 }
245
246 walk_dir(
247 &dirname,
248 &dirname,
249 &include_regexes,
250 &exclude_regexes,
251 &mut matched_files,
252 )?;
253
254 matched_files.sort();
256
257 Ok(matched_files)
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use std::fs;
264 use tempfile::TempDir;
265
266 #[test]
267 fn test_translate_pattern() {
268 assert_eq!(translate_pattern("*.rst"), "^[^/]*\\.rst$");
270 assert_eq!(translate_pattern("**"), "^.*$");
271 assert_eq!(translate_pattern("**/index.rst"), "^.*/index\\.rst$");
272 assert_eq!(translate_pattern("docs/*.rst"), "^docs/[^/]*\\.rst$");
273 assert_eq!(translate_pattern("***"), "^.*[^/]*$");
274
275 assert_eq!(translate_pattern("[abc].rst"), "^[abc]\\.rst$");
277 assert_eq!(translate_pattern("[!abc].rst"), "^[^/abc]\\.rst$");
278 assert_eq!(translate_pattern("[^abc].rst"), "^[\\^abc]\\.rst$");
279 assert_eq!(translate_pattern("[\\d]x"), "^[\\\\d]x$");
280 }
281
282 #[test]
283 fn test_double_star_matches_sphinx() {
284 assert!(!pattern_match("foo/bar", "foo/**/bar").unwrap());
288 assert!(pattern_match("foo/x/bar", "foo/**/bar").unwrap());
289 assert!(pattern_match("foo/x/y/bar", "foo/**/bar").unwrap());
290 assert!(!pattern_match("bar", "**/bar").unwrap());
291 assert!(pattern_match("x/bar", "**/bar").unwrap());
292 assert!(!pattern_match("foo", "foo/**").unwrap());
293 assert!(pattern_match("foo/x/y", "foo/**").unwrap());
294 assert!(pattern_match("foo/bar.rst", "**").unwrap());
295 assert!(!pattern_match("foo/bar", "*").unwrap());
296 assert!(pattern_match("foo", "*").unwrap());
297 assert!(!pattern_match("a/b.rst", "*.rst").unwrap());
298 assert!(pattern_match("a/b.rst", "**.rst").unwrap());
299 assert!(pattern_match("ab", "a**b").unwrap());
300 assert!(pattern_match("axx/yyb", "a**b").unwrap());
301 }
302
303 #[test]
304 fn test_character_class_matches_sphinx() {
305 assert!(pattern_match("bx", "[!a]x").unwrap());
306 assert!(!pattern_match("/x", "[!a]x").unwrap());
307 assert!(pattern_match("^x", "[^a]x").unwrap());
309 assert!(pattern_match("ax", "[^a]x").unwrap());
310 assert!(!pattern_match("bx", "[^a]x").unwrap());
311 assert!(pattern_match("dx", "[\\d]x").unwrap());
314 assert!(!pattern_match("5x", "[\\d]x").unwrap());
315 assert!(pattern_match("\\x", "[\\d]x").unwrap());
316 assert!(pattern_match("]", "[]a]").unwrap());
319 assert!(pattern_match("a", "[]a]").unwrap());
320 assert!(pattern_match("]a]", "[!]a]").unwrap());
323 assert!(pattern_match("xa]", "[!]a]").unwrap());
324 }
325
326 #[test]
327 fn test_pattern_match() {
328 assert!(pattern_match("index.rst", "*.rst").unwrap());
330 assert!(pattern_match("docs/index.rst", "**/*.rst").unwrap());
331 assert!(pattern_match("docs/api/module.rst", "**/api/*.rst").unwrap());
332
333 assert!(!pattern_match("_build/index.html", "*.rst").unwrap());
335 assert!(pattern_match("_build/index.html", "**").unwrap());
336
337 assert!(pattern_match("a.rst", "[abc].rst").unwrap());
339 assert!(!pattern_match("d.rst", "[abc].rst").unwrap());
340 assert!(!pattern_match("a.rst", "[!abc].rst").unwrap());
341 assert!(pattern_match("d.rst", "[!abc].rst").unwrap());
342 assert!(!pattern_match("/.rst", "[!abc].rst").unwrap());
344 }
345
346 #[test]
347 fn test_directory_pruning_matches_sphinx() {
348 let make_tree = || {
353 let temp_dir = TempDir::new().unwrap();
354 let base = temp_dir.path().to_path_buf();
355 fs::create_dir_all(base.join("_build/deep")).unwrap();
356 fs::write(base.join("index.rst"), "Index").unwrap();
357 fs::write(base.join("_build/stale.rst"), "Stale").unwrap();
358 fs::write(base.join("_build/deep/stale.rst"), "Stale").unwrap();
359 (temp_dir, base)
360 };
361
362 let include = vec!["**".to_string()];
363
364 let (_t1, base) = make_tree();
366 let files = get_matching_files(&base, &include, &["_build".to_string()]).unwrap();
367 let names: Vec<String> = files
368 .iter()
369 .map(|f| normalize_path(f.strip_prefix(base.canonicalize().unwrap()).unwrap()))
370 .collect();
371 assert_eq!(names, vec!["index.rst"]);
372
373 let (_t2, base) = make_tree();
375 let files = get_matching_files(&base, &include, &["_build/**".to_string()]).unwrap();
376 let names: Vec<String> = files
377 .iter()
378 .map(|f| normalize_path(f.strip_prefix(base.canonicalize().unwrap()).unwrap()))
379 .collect();
380 assert_eq!(names, vec!["index.rst"]);
381
382 let (_t3, base) = make_tree();
384 let files = get_matching_files(&base, &include, &["_build/".to_string()]).unwrap();
385 let names: Vec<String> = files
386 .iter()
387 .map(|f| normalize_path(f.strip_prefix(base.canonicalize().unwrap()).unwrap()))
388 .collect();
389 assert_eq!(
390 names,
391 vec!["_build/deep/stale.rst", "_build/stale.rst", "index.rst"]
392 );
393 }
394
395 #[test]
396 fn test_get_matching_files() {
397 let temp_dir = TempDir::new().unwrap();
398 let base_path = temp_dir.path();
399
400 fs::create_dir_all(base_path.join("docs")).unwrap();
402 fs::create_dir_all(base_path.join("_build")).unwrap();
403 fs::write(base_path.join("index.rst"), "content").unwrap();
404 fs::write(base_path.join("docs/api.rst"), "content").unwrap();
405 fs::write(base_path.join("_build/index.html"), "content").unwrap();
406 fs::write(base_path.join("README.md"), "content").unwrap();
407
408 let files = get_matching_files(base_path, &["**/*.rst".to_string()], &[]).unwrap();
411 assert_eq!(files.len(), 1);
412 assert!(!files.iter().any(|p| p.file_name().unwrap() == "index.rst"));
413 assert!(files.iter().any(|p| p.file_name().unwrap() == "api.rst"));
414
415 let files =
417 get_matching_files(base_path, &["**".to_string()], &["_build/**".to_string()]).unwrap();
418 assert!(!files.iter().any(|p| p.to_string_lossy().contains("_build")));
419
420 let files = get_matching_files(
424 base_path,
425 &["**/*.rst".to_string()],
426 &["docs/**".to_string()],
427 )
428 .unwrap();
429 assert!(files.is_empty());
430 }
431}