| 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 = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Ord, PartialOrd)] |
| 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(Default, Debug, PartialOrd, PartialEq, Copy, Clone, Hash, Ord, Eq)] |
| pub enum Case { |
| /// The case affects the match |
| #[default] |
| Sensitive, |
| /// Ignore the case of ascii characters. |
| Fold, |
| } |
| |
| /// Instantiation |
| 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, true).map(|(text, mode, first_wildcard_pos)| Pattern { |
| text: text.into(), |
| mode, |
| first_wildcard_pos, |
| }) |
| } |
| |
| /// Parse the given `text` as pattern without supporting leading `!` or `\\!` , or return `None` if `text` was empty. |
| /// |
| /// This assures that `text` remains entirely unaltered, but removes built-in support for negation as well. |
| pub fn from_bytes_without_negation(text: &[u8]) -> Option<Self> { |
| crate::parse::pattern(text, false).map(|(text, mode, first_wildcard_pos)| Pattern { |
| text: text.into(), |
| mode, |
| first_wildcard_pos, |
| }) |
| } |
| } |
| |
| /// Access |
| impl Pattern { |
| /// 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. |
| /// |
| /// `case` folding can be configured as well. |
| /// `mode` is used to control how [`crate::wildmatch()`] should operate. |
| pub fn matches_repo_relative_path( |
| &self, |
| path: &BStr, |
| basename_start_pos: Option<usize>, |
| is_dir: Option<bool>, |
| case: Case, |
| mode: wildmatch::Mode, |
| ) -> bool { |
| let is_dir = is_dir.unwrap_or(false); |
| if !is_dir && self.mode.contains(pattern::Mode::MUST_BE_DIR) { |
| return false; |
| } |
| |
| let flags = mode |
| | match case { |
| Case::Fold => wildmatch::Mode::IGNORE_CASE, |
| Case::Sensitive => wildmatch::Mode::empty(), |
| }; |
| #[cfg(debug_assertions)] |
| { |
| if basename_start_pos.is_some() { |
| 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, but falls back to |
| /// [wildmatch()][crate::wildmatch()] if these fail. |
| pub fn matches(&self, value: &BStr, mode: wildmatch::Mode) -> bool { |
| match self.first_wildcard_pos { |
| // "*literal" case, overrides starts-with |
| Some(pos) |
| if self.mode.contains(pattern::Mode::ENDS_WITH) |
| && (!mode.contains(wildmatch::Mode::NO_MATCH_SLASH_LITERAL) || !value.contains(&b'/')) => |
| { |
| let text = &self.text[pos + 1..]; |
| if mode.contains(wildmatch::Mode::IGNORE_CASE) { |
| value |
| .len() |
| .checked_sub(text.len()) |
| .map_or(false, |start| text.eq_ignore_ascii_case(&value[start..])) |
| } 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(()) |
| } |
| } |