1use sley_core::GitError;
22use std::cell::Cell;
23use std::fs;
24use std::path::Path;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct PathspecElement {
33 pattern: Vec<u8>,
35 exclude: bool,
37 icase: bool,
39 literal: bool,
41 glob: bool,
43 top: bool,
48 attrs: Vec<Vec<u8>>,
53 attr_requirements: Vec<PathspecAttrRequirement>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum PathspecAttrRequirement {
58 Set(Vec<u8>),
59 Unset(Vec<u8>),
60 Unspecified(Vec<u8>),
61 Value { name: Vec<u8>, value: Vec<u8> },
62}
63
64impl PathspecElement {
65 pub fn parse(arg: &[u8], defaults: PathspecMatchMagic) -> Result<Self, PathspecParseError> {
73 let mut exclude = false;
74 let mut icase = defaults.icase;
75 let mut literal = defaults.literal;
76 let mut glob = defaults.glob;
77 let mut top = false;
78 let mut attrs: Vec<Vec<u8>> = Vec::new();
79 let mut attr_requirements: Vec<PathspecAttrRequirement> = Vec::new();
80 let mut explicit_literal = false;
81 let mut explicit_glob = false;
82
83 let rest = if defaults.literal_pathspecs {
84 arg
85 } else if let Some(after) = arg.strip_prefix(b":(") {
86 let close = after
88 .iter()
89 .position(|&c| c == b')')
90 .ok_or(PathspecParseError::UnterminatedMagic)?;
91 let magic = &after[..close];
92 for word in split_magic(magic) {
93 match word.as_slice() {
94 b"exclude" => exclude = true,
95 b"icase" => icase = true,
96 b"literal" => {
97 explicit_literal = true;
98 literal = true;
99 glob = false;
100 }
101 b"glob" => {
102 explicit_glob = true;
103 glob = true;
104 literal = false;
105 }
106 b"top" => top = true,
107 other => {
108 if let Some(attr) = other.strip_prefix(b"attr:") {
109 if !attrs.is_empty() {
110 return Err(PathspecParseError::MultipleAttrMagic);
111 }
112 attrs.push(attr.to_vec());
113 attr_requirements = parse_attr_requirements(attr)?;
114 } else if other.is_empty() {
115 } else {
118 return Err(PathspecParseError::UnknownMagic(other.to_vec()));
119 }
120 }
121 }
122 }
123 &after[close + 1..]
124 } else if let Some(after) = arg.strip_prefix(b":") {
125 let mut idx = 0;
127 while idx < after.len() {
128 match after[idx] {
129 b'!' | b'^' => exclude = true,
130 b'/' => top = true,
131 _ => break,
132 }
133 idx += 1;
134 }
135 &after[idx..]
136 } else {
137 arg
138 };
139
140 if (glob && literal) || (explicit_glob && explicit_literal) {
142 return Err(PathspecParseError::GlobLiteralConflict);
143 }
144
145 Ok(PathspecElement {
146 pattern: rest.to_vec(),
147 exclude,
148 icase,
149 literal,
150 glob,
151 top,
152 attrs,
153 attr_requirements,
154 })
155 }
156
157 pub fn is_exclude(&self) -> bool {
159 self.exclude
160 }
161
162 pub fn is_top(&self) -> bool {
164 self.top
165 }
166
167 pub fn attrs(&self) -> &[Vec<u8>] {
169 &self.attrs
170 }
171
172 pub fn attr_requirements(&self) -> &[PathspecAttrRequirement] {
173 &self.attr_requirements
174 }
175
176 pub fn is_icase(&self) -> bool {
178 self.icase
179 }
180
181 pub fn is_glob(&self) -> bool {
183 self.glob
184 }
185
186 pub fn pattern(&self) -> &[u8] {
188 &self.pattern
189 }
190
191 pub fn magic(&self) -> PathspecMatchMagic {
193 PathspecMatchMagic {
194 literal: self.literal,
195 glob: self.glob,
196 icase: self.icase,
197 literal_pathspecs: false,
198 }
199 }
200
201 pub fn matches_path(&self, name: &[u8]) -> bool {
205 pathspec_item_matches(&self.pattern, name, self.magic())
206 }
207
208 pub fn with_pattern(mut self, pattern: Vec<u8>) -> Self {
209 self.pattern = pattern;
210 self
211 }
212}
213
214#[derive(Debug, Clone, Default, PartialEq, Eq)]
222pub struct Pathspec {
223 elements: Vec<PathspecElement>,
224}
225
226impl Pathspec {
227 pub fn parse<I, S>(args: I, defaults: PathspecMatchMagic) -> Result<Self, PathspecParseError>
230 where
231 I: IntoIterator<Item = S>,
232 S: AsRef<[u8]>,
233 {
234 let mut elements = Vec::new();
235 for arg in args {
236 elements.push(PathspecElement::parse(arg.as_ref(), defaults)?);
237 }
238 Ok(Pathspec { elements })
239 }
240
241 pub fn from_elements(elements: Vec<PathspecElement>) -> Self {
242 Self { elements }
243 }
244
245 pub fn is_empty(&self) -> bool {
247 self.elements.is_empty()
248 }
249
250 pub fn elements(&self) -> &[PathspecElement] {
252 &self.elements
253 }
254
255 pub fn matches(&self, path: &[u8]) -> bool {
260 if self.elements.is_empty() {
261 return true;
262 }
263 let mut have_include = false;
264 let mut included = false;
265 for element in &self.elements {
266 if element.exclude {
267 if element.matches_path(path) {
268 return false;
269 }
270 } else {
271 have_include = true;
272 if element.matches_path(path) {
273 included = true;
274 }
275 }
276 }
277 if have_include { included } else { true }
280 }
281}
282
283pub struct LsFilesPathFilter {
284 pub original: String,
285 pub recursive: bool,
286 pub is_glob: bool,
287 pub element: PathspecElement,
288 pub matched: Cell<bool>,
289}
290
291impl LsFilesPathFilter {
292 pub fn is_exclude(&self) -> bool {
293 self.element.is_exclude()
294 }
295
296 pub fn matches(&self, path: &[u8]) -> bool {
297 let path_no_slash = path.strip_suffix(b"/").unwrap_or(path);
300 self.element.matches_path(path)
301 || (path_no_slash.len() != path.len() && self.element.matches_path(path_no_slash))
302 }
303}
304
305pub fn pathspec_filters_match(filters: &[LsFilesPathFilter], path: &[u8]) -> bool {
306 pathspec_filters_match_with(filters, path, |filter, path| filter.matches(path))
307}
308
309pub fn pathspec_filters_have_include(filters: &[LsFilesPathFilter]) -> bool {
310 filters.iter().any(|filter| !filter.is_exclude())
311}
312
313pub fn pathspec_filters_match_with(
314 filters: &[LsFilesPathFilter],
315 path: &[u8],
316 mut matches: impl FnMut(&LsFilesPathFilter, &[u8]) -> bool,
317) -> bool {
318 let mut have_include = false;
319 let mut included = false;
320 for filter in filters {
321 if filter.is_exclude() {
322 if matches(filter, path) {
323 filter.matched.set(true);
324 return false;
325 }
326 } else {
327 have_include = true;
328 if matches(filter, path) {
329 filter.matched.set(true);
330 included = true;
331 }
332 }
333 }
334 !have_include || included
335}
336
337#[derive(Debug, Clone, PartialEq, Eq)]
338pub enum PathspecAttributeState {
339 Set,
340 Unset,
341 Value(Vec<u8>),
342}
343
344#[derive(Debug, Clone, PartialEq, Eq)]
345pub struct PathspecAttributeCheck {
346 pub attribute: Vec<u8>,
347 pub state: Option<PathspecAttributeState>,
348}
349
350pub fn pathspec_attrs_match_with(
351 element: &PathspecElement,
352 checks: impl FnOnce(&[Vec<u8>]) -> Vec<PathspecAttributeCheck>,
353) -> bool {
354 let requirements = element.attr_requirements();
355 if requirements.is_empty() {
356 return true;
357 }
358 let requested = requirements
359 .iter()
360 .map(|requirement| match requirement {
361 PathspecAttrRequirement::Set(name)
362 | PathspecAttrRequirement::Unset(name)
363 | PathspecAttrRequirement::Unspecified(name) => name.clone(),
364 PathspecAttrRequirement::Value { name, .. } => name.clone(),
365 })
366 .collect::<Vec<_>>();
367 let checks = checks(&requested);
368 requirements.iter().all(|requirement| {
369 let (name, expected) = match requirement {
370 PathspecAttrRequirement::Set(name) => (name, AttrRequirementKind::Set),
371 PathspecAttrRequirement::Unset(name) => (name, AttrRequirementKind::Unset),
372 PathspecAttrRequirement::Unspecified(name) => (name, AttrRequirementKind::Unspecified),
373 PathspecAttrRequirement::Value { name, value } => {
374 (name, AttrRequirementKind::Value(value))
375 }
376 };
377 let state = checks
378 .iter()
379 .find(|check| &check.attribute == name)
380 .and_then(|check| check.state.as_ref());
381 match expected {
382 AttrRequirementKind::Set => matches!(state, Some(PathspecAttributeState::Set)),
383 AttrRequirementKind::Unset => matches!(state, Some(PathspecAttributeState::Unset)),
384 AttrRequirementKind::Unspecified => state.is_none(),
385 AttrRequirementKind::Value(value) => {
386 matches!(state, Some(PathspecAttributeState::Value(actual)) if actual == value)
387 }
388 }
389 })
390}
391
392enum AttrRequirementKind<'a> {
393 Set,
394 Unset,
395 Unspecified,
396 Value(&'a [u8]),
397}
398
399pub fn parse_normalized_pathspec_element(
400 prefix: &[u8],
401 arg: &str,
402 magic: PathspecMatchMagic,
403) -> sley_core::Result<PathspecElement> {
404 let element = PathspecElement::parse(arg.as_bytes(), magic)
405 .map_err(|err| GitError::Command(format!("bad pathspec: {err}")))?;
406 let base = if element.is_top() {
407 b"".as_slice()
408 } else {
409 prefix
410 };
411 let pattern = normalize_ls_files_pathspec(base, &String::from_utf8_lossy(element.pattern()))?;
412 Ok(element.with_pattern(pattern))
413}
414
415pub fn normalized_revwalk_pathspec(
416 cwd: &Path,
417 worktree_root: Option<&Path>,
418 pathspecs: &[String],
419 magic: PathspecMatchMagic,
420) -> sley_core::Result<Pathspec> {
421 let (prefix, root_and_cwd) = if let Some(root) = worktree_root {
422 let root = fs::canonicalize(root)?;
423 let cwd = fs::canonicalize(cwd)?;
424 let prefix = cwd
425 .strip_prefix(&root)
426 .map(|relative| relative.to_string_lossy().replace('\\', "/").into_bytes())
427 .unwrap_or_default();
428 (prefix, Some((root, cwd)))
429 } else {
430 (Vec::new(), None)
431 };
432 let elements = pathspecs
433 .iter()
434 .map(|spec| {
435 let parse_spec = match root_and_cwd.as_ref() {
436 Some((root, cwd)) => normalize_absolute_pathspec_arg(root, cwd, spec)?,
437 None => spec.to_string(),
438 };
439 parse_normalized_pathspec_element(&prefix, &parse_spec, magic)
440 })
441 .collect::<sley_core::Result<Vec<_>>>()?;
442 Ok(Pathspec::from_elements(elements))
443}
444
445fn normalize_absolute_pathspec_arg(
446 root: &Path,
447 cwd: &Path,
448 arg: &str,
449) -> sley_core::Result<String> {
450 let path = Path::new(arg);
451 if !path.is_absolute() {
452 return Ok(arg.to_string());
453 }
454 let absolute = fs::canonicalize(path)?;
455 let relative = absolute
456 .strip_prefix(root)
457 .map_err(|_| GitError::InvalidPath(format!("pathspec {arg} is outside worktree")))?;
458 let repo_path = relative.to_string_lossy().replace('\\', "/");
459 if repo_path.is_empty() {
460 return Ok(":/".to_string());
461 }
462 if cwd == root {
463 return Ok(repo_path);
464 }
465 Ok(format!(":(top){repo_path}"))
466}
467
468pub fn normalize_ls_files_pathspec(prefix: &[u8], arg: &str) -> sley_core::Result<Vec<u8>> {
469 let mut components = prefix
470 .split(|byte| *byte == b'/')
471 .filter(|component| !component.is_empty())
472 .map(Vec::from)
473 .collect::<Vec<_>>();
474 for component in Path::new(arg).components() {
475 match component {
476 std::path::Component::CurDir => {}
477 std::path::Component::ParentDir => {
478 components.pop().ok_or_else(|| {
479 GitError::InvalidPath(format!("pathspec {arg} is outside worktree"))
480 })?;
481 }
482 std::path::Component::Normal(name) => {
483 components.push(name.to_string_lossy().as_bytes().to_vec());
484 }
485 std::path::Component::RootDir | std::path::Component::Prefix(_) => {
486 return Err(GitError::Unsupported(
487 "ls-files pathspecs currently support relative paths".into(),
488 ));
489 }
490 }
491 }
492 Ok(components.join(&b'/'))
493}
494
495fn split_magic(body: &[u8]) -> Vec<Vec<u8>> {
497 let mut words = Vec::new();
498 let mut word = Vec::new();
499 let mut escaped = false;
500 for &byte in body {
501 if escaped {
502 word.push(byte);
503 escaped = false;
504 } else if byte == b'\\' {
505 word.push(byte);
506 escaped = true;
507 } else if byte == b',' {
508 words.push(std::mem::take(&mut word));
509 } else {
510 word.push(byte);
511 }
512 }
513 words.push(word);
514 words
515}
516
517fn parse_attr_requirements(
518 body: &[u8],
519) -> Result<Vec<PathspecAttrRequirement>, PathspecParseError> {
520 if body.is_empty() {
521 return Err(PathspecParseError::EmptyAttrMagic);
522 }
523 let mut requirements = Vec::new();
524 for raw in body.split(|byte| byte.is_ascii_whitespace()) {
525 if raw.is_empty() {
526 continue;
527 }
528 requirements.push(parse_attr_requirement(raw)?);
529 }
530 if requirements.is_empty() {
531 return Err(PathspecParseError::EmptyAttrMagic);
532 }
533 Ok(requirements)
534}
535
536fn parse_attr_requirement(raw: &[u8]) -> Result<PathspecAttrRequirement, PathspecParseError> {
537 if let Some(rest) = raw.strip_prefix(b"-") {
538 if rest.contains(&b'=') {
539 return Err(PathspecParseError::InvalidAttrSpec(raw.to_vec()));
540 }
541 validate_attr_name(rest)?;
542 return Ok(PathspecAttrRequirement::Unset(rest.to_vec()));
543 }
544 if let Some(rest) = raw.strip_prefix(b"!") {
545 if rest.contains(&b'=') {
546 return Err(PathspecParseError::InvalidAttrSpec(raw.to_vec()));
547 }
548 validate_attr_name(rest)?;
549 return Ok(PathspecAttrRequirement::Unspecified(rest.to_vec()));
550 }
551 if let Some(equal) = raw.iter().position(|byte| *byte == b'=') {
552 let name = &raw[..equal];
553 let value = unescape_attr_value(&raw[equal + 1..])?;
554 validate_attr_name(name)?;
555 return Ok(PathspecAttrRequirement::Value {
556 name: name.to_vec(),
557 value,
558 });
559 }
560 validate_attr_name(raw)?;
561 Ok(PathspecAttrRequirement::Set(raw.to_vec()))
562}
563
564fn validate_attr_name(name: &[u8]) -> Result<(), PathspecParseError> {
565 if name.is_empty()
566 || !name
567 .iter()
568 .all(|byte| byte.is_ascii_alphanumeric() || matches!(*byte, b'-' | b'_' | b'.'))
569 {
570 return Err(PathspecParseError::InvalidAttrSpec(name.to_vec()));
571 }
572 Ok(())
573}
574
575fn unescape_attr_value(value: &[u8]) -> Result<Vec<u8>, PathspecParseError> {
576 let mut out = Vec::with_capacity(value.len());
577 let mut idx = 0usize;
578 while idx < value.len() {
579 if value[idx] != b'\\' {
580 out.push(value[idx]);
581 idx += 1;
582 continue;
583 }
584 let Some(&next) = value.get(idx + 1) else {
585 return Err(PathspecParseError::AttrValueTrailingBackslash);
586 };
587 if next != b',' {
588 return Err(PathspecParseError::AttrValueUnsupportedBackslash);
589 }
590 out.push(next);
591 idx += 2;
592 }
593 Ok(out)
594}
595
596#[derive(Debug, Clone, PartialEq, Eq)]
598pub enum PathspecParseError {
599 UnterminatedMagic,
601 UnknownMagic(Vec<u8>),
603 GlobLiteralConflict,
605 EmptyAttrMagic,
606 MultipleAttrMagic,
607 InvalidAttrSpec(Vec<u8>),
608 AttrValueTrailingBackslash,
609 AttrValueUnsupportedBackslash,
610}
611
612impl core::fmt::Display for PathspecParseError {
613 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
614 match self {
615 PathspecParseError::UnterminatedMagic => {
616 write!(f, "Missing ')' at end of pathspec magic")
617 }
618 PathspecParseError::UnknownMagic(word) => {
619 write!(
620 f,
621 "Invalid pathspec magic '{}'",
622 String::from_utf8_lossy(word)
623 )
624 }
625 PathspecParseError::GlobLiteralConflict => {
626 write!(f, "'literal' and 'glob' are incompatible")
627 }
628 PathspecParseError::EmptyAttrMagic => write!(f, "empty attr magic is not allowed"),
629 PathspecParseError::MultipleAttrMagic => {
630 write!(f, "Only one 'attr:' specification is allowed")
631 }
632 PathspecParseError::InvalidAttrSpec(spec) => write!(
633 f,
634 "invalid attribute specification '{}'",
635 String::from_utf8_lossy(spec)
636 ),
637 PathspecParseError::AttrValueTrailingBackslash => {
638 write!(
639 f,
640 "Escape character '\\' not allowed as last character in attr value"
641 )
642 }
643 PathspecParseError::AttrValueUnsupportedBackslash => {
644 write!(f, "Only '\\,' is supported for value matching")
645 }
646 }
647 }
648}
649
650impl std::error::Error for PathspecParseError {}
651
652#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
656pub struct PathspecMatchMagic {
657 pub literal: bool,
658 pub glob: bool,
659 pub icase: bool,
660 pub literal_pathspecs: bool,
663}
664
665fn is_glob_special(c: u8) -> bool {
667 matches!(c, b'*' | b'?' | b'[' | b'\\')
668}
669
670fn simple_length(s: &[u8]) -> usize {
673 for (i, &c) in s.iter().enumerate() {
674 if is_glob_special(c) {
675 return i;
676 }
677 }
678 s.len()
679}
680
681fn ps_strncmp(icase: bool, a: &[u8], b: &[u8], n: usize) -> bool {
683 let a = &a[..a.len().min(n)];
685 let b = &b[..b.len().min(n)];
686 if a.len() < n && b.len() < n && a.len() != b.len() {
687 return false;
688 }
689 let len = n.min(a.len()).min(b.len());
690 for i in 0..len {
691 let (mut ca, mut cb) = (a[i], b[i]);
692 if icase {
693 ca = ca.to_ascii_lowercase();
694 cb = cb.to_ascii_lowercase();
695 }
696 if ca != cb {
697 return false;
698 }
699 }
700 true
701}
702
703pub fn pathspec_is_glob(path: &[u8]) -> bool {
705 path.iter().any(|byte| is_glob_special(*byte))
706}
707
708pub fn pathspec_item_matches(match_: &[u8], name: &[u8], magic: PathspecMatchMagic) -> bool {
714 let icase = magic.icase;
715 let matchlen = match_.len();
716 let namelen = name.len();
717
718 let nowildcard_len = if magic.literal {
720 matchlen
721 } else {
722 simple_length(match_)
723 };
724
725 if matchlen == 0 {
727 return true;
728 }
729
730 if matchlen <= namelen && ps_strncmp(icase, match_, name, matchlen) {
732 if matchlen == namelen {
733 return true; }
735 if match_[matchlen - 1] == b'/' || name[matchlen] == b'/' {
736 return true; }
738 } else if match_[matchlen - 1] == b'/'
739 && namelen == matchlen - 1
740 && ps_strncmp(icase, match_, name, namelen)
741 {
742 return true;
744 }
745
746 if nowildcard_len < matchlen {
748 if nowildcard_len > 0 && !ps_strncmp(icase, match_, name, nowildcard_len) {
751 return false;
752 }
753 let pat = &match_[nowildcard_len..];
754 if name.len() < nowildcard_len {
755 return false;
756 }
757 let str_ = &name[nowildcard_len..];
758
759 let flags = if magic.glob && !magic.literal {
760 WM_PATHNAME | if icase { WM_CASEFOLD } else { 0 }
761 } else {
762 if icase { WM_CASEFOLD } else { 0 }
764 };
765 if wildmatch(pat, str_, flags) {
766 return true;
767 }
768 }
769
770 false
771}
772
773pub const WM_CASEFOLD: u32 = 1;
775pub const WM_PATHNAME: u32 = 2;
778
779const WM_MATCH: i32 = 0;
780const WM_NOMATCH: i32 = 1;
781const WM_ABORT_ALL: i32 = -1;
782const WM_ABORT_TO_STARSTAR: i32 = -2;
783
784#[inline]
785fn wm_isascii(c: u8) -> bool {
786 c < 0x80
787}
788#[inline]
789fn wm_isupper(c: u8) -> bool {
790 wm_isascii(c) && c.is_ascii_uppercase()
791}
792#[inline]
793fn wm_islower(c: u8) -> bool {
794 wm_isascii(c) && c.is_ascii_lowercase()
795}
796#[inline]
797fn wm_tolower(c: u8) -> u8 {
798 c.to_ascii_lowercase()
799}
800#[inline]
801fn wm_toupper(c: u8) -> u8 {
802 c.to_ascii_uppercase()
803}
804#[inline]
805fn wm_is_glob_special(c: u8) -> bool {
806 matches!(c, b'*' | b'?' | b'[' | b'\\')
807}
808
809fn wm_cc_eq(class: &[u8], lit: &[u8]) -> bool {
810 class == lit
811}
812
813fn wm_class_matches(class: &[u8], t_ch: u8, flags: u32) -> Option<bool> {
814 let m = if wm_cc_eq(class, b"alnum") {
817 wm_isascii(t_ch) && t_ch.is_ascii_alphanumeric()
818 } else if wm_cc_eq(class, b"alpha") {
819 wm_isascii(t_ch) && t_ch.is_ascii_alphabetic()
820 } else if wm_cc_eq(class, b"blank") {
821 wm_isascii(t_ch) && (t_ch == b' ' || t_ch == b'\t')
822 } else if wm_cc_eq(class, b"cntrl") {
823 wm_isascii(t_ch) && t_ch.is_ascii_control()
824 } else if wm_cc_eq(class, b"digit") {
825 wm_isascii(t_ch) && t_ch.is_ascii_digit()
826 } else if wm_cc_eq(class, b"graph") {
827 wm_isascii(t_ch) && t_ch.is_ascii_graphic()
828 } else if wm_cc_eq(class, b"lower") {
829 wm_islower(t_ch)
830 } else if wm_cc_eq(class, b"print") {
831 wm_isascii(t_ch) && (0x20..=0x7e).contains(&t_ch)
833 } else if wm_cc_eq(class, b"punct") {
834 wm_isascii(t_ch) && t_ch.is_ascii_punctuation()
835 } else if wm_cc_eq(class, b"space") {
836 wm_isascii(t_ch) && t_ch.is_ascii_whitespace()
837 } else if wm_cc_eq(class, b"upper") {
838 wm_isupper(t_ch) || ((flags & WM_CASEFOLD) != 0 && wm_islower(t_ch))
839 } else if wm_cc_eq(class, b"xdigit") {
840 wm_isascii(t_ch) && t_ch.is_ascii_hexdigit()
841 } else {
842 return None;
843 };
844 Some(m)
845}
846
847fn dowild(pattern: &[u8], text: &[u8], flags: u32) -> i32 {
850 let p = pattern;
851 let mut pi = 0usize;
852 let mut ti = 0usize;
853
854 while pi < p.len() {
855 let mut p_ch = p[pi];
856 let t_ch_raw = if ti < text.len() { text[ti] } else { 0 };
857 let mut t_ch = t_ch_raw;
858
859 if t_ch == 0 && p_ch != b'*' {
860 return WM_ABORT_ALL;
861 }
862 if (flags & WM_CASEFOLD) != 0 && wm_isupper(t_ch) {
863 t_ch = wm_tolower(t_ch);
864 }
865 if (flags & WM_CASEFOLD) != 0 && wm_isupper(p_ch) {
866 p_ch = wm_tolower(p_ch);
867 }
868
869 match p_ch {
870 b'?' => {
871 if (flags & WM_PATHNAME) != 0 && t_ch == b'/' {
872 return WM_NOMATCH;
873 }
874 pi += 1;
876 ti += 1;
877 continue;
878 }
879 b'*' => {
880 pi += 1;
881 let match_slash: bool;
882 if pi < p.len() && p[pi] == b'*' {
883 let prev_p = pi; while pi < p.len() && p[pi] == b'*' {
885 pi += 1;
886 }
887 if (flags & WM_PATHNAME) == 0 {
888 match_slash = true;
889 } else if (prev_p < 2 || p[prev_p - 2] == b'/')
890 && (pi == p.len()
891 || p[pi] == b'/'
892 || (p[pi] == b'\\' && pi + 1 < p.len() && p[pi + 1] == b'/'))
893 {
894 if pi < p.len()
895 && p[pi] == b'/'
896 && dowild(&p[pi + 1..], &text[ti..], flags) == WM_MATCH
897 {
898 return WM_MATCH;
899 }
900 match_slash = true;
901 } else {
902 match_slash = false;
903 }
904 } else {
905 match_slash = (flags & WM_PATHNAME) == 0;
906 }
907
908 if pi == p.len() {
909 if !match_slash && text[ti..].contains(&b'/') {
912 return WM_ABORT_TO_STARSTAR;
913 }
914 return WM_MATCH;
915 } else if !match_slash && p[pi] == b'/' {
916 match text[ti..].iter().position(|&c| c == b'/') {
919 None => return WM_ABORT_ALL,
920 Some(off) => {
921 ti += off; }
923 }
924 pi += 1;
926 ti += 1;
927 continue;
928 }
929
930 let mut cur_t = ti;
932 loop {
933 let mut tc = if cur_t < text.len() { text[cur_t] } else { 0 };
934 if tc == 0 {
935 break;
936 }
937 if !wm_is_glob_special(p[pi]) {
938 let mut pc = p[pi];
939 if (flags & WM_CASEFOLD) != 0 && wm_isupper(pc) {
940 pc = wm_tolower(pc);
941 }
942 loop {
943 tc = if cur_t < text.len() { text[cur_t] } else { 0 };
944 if tc == 0 {
945 break;
946 }
947 if !(match_slash || tc != b'/') {
948 break;
949 }
950 let mut tcf = tc;
951 if (flags & WM_CASEFOLD) != 0 && wm_isupper(tcf) {
952 tcf = wm_tolower(tcf);
953 }
954 if tcf == pc {
955 break;
956 }
957 cur_t += 1;
958 }
959 let tc_cmp = {
961 let raw = if cur_t < text.len() { text[cur_t] } else { 0 };
962 if (flags & WM_CASEFOLD) != 0 && wm_isupper(raw) {
963 wm_tolower(raw)
964 } else {
965 raw
966 }
967 };
968 if tc_cmp != pc {
969 if match_slash {
970 return WM_ABORT_ALL;
971 } else {
972 return WM_ABORT_TO_STARSTAR;
973 }
974 }
975 }
976 let matched = dowild(&p[pi..], &text[cur_t..], flags);
977 if matched != WM_NOMATCH {
978 if !match_slash || matched != WM_ABORT_TO_STARSTAR {
979 return matched;
980 }
981 } else {
982 let cur_raw = if cur_t < text.len() { text[cur_t] } else { 0 };
983 if !match_slash && cur_raw == b'/' {
984 return WM_ABORT_TO_STARSTAR;
985 }
986 }
987 cur_t += 1;
988 }
989 return WM_ABORT_ALL;
990 }
991 b'[' => {
992 pi += 1;
993 let mut p_ch2 = if pi < p.len() { p[pi] } else { 0 };
994 if p_ch2 == b'^' {
995 p_ch2 = b'!';
996 }
997 let negated = p_ch2 == b'!';
998 if negated {
999 pi += 1;
1000 p_ch2 = if pi < p.len() { p[pi] } else { 0 };
1001 }
1002 let mut prev_ch: u8 = 0;
1003 let mut matched = false;
1004 loop {
1005 if p_ch2 == 0 {
1006 return WM_ABORT_ALL;
1007 }
1008 let mut next_prev: u8 = p_ch2;
1009 let mut skip_class = false;
1010 if p_ch2 == b'\\' {
1011 pi += 1;
1012 p_ch2 = if pi < p.len() { p[pi] } else { 0 };
1013 if p_ch2 == 0 {
1014 return WM_ABORT_ALL;
1015 }
1016 if t_ch == p_ch2 {
1017 matched = true;
1018 }
1019 next_prev = p_ch2;
1020 } else if p_ch2 == b'-' && prev_ch != 0 && pi + 1 < p.len() && p[pi + 1] != b']'
1021 {
1022 pi += 1;
1023 p_ch2 = p[pi];
1024 if p_ch2 == b'\\' {
1025 pi += 1;
1026 p_ch2 = if pi < p.len() { p[pi] } else { 0 };
1027 if p_ch2 == 0 {
1028 return WM_ABORT_ALL;
1029 }
1030 }
1031 if t_ch <= p_ch2 && t_ch >= prev_ch {
1032 matched = true;
1033 } else if (flags & WM_CASEFOLD) != 0 && wm_islower(t_ch) {
1034 let t_up = wm_toupper(t_ch);
1035 if t_up <= p_ch2 && t_up >= prev_ch {
1036 matched = true;
1037 }
1038 }
1039 next_prev = 0;
1040 } else if p_ch2 == b'[' && pi + 1 < p.len() && p[pi + 1] == b':' {
1041 let s = pi + 2;
1043 let mut scan = s;
1044 loop {
1045 if scan >= p.len() {
1046 break;
1047 }
1048 if p[scan] == b']' {
1049 break;
1050 }
1051 scan += 1;
1052 }
1053 pi = scan;
1054 p_ch2 = if pi < p.len() { p[pi] } else { 0 };
1055 if p_ch2 == 0 {
1056 return WM_ABORT_ALL;
1057 }
1058 let class_end = pi; if class_end < s + 1 || p[class_end - 1] != b':' {
1061 pi = s.wrapping_sub(2);
1063 p_ch2 = b'[';
1064 if t_ch == p_ch2 {
1065 matched = true;
1066 }
1067 skip_class = true;
1068 next_prev = p_ch2;
1069 } else {
1070 let class = &p[s..class_end - 1];
1071 match wm_class_matches(class, t_ch, flags) {
1072 Some(true) => matched = true,
1073 Some(false) => {}
1074 None => return WM_ABORT_ALL,
1075 }
1076 next_prev = 0;
1077 }
1078 } else if t_ch == p_ch2 {
1079 matched = true;
1080 }
1081
1082 let _ = skip_class;
1083 prev_ch = next_prev;
1085 pi += 1;
1086 p_ch2 = if pi < p.len() { p[pi] } else { 0 };
1087 if p_ch2 == b']' {
1088 break;
1089 }
1090 }
1091 if matched == negated || ((flags & WM_PATHNAME) != 0 && t_ch == b'/') {
1092 return WM_NOMATCH;
1093 }
1094 pi += 1;
1095 ti += 1;
1096 continue;
1097 }
1098 b'\\' => {
1099 pi += 1;
1102 let lit = if pi < p.len() { p[pi] } else { 0 };
1103 let lit = if (flags & WM_CASEFOLD) != 0 && wm_isupper(lit) {
1104 wm_tolower(lit)
1105 } else {
1106 lit
1107 };
1108 if t_ch != lit {
1109 return WM_NOMATCH;
1110 }
1111 pi += 1;
1112 ti += 1;
1113 continue;
1114 }
1115 _ => {
1116 if t_ch != p_ch {
1117 return WM_NOMATCH;
1118 }
1119 pi += 1;
1120 ti += 1;
1121 continue;
1122 }
1123 }
1124 }
1125
1126 if ti < text.len() && text[ti] != 0 {
1127 WM_NOMATCH
1128 } else {
1129 WM_MATCH
1130 }
1131}
1132
1133pub fn wildmatch(pattern: &[u8], text: &[u8], flags: u32) -> bool {
1136 dowild(pattern, text, flags) == WM_MATCH
1137}
1138
1139#[cfg(test)]
1140mod tests {
1141 use super::*;
1142
1143 fn ps(args: &[&str]) -> Pathspec {
1144 Pathspec::parse(
1145 args.iter().map(|s| s.as_bytes()),
1146 PathspecMatchMagic::default(),
1147 )
1148 .expect("valid pathspec")
1149 }
1150
1151 #[test]
1152 fn empty_pathspec_matches_everything() {
1153 let p = Pathspec::default();
1154 assert!(p.is_empty());
1155 assert!(p.matches(b"any/path"));
1156 }
1157
1158 #[test]
1159 fn literal_prefix_matches_directory_recursively() {
1160 let p = ps(&["src"]);
1161 assert!(p.matches(b"src"));
1162 assert!(p.matches(b"src/lib.rs"));
1163 assert!(!p.matches(b"srcs/lib.rs"));
1164 assert!(!p.matches(b"other"));
1165 }
1166
1167 #[test]
1168 fn exclude_subtracts_from_includes() {
1169 let p = ps(&["src", ":(exclude)src/gen"]);
1170 assert!(p.matches(b"src/lib.rs"));
1171 assert!(!p.matches(b"src/gen/x.rs"));
1172 }
1173
1174 #[test]
1175 fn exclude_shorthand_sigils() {
1176 for spec in [":!foo", ":^foo"] {
1177 let p = ps(&[spec]);
1178 assert!(p.elements()[0].is_exclude());
1179 assert!(p.matches(b"bar"));
1181 assert!(!p.matches(b"foo"));
1182 }
1183 }
1184
1185 #[test]
1186 fn icase_magic_folds_case() {
1187 let p = ps(&[":(icase)readme"]);
1188 assert!(p.matches(b"README"));
1189 assert!(p.matches(b"readme"));
1190 let plain = ps(&["readme"]);
1191 assert!(!plain.matches(b"README"));
1192 }
1193
1194 #[test]
1195 fn glob_magic_is_pathname_aware() {
1196 let p = ps(&[":(glob)*.rs"]);
1198 assert!(p.matches(b"lib.rs"));
1199 assert!(!p.matches(b"src/lib.rs"));
1200 let pp = ps(&[":(glob)**/*.rs"]);
1202 assert!(pp.matches(b"src/lib.rs"));
1203 }
1204
1205 #[test]
1206 fn default_wildcard_can_cross_directory_separator() {
1207 let p = ps(&["*file3"]);
1208 assert!(p.matches(b"file3"));
1209 assert!(p.matches(b"subdir/file3"));
1210
1211 let glob = ps(&[":(glob)*file3"]);
1212 assert!(glob.matches(b"file3"));
1213 assert!(!glob.matches(b"subdir/file3"));
1214 }
1215
1216 #[test]
1217 fn literal_magic_disables_wildcards() {
1218 let p = ps(&[":(literal)a*b"]);
1219 assert!(p.matches(b"a*b"));
1220 assert!(!p.matches(b"axxb"));
1221 }
1222
1223 #[test]
1224 fn backslash_marks_pathspec_as_glob_special() {
1225 assert!(pathspec_is_glob(br"a\*b"));
1226 assert!(pathspec_is_glob(br"a\?b"));
1227 assert!(pathspec_is_glob(br"a\[b"));
1228 assert!(!pathspec_is_glob(b"plain/path"));
1229 }
1230
1231 #[test]
1232 fn escaped_wildcards_match_literal_bytes() {
1233 let p = ps(&[r"a\*b", r"a\?b", r"a\[b"]);
1234 assert!(p.matches(b"a*b"));
1235 assert!(p.matches(b"a?b"));
1236 assert!(p.matches(b"a[b"));
1237 assert!(!p.matches(b"axxb"));
1238 assert!(!p.matches(b"acb"));
1239 }
1240
1241 #[test]
1242 fn explicit_glob_literal_magic_overrides_global_defaults() {
1243 let noglob_default = PathspecMatchMagic {
1244 literal: true,
1245 glob: false,
1246 icase: false,
1247 literal_pathspecs: false,
1248 };
1249 let glob = PathspecElement::parse(b":(glob)*.rs", noglob_default).expect("glob override");
1250 assert!(glob.is_glob());
1251 assert!(!glob.magic().literal);
1252 assert!(glob.matches_path(b"lib.rs"));
1253 assert!(!glob.matches_path(b"src/lib.rs"));
1254
1255 let glob_default = PathspecMatchMagic {
1256 literal: false,
1257 glob: true,
1258 icase: false,
1259 literal_pathspecs: false,
1260 };
1261 let literal =
1262 PathspecElement::parse(b":(literal)*.rs", glob_default).expect("literal override");
1263 assert!(!literal.is_glob());
1264 assert!(literal.magic().literal);
1265 assert!(literal.matches_path(b"*.rs"));
1266 assert!(!literal.matches_path(b"lib.rs"));
1267 }
1268
1269 #[test]
1270 fn top_magic_is_parsed() {
1271 let p = ps(&[":(top)src", ":/other"]);
1272 assert!(p.elements()[0].is_top());
1273 assert!(p.elements()[1].is_top());
1274 }
1275
1276 #[test]
1277 fn attr_magic_is_retained() {
1278 let p = ps(&[":(attr:binary)data"]);
1279 assert_eq!(p.elements()[0].attrs(), &[b"binary".to_vec()]);
1280 assert_eq!(p.elements()[0].pattern(), b"data");
1281 }
1282
1283 #[test]
1284 fn combined_magic_words() {
1285 let p = ps(&[":(exclude,icase)Cargo.lock"]);
1286 let el = &p.elements()[0];
1287 assert!(el.is_exclude());
1288 assert!(!p.matches(b"CARGO.LOCK"));
1290 }
1291
1292 fn parse_err(arg: &[u8]) -> PathspecParseError {
1293 match Pathspec::parse([arg], PathspecMatchMagic::default()) {
1294 Ok(_) => panic!(
1295 "expected parse error for {:?}",
1296 String::from_utf8_lossy(arg)
1297 ),
1298 Err(e) => e,
1299 }
1300 }
1301
1302 #[test]
1303 fn glob_literal_conflict_is_error() {
1304 assert_eq!(
1305 parse_err(b":(glob,literal)x"),
1306 PathspecParseError::GlobLiteralConflict
1307 );
1308 }
1309
1310 #[test]
1311 fn unknown_magic_is_error() {
1312 assert!(matches!(
1313 parse_err(b":(bogus)x"),
1314 PathspecParseError::UnknownMagic(_)
1315 ));
1316 }
1317
1318 #[test]
1319 fn unterminated_magic_is_error() {
1320 assert_eq!(
1321 parse_err(b":(exclude"),
1322 PathspecParseError::UnterminatedMagic
1323 );
1324 }
1325
1326 #[test]
1327 fn exclude_only_keeps_unmatched() {
1328 let p = ps(&[":(exclude)target"]);
1329 assert!(p.matches(b"src/lib.rs"));
1330 assert!(!p.matches(b"target/debug"));
1331 }
1332}