1use std::fmt;
2
3use bitflags::bitflags;
4use bstr::{BStr, ByteSlice};
5
6use crate::{pattern, wildmatch, Pattern};
7
8bitflags! {
9 #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
16 pub struct Mode: u32 {
17 const NO_SUB_DIR = 1 << 0;
19 const ENDS_WITH = 1 << 1;
21 const MUST_BE_DIR = 1 << 2;
23 const NEGATIVE = 1 << 3;
25 const ABSOLUTE = 1 << 4;
27 }
28}
29
30#[derive(Debug, PartialOrd, PartialEq, Copy, Clone, Hash, Ord, Eq)]
34pub enum Case {
35 Sensitive,
37 Fold,
39}
40
41impl Default for Case {
42 fn default() -> Self {
43 Case::Sensitive
44 }
45}
46
47impl Pattern {
48 pub fn from_bytes(text: &[u8]) -> Option<Self> {
50 crate::parse::pattern(text).map(|(text, mode, first_wildcard_pos)| Pattern {
51 text,
52 mode,
53 first_wildcard_pos,
54 })
55 }
56
57 pub fn is_negative(&self) -> bool {
59 self.mode.contains(Mode::NEGATIVE)
60 }
61
62 pub fn matches_repo_relative_path<'a>(
70 &self,
71 path: impl Into<&'a BStr>,
72 basename_start_pos: Option<usize>,
73 is_dir: Option<bool>,
74 case: Case,
75 ) -> bool {
76 let is_dir = is_dir.unwrap_or(false);
77 if !is_dir && self.mode.contains(pattern::Mode::MUST_BE_DIR) {
78 return false;
79 }
80
81 let flags = wildmatch::Mode::NO_MATCH_SLASH_LITERAL
82 | match case {
83 Case::Fold => wildmatch::Mode::IGNORE_CASE,
84 Case::Sensitive => wildmatch::Mode::empty(),
85 };
86 let path = path.into();
87 debug_assert_eq!(
88 basename_start_pos,
89 path.rfind_byte(b'/').map(|p| p + 1),
90 "BUG: invalid cached basename_start_pos provided"
91 );
92 debug_assert!(!path.starts_with(b"/"), "input path must be relative");
93
94 if self.mode.contains(pattern::Mode::NO_SUB_DIR) && !self.mode.contains(pattern::Mode::ABSOLUTE) {
95 let basename = &path[basename_start_pos.unwrap_or_default()..];
96 self.matches(basename, flags)
97 } else {
98 self.matches(path, flags)
99 }
100 }
101
102 fn matches<'a>(&self, value: impl Into<&'a BStr>, mode: wildmatch::Mode) -> bool {
109 let value = value.into();
110 match self.first_wildcard_pos {
111 Some(pos) if self.mode.contains(pattern::Mode::ENDS_WITH) && !value.contains(&b'/') => {
113 let text = &self.text[pos + 1..];
114 if mode.contains(wildmatch::Mode::IGNORE_CASE) {
115 value
116 .len()
117 .checked_sub(text.len())
118 .map(|start| text.eq_ignore_ascii_case(&value[start..]))
119 .unwrap_or(false)
120 } else {
121 value.ends_with(text.as_ref())
122 }
123 }
124 Some(pos) => {
125 if mode.contains(wildmatch::Mode::IGNORE_CASE) {
126 if !value
127 .get(..pos)
128 .map_or(false, |value| value.eq_ignore_ascii_case(&self.text[..pos]))
129 {
130 return false;
131 }
132 } else if !value.starts_with(&self.text[..pos]) {
133 return false;
134 }
135 crate::wildmatch(self.text.as_bstr(), value, mode)
136 }
137 None => {
138 if mode.contains(wildmatch::Mode::IGNORE_CASE) {
139 self.text.eq_ignore_ascii_case(value)
140 } else {
141 self.text == value
142 }
143 }
144 }
145 }
146}
147
148impl fmt::Display for Pattern {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 if self.mode.contains(Mode::NEGATIVE) {
151 "!".fmt(f)?;
152 }
153 if self.mode.contains(Mode::ABSOLUTE) {
154 "/".fmt(f)?;
155 }
156 self.text.fmt(f)?;
157 if self.mode.contains(Mode::MUST_BE_DIR) {
158 "/".fmt(f)?;
159 }
160 Ok(())
161 }
162}