Sebastian Thielmake fmt251b6df4 years agoHistory
162 lines · 5.9 KB · Rust
RawDownload
use std::fmt; use bitflags::bitflags;use bstr::{BStr, ByteSlice}; use crate::{pattern, wildmatch, Pattern}; bitflags! {    /// Information about a [`Pattern`].    ///    /// Its main purpose is to accelerate pattern matching, or to negate the match result or to    /// keep special rules only applicable when matching paths.    ///    /// The mode is typically created when parsing the pattern by inspecting it and isn't typically handled by the user.    #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]    pub struct Mode: u32 {        /// The pattern does not contain a sub-directory and - it doesn't contain slashes after removing the trailing one.        const NO_SUB_DIR = 1 << 0;        /// A pattern that is '*literal', meaning that it ends with what's given here        const ENDS_WITH = 1 << 1;        /// The pattern must match a directory, and not a file.        const MUST_BE_DIR = 1 << 2;        /// The pattern matches, but should be negated. Note that this mode has to be checked and applied by the caller.        const NEGATIVE = 1 << 3;        /// The pattern starts with a slash and thus matches only from the beginning.        const ABSOLUTE = 1 << 4;    }} /// Describes whether to match a path case sensitively or not.////// Used in [Pattern::matches_repo_relative_path()].#[derive(Debug, PartialOrd, PartialEq, Copy, Clone, Hash, Ord, Eq)]pub enum Case {    /// The case affects the match    Sensitive,    /// Ignore the case of ascii characters.    Fold,} impl Default for Case {    fn default() -> Self {        Case::Sensitive    }} impl Pattern {    /// Parse the given `text` as pattern, or return `None` if `text` was empty.    pub fn from_bytes(text: &[u8]) -> Option<Self> {        crate::parse::pattern(text).map(|(text, mode, first_wildcard_pos)| Pattern {            text,            mode,            first_wildcard_pos,        })    }     /// Return true if a match is negated.    pub fn is_negative(&self) -> bool {        self.mode.contains(Mode::NEGATIVE)    }     /// Match the given `path` which takes slashes (and only slashes) literally, and is relative to the repository root.    /// Note that `path` is assumed to be relative to the repository.    ///    /// We may take various shortcuts which is when `basename_start_pos` and `is_dir` come into play.    /// `basename_start_pos` is the index at which the `path`'s basename starts.    ///    /// Lastly, `case` folding can be configured as well.    pub fn matches_repo_relative_path<'a>(        &self,        path: impl Into<&'a BStr>,        basename_start_pos: Option<usize>,        is_dir: Option<bool>,        case: Case,    ) -> bool {        let is_dir = is_dir.unwrap_or(false);        if !is_dir && self.mode.contains(pattern::Mode::MUST_BE_DIR) {            return false;        }         let flags = wildmatch::Mode::NO_MATCH_SLASH_LITERAL            | match case {                Case::Fold => wildmatch::Mode::IGNORE_CASE,                Case::Sensitive => wildmatch::Mode::empty(),            };        let path = path.into();        debug_assert_eq!(            basename_start_pos,            path.rfind_byte(b'/').map(|p| p + 1),            "BUG: invalid cached basename_start_pos provided"        );        debug_assert!(!path.starts_with(b"/"), "input path must be relative");         if self.mode.contains(pattern::Mode::NO_SUB_DIR) && !self.mode.contains(pattern::Mode::ABSOLUTE) {            let basename = &path[basename_start_pos.unwrap_or_default()..];            self.matches(basename, flags)        } else {            self.matches(path, flags)        }    }     /// See if `value` matches this pattern in the given `mode`.    ///    /// `mode` can identify `value` as path which won't match the slash character, and can match    /// strings with cases ignored as well. Note that the case folding performed here is ASCII only.    ///    /// Note that this method uses some shortcuts to accelerate simple patterns.    fn matches<'a>(&self, value: impl Into<&'a BStr>, mode: wildmatch::Mode) -> bool {        let value = value.into();        match self.first_wildcard_pos {            // "*literal" case, overrides starts-with            Some(pos) if self.mode.contains(pattern::Mode::ENDS_WITH) && !value.contains(&b'/') => {                let text = &self.text[pos + 1..];                if mode.contains(wildmatch::Mode::IGNORE_CASE) {                    value                        .len()                        .checked_sub(text.len())                        .map(|start| text.eq_ignore_ascii_case(&value[start..]))                        .unwrap_or(false)                } else {                    value.ends_with(text.as_ref())                }            }            Some(pos) => {                if mode.contains(wildmatch::Mode::IGNORE_CASE) {                    if !value                        .get(..pos)                        .map_or(false, |value| value.eq_ignore_ascii_case(&self.text[..pos]))                    {                        return false;                    }                } else if !value.starts_with(&self.text[..pos]) {                    return false;                }                crate::wildmatch(self.text.as_bstr(), value, mode)            }            None => {                if mode.contains(wildmatch::Mode::IGNORE_CASE) {                    self.text.eq_ignore_ascii_case(value)                } else {                    self.text == value                }            }        }    }} impl fmt::Display for Pattern {    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {        if self.mode.contains(Mode::NEGATIVE) {            "!".fmt(f)?;        }        if self.mode.contains(Mode::ABSOLUTE) {            "/".fmt(f)?;        }        self.text.fmt(f)?;        if self.mode.contains(Mode::MUST_BE_DIR) {            "/".fmt(f)?;        }        Ok(())    }}