1#![no_std]
20#![doc(html_root_url = "https://docs.rs/glob-parent/0.1.0")]
21#![allow(
24 clippy::cast_possible_wrap,
25 clippy::cast_sign_loss,
26 clippy::cast_possible_truncation
27)]
28
29extern crate alloc;
30
31use alloc::string::{String, ToString};
32use alloc::vec::Vec;
33
34#[cfg(doctest)]
36#[doc = include_str!("../README.md")]
37struct ReadmeDoctests;
38
39#[must_use]
47pub fn glob_parent(glob: &str) -> String {
48 glob_parent_with_options(glob, true)
49}
50
51#[must_use]
57pub fn glob_parent_with_options(glob: &str, flip_backslashes: bool) -> String {
58 let mut s = glob.to_string();
59
60 if flip_backslashes && cfg!(windows) && !s.contains('/') {
61 s = s.replace('\\', "/");
62 }
63
64 if is_enclosure(&s) {
66 s.push('/');
67 }
68
69 s.push('a');
71
72 loop {
74 s = posix_dirname(&s);
75 if !is_globby(&s) {
76 break;
77 }
78 }
79
80 unescape_glob(&s)
81}
82
83#[must_use]
90pub fn is_glob(s: &str) -> bool {
91 if s.is_empty() {
92 return false;
93 }
94 if is_extglob(s) {
95 return true;
96 }
97 let units: Vec<u16> = s.encode_utf16().collect();
99 is_glob_strict(&units)
100}
101
102#[must_use]
110pub fn is_extglob(s: &str) -> bool {
111 if s.is_empty() {
112 return false;
113 }
114 let units: Vec<u16> = s.encode_utf16().collect();
117 let len = units.len();
118 let mut start = 0;
119 while start < len {
120 let mut hit: Option<(usize, bool)> = None;
121 let mut p = start;
122 while p < len {
123 if units[p] == cu('\\') && units.get(p + 1).is_some_and(|&n| !is_line_terminator(n)) {
125 hit = Some((p, false));
126 break;
127 }
128 if is_extglob_char(units[p])
130 && units.get(p + 1) == Some(&cu('('))
131 && close_paren_before_line_terminator(&units[p + 2..])
132 {
133 hit = Some((p, true));
134 break;
135 }
136 p += 1;
137 }
138 match hit {
139 None => return false,
140 Some((_, true)) => return true,
141 Some((p, false)) => start = p + 2,
142 }
143 }
144 false
145}
146
147const fn cu(c: char) -> u16 {
149 c as u16
150}
151
152fn is_line_terminator(u: u16) -> bool {
154 matches!(u, 0x000A | 0x000D | 0x2028 | 0x2029)
155}
156
157fn is_extglob_char(u: u16) -> bool {
158 u == cu('@') || u == cu('?') || u == cu('!') || u == cu('+') || u == cu('*')
159}
160
161fn close_paren_before_line_terminator(units: &[u16]) -> bool {
163 for &u in units {
164 if u == cu(')') {
165 return true;
166 }
167 if is_line_terminator(u) {
168 return false;
169 }
170 }
171 false
172}
173
174fn find_from(units: &[u16], c: u16, from: usize) -> i64 {
180 if from > units.len() {
181 return -1;
182 }
183 units[from..]
184 .iter()
185 .position(|&x| x == c)
186 .map_or(-1, |p| (from + p) as i64)
187}
188
189#[allow(clippy::too_many_lines)]
191fn is_glob_strict(units: &[u16]) -> bool {
192 let len = units.len();
193 if units.first() == Some(&cu('!')) {
194 return true;
195 }
196 let mut index: usize = 0;
197 let mut pipe_index: i64 = -2;
198 let mut close_square: i64 = -2;
199 let mut close_curly: i64 = -2;
200 let mut close_paren: i64 = -2;
201 let mut back_slash: i64 = -2;
202
203 while index < len {
204 let c = units[index];
205 if c == cu('*') {
206 return true;
207 }
208 if units.get(index + 1) == Some(&cu('?'))
209 && (c == cu(']') || c == cu('.') || c == cu('+') || c == cu(')'))
210 {
211 return true;
212 }
213
214 if close_square != -1 && c == cu('[') && units.get(index + 1) != Some(&cu(']')) {
215 if close_square < index as i64 {
216 close_square = find_from(units, cu(']'), index);
217 }
218 if close_square > index as i64 {
219 if back_slash == -1 || back_slash > close_square {
220 return true;
221 }
222 back_slash = find_from(units, cu('\\'), index);
223 if back_slash == -1 || back_slash > close_square {
224 return true;
225 }
226 }
227 }
228
229 if close_curly != -1 && c == cu('{') && units.get(index + 1) != Some(&cu('}')) {
230 close_curly = find_from(units, cu('}'), index);
231 if close_curly > index as i64 {
232 back_slash = find_from(units, cu('\\'), index);
233 if back_slash == -1 || back_slash > close_curly {
234 return true;
235 }
236 }
237 }
238
239 if close_paren != -1
240 && c == cu('(')
241 && units.get(index + 1) == Some(&cu('?'))
242 && matches!(units.get(index + 2).copied(), Some(x) if x == cu(':') || x == cu('!') || x == cu('='))
243 && units.get(index + 3) != Some(&cu(')'))
244 {
245 close_paren = find_from(units, cu(')'), index);
246 if close_paren > index as i64 {
247 back_slash = find_from(units, cu('\\'), index);
248 if back_slash == -1 || back_slash > close_paren {
249 return true;
250 }
251 }
252 }
253
254 if pipe_index != -1 && c == cu('(') && units.get(index + 1) != Some(&cu('|')) {
255 if pipe_index < index as i64 {
256 pipe_index = find_from(units, cu('|'), index);
257 }
258 if pipe_index != -1 && units.get(pipe_index as usize + 1) != Some(&cu(')')) {
259 close_paren = find_from(units, cu(')'), pipe_index as usize);
260 if close_paren > pipe_index {
261 back_slash = find_from(units, cu('\\'), pipe_index as usize);
262 if back_slash == -1 || back_slash > close_paren {
263 return true;
264 }
265 }
266 }
267 }
268
269 if c == cu('\\') {
270 let open = units.get(index + 1).copied();
271 index += 2;
272 let close = if open == Some(cu('{')) {
273 Some(cu('}'))
274 } else if open == Some(cu('(')) {
275 Some(cu(')'))
276 } else if open == Some(cu('[')) {
277 Some(cu(']'))
278 } else {
279 None
280 };
281 if let Some(close) = close {
282 let n = find_from(units, close, index);
283 if n != -1 {
284 index = n as usize + 1;
285 }
286 }
287 if units.get(index) == Some(&cu('!')) {
288 return true;
289 }
290 } else {
291 index += 1;
292 }
293 }
294 false
295}
296
297fn posix_dirname(s: &str) -> String {
299 let chars: Vec<char> = s.chars().collect();
300 let len = chars.len();
301 if len == 0 {
302 return ".".to_string();
303 }
304 let has_root = chars[0] == '/';
305 let mut end: i64 = -1;
306 let mut matched_slash = true;
307 let mut i = len as i64 - 1;
308 while i >= 1 {
309 if chars[i as usize] == '/' {
310 if !matched_slash {
311 end = i;
312 break;
313 }
314 } else {
315 matched_slash = false;
316 }
317 i -= 1;
318 }
319 if end == -1 {
320 return if has_root { "/" } else { "." }.to_string();
321 }
322 if has_root && end == 1 {
323 return "//".to_string();
324 }
325 chars[..end as usize].iter().collect()
326}
327
328fn is_enclosure(s: &str) -> bool {
330 let chars: Vec<char> = s.chars().collect();
331 let n = chars.len();
332 if n == 0 {
333 return false;
334 }
335 let start = match chars[n - 1] {
336 '}' => '{',
337 ']' => '[',
338 _ => return false,
339 };
340 let Some(found) = chars.iter().position(|&c| c == start) else {
341 return false;
342 };
343 chars[found + 1..n - 1].contains(&'/')
344}
345
346fn is_globby(s: &str) -> bool {
348 let chars: Vec<char> = s.chars().collect();
349 if open_paren_tail(&chars) {
351 return true;
352 }
353 if matches!(chars.first(), Some('{' | '[')) {
354 return true;
355 }
356 for i in 1..chars.len() {
358 if matches!(chars[i], '{' | '[') && chars[i - 1] != '\\' {
359 return true;
360 }
361 }
362 is_glob(s)
364}
365
366fn open_paren_tail(chars: &[char]) -> bool {
369 let mut last = None;
370 for (i, &c) in chars.iter().enumerate() {
371 if c == '(' || c == ')' {
372 last = Some((i, c));
373 }
374 }
375 matches!(last, Some((i, '(')) if i + 1 < chars.len())
376}
377
378fn unescape_glob(s: &str) -> String {
380 let chars: Vec<char> = s.chars().collect();
381 let mut out = String::with_capacity(s.len());
382 let mut i = 0;
383 while i < chars.len() {
384 if chars[i] == '\\'
385 && matches!(
386 chars.get(i + 1),
387 Some('!' | '*' | '?' | '|' | '[' | ']' | '(' | ')' | '{' | '}')
388 )
389 {
390 out.push(chars[i + 1]);
391 i += 2;
392 } else {
393 out.push(chars[i]);
394 i += 1;
395 }
396 }
397 out
398}