1use std::collections::BTreeSet;
7
8use crate::wildmatch::{wildmatch, WM_PATHNAME};
9
10#[derive(Debug, Clone)]
12pub struct NonConePatterns {
13 lines: Vec<String>,
14}
15
16impl NonConePatterns {
17 #[must_use]
19 pub fn from_lines(lines: Vec<String>) -> Self {
20 Self { lines }
21 }
22
23 #[must_use]
25 pub fn parse(content: &str) -> Self {
26 let lines = content
27 .lines()
28 .map(str::trim)
29 .filter(|l| !l.is_empty() && !l.starts_with('#'))
30 .map(String::from)
31 .collect();
32 Self { lines }
33 }
34
35 #[must_use]
37 pub fn path_included(&self, path: &str) -> bool {
38 let mut included = false;
39 for raw in &self.lines {
40 let (negated, core) = match raw.strip_prefix('!') {
41 Some(rest) => (true, rest),
42 None => (false, raw.as_str()),
43 };
44 let core = core.trim();
45 if core.is_empty() || core.starts_with('#') {
46 continue;
47 }
48 if non_cone_line_matches(core, path) {
49 included = !negated;
50 }
51 }
52 included
53 }
54}
55
56fn glob_special_unescaped(name: &[u8]) -> bool {
57 let mut i = 0usize;
58 while i < name.len() {
59 if name[i] == b'\\' {
60 i += 2;
61 continue;
62 }
63 if matches!(name[i], b'*' | b'?' | b'[') {
64 return true;
65 }
66 i += 1;
67 }
68 false
69}
70
71fn sparse_glob_match_star_crosses_slash(pattern: &[u8], text: &[u8]) -> bool {
72 let (mut pi, mut ti) = (0usize, 0usize);
73 let (mut star_p, mut star_t) = (usize::MAX, 0usize);
74 while ti < text.len() {
75 if pi < pattern.len() && (pattern[pi] == b'?' || pattern[pi] == text[ti]) {
76 pi += 1;
77 ti += 1;
78 } else if pi < pattern.len() && pattern[pi] == b'*' {
79 star_p = pi;
80 star_t = ti;
81 pi += 1;
82 } else if star_p != usize::MAX {
83 pi = star_p + 1;
84 star_t += 1;
85 ti = star_t;
86 } else {
87 return false;
88 }
89 }
90 while pi < pattern.len() && pattern[pi] == b'*' {
91 pi += 1;
92 }
93 pi == pattern.len()
94}
95
96fn sparse_pattern_matches_git_non_cone(pattern: &str, path: &str) -> bool {
98 let pat = pattern.trim();
99 if pat.is_empty() {
100 return false;
101 }
102
103 let anchored = pat.starts_with('/');
104 let pat = pat.trim_start_matches('/');
105
106 if let Some(dir) = pat.strip_suffix('/') {
107 if anchored && dir == "*" {
108 return path.contains('/');
109 }
110 if anchored {
111 return path == dir || path.starts_with(&format!("{dir}/"));
112 }
113 return path == dir
114 || path.starts_with(&format!("{dir}/"))
115 || path.split('/').any(|component| component == dir);
116 }
117
118 if anchored {
119 return sparse_glob_match_star_crosses_slash(pat.as_bytes(), path.as_bytes());
120 }
121 sparse_glob_match_star_crosses_slash(pat.as_bytes(), path.as_bytes())
122 || path.rsplit('/').next().is_some_and(|base| {
123 sparse_glob_match_star_crosses_slash(pat.as_bytes(), base.as_bytes())
124 })
125}
126
127fn non_cone_line_matches(pattern: &str, path: &str) -> bool {
128 sparse_pattern_matches_git_non_cone(pattern, path)
129}
130
131#[derive(Debug, Clone, Default)]
133pub struct ConePatterns {
134 pub full_cone: bool,
135 pub recursive_slash: BTreeSet<String>,
136 pub parent_slash: BTreeSet<String>,
137}
138
139#[derive(Clone, Copy, PartialEq, Eq)]
140enum ConeMatch {
141 Undecided,
142 Matched,
143 MatchedRecursive,
144 NotMatched,
145}
146
147impl ConePatterns {
148 #[must_use]
151 pub fn try_parse_with_warnings(content: &str, warnings: &mut Vec<String>) -> Option<Self> {
152 let lines: Vec<&str> = content
153 .lines()
154 .map(str::trim)
155 .filter(|l| !l.is_empty() && !l.starts_with('#'))
156 .collect();
157
158 let mut full_cone = false;
159 let mut recursive: BTreeSet<String> = BTreeSet::new();
160 let mut parents: BTreeSet<String> = BTreeSet::new();
161
162 for line in lines {
163 let (negated, rest) = if let Some(r) = line.strip_prefix('!') {
164 (true, r)
165 } else {
166 (false, line)
167 };
168
169 if negated && rest == "/*/" {
170 full_cone = false;
171 continue;
172 }
173 if !negated && rest == "/*" {
174 full_cone = true;
175 continue;
176 }
177
178 if negated && rest.ends_with("/*/") && rest.starts_with('/') && rest.len() > 4 {
179 let inner = &rest[1..rest.len() - 3];
180 if inner.is_empty()
181 || inner.contains('/')
182 || glob_special_unescaped(inner.as_bytes())
183 {
184 warnings.push(format!("warning: unrecognized negative pattern: '{rest}'"));
185 warnings.push("warning: disabling cone pattern matching".to_string());
186 return None;
187 }
188 let key = format!("/{inner}");
189 if !recursive.contains(&key) {
190 warnings.push(format!("warning: unrecognized negative pattern: '{rest}'"));
191 warnings.push("warning: disabling cone pattern matching".to_string());
192 return None;
193 }
194 recursive.remove(&key);
195 parents.insert(key);
196 continue;
197 }
198
199 if negated {
200 warnings.push(format!("warning: unrecognized negative pattern: '{rest}'"));
201 warnings.push("warning: disabling cone pattern matching".to_string());
202 return None;
203 }
204
205 if rest == "/*" {
206 continue;
207 }
208
209 if !rest.starts_with('/') {
210 warnings.push(format!("warning: unrecognized pattern: '{rest}'"));
211 warnings.push("warning: disabling cone pattern matching".to_string());
212 return None;
213 }
214 if rest.contains("**") {
215 warnings.push(format!("warning: unrecognized pattern: '{rest}'"));
216 warnings.push("warning: disabling cone pattern matching".to_string());
217 return None;
218 }
219 if rest.len() < 2 {
220 warnings.push(format!("warning: unrecognized pattern: '{rest}'"));
221 warnings.push("warning: disabling cone pattern matching".to_string());
222 return None;
223 }
224
225 let must_be_dir = rest.ends_with('/');
226 let body = rest[1..].trim_end_matches('/');
227 if body.is_empty() {
228 warnings.push(format!("warning: unrecognized pattern: '{rest}'"));
229 warnings.push("warning: disabling cone pattern matching".to_string());
230 return None;
231 }
232 if !must_be_dir {
233 warnings.push(format!("warning: unrecognized pattern: '{rest}'"));
234 warnings.push("warning: disabling cone pattern matching".to_string());
235 return None;
236 }
237 if glob_special_unescaped(body.as_bytes()) {
238 warnings.push(format!("warning: unrecognized pattern: '{rest}'"));
239 warnings.push("warning: disabling cone pattern matching".to_string());
240 return None;
241 }
242
243 let key = format!("/{body}");
244 if parents.contains(&key) {
245 warnings.push(format!(
246 "warning: your sparse-checkout file may have issues: pattern '{rest}' is repeated"
247 ));
248 warnings.push("warning: disabling cone pattern matching".to_string());
249 return None;
250 }
251 recursive.insert(key.clone());
252 let parts: Vec<&str> = body.split('/').collect();
253 for i in 1..parts.len() {
254 let prefix = parts[..i].join("/");
255 parents.insert(format!("/{prefix}"));
256 }
257 }
258
259 Some(ConePatterns {
260 full_cone,
261 recursive_slash: recursive,
262 parent_slash: parents,
263 })
264 }
265
266 #[must_use]
267 pub fn try_parse(content: &str) -> Option<Self> {
268 let mut w = Vec::new();
269 Self::try_parse_with_warnings(content, &mut w)
270 }
271
272 fn recursive_contains_parent(path: &str, recursive: &BTreeSet<String>) -> bool {
273 let mut buf = String::from("/");
274 buf.push_str(path);
275 let mut slash_pos = buf.rfind('/');
276 while let Some(pos) = slash_pos {
277 if pos == 0 {
278 break;
279 }
280 buf.truncate(pos);
281 if recursive.contains(&buf) {
282 return true;
283 }
284 slash_pos = buf.rfind('/');
285 }
286 false
287 }
288
289 fn path_matches_pattern_list(&self, pathname: &str) -> ConeMatch {
291 if self.full_cone {
292 return ConeMatch::Matched;
293 }
294
295 let mut parent_pathname = String::with_capacity(pathname.len() + 2);
296 parent_pathname.push('/');
297 parent_pathname.push_str(pathname);
298
299 let slash_pos = if parent_pathname.ends_with('/') {
300 let sp = parent_pathname.len() - 1;
301 parent_pathname.push('-');
302 sp
303 } else {
304 parent_pathname.rfind('/').unwrap_or(0)
305 };
306
307 if self.recursive_slash.contains(&parent_pathname) {
308 return ConeMatch::MatchedRecursive;
309 }
310
311 if slash_pos == 0 {
312 return ConeMatch::Matched;
313 }
314
315 let parent_key = parent_pathname[..slash_pos].to_string();
316 if self.parent_slash.contains(&parent_key) {
317 return ConeMatch::Matched;
318 }
319
320 if Self::recursive_contains_parent(pathname, &self.recursive_slash) {
321 return ConeMatch::MatchedRecursive;
322 }
323
324 ConeMatch::NotMatched
325 }
326
327 #[must_use]
329 pub fn path_included(&self, path: &str) -> bool {
330 if path.is_empty() {
331 return true;
332 }
333
334 let bytes = path.as_bytes();
335 let mut end = bytes.len();
336 let mut match_result = ConeMatch::Undecided;
337
338 while end > 0 && match_result == ConeMatch::Undecided {
339 let slice = path.get(..end).unwrap_or("");
340 match_result = self.path_matches_pattern_list(slice);
341
342 let mut slash = end.saturating_sub(1);
343 while slash > 0 && bytes[slash] != b'/' {
344 slash -= 1;
345 }
346 end = if bytes.get(slash) == Some(&b'/') {
347 slash
348 } else {
349 0
350 };
351 }
352
353 matches!(
354 match_result,
355 ConeMatch::Matched | ConeMatch::MatchedRecursive
356 )
357 }
358}
359
360#[must_use]
362pub fn load_sparse_checkout(
363 git_dir: &std::path::Path,
364 cone_config: bool,
365) -> (bool, Option<ConePatterns>, NonConePatterns) {
366 let mut w = Vec::new();
367 load_sparse_checkout_with_warnings(git_dir, cone_config, &mut w)
368}
369
370pub fn load_sparse_checkout_with_warnings(
372 git_dir: &std::path::Path,
373 cone_config: bool,
374 warnings: &mut Vec<String>,
375) -> (bool, Option<ConePatterns>, NonConePatterns) {
376 let path = git_dir.join("info").join("sparse-checkout");
377 let Ok(content) = std::fs::read_to_string(&path) else {
378 return (false, None, NonConePatterns { lines: Vec::new() });
379 };
380 let non_cone = NonConePatterns::parse(&content);
381 if !cone_config {
382 return (false, None, non_cone);
383 }
384 match ConePatterns::try_parse_with_warnings(&content, warnings) {
385 Some(cone) => (true, Some(cone), non_cone),
386 None => (false, None, non_cone),
387 }
388}
389
390#[must_use]
392pub fn path_in_sparse_checkout(
393 path: &str,
394 cone_config: bool,
395 cone: Option<&ConePatterns>,
396 non_cone: &NonConePatterns,
397) -> bool {
398 if cone_config {
399 if let Some(c) = cone {
400 return c.path_included(path);
401 }
402 }
403 non_cone.path_included(path)
404}
405
406#[derive(Debug, Clone, Default)]
408pub struct ConeWorkspace {
409 pub recursive_slash: BTreeSet<String>,
410 pub parent_slash: BTreeSet<String>,
411}
412
413impl ConeWorkspace {
414 #[must_use]
416 pub fn from_cone_patterns(cp: &ConePatterns) -> Self {
417 Self {
418 recursive_slash: cp.recursive_slash.clone(),
419 parent_slash: cp.parent_slash.clone(),
420 }
421 }
422
423 #[must_use]
425 pub fn from_directory_list(dirs: &[String]) -> Self {
426 let mut pruned: Vec<String> = dirs
427 .iter()
428 .map(|s| s.trim_start_matches('/').trim_end_matches('/').to_string())
429 .filter(|s| !s.is_empty())
430 .collect();
431 pruned.sort();
432 let mut kept: Vec<String> = Vec::new();
433 for d in pruned {
434 if kept
435 .iter()
436 .any(|p| d.starts_with(p) && d.as_bytes().get(p.len()) == Some(&b'/'))
437 {
438 continue;
439 }
440 kept.retain(|k| !(k.starts_with(&d) && k.as_bytes().get(d.len()) == Some(&b'/')));
441 kept.push(d);
442 }
443 let mut ws = ConeWorkspace::default();
444 for d in kept {
445 ws.insert_directory(&d);
446 }
447 ws
448 }
449
450 pub fn insert_directory(&mut self, rel: &str) {
452 let rel = rel.trim_start_matches('/');
453 let rel = rel.trim_end_matches('/');
454 if rel.is_empty() {
455 return;
456 }
457 let key = format!("/{rel}");
458 if self.parent_slash.contains(&key) {
459 return;
460 }
461 self.recursive_slash.insert(key.clone());
462 let parts: Vec<&str> = rel.split('/').collect();
463 for i in 1..parts.len() {
464 let prefix = parts[..i].join("/");
465 self.parent_slash.insert(format!("/{prefix}"));
466 }
467 }
468
469 fn recursive_contains_parent(path_slash: &str, recursive: &BTreeSet<String>) -> bool {
470 let mut buf = String::from(path_slash);
471 let mut slash_pos = buf.rfind('/');
472 while let Some(pos) = slash_pos {
473 if pos == 0 {
474 break;
475 }
476 buf.truncate(pos);
477 if recursive.contains(&buf) {
478 return true;
479 }
480 slash_pos = buf.rfind('/');
481 }
482 false
483 }
484
485 #[must_use]
487 pub fn to_sparse_checkout_file(&self) -> String {
488 let mut parent_only: Vec<&String> = self
489 .parent_slash
490 .iter()
491 .filter(|p| {
492 !self.recursive_slash.contains(*p)
493 && !Self::recursive_contains_parent(p, &self.recursive_slash)
494 })
495 .collect();
496 parent_only.sort();
497
498 let mut out = String::new();
499 out.push_str("/*\n!/*/\n");
500
501 for p in parent_only {
502 let esc = escape_cone_path_component(p);
503 out.push_str(&esc);
504 out.push_str("/\n!");
505 out.push_str(&esc);
506 out.push_str("/*/\n");
507 }
508
509 let mut rec_only: Vec<&String> = self
510 .recursive_slash
511 .iter()
512 .filter(|p| !Self::recursive_contains_parent(p, &self.recursive_slash))
513 .collect();
514 rec_only.sort();
515
516 for p in rec_only {
517 let esc = escape_cone_path_component(p);
518 out.push_str(&esc);
519 out.push_str("/\n");
520 }
521 out
522 }
523
524 #[must_use]
526 pub fn list_cone_directories(&self) -> Vec<String> {
527 let mut v: Vec<String> = self
528 .recursive_slash
529 .iter()
530 .map(|s| s.trim_start_matches('/').to_string())
531 .collect();
532 v.sort();
533 v
534 }
535}
536
537fn escape_cone_path_component(path_with_leading_slash: &str) -> String {
538 let mut out = String::new();
539 for ch in path_with_leading_slash.chars() {
540 if matches!(ch, '*' | '?' | '[' | '\\') {
541 out.push('\\');
542 }
543 out.push(ch);
544 }
545 out
546}
547
548pub fn parse_sparse_checkout_file(content: &str) -> Vec<String> {
550 content
551 .lines()
552 .map(|l| l.trim())
553 .filter(|l| !l.is_empty() && !l.starts_with('#'))
554 .map(String::from)
555 .collect()
556}
557
558pub fn sparse_checkout_lines_look_like_expanded_cone(lines: &[String]) -> bool {
561 lines.len() >= 2 && lines[0] == "/*" && lines[1] == "!/*/"
562}
563
564fn parse_expanded_cone_parent_recursive(lines: &[String]) -> Option<(Vec<String>, Vec<String>)> {
567 if !sparse_checkout_lines_look_like_expanded_cone(lines) {
568 return None;
569 }
570 let mut parents = Vec::new();
571 let mut recursive = Vec::new();
572 let mut i = 2usize;
573 while i + 1 < lines.len() {
574 let a = &lines[i];
575 let b = &lines[i + 1];
576 if !a.starts_with('/') || !a.ends_with('/') || !b.starts_with('!') {
577 break;
578 }
579 let inner_a = a.trim_start_matches('/').trim_end_matches('/');
580 let expected_neg = format!("!/{inner_a}/*/");
581 if b != &expected_neg {
582 break;
583 }
584 parents.push(inner_a.to_string());
585 i += 2;
586 }
587 while i < lines.len() {
588 let line = &lines[i];
589 if line.starts_with('!') {
590 return None;
591 }
592 if !line.starts_with('/') || !line.ends_with('/') {
593 return None;
594 }
595 let body = line.trim_start_matches('/').trim_end_matches('/');
596 if body.is_empty() {
597 return None;
598 }
599 recursive.push(body.to_string());
600 i += 1;
601 }
602 Some((parents, recursive))
603}
604
605fn path_in_expanded_cone(path: &str, lines: &[String]) -> bool {
606 let Some((parents, recursive)) = parse_expanded_cone_parent_recursive(lines) else {
607 return false;
608 };
609 let path = path.trim_start_matches('/').trim_end_matches('/');
610
611 if !path.contains('/') {
612 return true;
613 }
614
615 for r in &recursive {
616 if path == *r || path.starts_with(&format!("{r}/")) {
617 return true;
618 }
619 }
620
621 for p in &parents {
622 let p_slash = format!("{}/", p);
623 if path == *p {
624 return true;
625 }
626 if !path.starts_with(&p_slash) {
627 continue;
628 }
629 let rest = &path[p_slash.len()..];
630 let Some(slash_pos) = rest.find('/') else {
631 let combined = format!("{}/{}", p, rest);
634 return recursive
635 .iter()
636 .any(|r| r == &combined || r.starts_with(&format!("{combined}/")));
637 };
638 let first = &rest[..slash_pos];
639 let combined = format!("{}/{}", p, first);
640 for r in &recursive {
641 let under_r = path == *r || path.starts_with(&format!("{r}/"));
642 let r_covers = r == &combined || r.starts_with(&format!("{combined}/"));
643 if r_covers && under_r {
644 return true;
645 }
646 }
647 }
648
649 false
650}
651
652#[must_use]
658pub fn effective_cone_mode_for_sparse_file(cone_config: bool, lines: &[String]) -> bool {
659 cone_config && sparse_checkout_lines_look_like_expanded_cone(lines)
660}
661
662pub fn build_expanded_cone_sparse_checkout_lines(dirs: &[String]) -> Vec<String> {
668 let mut recursive: BTreeSet<String> = BTreeSet::new();
669 for d in dirs {
670 let t = d.trim().trim_start_matches('/').trim_end_matches('/');
671 if t.is_empty() {
672 continue;
673 }
674 recursive.insert(format!("/{t}"));
675 }
676
677 let mut parents: BTreeSet<String> = BTreeSet::new();
678 for r in &recursive {
679 let mut cur = r.clone();
680 loop {
681 let Some(slash) = cur.rfind('/') else {
682 break;
683 };
684 if slash == 0 {
685 break;
686 }
687 cur.truncate(slash);
688 parents.insert(cur.clone());
689 }
690 }
691
692 let mut out = vec!["/*".to_owned(), "!/*/".to_owned()];
693
694 for p in parents.iter() {
695 if recursive.contains(p) {
696 continue;
697 }
698 if recursive_set_has_strict_ancestor(&recursive, p) {
699 continue;
700 }
701 let esc = escape_cone_pattern_path(p);
702 out.push(format!("{esc}/"));
703 out.push(format!("!{esc}/*/"));
704 }
705
706 for r in recursive.iter() {
707 if recursive_set_has_strict_ancestor(&recursive, r) {
708 continue;
709 }
710 let esc = escape_cone_pattern_path(r);
711 out.push(format!("{esc}/"));
712 }
713
714 out
715}
716
717fn escape_cone_pattern_path(path_with_leading_slash: &str) -> String {
718 let mut out = String::with_capacity(path_with_leading_slash.len() + 8);
721 for ch in path_with_leading_slash.chars() {
722 match ch {
723 '\\' | '[' | '*' | '?' | '#' => {
724 out.push('\\');
725 out.push(ch);
726 }
727 _ => out.push(ch),
728 }
729 }
730 out
731}
732
733fn recursive_set_has_strict_ancestor(recursive: &BTreeSet<String>, path: &str) -> bool {
734 let mut cur = path.to_string();
735 loop {
736 let Some(slash) = cur.rfind('/') else {
737 break;
738 };
739 if slash == 0 {
740 break;
741 }
742 cur.truncate(slash);
743 if recursive.contains(&cur) {
744 return true;
745 }
746 }
747 false
748}
749
750pub fn parse_expanded_cone_recursive_dirs(lines: &[String]) -> Vec<String> {
753 if !sparse_checkout_lines_look_like_expanded_cone(lines) {
754 return Vec::new();
755 }
756 let mut i = 2usize;
757 let mut out = Vec::new();
758 while i < lines.len() {
759 let line = &lines[i];
760 if line.starts_with('!') {
761 i += 1;
762 continue;
763 }
764 if !line.ends_with('/') || !line.starts_with('/') {
765 i += 1;
766 continue;
767 }
768 let trimmed = line.trim_end_matches('/');
769 let body = trimmed.trim_start_matches('/');
770 let esc = escape_cone_pattern_path(trimmed);
771 let expected_neg = format!("!{esc}/*/");
772 if i + 1 < lines.len() && lines[i + 1] == expected_neg {
773 i += 2;
774 continue;
775 }
776 out.push(body.to_owned());
777 i += 1;
778 }
779 out
780}
781
782pub fn path_in_sparse_checkout_patterns(path: &str, patterns: &[String], cone_mode: bool) -> bool {
790 if path.is_empty() || patterns.is_empty() {
791 return true;
792 }
793
794 if sparse_checkout_lines_look_like_expanded_cone(patterns) {
797 return path_in_expanded_cone(path, patterns);
798 }
799
800 let use_cone_prefix = cone_mode;
802
803 let mut end = path.len();
804 while end > 0 {
805 if path_matches_sparse_patterns(&path[..end], patterns, use_cone_prefix) {
806 return true;
807 }
808 let Some(slash) = path[..end].rfind('/') else {
809 break;
810 };
811 end = slash;
812 }
813 false
814}
815
816pub fn path_in_cone_mode_sparse_checkout(
821 path: &str,
822 patterns: &[String],
823 cone_enabled: bool,
824) -> bool {
825 if !cone_enabled || patterns.is_empty() {
826 return true;
827 }
828 path_in_sparse_checkout_patterns(path, patterns, true)
829}
830
831pub fn path_matches_sparse_patterns(path: &str, patterns: &[String], cone_mode: bool) -> bool {
834 let expanded_cone = sparse_checkout_lines_look_like_expanded_cone(patterns);
835 if expanded_cone {
836 return path_in_expanded_cone(path, patterns);
837 }
838 let raw_cone_prefix = cone_mode && !expanded_cone;
841
842 if raw_cone_prefix {
843 if !path.contains('/') {
844 return true;
845 }
846
847 for pattern in patterns {
848 let prefix = pattern.trim_end_matches('/');
849 if path.starts_with(prefix) && path.as_bytes().get(prefix.len()) == Some(&b'/') {
850 return true;
851 }
852 if path == prefix {
853 return true;
854 }
855 }
856 return false;
857 }
858
859 let mut included = false;
860 for raw_pattern in patterns {
861 let pattern = raw_pattern.trim();
862 if pattern.is_empty() || pattern.starts_with('#') {
863 continue;
864 }
865
866 let (negated, core_pattern) = if let Some(rest) = pattern.strip_prefix('!') {
867 (true, rest)
868 } else {
869 (false, pattern)
870 };
871 if core_pattern.is_empty() || core_pattern == "/" {
872 continue;
873 }
874
875 let matches = if let Some(prefix_with_slash) = core_pattern.strip_suffix('/') {
876 let inner = prefix_with_slash.trim_start_matches('/');
878 if inner.is_empty() {
879 false
880 } else if negated && core_pattern == "/*/" {
881 let trimmed = path.trim_end_matches('/');
884 trimmed.contains('/')
885 } else if inner.contains('*') || inner.contains('?') || inner.contains('[') {
886 let pat = format!("{prefix_with_slash}/");
888 let text = format!("/{path}/");
889 wildmatch(pat.as_bytes(), text.as_bytes(), WM_PATHNAME)
890 } else {
891 path == inner || path.starts_with(&format!("{inner}/"))
892 }
893 } else if core_pattern.starts_with('/') {
894 let text = format!("/{}", path.trim_start_matches('/'));
896 wildmatch(core_pattern.as_bytes(), text.as_bytes(), WM_PATHNAME)
897 } else {
898 wildmatch(core_pattern.as_bytes(), path.as_bytes(), WM_PATHNAME)
899 };
900
901 if matches {
902 included = !negated;
903 }
904 }
905
906 included
907}