1use regex::Regex;
7
8const STAR: &str = "__OPYS_GLOBSTAR_";
9
10fn is_rx_meta(c: char) -> bool {
11 matches!(c, '.' | '+' | '^' | '$' | '(' | ')' | '|' | '[' | ']' | '\\')
12}
13
14fn is_alt_meta(c: char) -> bool {
15 matches!(
16 c,
17 '.' | '+' | '^' | '$' | '(' | ')' | '|' | '[' | ']' | '\\' | '*' | '?' | '{' | '}'
18 )
19}
20
21pub fn glob_to_regex(glob: &str) -> Regex {
23 let mut s = glob.to_owned();
24 let null = '\0';
25 s = replace_pattern(&s, r"/\*\*$", &format!("{null}{STAR}END{null}"));
27 s = replace_pattern(&s, r"/\*\*/", &format!("{null}{STAR}MID{null}"));
28 s = replace_pattern(&s, r"^\*\*/", &format!("{null}{STAR}START{null}"));
29 s = replace_pattern(&s, r"\*\*", &format!("{null}{STAR}BARE{null}"));
30
31 let mut out = String::with_capacity(s.len() * 2);
32 let chars: Vec<char> = s.chars().collect();
33 let mut i = 0;
34 while i < chars.len() {
35 let ch = chars[i];
36 if ch == '\0' {
37 let close = chars[i + 1..]
38 .iter()
39 .position(|&c| c == '\0')
40 .map(|p| p + i + 1)
41 .expect("matching sentinel");
42 let tag: String = chars[i + 1 + STAR.len()..close].iter().collect();
43 out.push_str(match tag.as_str() {
44 "END" => "(?:/.*)?",
45 "MID" => "(?:/.*)?/",
46 "START" => "(?:.*/)?",
47 _ => ".*", });
49 i = close + 1;
50 } else if ch == '*' {
51 out.push_str("[^/]*");
52 i += 1;
53 } else if ch == '?' {
54 out.push_str("[^/]");
55 i += 1;
56 } else if ch == '{' {
57 if let Some(end) = chars[i..].iter().position(|&c| c == '}') {
58 let end = end + i;
59 let parts: String = chars[i + 1..end].iter().collect();
60 let alts: Vec<String> = parts
61 .split(',')
62 .map(|p| {
63 let mut esc = String::with_capacity(p.len() * 2);
64 for c in p.chars() {
65 if is_alt_meta(c) {
66 esc.push('\\');
67 }
68 esc.push(c);
69 }
70 esc
71 })
72 .collect();
73 out.push_str("(?:");
74 out.push_str(&alts.join("|"));
75 out.push(')');
76 i = end + 1;
77 } else {
78 out.push_str("\\{");
79 i += 1;
80 }
81 } else if is_rx_meta(ch) {
82 out.push('\\');
83 out.push(ch);
84 i += 1;
85 } else {
86 out.push(ch);
87 i += 1;
88 }
89 }
90 Regex::new(&format!("^{out}$")).expect("valid glob → regex")
91}
92
93fn replace_pattern(haystack: &str, pattern: &str, replacement: &str) -> String {
94 Regex::new(pattern).unwrap().replace_all(haystack, replacement).into_owned()
95}
96
97pub fn glob_base(glob: &str) -> String {
99 let mut last_slash: Option<usize> = None;
100 for (i, ch) in glob.char_indices() {
101 if matches!(ch, '*' | '?' | '{' | '[') {
102 break;
103 }
104 if ch == '/' {
105 last_slash = Some(i);
106 }
107 }
108 match last_slash {
109 Some(i) => glob[..i].to_owned(),
110 None => String::new(),
111 }
112}