]> git.proxmox.com Git - pathpatterns.git/blob - src/match_list.rs
Replace MatchList type with a trait.
[pathpatterns.git] / src / match_list.rs
1 //! Helpers for include/exclude lists.
2
3 use bitflags::bitflags;
4
5 use crate::PatternFlag;
6
7 #[rustfmt::skip]
8 bitflags! {
9 /// These flags influence what kind of paths should be matched.
10 pub struct MatchFlag: u16 {
11 /// Match only a complete entry. The pattern `bar` will not match `/foo/bar`.
12 const ANCHORED = 0x00_01;
13
14 const MATCH_DIRECTORIES = 0x01_00;
15 const MATCH_REGULAR_FILES = 0x02_00;
16 const MATCH_SYMLINKS = 0x04_00;
17 const MATCH_SOCKETS = 0x08_00;
18 const MATCH_FIFOS = 0x10_00;
19 const MATCH_CHARDEVS = 0x20_00;
20 const MATCH_BLOCKDEVS = 0x40_00;
21 const MATCH_DEVICES =
22 MatchFlag::MATCH_CHARDEVS.bits() | MatchFlag::MATCH_BLOCKDEVS.bits();
23
24 /// This is the default.
25 const ANY_FILE_TYPE =
26 MatchFlag::MATCH_DIRECTORIES.bits()
27 | MatchFlag::MATCH_REGULAR_FILES.bits()
28 | MatchFlag::MATCH_SYMLINKS.bits()
29 | MatchFlag::MATCH_SOCKETS.bits()
30 | MatchFlag::MATCH_FIFOS.bits()
31 | MatchFlag::MATCH_CHARDEVS.bits()
32 | MatchFlag::MATCH_BLOCKDEVS.bits();
33 }
34 }
35
36 impl Default for MatchFlag {
37 fn default() -> Self {
38 Self::ANY_FILE_TYPE
39 }
40 }
41
42 /// A pattern entry. For now this only contains glob patterns, but we may want to add regex
43 /// patterns or user defined callback functions later on as well.
44 ///
45 /// For regex we'd likely use the POSIX extended REs via `regexec(3)`, since we're targetting
46 /// command line interfaces and want something command line users are used to.
47 #[derive(Clone, Debug)]
48 pub enum MatchPattern {
49 /// A glob pattern.
50 Pattern(crate::Pattern),
51
52 /// A literal match.
53 Literal(Vec<u8>),
54 }
55
56 impl From<crate::Pattern> for MatchPattern {
57 fn from(pattern: crate::Pattern) -> Self {
58 MatchPattern::Pattern(pattern)
59 }
60 }
61
62 impl MatchPattern {
63 pub fn literal(literal: impl Into<Vec<u8>>) -> Self {
64 MatchPattern::Literal(literal.into())
65 }
66 }
67
68 /// A pattern can be used as an include or an exclude pattern. In a list of `MatchEntry`s, later
69 /// patterns take precedence over earlier patterns and the order of includes vs excludes makes a
70 /// difference.
71 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
72 pub enum MatchType {
73 Include,
74 Exclude,
75 }
76
77 /// Convenience helpers
78 impl MatchType {
79 pub fn is_include(self) -> bool {
80 self == MatchType::Include
81 }
82
83 pub fn is_exclude(self) -> bool {
84 self == MatchType::Exclude
85 }
86 }
87
88 impl std::ops::Not for MatchType {
89 type Output = MatchType;
90
91 fn not(self) -> Self::Output {
92 match self {
93 MatchType::Include => MatchType::Exclude,
94 MatchType::Exclude => MatchType::Include,
95 }
96 }
97 }
98
99 /// A single entry in a `MatchList`.
100 #[derive(Clone, Debug)]
101 pub struct MatchEntry {
102 pattern: MatchPattern,
103 ty: MatchType,
104 flags: MatchFlag,
105 }
106
107 impl MatchEntry {
108 /// Create a new match entry.
109 pub fn new<T: Into<MatchPattern>>(pattern: T, ty: MatchType) -> Self {
110 Self {
111 pattern: pattern.into(),
112 ty,
113 flags: MatchFlag::default(),
114 }
115 }
116
117 /// Create a new include-type match entry with default flags.
118 pub fn include<T: Into<MatchPattern>>(pattern: T) -> Self {
119 Self::new(pattern.into(), MatchType::Include)
120 }
121
122 /// Create a new exclude-type match entry with default flags.
123 pub fn exclude<T: Into<MatchPattern>>(pattern: T) -> Self {
124 Self::new(pattern.into(), MatchType::Exclude)
125 }
126
127 /// Builder method to set the match flags to a specific value.
128 pub fn flags(mut self, flags: MatchFlag) -> Self {
129 self.flags = flags;
130 self
131 }
132
133 /// Builder method to add flag bits to the already present ones.
134 pub fn add_flags(mut self, flags: MatchFlag) -> Self {
135 self.flags.insert(flags);
136 self
137 }
138
139 /// Builder method to remove match flag bits.
140 pub fn remove_flags(mut self, flags: MatchFlag) -> Self {
141 self.flags.remove(flags);
142 self
143 }
144
145 /// Builder method to toggle flag bits.
146 pub fn toggle_flags(mut self, flags: MatchFlag) -> Self {
147 self.flags.toggle(flags);
148 self
149 }
150
151 #[inline]
152 pub fn match_type(&self) -> MatchType {
153 self.ty
154 }
155
156 /// Non-Builder method to change the match type.
157 pub fn match_type_mut(&mut self) -> &mut MatchType {
158 &mut self.ty
159 }
160
161 /// Directly access the pattern.
162 pub fn pattern(&self) -> &MatchPattern {
163 &self.pattern
164 }
165
166 /// Non-Builder method to change the pattern.
167 pub fn pattern_mut(&mut self) -> &mut MatchPattern {
168 &mut self.pattern
169 }
170
171 /// Directly access the match flags.
172 pub fn match_flags(&mut self) -> MatchFlag {
173 self.flags
174 }
175
176 /// Non-Builder method to change the flags.
177 pub fn match_flags_mut(&mut self) -> &mut MatchFlag {
178 &mut self.flags
179 }
180
181 /// Parse a pattern into a `MatchEntry` while interpreting a leading exclamation mark as
182 /// inversion and trailing slashes to match only directories.
183 pub fn parse_pattern<T: AsRef<[u8]>>(
184 pattern: T,
185 pattern_flags: PatternFlag,
186 ty: MatchType,
187 ) -> Result<Self, crate::ParseError> {
188 Self::parse_pattern_do(pattern.as_ref(), pattern_flags, ty)
189 }
190
191 fn parse_pattern_do(
192 pattern: &[u8],
193 pattern_flags: PatternFlag,
194 ty: MatchType,
195 ) -> Result<Self, crate::ParseError> {
196 let (pattern, ty) = if pattern.get(0).copied() == Some(b'!') {
197 (&pattern[1..], !ty)
198 } else {
199 (pattern, ty)
200 };
201
202 let (pattern, flags) = match pattern.iter().rposition(|&b| b != b'/') {
203 Some(pos) if (pos + 1) == pattern.len() => (pattern, MatchFlag::default()),
204 Some(pos) => (&pattern[..=pos], MatchFlag::MATCH_DIRECTORIES),
205 None => (b"/".as_ref(), MatchFlag::MATCH_DIRECTORIES),
206 };
207
208 Ok(Self::new(crate::Pattern::new(pattern, pattern_flags)?, ty).flags(flags))
209 }
210
211 /// Test this entry's file type restrictions against a file mode retrieved from `stat()`.
212 pub fn matches_mode(&self, file_mode: u32) -> bool {
213 // bitflags' `.contains` means ALL bits must be set, if they are all set we don't
214 // need to check the mode...
215 if self.flags.contains(MatchFlag::ANY_FILE_TYPE) {
216 return true;
217 }
218
219 let flag = match file_mode & libc::S_IFMT {
220 libc::S_IFDIR => MatchFlag::MATCH_DIRECTORIES,
221 libc::S_IFREG => MatchFlag::MATCH_REGULAR_FILES,
222 libc::S_IFLNK => MatchFlag::MATCH_SYMLINKS,
223 libc::S_IFSOCK => MatchFlag::MATCH_SOCKETS,
224 libc::S_IFIFO => MatchFlag::MATCH_FIFOS,
225 libc::S_IFCHR => MatchFlag::MATCH_CHARDEVS,
226 libc::S_IFBLK => MatchFlag::MATCH_BLOCKDEVS,
227 _unknown => return false,
228 };
229 self.flags.intersects(flag)
230 }
231
232 /// Test whether this entry's pattern matches any complete suffix of a path.
233 ///
234 /// For the path `/foo/bar/baz`, this tests whether `baz`, `bar/baz` or `foo/bar/baz` is
235 /// matched.
236 pub fn matches_path_suffix<T: AsRef<[u8]>>(&self, path: T) -> bool {
237 self.matches_path_suffix_do(path.as_ref())
238 }
239
240 fn matches_path_suffix_do(&self, path: &[u8]) -> bool {
241 if self.flags.intersects(MatchFlag::ANCHORED) {
242 return self.matches_path_exact(path);
243 }
244
245 if path.is_empty() {
246 return false;
247 }
248
249 for start in (0..path.len()).rev() {
250 if path[start] == b'/' && self.matches_path_exact(&path[(start + 1)..]) {
251 return true;
252 }
253 }
254
255 if path[0] != b'/' {
256 // we had "foo/bar", so we haven't yet tried to match the whole string:
257 self.matches_path_exact(path)
258 } else {
259 false
260 }
261 }
262
263 /// Test whether this entry's pattern matches a path exactly.
264 pub fn matches_path_exact<T: AsRef<[u8]>>(&self, path: T) -> bool {
265 self.matches_path_exact_do(path.as_ref())
266 }
267
268 fn matches_path_exact_do(&self, path: &[u8]) -> bool {
269 match &self.pattern {
270 MatchPattern::Pattern(pattern) => pattern.matches(path),
271 MatchPattern::Literal(literal) => path == &literal[..],
272 }
273 }
274
275 /// Check whether the path contains a matching suffix and the file mode match the expected file modes.
276 /// This is a combination of using `.matches_mode()` and `.matches_path_suffix()`.
277 pub fn matches<T: AsRef<[u8]>>(&self, path: T, file_mode: Option<u32>) -> bool {
278 self.matches_do(path.as_ref(), file_mode)
279 }
280
281 fn matches_do(&self, path: &[u8], file_mode: Option<u32>) -> bool {
282 if let Some(mode) = file_mode {
283 if !self.matches_mode(mode) {
284 return false;
285 }
286 }
287
288 self.matches_path_suffix(path)
289 }
290
291 /// Check whether the path contains a matching suffix and the file mode match the expected file modes.
292 /// This is a combination of using `.matches_mode()` and `.matches_path_exact()`.
293 pub fn matches_exact<T: AsRef<[u8]>>(&self, path: T, file_mode: Option<u32>) -> bool {
294 self.matches_exact_do(path.as_ref(), file_mode)
295 }
296
297 fn matches_exact_do(&self, path: &[u8], file_mode: Option<u32>) -> bool {
298 if let Some(mode) = file_mode {
299 if !self.matches_mode(mode) {
300 return false;
301 }
302 }
303
304 self.matches_path_exact(path)
305 }
306 }
307
308 #[doc(hidden)]
309 pub trait MatchListEntry {
310 fn entry_matches(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType>;
311 fn entry_matches_exact(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType>;
312 }
313
314 impl MatchListEntry for &'_ MatchEntry {
315 fn entry_matches(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
316 if self.matches(path, file_mode) {
317 Some(self.match_type())
318 } else {
319 None
320 }
321 }
322
323 fn entry_matches_exact(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
324 if self.matches_exact(path, file_mode) {
325 Some(self.match_type())
326 } else {
327 None
328 }
329 }
330 }
331
332 pub trait MatchList: Sized {
333 /// Check whether this list contains anything matching a prefix of the specified path, and the
334 /// specified file mode.
335 fn matches<T: AsRef<[u8]>>(self, path: T, file_mode: Option<u32>) -> Option<MatchType> {
336 self.matches_do(path.as_ref(), file_mode)
337 }
338
339 fn matches_do(self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType>;
340
341 /// Check whether this list contains anything exactly matching the path and mode.
342 fn matches_exact<T: AsRef<[u8]>>(
343 self,
344 path: T,
345 file_mode: Option<u32>,
346 ) -> Option<MatchType> {
347 self.matches_exact_do(path.as_ref(), file_mode)
348 }
349
350 fn matches_exact_do(self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType>;
351 }
352
353 impl<T> MatchList for T
354 where
355 T: IntoIterator,
356 <T as IntoIterator>::IntoIter: DoubleEndedIterator,
357 <T as IntoIterator>::Item: MatchListEntry,
358 {
359 fn matches_do(self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
360 for m in self.into_iter().rev() {
361 if let Some(mt) = m.entry_matches(path, file_mode) {
362 return Some(mt);
363 }
364 }
365
366 None
367 }
368
369 fn matches_exact_do(self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
370 for m in self.into_iter().rev() {
371 if let Some(mt) = m.entry_matches_exact(path, file_mode) {
372 return Some(mt);
373 }
374 }
375
376 None
377 }
378 }
379
380 #[test]
381 fn assert_containers_implement_match_list() {
382 use std::iter::FromIterator;
383
384 let vec = vec![MatchEntry::include(crate::Pattern::path("a*").unwrap())];
385 assert_eq!(vec.matches("asdf", None), Some(MatchType::Include));
386
387 // FIXME: ideally we can make this work as well!
388 let vd = std::collections::VecDeque::<MatchEntry>::from_iter(vec.clone());
389 assert_eq!(vd.matches("asdf", None), Some(MatchType::Include));
390
391 let list: &[MatchEntry] = &vec[..];
392 assert_eq!(list.matches("asdf", None), Some(MatchType::Include));
393
394 let list: Vec<&MatchEntry> = vec.iter().collect();
395 assert_eq!(list.matches("asdf", None), Some(MatchType::Include));
396 }
397
398 #[test]
399 fn test_file_type_matches() {
400 let matchlist = vec![
401 MatchEntry::parse_pattern("a_dir/", PatternFlag::PATH_NAME, MatchType::Include)
402 .unwrap(),
403 MatchEntry::parse_pattern("!a_file", PatternFlag::PATH_NAME, MatchType::Include)
404 .unwrap()
405 .flags(MatchFlag::MATCH_REGULAR_FILES),
406 MatchEntry::parse_pattern("!another_dir//", PatternFlag::PATH_NAME, MatchType::Include)
407 .unwrap(),
408 ];
409 assert_eq!(
410 matchlist.matches("a_dir", Some(libc::S_IFDIR)),
411 Some(MatchType::Include)
412 );
413 assert_eq!(
414 matchlist.matches("/a_dir", Some(libc::S_IFDIR)),
415 Some(MatchType::Include)
416 );
417 assert_eq!(matchlist.matches("/a_dir", Some(libc::S_IFREG)), None);
418
419 assert_eq!(
420 matchlist.matches("/a_file", Some(libc::S_IFREG)),
421 Some(MatchType::Exclude)
422 );
423 assert_eq!(matchlist.matches("/a_file", Some(libc::S_IFDIR)), None);
424
425 assert_eq!(
426 matchlist.matches("/another_dir", Some(libc::S_IFDIR)),
427 Some(MatchType::Exclude)
428 );
429 assert_eq!(matchlist.matches("/another_dir", Some(libc::S_IFREG)), None);
430 }
431
432 #[test]
433 fn test_anchored_matches() {
434 use crate::Pattern;
435
436 let matchlist = vec![
437 MatchEntry::new(Pattern::path("file-a").unwrap(), MatchType::Include),
438 MatchEntry::new(Pattern::path("some/path").unwrap(), MatchType::Include)
439 .flags(MatchFlag::ANCHORED),
440 ];
441
442 assert_eq!(matchlist.matches("file-a", None), Some(MatchType::Include));
443 assert_eq!(
444 matchlist.matches("another/file-a", None),
445 Some(MatchType::Include)
446 );
447
448 assert_eq!(matchlist.matches("some", None), None);
449 assert_eq!(matchlist.matches("path", None), None);
450 assert_eq!(
451 matchlist.matches("some/path", None),
452 Some(MatchType::Include)
453 );
454 assert_eq!(matchlist.matches("another/some/path", None), None);
455 }