]> git.proxmox.com Git - pathpatterns.git/blob - src/match_list.rs
tests for relative and absolute, anchored and unanchored mixes
[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(&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 // and try the whole string as well:
256 self.matches_path_exact(path)
257 }
258
259 /// Test whether this entry's pattern matches a path exactly.
260 pub fn matches_path_exact<T: AsRef<[u8]>>(&self, path: T) -> bool {
261 self.matches_path_exact_do(path.as_ref())
262 }
263
264 fn matches_path_exact_do(&self, path: &[u8]) -> bool {
265 match &self.pattern {
266 MatchPattern::Pattern(pattern) => pattern.matches(path),
267 MatchPattern::Literal(literal) => path == &literal[..],
268 }
269 }
270
271 /// Check whether the path contains a matching suffix and the file mode match the expected file modes.
272 /// This is a combination of using `.matches_mode()` and `.matches_path_suffix()`.
273 pub fn matches<T: AsRef<[u8]>>(&self, path: T, file_mode: Option<u32>) -> bool {
274 self.matches_do(path.as_ref(), file_mode)
275 }
276
277 fn matches_do(&self, path: &[u8], file_mode: Option<u32>) -> bool {
278 if let Some(mode) = file_mode {
279 if !self.matches_mode(mode) {
280 return false;
281 }
282 }
283
284 self.matches_path_suffix(path)
285 }
286
287 /// Check whether the path contains a matching suffix and the file mode match the expected file modes.
288 /// This is a combination of using `.matches_mode()` and `.matches_path_exact()`.
289 pub fn matches_exact<T: AsRef<[u8]>>(&self, path: T, file_mode: Option<u32>) -> bool {
290 self.matches_exact_do(path.as_ref(), file_mode)
291 }
292
293 fn matches_exact_do(&self, path: &[u8], file_mode: Option<u32>) -> bool {
294 if let Some(mode) = file_mode {
295 if !self.matches_mode(mode) {
296 return false;
297 }
298 }
299
300 self.matches_path_exact(path)
301 }
302 }
303
304 #[doc(hidden)]
305 pub trait MatchListEntry {
306 fn entry_matches(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType>;
307 fn entry_matches_exact(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType>;
308 }
309
310 impl MatchListEntry for &'_ MatchEntry {
311 fn entry_matches(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
312 if self.matches(path, file_mode) {
313 Some(self.match_type())
314 } else {
315 None
316 }
317 }
318
319 fn entry_matches_exact(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
320 if self.matches_exact(path, file_mode) {
321 Some(self.match_type())
322 } else {
323 None
324 }
325 }
326 }
327
328 impl MatchListEntry for &'_ &'_ MatchEntry {
329 fn entry_matches(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
330 if self.matches(path, file_mode) {
331 Some(self.match_type())
332 } else {
333 None
334 }
335 }
336
337 fn entry_matches_exact(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
338 if self.matches_exact(path, file_mode) {
339 Some(self.match_type())
340 } else {
341 None
342 }
343 }
344 }
345
346 /// This provides `matches` and `matches_exact` methods to lists of `MatchEntry`s.
347 ///
348 /// Technically this is implemented for anything you can turn into a `DoubleEndedIterator` over
349 /// `MatchEntry` or `&MatchEntry`.
350 ///
351 /// In practice this means you can use it with slices or references to `Vec` or `VecDeque` etc.
352 /// This makes it easier to use slices over entries or references to entries.
353 pub trait MatchList {
354 /// Check whether this list contains anything matching a prefix of the specified path, and the
355 /// specified file mode.
356 fn matches<T: AsRef<[u8]>>(&self, path: T, file_mode: Option<u32>) -> Option<MatchType> {
357 self.matches_do(path.as_ref(), file_mode)
358 }
359
360 fn matches_do(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType>;
361
362 /// Check whether this list contains anything exactly matching the path and mode.
363 fn matches_exact<T: AsRef<[u8]>>(&self, path: T, file_mode: Option<u32>) -> Option<MatchType> {
364 self.matches_exact_do(path.as_ref(), file_mode)
365 }
366
367 fn matches_exact_do(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType>;
368 }
369
370 impl<'a, T> MatchList for T
371 where
372 T: 'a + ?Sized,
373 &'a T: IntoIterator,
374 <&'a T as IntoIterator>::IntoIter: DoubleEndedIterator,
375 <&'a T as IntoIterator>::Item: MatchListEntry,
376 {
377 fn matches_do(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
378 // This is an &self method on a `T where T: 'a`.
379 let this: &'a Self = unsafe { std::mem::transmute(self) };
380
381 for m in this.into_iter().rev() {
382 if let Some(mt) = m.entry_matches(path, file_mode) {
383 return Some(mt);
384 }
385 }
386
387 None
388 }
389
390 fn matches_exact_do(&self, path: &[u8], file_mode: Option<u32>) -> Option<MatchType> {
391 // This is an &self method on a `T where T: 'a`.
392 let this: &'a Self = unsafe { std::mem::transmute(self) };
393
394 for m in this.into_iter().rev() {
395 if let Some(mt) = m.entry_matches_exact(path, file_mode) {
396 return Some(mt);
397 }
398 }
399
400 None
401 }
402 }
403
404 #[test]
405 fn assert_containers_implement_match_list() {
406 use std::iter::FromIterator;
407
408 let vec = vec![MatchEntry::include(crate::Pattern::path("a*").unwrap())];
409 assert_eq!(vec.matches("asdf", None), Some(MatchType::Include));
410
411 // FIXME: ideally we can make this work as well!
412 let vd = std::collections::VecDeque::<MatchEntry>::from_iter(vec.clone());
413 assert_eq!(vd.matches("asdf", None), Some(MatchType::Include));
414
415 let list: &[MatchEntry] = &vec[..];
416 assert_eq!(list.matches("asdf", None), Some(MatchType::Include));
417
418 let list: Vec<&MatchEntry> = vec.iter().collect();
419 assert_eq!(list.matches("asdf", None), Some(MatchType::Include));
420
421 let list: &[&MatchEntry] = &list[..];
422 assert_eq!(list.matches("asdf", None), Some(MatchType::Include));
423 }
424
425 #[test]
426 fn test_file_type_matches() {
427 let matchlist = vec![
428 MatchEntry::parse_pattern("a_dir/", PatternFlag::PATH_NAME, MatchType::Include).unwrap(),
429 MatchEntry::parse_pattern("!a_file", PatternFlag::PATH_NAME, MatchType::Include)
430 .unwrap()
431 .flags(MatchFlag::MATCH_REGULAR_FILES),
432 MatchEntry::parse_pattern("!another_dir//", PatternFlag::PATH_NAME, MatchType::Include)
433 .unwrap(),
434 ];
435 assert_eq!(
436 matchlist.matches("a_dir", Some(libc::S_IFDIR)),
437 Some(MatchType::Include)
438 );
439 assert_eq!(
440 matchlist.matches("/a_dir", Some(libc::S_IFDIR)),
441 Some(MatchType::Include)
442 );
443 assert_eq!(matchlist.matches("/a_dir", Some(libc::S_IFREG)), None);
444
445 assert_eq!(
446 matchlist.matches("/a_file", Some(libc::S_IFREG)),
447 Some(MatchType::Exclude)
448 );
449 assert_eq!(matchlist.matches("/a_file", Some(libc::S_IFDIR)), None);
450
451 assert_eq!(
452 matchlist.matches("/another_dir", Some(libc::S_IFDIR)),
453 Some(MatchType::Exclude)
454 );
455 assert_eq!(matchlist.matches("/another_dir", Some(libc::S_IFREG)), None);
456 }
457
458 #[test]
459 fn test_anchored_matches() {
460 use crate::Pattern;
461
462 let matchlist = vec![
463 MatchEntry::new(Pattern::path("file-a").unwrap(), MatchType::Include),
464 MatchEntry::new(Pattern::path("some/path").unwrap(), MatchType::Include)
465 .flags(MatchFlag::ANCHORED),
466 ];
467
468 assert_eq!(matchlist.matches("file-a", None), Some(MatchType::Include));
469 assert_eq!(
470 matchlist.matches("another/file-a", None),
471 Some(MatchType::Include)
472 );
473
474 assert_eq!(matchlist.matches("some", None), None);
475 assert_eq!(matchlist.matches("path", None), None);
476 assert_eq!(
477 matchlist.matches("some/path", None),
478 Some(MatchType::Include)
479 );
480 assert_eq!(matchlist.matches("another/some/path", None), None);
481 }
482
483 #[test]
484 fn test_literal_matches() {
485 let matchlist = vec![MatchEntry::new(
486 MatchPattern::Literal(b"/bin/mv".to_vec()),
487 MatchType::Include,
488 )];
489 assert_eq!(matchlist.matches("/bin/mv", None), Some(MatchType::Include));
490 }
491
492 #[test]
493 fn test_path_relativity() {
494 use crate::Pattern;
495 let matchlist = vec![
496 MatchEntry::new(Pattern::path("noslash").unwrap(), MatchType::Include),
497 MatchEntry::new(Pattern::path("noslash-a").unwrap(), MatchType::Include)
498 .flags(MatchFlag::ANCHORED),
499 MatchEntry::new(Pattern::path("/slash").unwrap(), MatchType::Include),
500 MatchEntry::new(Pattern::path("/slash-a").unwrap(), MatchType::Include)
501 .flags(MatchFlag::ANCHORED),
502 ];
503 assert_eq!(matchlist.matches("noslash", None), Some(MatchType::Include));
504 assert_eq!(
505 matchlist.matches("noslash-a", None),
506 Some(MatchType::Include)
507 );
508 assert_eq!(matchlist.matches("slash", None), None);
509 assert_eq!(matchlist.matches("/slash", None), Some(MatchType::Include));
510 assert_eq!(matchlist.matches("slash-a", None), None);
511 assert_eq!(
512 matchlist.matches("/slash-a", None),
513 Some(MatchType::Include)
514 );
515
516 assert_eq!(
517 matchlist.matches("foo/noslash", None),
518 Some(MatchType::Include)
519 );
520 assert_eq!(matchlist.matches("foo/noslash-a", None), None);
521 assert_eq!(matchlist.matches("foo/slash", None), None);
522 assert_eq!(matchlist.matches("foo/slash-a", None), None);
523 }