1 //! `MatchPattern` defines a match pattern used to match filenames encountered
2 //! during encoding or decoding of a `pxar` archive.
3 //! `fnmatch` is used internally to match filenames against the patterns.
4 //! Shell wildcard pattern can be used to match multiple filenames, see manpage
6 //! `**` is treated special, as it matches multiple directories in a path.
8 use std
::ffi
::{CStr, CString}
;
11 use std
::os
::unix
::io
::{FromRawFd, RawFd}
;
13 use failure
::{bail, Error}
;
14 use libc
::{c_char, c_int}
;
15 use nix
::errno
::Errno
;
17 use nix
::fcntl
::{AtFlags, OFlag}
;
19 use nix
::sys
::stat
::{FileStat, Mode}
;
22 pub const FNM_NOMATCH
: c_int
= 1;
25 fn fnmatch(pattern
: *const c_char
, string
: *const c_char
, flags
: c_int
) -> c_int
;
28 #[derive(Debug, PartialEq, Clone, Copy)]
37 /// `MatchPattern` provides functionality for filename glob pattern matching
38 /// based on glibc's `fnmatch`.
39 /// Positive matches return `MatchType::PartialPositive` or `MatchType::Positive`.
40 /// Patterns starting with `!` are interpreted as negation, meaning they will
41 /// return `MatchType::PartialNegative` or `MatchType::Negative`.
42 /// No matches result in `MatchType::None`.
45 /// # use std::ffi::CString;
46 /// # use self::proxmox_backup::pxar::{MatchPattern, MatchType};
47 /// # fn main() -> Result<(), failure::Error> {
48 /// let filename = CString::new("some.conf")?;
49 /// let is_dir = false;
51 /// /// Positive match of any file ending in `.conf` in any subdirectory
52 /// let positive = MatchPattern::from_line(b"**/*.conf")?.unwrap();
53 /// let m_positive = positive.as_slice().matches_filename(&filename, is_dir)?;
54 /// assert!(m_positive == MatchType::Positive);
56 /// /// Negative match of filenames starting with `s`
57 /// let negative = MatchPattern::from_line(b"![s]*")?.unwrap();
58 /// let m_negative = negative.as_slice().matches_filename(&filename, is_dir)?;
59 /// assert!(m_negative == MatchType::Negative);
63 #[derive(Eq, PartialOrd)]
64 pub struct MatchPattern
{
70 impl std
::cmp
::PartialEq
for MatchPattern
{
71 fn eq(&self, other
: &Self) -> bool
{
72 self.pattern
== other
.pattern
73 && self.match_positive
== other
.match_positive
74 && self.match_dir_only
== other
.match_dir_only
78 impl std
::cmp
::Ord
for MatchPattern
{
79 fn cmp(&self, other
: &Self) -> std
::cmp
::Ordering
{
80 (&self.pattern
, &self.match_positive
, &self.match_dir_only
)
81 .cmp(&(&other
.pattern
, &other
.match_positive
, &other
.match_dir_only
))
86 /// Read a list of `MatchPattern` from file.
87 /// The file is read line by line (lines terminated by newline character),
88 /// each line may only contain one pattern.
89 /// Leading `/` are ignored and lines starting with `#` are interpreted as
90 /// comments and not included in the resulting list.
91 /// Patterns ending in `/` will match only directories.
93 /// On success, a list of match pattern is returned as well as the raw file
94 /// byte buffer together with the files stats.
95 /// This is done in order to avoid reading the file more than once during
96 /// encoding of the archive.
97 pub fn from_file
<P
: ?Sized
+ NixPath
>(
100 ) -> Result
<Option
<(Vec
<MatchPattern
>, Vec
<u8>, FileStat
)>, nix
::Error
> {
101 let stat
= match stat
::fstatat(parent_fd
, filename
, AtFlags
::AT_SYMLINK_NOFOLLOW
) {
103 Err(nix
::Error
::Sys(Errno
::ENOENT
)) => return Ok(None
),
104 Err(err
) => return Err(err
),
107 let filefd
= fcntl
::openat(parent_fd
, filename
, OFlag
::O_NOFOLLOW
, Mode
::empty())?
;
108 let mut file
= unsafe { File::from_raw_fd(filefd) }
;
110 let mut content_buffer
= Vec
::new();
111 let _bytes
= file
.read_to_end(&mut content_buffer
)
112 .map_err(|_
| Errno
::EIO
)?
;
114 let mut match_pattern
= Vec
::new();
115 for line
in content_buffer
.split(|&c
| c
== b'
\n'
) {
119 if let Some(pattern
) = Self::from_line(line
)?
{
120 match_pattern
.push(pattern
);
124 Ok(Some((match_pattern
, content_buffer
, stat
)))
127 /// Interprete a byte buffer as a sinlge line containing a valid
129 /// Pattern starting with `#` are interpreted as comments, returning `Ok(None)`.
130 /// Pattern starting with '!' are interpreted as negative match pattern.
131 /// Pattern with trailing `/` match only against directories.
132 /// `.` as well as `..` and any pattern containing `\0` are invalid and will
133 /// result in an error with Errno::EINVAL.
134 pub fn from_line(line
: &[u8]) -> Result
<Option
<MatchPattern
>, nix
::Error
> {
135 let mut input
= line
;
137 if input
.starts_with(b
"#") {
141 let match_positive
= if input
.starts_with(b
"!") {
142 // Reduce slice view to exclude "!"
149 // Paths ending in / match only directory names (no filenames)
150 let match_dir_only
= if input
.ends_with(b
"/") {
151 let len
= input
.len();
152 input
= &input
[..len
- 1];
158 // Ignore initial slash
159 if input
.starts_with(b
"/") {
163 if input
.is_empty() || input
== b
"." || input
== b
".." || input
.contains(&b'
\0'
) {
164 return Err(nix
::Error
::Sys(Errno
::EINVAL
));
167 Ok(Some(MatchPattern
{
168 pattern
: input
.to_vec(),
175 /// Create a `MatchPatternSlice` of the `MatchPattern` to give a view of the
176 /// `MatchPattern` without copying its content.
177 pub fn as_slice
<'a
>(&'a
self) -> MatchPatternSlice
<'a
> {
179 pattern
: self.pattern
.as_slice(),
180 match_positive
: self.match_positive
,
181 match_dir_only
: self.match_dir_only
,
185 /// Dump the content of the `MatchPattern` to stdout.
186 /// Intended for debugging purposes only.
188 match (self.match_positive
, self.match_dir_only
) {
189 (true, true) => println
!("{:#?}/", self.pattern
),
190 (true, false) => println
!("{:#?}", self.pattern
),
191 (false, true) => println
!("!{:#?}/", self.pattern
),
192 (false, false) => println
!("!{:#?}", self.pattern
),
196 /// Convert a list of MatchPattern to bytes in order to write them to e.g.
198 pub fn to_bytes(patterns
: &[MatchPattern
]) -> Vec
<u8> {
199 let mut slices
= Vec
::new();
200 for pattern
in patterns
{
201 slices
.push(pattern
.as_slice());
204 MatchPatternSlice
::to_bytes(&slices
)
207 /// Invert the match type for this MatchPattern.
208 pub fn invert(&mut self) {
209 self.match_positive
= !self.match_positive
;
214 pub struct MatchPatternSlice
<'a
> {
216 match_positive
: bool
,
217 match_dir_only
: bool
,
220 impl<'a
> MatchPatternSlice
<'a
> {
221 /// Returns the pattern before the first `/` encountered as `MatchPatternSlice`.
222 /// If no slash is encountered, the `MatchPatternSlice` will be a copy of the
223 /// original pattern.
225 /// # use self::proxmox_backup::pxar::{MatchPattern, MatchPatternSlice, MatchType};
226 /// # fn main() -> Result<(), failure::Error> {
227 /// let pattern = MatchPattern::from_line(b"some/match/pattern/")?.unwrap();
228 /// let slice = pattern.as_slice();
229 /// let front = slice.get_front_pattern();
230 /// /// ... will be the same as ...
231 /// let front_pattern = MatchPattern::from_line(b"some")?.unwrap();
232 /// let front_slice = front_pattern.as_slice();
236 pub fn get_front_pattern(&'a
self) -> MatchPatternSlice
<'a
> {
237 let (front
, _
) = self.split_at_slash();
240 match_positive
: self.match_positive
,
241 match_dir_only
: self.match_dir_only
,
245 /// Returns the pattern after the first encountered `/` as `MatchPatternSlice`.
246 /// If no slash is encountered, the `MatchPatternSlice` will be empty.
248 /// # use self::proxmox_backup::pxar::{MatchPattern, MatchPatternSlice, MatchType};
249 /// # fn main() -> Result<(), failure::Error> {
250 /// let pattern = MatchPattern::from_line(b"some/match/pattern/")?.unwrap();
251 /// let slice = pattern.as_slice();
252 /// let rest = slice.get_rest_pattern();
253 /// /// ... will be the same as ...
254 /// let rest_pattern = MatchPattern::from_line(b"match/pattern/")?.unwrap();
255 /// let rest_slice = rest_pattern.as_slice();
259 pub fn get_rest_pattern(&'a
self) -> MatchPatternSlice
<'a
> {
260 let (_
, rest
) = self.split_at_slash();
263 match_positive
: self.match_positive
,
264 match_dir_only
: self.match_dir_only
,
268 /// Splits the `MatchPatternSlice` at the first slash encountered and returns the
269 /// content before (front pattern) and after the slash (rest pattern),
270 /// omitting the slash itself.
271 /// Slices starting with `**/` are an exception to this, as the corresponding
272 /// `MatchPattern` is intended to match multiple directories.
273 /// These pattern slices therefore return a `*` as front pattern and the original
274 /// pattern itself as rest pattern.
275 fn split_at_slash(&'a
self) -> (&'a
[u8], &'a
[u8]) {
276 let pattern
= if self.pattern
.starts_with(b
"./") {
282 let (mut front
, mut rest
) = match pattern
.iter().position(|&c
| c
== b'
/'
) {
284 let (front
, rest
) = pattern
.split_at(ind
);
287 None
=> (pattern
, &pattern
[0..0]),
289 // '**' is treated such that it maches any directory
298 /// Convert a list of `MatchPatternSlice`s to bytes in order to write them to e.g.
300 pub fn to_bytes(patterns
: &[MatchPatternSlice
]) -> Vec
<u8> {
301 let mut buffer
= Vec
::new();
302 for pattern
in patterns
{
303 if !pattern
.match_positive { buffer.push(b'!'); }
304 buffer
.extend_from_slice(&pattern
.pattern
);
305 if pattern
.match_dir_only { buffer.push(b'/'); }
311 /// Match the given filename against this `MatchPatternSlice`.
312 /// If the filename matches the pattern completely, `MatchType::Positive` or
313 /// `MatchType::Negative` is returned, depending if the match pattern is was
314 /// declared as positive (no `!` prefix) or negative (`!` prefix).
315 /// If the pattern matched only up to the first slash of the pattern,
316 /// `MatchType::PartialPositive` or `MatchType::PartialNegatie` is returned.
317 /// If the pattern was postfixed by a trailing `/` a match is only valid if
318 /// the parameter `is_dir` equals `true`.
319 /// No match results in `MatchType::None`.
320 pub fn matches_filename(&self, filename
: &CStr
, is_dir
: bool
) -> Result
<MatchType
, Error
> {
321 let mut res
= MatchType
::None
;
322 let (front
, _
) = self.split_at_slash();
324 let front
= CString
::new(front
).unwrap();
325 let fnmatch_res
= unsafe {
326 let front_ptr
= front
.as_ptr() as *const libc
::c_char
;
327 let filename_ptr
= filename
.as_ptr() as *const libc
::c_char
;
328 fnmatch(front_ptr
, filename_ptr
, 0)
331 bail
!("error in fnmatch inside of MatchPattern");
333 if fnmatch_res
== 0 {
334 res
= if self.match_positive
{
335 MatchType
::PartialPositive
337 MatchType
::PartialNegative
341 let full
= if self.pattern
.starts_with(b
"**/") {
342 CString
::new(&self.pattern
[3..]).unwrap()
344 CString
::new(&self.pattern
[..]).unwrap()
346 let fnmatch_res
= unsafe {
347 let full_ptr
= full
.as_ptr() as *const libc
::c_char
;
348 let filename_ptr
= filename
.as_ptr() as *const libc
::c_char
;
349 fnmatch(full_ptr
, filename_ptr
, 0)
352 bail
!("error in fnmatch inside of MatchPattern");
354 if fnmatch_res
== 0 {
355 res
= if self.match_positive
{
362 if !is_dir
&& self.match_dir_only
{
363 res
= MatchType
::None
;
366 if !is_dir
&& (res
== MatchType
::PartialPositive
|| res
== MatchType
::PartialNegative
) {
367 res
= MatchType
::None
;
373 /// Match the given filename against the set of `MatchPatternSlice`s.
375 /// A positive match is intended to includes the full subtree (unless another
376 /// negative match excludes entries later).
377 /// The `MatchType` together with an updated `MatchPatternSlice` list for passing
378 /// to the matched child is returned.
380 /// # use std::ffi::CString;
381 /// # use self::proxmox_backup::pxar::{MatchPattern, MatchPatternSlice, MatchType};
382 /// # fn main() -> Result<(), failure::Error> {
383 /// let patterns = vec![
384 /// MatchPattern::from_line(b"some/match/pattern/")?.unwrap(),
385 /// MatchPattern::from_line(b"to_match/")?.unwrap()
387 /// let mut slices = Vec::new();
388 /// for pattern in &patterns {
389 /// slices.push(pattern.as_slice());
391 /// let filename = CString::new("some")?;
392 /// let is_dir = true;
393 /// let (match_type, child_pattern) = MatchPatternSlice::match_filename_include(
398 /// assert_eq!(match_type, MatchType::PartialPositive);
399 /// /// child pattern will be the same as ...
400 /// let pattern = MatchPattern::from_line(b"match/pattern/")?.unwrap();
401 /// let slice = pattern.as_slice();
403 /// let filename = CString::new("to_match")?;
404 /// let is_dir = true;
405 /// let (match_type, child_pattern) = MatchPatternSlice::match_filename_include(
410 /// assert_eq!(match_type, MatchType::Positive);
411 /// /// child pattern will be the same as ...
412 /// let pattern = MatchPattern::from_line(b"**/*")?.unwrap();
413 /// let slice = pattern.as_slice();
417 pub fn match_filename_include(
420 match_pattern
: &'a
[MatchPatternSlice
<'a
>],
421 ) -> Result
<(MatchType
, Vec
<MatchPatternSlice
<'a
>>), Error
> {
422 let mut child_pattern
= Vec
::new();
423 let mut match_state
= MatchType
::None
;
425 for pattern
in match_pattern
{
426 match pattern
.matches_filename(filename
, is_dir
)?
{
427 MatchType
::None
=> continue,
428 MatchType
::Positive
=> match_state
= MatchType
::Positive
,
429 MatchType
::Negative
=> match_state
= MatchType
::Negative
,
430 MatchType
::PartialPositive
=> {
431 if match_state
!= MatchType
::Negative
&& match_state
!= MatchType
::Positive
{
432 match_state
= MatchType
::PartialPositive
;
434 child_pattern
.push(pattern
.get_rest_pattern());
436 MatchType
::PartialNegative
=> {
437 if match_state
== MatchType
::PartialPositive
{
438 match_state
= MatchType
::PartialNegative
;
440 child_pattern
.push(pattern
.get_rest_pattern());
445 Ok((match_state
, child_pattern
))
448 /// Match the given filename against the set of `MatchPatternSlice`s.
450 /// A positive match is intended to exclude the full subtree, independent of
451 /// matches deeper down the tree.
452 /// The `MatchType` together with an updated `MatchPattern` list for passing
453 /// to the matched child is returned.
455 /// # use std::ffi::CString;
456 /// # use self::proxmox_backup::pxar::{MatchPattern, MatchPatternSlice, MatchType};
457 /// # fn main() -> Result<(), failure::Error> {
458 /// let patterns = vec![
459 /// MatchPattern::from_line(b"some/match/pattern/")?.unwrap(),
460 /// MatchPattern::from_line(b"to_match/")?.unwrap()
462 /// let mut slices = Vec::new();
463 /// for pattern in &patterns {
464 /// slices.push(pattern.as_slice());
466 /// let filename = CString::new("some")?;
467 /// let is_dir = true;
468 /// let (match_type, child_pattern) = MatchPatternSlice::match_filename_exclude(
473 /// assert_eq!(match_type, MatchType::PartialPositive);
474 /// /// child pattern will be the same as ...
475 /// let pattern = MatchPattern::from_line(b"match/pattern/")?.unwrap();
476 /// let slice = pattern.as_slice();
478 /// let filename = CString::new("to_match")?;
479 /// let is_dir = true;
480 /// let (match_type, child_pattern) = MatchPatternSlice::match_filename_exclude(
485 /// assert_eq!(match_type, MatchType::Positive);
486 /// /// child pattern will be empty
490 pub fn match_filename_exclude(
493 match_pattern
: &'a
[MatchPatternSlice
<'a
>],
494 ) -> Result
<(MatchType
, Vec
<MatchPatternSlice
<'a
>>), Error
> {
495 let mut child_pattern
= Vec
::new();
496 let mut match_state
= MatchType
::None
;
498 for pattern
in match_pattern
{
499 match pattern
.matches_filename(filename
, is_dir
)?
{
500 MatchType
::None
=> {}
501 MatchType
::Positive
=> match_state
= MatchType
::Positive
,
502 MatchType
::Negative
=> match_state
= MatchType
::Negative
,
504 if match_state
!= MatchType
::Positive
&& match_state
!= MatchType
::Negative
{
505 match_state
= match_type
;
507 child_pattern
.push(pattern
.get_rest_pattern());
512 Ok((match_state
, child_pattern
))