Skip to main content

gix_pathspec/
parse.rs

1use gix_error::{OptionExt, Result, bail, message, validation};
2use std::borrow::Cow;
3
4use bstr::{BStr, BString, ByteSlice};
5
6use crate::{Defaults, MagicSignature, Pattern, SearchMode};
7
8impl Pattern {
9    /// Try to parse a path-spec pattern from the given `input` bytes.
10    /// Errors store the offending input, keyword, or attribute value bytes as `input`
11    /// [metadata](gix_error::Error::metadata()).
12    pub fn from_bytes(
13        input: &[u8],
14        Defaults {
15            signature,
16            search_mode,
17            literal,
18        }: Defaults,
19    ) -> Result<Self> {
20        if input.is_empty() {
21            bail!(validation("An empty string is not a valid pathspec").with_input(input));
22        }
23        if literal {
24            return Ok(Self::from_literal(input, signature));
25        }
26        if input.as_bstr() == ":" {
27            return Ok(Pattern {
28                nil: true,
29                ..Default::default()
30            });
31        }
32
33        let mut p = Pattern {
34            signature,
35            search_mode: SearchMode::default(),
36            ..Default::default()
37        };
38
39        let mut cursor = 0;
40        if input.first() == Some(&b':') {
41            cursor += 1;
42            p.signature |= parse_short_keywords(input, &mut cursor)?;
43            if let Some(b'(') = input.get(cursor) {
44                cursor += 1;
45                parse_long_keywords(input, &mut p, &mut cursor)?;
46            }
47        }
48
49        if search_mode != Default::default() && p.search_mode == Default::default() {
50            p.search_mode = search_mode;
51        }
52        let mut path = &input[cursor..];
53        if path.last() == Some(&b'/') {
54            p.signature |= MagicSignature::MUST_BE_DIR;
55            path = &path[..path.len() - 1];
56        }
57        p.path = path.into();
58        Ok(p)
59    }
60
61    /// Take `input` literally without parsing anything. This will also set our mode to `literal` to allow this pathspec to match `input` verbatim, and
62    /// use `default_signature` as magic signature.
63    pub fn from_literal(input: &[u8], default_signature: MagicSignature) -> Self {
64        Pattern {
65            path: input.into(),
66            signature: default_signature,
67            search_mode: SearchMode::Literal,
68            ..Default::default()
69        }
70    }
71}
72
73fn parse_short_keywords(input: &[u8], cursor: &mut usize) -> Result<MagicSignature> {
74    let unimplemented_chars = b"\"#%&'-',;<=>@_`~";
75
76    let mut signature = MagicSignature::empty();
77    while let Some(&b) = input.get(*cursor) {
78        *cursor += 1;
79        signature |= match b {
80            b'/' => MagicSignature::TOP,
81            b'^' | b'!' => MagicSignature::EXCLUDE,
82            b':' => break,
83            _ if unimplemented_chars.contains(&b) => {
84                bail!(validation("Unimplemented short keyword").with_input(vec![b]));
85            }
86            _ => {
87                *cursor -= 1;
88                break;
89            }
90        }
91    }
92
93    Ok(signature)
94}
95
96fn parse_long_keywords(input: &[u8], p: &mut Pattern, cursor: &mut usize) -> Result {
97    let end = input
98        .find(")")
99        .ok_or_raise(|| validation("Missing ')' at the end of pathspec signature").with_input(input))?;
100
101    let input = &input[*cursor..end];
102    *cursor = end + 1;
103
104    if input.is_empty() {
105        return Ok(());
106    }
107
108    split_on_non_escaped_char(input, b',', |keyword| {
109        // Git skips empty keywords instead of rejecting them, so `:(top,)`, `:(,top)` and
110        // `:(top,,icase)` are all valid there.
111        if keyword.is_empty() {
112            return Ok(());
113        }
114        let attr_prefix = b"attr:";
115        match keyword {
116            b"attr" => {}
117            b"top" => p.signature |= MagicSignature::TOP,
118            b"icase" => p.signature |= MagicSignature::ICASE,
119            b"exclude" => p.signature |= MagicSignature::EXCLUDE,
120            b"literal" => match p.search_mode {
121                SearchMode::PathAwareGlob => {
122                    bail!(
123                        validation("'literal' and 'glob' keywords cannot be used together in the same pathspec",)
124                            .with_input(keyword)
125                    );
126                }
127                _ => p.search_mode = SearchMode::Literal,
128            },
129            b"glob" => match p.search_mode {
130                SearchMode::Literal => {
131                    bail!(
132                        validation("'literal' and 'glob' keywords cannot be used together in the same pathspec",)
133                            .with_input(keyword)
134                    );
135                }
136                _ => p.search_mode = SearchMode::PathAwareGlob,
137            },
138            _ if keyword.starts_with(attr_prefix) => {
139                if p.attributes.is_empty() {
140                    p.attributes = parse_attributes(&keyword[attr_prefix.len()..])?;
141                } else {
142                    bail!(
143                        validation("Only one attribute specification is allowed in the same pathspec",)
144                            .with_input(keyword)
145                    );
146                }
147            }
148            _ => {
149                bail!(validation("Found invalid keyword in pathspec signature").with_input(keyword));
150            }
151        }
152        Ok(())
153    })
154}
155
156fn split_on_non_escaped_char(input: &[u8], split_char: u8, mut f: impl FnMut(&[u8]) -> Result) -> Result {
157    // Mirrors `strcspn_escaped()` in Git's `pathspec.c`: a backslash consumes the byte that
158    // follows it, so `\,` is a literal comma while `\\,` is an escaped backslash followed by a
159    // separator. Scanning byte-by-byte also lets a separator at index 0 be seen, which a
160    // two-byte window cannot.
161    let mut i = 0;
162    let mut last = 0;
163    while i < input.len() {
164        if input[i] == b'\\' {
165            i += 2;
166            continue;
167        }
168        if input[i] == split_char {
169            f(&input[last..i])?;
170            last = i + 1;
171        }
172        i += 1;
173    }
174    f(&input[last..])
175}
176
177fn parse_attributes(input: &[u8]) -> Result<Vec<gix_attributes::Assignment>> {
178    if input.is_empty() {
179        bail!(validation("Attribute specification cannot be empty"));
180    }
181
182    input
183        .split(|&b| b == b' ')
184        .filter(|attr| !attr.is_empty())
185        .map(|attr| {
186            let (name, state) = match attr.first() {
187                Some(b'!') => (&attr[1..], gix_attributes::State::Unspecified),
188                Some(b'-') => (&attr[1..], gix_attributes::State::Unset),
189                _ => match attr.find_byte(b'=') {
190                    Some(pos) => {
191                        let (name, value) = attr.split_at(pos);
192                        let value = &value[1..];
193                        let value = if value.contains(&b'\\') {
194                            Cow::Owned(unescape_and_check_attr_value(value.into())?)
195                        } else {
196                            check_attribute_value(value.into())?;
197                            Cow::Borrowed(value.as_bstr())
198                        };
199                        (name, gix_attributes::StateRef::from_bytes(value.as_ref()).to_owned())
200                    }
201                    None => (attr, gix_attributes::State::Set),
202                },
203            };
204            let name = gix_attributes::NameRef::try_from(name.as_bstr())?;
205            Ok(gix_attributes::Assignment {
206                name: name.to_owned(),
207                state,
208            })
209        })
210        .collect()
211}
212
213fn unescape_and_check_attr_value(value: &BStr) -> Result<BString> {
214    let mut out = BString::from(Vec::with_capacity(value.len()));
215    let mut bytes = value.iter();
216    while let Some(mut b) = bytes.next().copied() {
217        if b == b'\\' {
218            b = *bytes.next().ok_or_raise(|| {
219                message!(r"Escape character '\' is not allowed as the last character in an attribute value")
220                    .validation()
221                    .with_input(value)
222            })?;
223        }
224
225        out.push(validated_attr_value_byte(b)?);
226    }
227    Ok(out)
228}
229
230fn check_attribute_value(input: &BStr) -> Result {
231    match input.iter().copied().find(|b| !is_valid_attr_value(*b)) {
232        Some(b) => Err(message("Invalid character in attribute value")
233            .with_input(vec![b])
234            .validation_error()),
235        None => Ok(()),
236    }
237}
238
239fn is_valid_attr_value(byte: u8) -> bool {
240    byte.is_ascii_alphanumeric() || b",-_".contains(&byte)
241}
242
243fn validated_attr_value_byte(byte: u8) -> Result<u8> {
244    if is_valid_attr_value(byte) {
245        Ok(byte)
246    } else {
247        Err(message("Invalid character in attribute value")
248            .with_input(vec![byte])
249            .validation_error())
250    }
251}