]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/pxar/match_pattern.rs
switch from failure to anyhow
[proxmox-backup.git] / src / pxar / match_pattern.rs
index 6da90261f0fbe75da56a0076e0f0bde5aa854b1e..9b979669b3215e9fe5c2008bd14f56d8320d65d1 100644 (file)
@@ -1,16 +1,23 @@
-use std::io::Read;
+//! `MatchPattern` defines a match pattern used to match filenames encountered
+//! during encoding or decoding of a `pxar` archive.
+//! `fnmatch` is used internally to match filenames against the patterns.
+//! Shell wildcard pattern can be used to match multiple filenames, see manpage
+//! `glob(7)`.
+//! `**` is treated special, as it matches multiple directories in a path.
+
 use std::ffi::{CStr, CString};
 use std::fs::File;
+use std::io::Read;
 use std::os::unix::io::{FromRawFd, RawFd};
 
-use failure::*;
+use anyhow::{bail, Error};
 use libc::{c_char, c_int};
+use nix::errno::Errno;
 use nix::fcntl;
 use nix::fcntl::{AtFlags, OFlag};
-use nix::errno::Errno;
-use nix::NixPath;
 use nix::sys::stat;
 use nix::sys::stat::{FileStat, Mode};
+use nix::NixPath;
 
 pub const FNM_NOMATCH: c_int = 1;
 
@@ -27,33 +34,82 @@ pub enum MatchType {
     PartialNegative,
 }
 
-#[derive(Clone)]
+/// `MatchPattern` provides functionality for filename glob pattern matching
+/// based on glibc's `fnmatch`.
+/// Positive matches return `MatchType::PartialPositive` or `MatchType::Positive`.
+/// Patterns starting with `!` are interpreted as negation, meaning they will
+/// return `MatchType::PartialNegative` or `MatchType::Negative`.
+/// No matches result in `MatchType::None`.
+/// # Examples:
+/// ```
+/// # use std::ffi::CString;
+/// # use self::proxmox_backup::pxar::{MatchPattern, MatchType};
+/// # fn main() -> Result<(), anyhow::Error> {
+/// let filename = CString::new("some.conf")?;
+/// let is_dir = false;
+///
+/// /// Positive match of any file ending in `.conf` in any subdirectory
+/// let positive = MatchPattern::from_line(b"**/*.conf")?.unwrap();
+/// let m_positive = positive.as_slice().matches_filename(&filename, is_dir)?;
+/// assert!(m_positive == MatchType::Positive);
+///
+/// /// Negative match of filenames starting with `s`
+/// let negative = MatchPattern::from_line(b"![s]*")?.unwrap();
+/// let m_negative = negative.as_slice().matches_filename(&filename, is_dir)?;
+/// assert!(m_negative == MatchType::Negative);
+/// # Ok(())
+/// # }
+/// ```
+#[derive(Clone, Eq, PartialOrd)]
 pub struct MatchPattern {
-    pattern: CString,
+    pattern: Vec<u8>,
     match_positive: bool,
     match_dir_only: bool,
-    split_pattern: (CString, CString),
+}
+
+impl std::cmp::PartialEq for MatchPattern {
+    fn eq(&self, other: &Self) -> bool {
+        self.pattern == other.pattern
+        && self.match_positive == other.match_positive
+        && self.match_dir_only == other.match_dir_only
+    }
+}
+
+impl std::cmp::Ord for MatchPattern {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        (&self.pattern, &self.match_positive, &self.match_dir_only)
+            .cmp(&(&other.pattern, &other.match_positive, &other.match_dir_only))
+    }
 }
 
 impl MatchPattern {
+    /// Read a list of `MatchPattern` from file.
+    /// The file is read line by line (lines terminated by newline character),
+    /// each line may only contain one pattern.
+    /// Leading `/` are ignored and lines starting with `#` are interpreted as
+    /// comments and not included in the resulting list.
+    /// Patterns ending in `/` will match only directories.
+    ///
+    /// On success, a list of match pattern is returned as well as the raw file
+    /// byte buffer together with the files stats.
+    /// This is done in order to avoid reading the file more than once during
+    /// encoding of the archive.
     pub fn from_file<P: ?Sized + NixPath>(
         parent_fd: RawFd,
         filename: &P,
-    ) -> Result<Option<(Vec<MatchPattern>, Vec<u8>, FileStat)>, Error> {
-
+    ) -> Result<Option<(Vec<MatchPattern>, Vec<u8>, FileStat)>, nix::Error> {
         let stat = match stat::fstatat(parent_fd, filename, AtFlags::AT_SYMLINK_NOFOLLOW) {
             Ok(stat) => stat,
             Err(nix::Error::Sys(Errno::ENOENT)) => return Ok(None),
-            Err(err) => bail!("stat failed - {}", err),
+            Err(err) => return Err(err),
         };
 
         let filefd = fcntl::openat(parent_fd, filename, OFlag::O_NOFOLLOW, Mode::empty())?;
-        let mut file = unsafe {
-            File::from_raw_fd(filefd)
-        };
+        let mut file = unsafe { File::from_raw_fd(filefd) };
 
         let mut content_buffer = Vec::new();
-        let _bytes = file.read_to_end(&mut content_buffer)?;
+        let _bytes = file.read_to_end(&mut content_buffer)
+            .map_err(|_| Errno::EIO)?;
 
         let mut match_pattern = Vec::new();
         for line in content_buffer.split(|&c| c == b'\n') {
@@ -68,7 +124,14 @@ impl MatchPattern {
         Ok(Some((match_pattern, content_buffer, stat)))
     }
 
-    pub fn from_line(line: &[u8]) -> Result<Option<MatchPattern>, Error> {
+    /// Interprete a byte buffer as a sinlge line containing a valid
+    /// `MatchPattern`.
+    /// Pattern starting with `#` are interpreted as comments, returning `Ok(None)`.
+    /// Pattern starting with '!' are interpreted as negative match pattern.
+    /// Pattern with trailing `/` match only against directories.
+    /// `.` as well as `..` and any pattern containing `\0` are invalid and will
+    /// result in an error with Errno::EINVAL.
+    pub fn from_line(line: &[u8]) -> Result<Option<MatchPattern>, nix::Error> {
         let mut input = line;
 
         if input.starts_with(b"#") {
@@ -97,43 +160,30 @@ impl MatchPattern {
             input = &input[1..];
         }
 
-        if input.is_empty() || input == b"." ||
-            input == b".." || input.contains(&b'\0') {
-            bail!("invalid path component encountered");
+        if input.is_empty() || input == b"." || input == b".." || input.contains(&b'\0') {
+            return Err(nix::Error::Sys(Errno::EINVAL));
         }
 
-        // This will fail if the line contains b"\0"
-        let pattern = CString::new(input)?;
-        let split_pattern = split_at_slash(&pattern);
-
         Ok(Some(MatchPattern {
-            pattern,
+            pattern: input.to_vec(),
             match_positive,
             match_dir_only,
-            split_pattern,
         }))
     }
 
-    pub fn get_front_pattern(&self) -> MatchPattern {
-        let pattern = split_at_slash(&self.split_pattern.0);
-        MatchPattern {
-            pattern: self.split_pattern.0.clone(),
-            match_positive: self.match_positive,
-            match_dir_only: self.match_dir_only,
-            split_pattern: pattern,
-        }
-    }
 
-    pub fn get_rest_pattern(&self) -> MatchPattern {
-        let pattern = split_at_slash(&self.split_pattern.1);
-        MatchPattern {
-            pattern: self.split_pattern.1.clone(),
+    /// Create a `MatchPatternSlice` of the `MatchPattern` to give a view of the
+    /// `MatchPattern` without copying its content.
+    pub fn as_slice<'a>(&'a self) -> MatchPatternSlice<'a> {
+        MatchPatternSlice {
+            pattern: self.pattern.as_slice(),
             match_positive: self.match_positive,
             match_dir_only: self.match_dir_only,
-            split_pattern: pattern,
         }
     }
 
+    /// Dump the content of the `MatchPattern` to stdout.
+    /// Intended for debugging purposes only.
     pub fn dump(&self) {
         match (self.match_positive, self.match_dir_only) {
             (true, true) => println!("{:#?}/", self.pattern),
@@ -143,16 +193,143 @@ impl MatchPattern {
         }
     }
 
-    pub fn matches_filename(&self, filename: &CStr, is_dir: bool) -> MatchType {
+    /// Convert a list of MatchPattern to bytes in order to write them to e.g.
+    /// a file.
+    pub fn to_bytes(patterns: &[MatchPattern]) -> Vec<u8> {
+        let mut slices = Vec::new();
+        for pattern in patterns {
+            slices.push(pattern.as_slice());
+        }
+
+        MatchPatternSlice::to_bytes(&slices)
+    }
+
+    /// Invert the match type for this MatchPattern.
+    pub fn invert(&mut self) {
+        self.match_positive = !self.match_positive;
+    }
+}
+
+#[derive(Clone)]
+pub struct MatchPatternSlice<'a> {
+    pattern: &'a [u8],
+    match_positive: bool,
+    match_dir_only: bool,
+}
+
+impl<'a> MatchPatternSlice<'a> {
+    /// Returns the pattern before the first `/` encountered as `MatchPatternSlice`.
+    /// If no slash is encountered, the `MatchPatternSlice` will be a copy of the
+    /// original pattern.
+    /// ```
+    /// # use self::proxmox_backup::pxar::{MatchPattern, MatchPatternSlice, MatchType};
+    /// # fn main() -> Result<(), anyhow::Error> {
+    /// let pattern = MatchPattern::from_line(b"some/match/pattern/")?.unwrap();
+    /// let slice = pattern.as_slice();
+    /// let front = slice.get_front_pattern();
+    /// /// ... will be the same as ...
+    /// let front_pattern = MatchPattern::from_line(b"some")?.unwrap();
+    /// let front_slice = front_pattern.as_slice();
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn get_front_pattern(&'a self) -> MatchPatternSlice<'a> {
+        let (front, _) = self.split_at_slash();
+        MatchPatternSlice {
+            pattern: front,
+            match_positive: self.match_positive,
+            match_dir_only: self.match_dir_only,
+        }
+    }
+
+    /// Returns the pattern after the first encountered `/` as `MatchPatternSlice`.
+    /// If no slash is encountered, the `MatchPatternSlice` will be empty.
+    /// ```
+    /// # use self::proxmox_backup::pxar::{MatchPattern, MatchPatternSlice, MatchType};
+    /// # fn main() -> Result<(), anyhow::Error> {
+    /// let pattern = MatchPattern::from_line(b"some/match/pattern/")?.unwrap();
+    /// let slice = pattern.as_slice();
+    /// let rest = slice.get_rest_pattern();
+    /// /// ... will be the same as ...
+    /// let rest_pattern = MatchPattern::from_line(b"match/pattern/")?.unwrap();
+    /// let rest_slice = rest_pattern.as_slice();
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn get_rest_pattern(&'a self) -> MatchPatternSlice<'a> {
+        let (_, rest) = self.split_at_slash();
+        MatchPatternSlice {
+            pattern: rest,
+            match_positive: self.match_positive,
+            match_dir_only: self.match_dir_only,
+        }
+    }
+
+    /// Splits the `MatchPatternSlice` at the first slash encountered and returns the
+    /// content before (front pattern) and after the slash (rest pattern),
+    /// omitting the slash itself.
+    /// Slices starting with `**/` are an exception to this, as the corresponding
+    /// `MatchPattern` is intended to match multiple directories.
+    /// These pattern slices therefore return a `*` as front pattern and the original
+    /// pattern itself as rest pattern.
+    fn split_at_slash(&'a self) -> (&'a [u8], &'a [u8]) {
+        let pattern = if self.pattern.starts_with(b"./") {
+            &self.pattern[2..]
+        } else {
+            self.pattern
+        };
+
+        let (mut front, mut rest) = match pattern.iter().position(|&c| c == b'/') {
+            Some(ind) => {
+                let (front, rest) = pattern.split_at(ind);
+                (front, &rest[1..])
+            }
+            None => (pattern, &pattern[0..0]),
+        };
+        // '**' is treated such that it maches any directory
+        if front == b"**" {
+            front = b"*";
+            rest = pattern;
+        }
+
+        (front, rest)
+    }
+
+    /// Convert a list of `MatchPatternSlice`s to bytes in order to write them to e.g.
+    /// a file.
+    pub fn to_bytes(patterns: &[MatchPatternSlice]) -> Vec<u8> {
+        let mut buffer = Vec::new();
+        for pattern in patterns {
+            if !pattern.match_positive { buffer.push(b'!'); }
+            buffer.extend_from_slice(&pattern.pattern);
+            if pattern.match_dir_only { buffer.push(b'/'); }
+            buffer.push(b'\n');
+        }
+        buffer
+    }
+
+    /// Match the given filename against this `MatchPatternSlice`.
+    /// If the filename matches the pattern completely, `MatchType::Positive` or
+    /// `MatchType::Negative` is returned, depending if the match pattern is was
+    /// declared as positive (no `!` prefix) or negative (`!` prefix).
+    /// If the pattern matched only up to the first slash of the pattern,
+    /// `MatchType::PartialPositive` or `MatchType::PartialNegatie` is returned.
+    /// If the pattern was postfixed by a trailing `/` a match is only valid if
+    /// the parameter `is_dir` equals `true`.
+    /// No match results in `MatchType::None`.
+    pub fn matches_filename(&self, filename: &CStr, is_dir: bool) -> Result<MatchType, Error> {
         let mut res = MatchType::None;
-        let (front, _) = &self.split_pattern;
+        let (front, _) = self.split_at_slash();
 
+        let front = CString::new(front).unwrap();
         let fnmatch_res = unsafe {
             let front_ptr = front.as_ptr() as *const libc::c_char;
             let filename_ptr = filename.as_ptr() as *const libc::c_char;
-            fnmatch(front_ptr, filename_ptr , 0)
+            fnmatch(front_ptr, filename_ptr, 0)
         };
-        // TODO error cases
+        if fnmatch_res < 0 {
+            bail!("error in fnmatch inside of MatchPattern");
+        }
         if fnmatch_res == 0 {
             res = if self.match_positive {
                 MatchType::PartialPositive
@@ -161,17 +338,19 @@ impl MatchPattern {
             };
         }
 
-        let full = if self.pattern.to_bytes().starts_with(b"**/") {
-            CString::new(&self.pattern.to_bytes()[3..]).unwrap()
+        let full = if self.pattern.starts_with(b"**/") {
+            CString::new(&self.pattern[3..]).unwrap()
         } else {
-            CString::new(&self.pattern.to_bytes()[..]).unwrap()
+            CString::new(&self.pattern[..]).unwrap()
         };
         let fnmatch_res = unsafe {
             let full_ptr = full.as_ptr() as *const libc::c_char;
             let filename_ptr = filename.as_ptr() as *const libc::c_char;
             fnmatch(full_ptr, filename_ptr, 0)
         };
-        // TODO error cases
+        if fnmatch_res < 0 {
+            bail!("error in fnmatch inside of MatchPattern");
+        }
         if fnmatch_res == 0 {
             res = if self.match_positive {
                 MatchType::Positive
@@ -188,34 +367,148 @@ impl MatchPattern {
             res = MatchType::None;
         }
 
-        res
+        Ok(res)
     }
-}
 
-fn split_at_slash(match_pattern: &CStr) -> (CString, CString) {
-    let match_pattern = match_pattern.to_bytes();
-
-    let pattern = if match_pattern.starts_with(b"./") {
-        &match_pattern[2..]
-    } else {
-        match_pattern
-    };
-
-    let (mut front, mut rest) = match pattern.iter().position(|&c| c == b'/') {
-        Some(ind) => {
-            let (front, rest) = pattern.split_at(ind);
-            (front, &rest[1..])
-        },
-        None => (pattern, &pattern[0..0]),
-    };
-    // '**' is treated such that it maches any directory
-    if front == b"**" {
-        front = b"*";
-        rest = pattern;
+    /// Match the given filename against the set of `MatchPatternSlice`s.
+    ///
+    /// A positive match is intended to includes the full subtree (unless another
+    /// negative match excludes entries later).
+    /// The `MatchType` together with an updated `MatchPatternSlice` list for passing
+    /// to the matched child is returned.
+    /// ```
+    /// # use std::ffi::CString;
+    /// # use self::proxmox_backup::pxar::{MatchPattern, MatchPatternSlice, MatchType};
+    /// # fn main() -> Result<(), anyhow::Error> {
+    /// let patterns = vec![
+    ///     MatchPattern::from_line(b"some/match/pattern/")?.unwrap(),
+    ///     MatchPattern::from_line(b"to_match/")?.unwrap()
+    /// ];
+    /// let mut slices = Vec::new();
+    /// for pattern in &patterns {
+    ///     slices.push(pattern.as_slice());
+    /// }
+    /// let filename = CString::new("some")?;
+    /// let is_dir = true;
+    /// let (match_type, child_pattern) = MatchPatternSlice::match_filename_include(
+    ///     &filename,
+    ///     is_dir,
+    ///     &slices
+    /// )?;
+    /// assert_eq!(match_type, MatchType::PartialPositive);
+    /// /// child pattern will be the same as ...
+    /// let pattern = MatchPattern::from_line(b"match/pattern/")?.unwrap();
+    /// let slice = pattern.as_slice();
+    ///
+    /// let filename = CString::new("to_match")?;
+    /// let is_dir = true;
+    /// let (match_type, child_pattern) = MatchPatternSlice::match_filename_include(
+    ///     &filename,
+    ///     is_dir,
+    ///     &slices
+    /// )?;
+    /// assert_eq!(match_type, MatchType::Positive);
+    /// /// child pattern will be the same as ...
+    /// let pattern = MatchPattern::from_line(b"**/*")?.unwrap();
+    /// let slice = pattern.as_slice();
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn match_filename_include(
+        filename: &CStr,
+        is_dir: bool,
+        match_pattern: &'a [MatchPatternSlice<'a>],
+    ) -> Result<(MatchType, Vec<MatchPatternSlice<'a>>), Error> {
+        let mut child_pattern = Vec::new();
+        let mut match_state = MatchType::None;
+
+        for pattern in match_pattern {
+            match pattern.matches_filename(filename, is_dir)? {
+                MatchType::None => continue,
+                MatchType::Positive => match_state = MatchType::Positive,
+                MatchType::Negative => match_state = MatchType::Negative,
+                MatchType::PartialPositive => {
+                    if match_state != MatchType::Negative && match_state != MatchType::Positive {
+                        match_state = MatchType::PartialPositive;
+                    }
+                    child_pattern.push(pattern.get_rest_pattern());
+                }
+                MatchType::PartialNegative => {
+                    if match_state == MatchType::PartialPositive {
+                        match_state = MatchType::PartialNegative;
+                    }
+                    child_pattern.push(pattern.get_rest_pattern());
+                }
+            }
+        }
+
+        Ok((match_state, child_pattern))
     }
 
-    // Pattern where valid CStrings before, so it is safe to unwrap the Result
-    let front_pattern = CString::new(front).unwrap();
-    let rest_pattern = CString::new(rest).unwrap();
-    (front_pattern, rest_pattern)
+    /// Match the given filename against the set of `MatchPatternSlice`s.
+    ///
+    /// A positive match is intended to exclude the full subtree, independent of
+    /// matches deeper down the tree.
+    /// The `MatchType` together with an updated `MatchPattern` list for passing
+    /// to the matched child is returned.
+    /// ```
+    /// # use std::ffi::CString;
+    /// # use self::proxmox_backup::pxar::{MatchPattern, MatchPatternSlice, MatchType};
+    /// # fn main() -> Result<(), anyhow::Error> {
+    /// let patterns = vec![
+    ///     MatchPattern::from_line(b"some/match/pattern/")?.unwrap(),
+    ///     MatchPattern::from_line(b"to_match/")?.unwrap()
+    /// ];
+    /// let mut slices = Vec::new();
+    /// for pattern in &patterns {
+    ///     slices.push(pattern.as_slice());
+    /// }
+    /// let filename = CString::new("some")?;
+    /// let is_dir = true;
+    /// let (match_type, child_pattern) = MatchPatternSlice::match_filename_exclude(
+    ///     &filename,
+    ///     is_dir,
+    ///     &slices,
+    /// )?;
+    /// assert_eq!(match_type, MatchType::PartialPositive);
+    /// /// child pattern will be the same as ...
+    /// let pattern = MatchPattern::from_line(b"match/pattern/")?.unwrap();
+    /// let slice = pattern.as_slice();
+    ///
+    /// let filename = CString::new("to_match")?;
+    /// let is_dir = true;
+    /// let (match_type, child_pattern) = MatchPatternSlice::match_filename_exclude(
+    ///     &filename,
+    ///     is_dir,
+    ///     &slices,
+    /// )?;
+    /// assert_eq!(match_type, MatchType::Positive);
+    /// /// child pattern will be empty
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn match_filename_exclude(
+        filename: &CStr,
+        is_dir: bool,
+        match_pattern: &'a [MatchPatternSlice<'a>],
+    ) -> Result<(MatchType, Vec<MatchPatternSlice<'a>>), Error> {
+        let mut child_pattern = Vec::new();
+        let mut match_state = MatchType::None;
+
+        for pattern in match_pattern {
+            match pattern.matches_filename(filename, is_dir)? {
+                MatchType::None => {}
+                MatchType::Positive => match_state = MatchType::Positive,
+                MatchType::Negative => match_state = MatchType::Negative,
+                match_type => {
+                    if match_state != MatchType::Positive && match_state != MatchType::Negative {
+                        match_state = match_type;
+                    }
+                    child_pattern.push(pattern.get_rest_pattern());
+                }
+            }
+        }
+
+        Ok((match_state, child_pattern))
+    }
 }