1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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(())
    }
}