Coverage Report

Created: 2025-12-31 07:37

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gitoxide/gix-attributes/src/parse.rs
Line
Count
Source
1
use std::borrow::Cow;
2
3
use bstr::{BStr, ByteSlice};
4
use kstring::KStringRef;
5
6
use crate::{name, AssignmentRef, Name, NameRef, StateRef};
7
8
/// The kind of attribute that was parsed.
9
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
10
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11
pub enum Kind {
12
    /// A pattern to match paths against
13
    Pattern(gix_glob::Pattern),
14
    /// The name of the macro to define, always a valid attribute name
15
    Macro(Name),
16
}
17
18
mod error {
19
    use bstr::BString;
20
    /// The error returned by [`parse::Lines`][crate::parse::Lines].
21
    #[derive(thiserror::Error, Debug)]
22
    #[allow(missing_docs)]
23
    pub enum Error {
24
        #[error(r"Line {line_number} has a negative pattern, for literal characters use \!: {line}")]
25
        PatternNegation { line_number: usize, line: BString },
26
        #[error("Attribute in line {line_number} has non-ascii characters or starts with '-': {attribute}")]
27
        AttributeName { line_number: usize, attribute: BString },
28
        #[error("Macro in line {line_number} has non-ascii characters or starts with '-': {macro_name}")]
29
        MacroName { line_number: usize, macro_name: BString },
30
        #[error("Could not unquote attributes line")]
31
        Unquote(#[from] gix_quote::ansi_c::undo::Error),
32
    }
33
}
34
pub use error::Error;
35
36
/// An iterator over attribute assignments, parsed line by line.
37
pub struct Lines<'a> {
38
    lines: bstr::Lines<'a>,
39
    line_no: usize,
40
}
41
42
/// An iterator over attribute assignments in a single line.
43
pub struct Iter<'a> {
44
    attrs: bstr::Fields<'a>,
45
}
46
47
impl<'a> Iter<'a> {
48
    /// Create a new instance to parse attribute assignments from `input`.
49
2.07M
    pub fn new(input: &'a BStr) -> Self {
50
2.07M
        Iter { attrs: input.fields() }
51
2.07M
    }
Unexecuted instantiation: <gix_attributes::parse::Iter>::new
<gix_attributes::parse::Iter>::new
Line
Count
Source
49
2.06M
    pub fn new(input: &'a BStr) -> Self {
50
2.06M
        Iter { attrs: input.fields() }
51
2.06M
    }
<gix_attributes::parse::Iter>::new
Line
Count
Source
49
2.97k
    pub fn new(input: &'a BStr) -> Self {
50
2.97k
        Iter { attrs: input.fields() }
51
2.97k
    }
52
53
13.6M
    fn parse_attr(&self, attr: &'a [u8]) -> Result<AssignmentRef<'a>, name::Error> {
54
57.6M
        let mut tokens = attr.splitn(2, |b| *b == b'=');
Unexecuted instantiation: <gix_attributes::parse::Iter>::parse_attr::{closure#0}
<gix_attributes::parse::Iter>::parse_attr::{closure#0}
Line
Count
Source
54
26.2M
        let mut tokens = attr.splitn(2, |b| *b == b'=');
<gix_attributes::parse::Iter>::parse_attr::{closure#0}
Line
Count
Source
54
31.3M
        let mut tokens = attr.splitn(2, |b| *b == b'=');
55
13.6M
        let attr = tokens.next().expect("attr itself").as_bstr();
56
13.6M
        let possibly_value = tokens.next();
57
13.6M
        let (attr, state) = if attr.first() == Some(&b'-') {
58
22.9k
            (&attr[1..], StateRef::Unset)
59
13.6M
        } else if attr.first() == Some(&b'!') {
60
25.7k
            (&attr[1..], StateRef::Unspecified)
61
        } else {
62
13.6M
            (attr, possibly_value.map_or(StateRef::Set, StateRef::from_bytes))
63
        };
64
13.6M
        Ok(AssignmentRef::new(check_attr(attr)?, state))
65
13.6M
    }
Unexecuted instantiation: <gix_attributes::parse::Iter>::parse_attr
<gix_attributes::parse::Iter>::parse_attr
Line
Count
Source
53
7.03M
    fn parse_attr(&self, attr: &'a [u8]) -> Result<AssignmentRef<'a>, name::Error> {
54
7.03M
        let mut tokens = attr.splitn(2, |b| *b == b'=');
55
7.03M
        let attr = tokens.next().expect("attr itself").as_bstr();
56
7.03M
        let possibly_value = tokens.next();
57
7.03M
        let (attr, state) = if attr.first() == Some(&b'-') {
58
6.55k
            (&attr[1..], StateRef::Unset)
59
7.02M
        } else if attr.first() == Some(&b'!') {
60
22.1k
            (&attr[1..], StateRef::Unspecified)
61
        } else {
62
7.00M
            (attr, possibly_value.map_or(StateRef::Set, StateRef::from_bytes))
63
        };
64
7.03M
        Ok(AssignmentRef::new(check_attr(attr)?, state))
65
7.03M
    }
<gix_attributes::parse::Iter>::parse_attr
Line
Count
Source
53
6.63M
    fn parse_attr(&self, attr: &'a [u8]) -> Result<AssignmentRef<'a>, name::Error> {
54
6.63M
        let mut tokens = attr.splitn(2, |b| *b == b'=');
55
6.63M
        let attr = tokens.next().expect("attr itself").as_bstr();
56
6.63M
        let possibly_value = tokens.next();
57
6.63M
        let (attr, state) = if attr.first() == Some(&b'-') {
58
16.3k
            (&attr[1..], StateRef::Unset)
59
6.61M
        } else if attr.first() == Some(&b'!') {
60
3.68k
            (&attr[1..], StateRef::Unspecified)
61
        } else {
62
6.60M
            (attr, possibly_value.map_or(StateRef::Set, StateRef::from_bytes))
63
        };
64
6.63M
        Ok(AssignmentRef::new(check_attr(attr)?, state))
65
6.63M
    }
66
}
67
68
13.6M
fn check_attr(attr: &BStr) -> Result<NameRef<'_>, name::Error> {
69
13.6M
    fn attr_valid(attr: &BStr) -> bool {
70
13.6M
        if attr.first() == Some(&b'-') {
71
695
            return false;
72
13.6M
        }
73
74
13.6M
        attr.bytes()
75
43.8M
            .all(|b| matches!(b, b'-' | b'.' | b'_' | b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'))
Unexecuted instantiation: gix_attributes::parse::check_attr::attr_valid::{closure#0}
gix_attributes::parse::check_attr::attr_valid::{closure#0}
Line
Count
Source
75
19.3M
            .all(|b| matches!(b, b'-' | b'.' | b'_' | b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'))
gix_attributes::parse::check_attr::attr_valid::{closure#0}
Line
Count
Source
75
24.4M
            .all(|b| matches!(b, b'-' | b'.' | b'_' | b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'))
76
13.6M
    }
Unexecuted instantiation: gix_attributes::parse::check_attr::attr_valid
gix_attributes::parse::check_attr::attr_valid
Line
Count
Source
69
7.05M
    fn attr_valid(attr: &BStr) -> bool {
70
7.05M
        if attr.first() == Some(&b'-') {
71
689
            return false;
72
7.05M
        }
73
74
7.05M
        attr.bytes()
75
7.05M
            .all(|b| matches!(b, b'-' | b'.' | b'_' | b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'))
76
7.05M
    }
gix_attributes::parse::check_attr::attr_valid
Line
Count
Source
69
6.63M
    fn attr_valid(attr: &BStr) -> bool {
70
6.63M
        if attr.first() == Some(&b'-') {
71
6
            return false;
72
6.63M
        }
73
74
6.63M
        attr.bytes()
75
6.63M
            .all(|b| matches!(b, b'-' | b'.' | b'_' | b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'))
76
6.63M
    }
77
78
13.6M
    attr_valid(attr)
79
13.6M
        .then(|| NameRef(KStringRef::from_ref(attr.to_str().expect("no illformed utf8"))))
Unexecuted instantiation: gix_attributes::parse::check_attr::{closure#0}
gix_attributes::parse::check_attr::{closure#0}
Line
Count
Source
79
7.01M
        .then(|| NameRef(KStringRef::from_ref(attr.to_str().expect("no illformed utf8"))))
gix_attributes::parse::check_attr::{closure#0}
Line
Count
Source
79
6.62M
        .then(|| NameRef(KStringRef::from_ref(attr.to_str().expect("no illformed utf8"))))
80
13.6M
        .ok_or_else(|| name::Error { attribute: attr.into() })
Unexecuted instantiation: gix_attributes::parse::check_attr::{closure#1}
gix_attributes::parse::check_attr::{closure#1}
Line
Count
Source
80
36.1k
        .ok_or_else(|| name::Error { attribute: attr.into() })
gix_attributes::parse::check_attr::{closure#1}
Line
Count
Source
80
684
        .ok_or_else(|| name::Error { attribute: attr.into() })
81
13.6M
}
Unexecuted instantiation: gix_attributes::parse::check_attr
gix_attributes::parse::check_attr
Line
Count
Source
68
7.05M
fn check_attr(attr: &BStr) -> Result<NameRef<'_>, name::Error> {
69
    fn attr_valid(attr: &BStr) -> bool {
70
        if attr.first() == Some(&b'-') {
71
            return false;
72
        }
73
74
        attr.bytes()
75
            .all(|b| matches!(b, b'-' | b'.' | b'_' | b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'))
76
    }
77
78
7.05M
    attr_valid(attr)
79
7.05M
        .then(|| NameRef(KStringRef::from_ref(attr.to_str().expect("no illformed utf8"))))
80
7.05M
        .ok_or_else(|| name::Error { attribute: attr.into() })
81
7.05M
}
gix_attributes::parse::check_attr
Line
Count
Source
68
6.63M
fn check_attr(attr: &BStr) -> Result<NameRef<'_>, name::Error> {
69
    fn attr_valid(attr: &BStr) -> bool {
70
        if attr.first() == Some(&b'-') {
71
            return false;
72
        }
73
74
        attr.bytes()
75
            .all(|b| matches!(b, b'-' | b'.' | b'_' | b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'))
76
    }
77
78
6.63M
    attr_valid(attr)
79
6.63M
        .then(|| NameRef(KStringRef::from_ref(attr.to_str().expect("no illformed utf8"))))
80
6.63M
        .ok_or_else(|| name::Error { attribute: attr.into() })
81
6.63M
}
82
83
impl<'a> Iterator for Iter<'a> {
84
    type Item = Result<AssignmentRef<'a>, name::Error>;
85
86
15.6M
    fn next(&mut self) -> Option<Self::Item> {
87
15.6M
        let attr = self.attrs.next().filter(|a| !a.is_empty())?;
Unexecuted instantiation: <gix_attributes::parse::Iter as core::iter::traits::iterator::Iterator>::next::{closure#0}
<gix_attributes::parse::Iter as core::iter::traits::iterator::Iterator>::next::{closure#0}
Line
Count
Source
87
7.03M
        let attr = self.attrs.next().filter(|a| !a.is_empty())?;
<gix_attributes::parse::Iter as core::iter::traits::iterator::Iterator>::next::{closure#0}
Line
Count
Source
87
6.63M
        let attr = self.attrs.next().filter(|a| !a.is_empty())?;
88
13.6M
        self.parse_attr(attr).into()
89
15.6M
    }
Unexecuted instantiation: <gix_attributes::parse::Iter as core::iter::traits::iterator::Iterator>::next
<gix_attributes::parse::Iter as core::iter::traits::iterator::Iterator>::next
Line
Count
Source
86
9.06M
    fn next(&mut self) -> Option<Self::Item> {
87
9.06M
        let attr = self.attrs.next().filter(|a| !a.is_empty())?;
88
7.03M
        self.parse_attr(attr).into()
89
9.06M
    }
<gix_attributes::parse::Iter as core::iter::traits::iterator::Iterator>::next
Line
Count
Source
86
6.63M
    fn next(&mut self) -> Option<Self::Item> {
87
6.63M
        let attr = self.attrs.next().filter(|a| !a.is_empty())?;
88
6.63M
        self.parse_attr(attr).into()
89
6.63M
    }
90
}
91
92
/// Instantiation
93
impl<'a> Lines<'a> {
94
    /// Create a new instance to parse all attributes in all lines of the input `bytes`.
95
7.32k
    pub fn new(bytes: &'a [u8]) -> Self {
96
7.32k
        let bom = unicode_bom::Bom::from(bytes);
97
7.32k
        Lines {
98
7.32k
            lines: bytes[bom.len()..].lines(),
99
7.32k
            line_no: 0,
100
7.32k
        }
101
7.32k
    }
Unexecuted instantiation: <gix_attributes::parse::Lines>::new
<gix_attributes::parse::Lines>::new
Line
Count
Source
95
7.32k
    pub fn new(bytes: &'a [u8]) -> Self {
96
7.32k
        let bom = unicode_bom::Bom::from(bytes);
97
7.32k
        Lines {
98
7.32k
            lines: bytes[bom.len()..].lines(),
99
7.32k
            line_no: 0,
100
7.32k
        }
101
7.32k
    }
Unexecuted instantiation: <gix_attributes::parse::Lines>::new
102
}
103
104
impl<'a> Iterator for Lines<'a> {
105
    type Item = Result<(Kind, Iter<'a>, usize), Error>;
106
107
2.09M
    fn next(&mut self) -> Option<Self::Item> {
108
4.07M
        fn skip_blanks(line: &BStr) -> &BStr {
109
4.07M
            line.find_not_byteset(BLANKS).map_or(line, |pos| &line[pos..])
Unexecuted instantiation: <gix_attributes::parse::Lines as core::iter::traits::iterator::Iterator>::next::skip_blanks::{closure#0}
<gix_attributes::parse::Lines as core::iter::traits::iterator::Iterator>::next::skip_blanks::{closure#0}
Line
Count
Source
109
2.13M
            line.find_not_byteset(BLANKS).map_or(line, |pos| &line[pos..])
Unexecuted instantiation: <gix_attributes::parse::Lines as core::iter::traits::iterator::Iterator>::next::skip_blanks::{closure#0}
110
4.07M
        }
Unexecuted instantiation: <gix_attributes::parse::Lines as core::iter::traits::iterator::Iterator>::next::skip_blanks
<gix_attributes::parse::Lines as core::iter::traits::iterator::Iterator>::next::skip_blanks
Line
Count
Source
108
4.07M
        fn skip_blanks(line: &BStr) -> &BStr {
109
4.07M
            line.find_not_byteset(BLANKS).map_or(line, |pos| &line[pos..])
110
4.07M
        }
Unexecuted instantiation: <gix_attributes::parse::Lines as core::iter::traits::iterator::Iterator>::next::skip_blanks
111
4.07M
        for line in self.lines.by_ref() {
112
4.07M
            self.line_no += 1;
113
4.07M
            let line = skip_blanks(line.into());
114
4.07M
            if line.first() == Some(&b'#') {
115
51.6k
                continue;
116
4.02M
            }
117
4.02M
            match parse_line(line, self.line_no) {
118
1.93M
                None => continue,
119
2.08M
                Some(res) => return Some(res),
120
            }
121
        }
122
7.32k
        None
123
2.09M
    }
Unexecuted instantiation: <gix_attributes::parse::Lines as core::iter::traits::iterator::Iterator>::next
<gix_attributes::parse::Lines as core::iter::traits::iterator::Iterator>::next
Line
Count
Source
107
2.09M
    fn next(&mut self) -> Option<Self::Item> {
108
        fn skip_blanks(line: &BStr) -> &BStr {
109
            line.find_not_byteset(BLANKS).map_or(line, |pos| &line[pos..])
110
        }
111
4.07M
        for line in self.lines.by_ref() {
112
4.07M
            self.line_no += 1;
113
4.07M
            let line = skip_blanks(line.into());
114
4.07M
            if line.first() == Some(&b'#') {
115
51.6k
                continue;
116
4.02M
            }
117
4.02M
            match parse_line(line, self.line_no) {
118
1.93M
                None => continue,
119
2.08M
                Some(res) => return Some(res),
120
            }
121
        }
122
7.32k
        None
123
2.09M
    }
Unexecuted instantiation: <gix_attributes::parse::Lines as core::iter::traits::iterator::Iterator>::next
124
}
125
126
4.02M
fn parse_line(line: &BStr, line_number: usize) -> Option<Result<(Kind, Iter<'_>, usize), Error>> {
127
4.02M
    if line.is_empty() {
128
1.93M
        return None;
129
2.08M
    }
130
131
2.08M
    let (line, attrs): (Cow<'_, _>, _) = if line.starts_with(b"\"") {
132
28.0k
        let (unquoted, consumed) = match gix_quote::ansi_c::undo(line) {
133
20.8k
            Ok(res) => res,
134
7.11k
            Err(err) => return Some(Err(err.into())),
135
        };
136
20.8k
        (unquoted, &line[consumed..])
137
    } else {
138
2.05M
        line.find_byteset(BLANKS)
139
2.05M
            .map(|pos| (line[..pos].as_bstr().into(), line[pos..].as_bstr()))
Unexecuted instantiation: gix_attributes::parse::parse_line::{closure#0}
gix_attributes::parse::parse_line::{closure#0}
Line
Count
Source
139
783k
            .map(|pos| (line[..pos].as_bstr().into(), line[pos..].as_bstr()))
Unexecuted instantiation: gix_attributes::parse::parse_line::{closure#0}
140
2.05M
            .unwrap_or((line.into(), [].as_bstr()))
141
    };
142
143
2.08M
    let kind_res = match line.strip_prefix(b"[attr]") {
144
20.2k
        Some(macro_name) => check_attr(macro_name.into())
145
20.2k
            .map_err(|err| Error::MacroName {
146
2.10k
                line_number,
147
2.10k
                macro_name: err.attribute,
148
2.10k
            })
Unexecuted instantiation: gix_attributes::parse::parse_line::{closure#1}
gix_attributes::parse::parse_line::{closure#1}
Line
Count
Source
146
2.10k
                line_number,
147
2.10k
                macro_name: err.attribute,
148
2.10k
            })
Unexecuted instantiation: gix_attributes::parse::parse_line::{closure#1}
149
20.2k
            .map(|name| Kind::Macro(name.to_owned())),
Unexecuted instantiation: gix_attributes::parse::parse_line::{closure#2}
gix_attributes::parse::parse_line::{closure#2}
Line
Count
Source
149
18.1k
            .map(|name| Kind::Macro(name.to_owned())),
Unexecuted instantiation: gix_attributes::parse::parse_line::{closure#2}
150
        None => {
151
2.06M
            let pattern = gix_glob::Pattern::from_bytes(line.as_ref())?;
152
2.05M
            if pattern.mode.contains(gix_glob::pattern::Mode::NEGATIVE) {
153
5.94k
                Err(Error::PatternNegation {
154
5.94k
                    line: line.into_owned(),
155
5.94k
                    line_number,
156
5.94k
                })
157
            } else {
158
2.04M
                Ok(Kind::Pattern(pattern))
159
            }
160
        }
161
    };
162
2.07M
    let kind = match kind_res {
163
2.06M
        Ok(kind) => kind,
164
8.05k
        Err(err) => return Some(Err(err)),
165
    };
166
2.06M
    Ok((kind, Iter::new(attrs), line_number)).into()
167
4.02M
}
Unexecuted instantiation: gix_attributes::parse::parse_line
gix_attributes::parse::parse_line
Line
Count
Source
126
4.02M
fn parse_line(line: &BStr, line_number: usize) -> Option<Result<(Kind, Iter<'_>, usize), Error>> {
127
4.02M
    if line.is_empty() {
128
1.93M
        return None;
129
2.08M
    }
130
131
2.08M
    let (line, attrs): (Cow<'_, _>, _) = if line.starts_with(b"\"") {
132
28.0k
        let (unquoted, consumed) = match gix_quote::ansi_c::undo(line) {
133
20.8k
            Ok(res) => res,
134
7.11k
            Err(err) => return Some(Err(err.into())),
135
        };
136
20.8k
        (unquoted, &line[consumed..])
137
    } else {
138
2.05M
        line.find_byteset(BLANKS)
139
2.05M
            .map(|pos| (line[..pos].as_bstr().into(), line[pos..].as_bstr()))
140
2.05M
            .unwrap_or((line.into(), [].as_bstr()))
141
    };
142
143
2.08M
    let kind_res = match line.strip_prefix(b"[attr]") {
144
20.2k
        Some(macro_name) => check_attr(macro_name.into())
145
20.2k
            .map_err(|err| Error::MacroName {
146
                line_number,
147
                macro_name: err.attribute,
148
            })
149
20.2k
            .map(|name| Kind::Macro(name.to_owned())),
150
        None => {
151
2.06M
            let pattern = gix_glob::Pattern::from_bytes(line.as_ref())?;
152
2.05M
            if pattern.mode.contains(gix_glob::pattern::Mode::NEGATIVE) {
153
5.94k
                Err(Error::PatternNegation {
154
5.94k
                    line: line.into_owned(),
155
5.94k
                    line_number,
156
5.94k
                })
157
            } else {
158
2.04M
                Ok(Kind::Pattern(pattern))
159
            }
160
        }
161
    };
162
2.07M
    let kind = match kind_res {
163
2.06M
        Ok(kind) => kind,
164
8.05k
        Err(err) => return Some(Err(err)),
165
    };
166
2.06M
    Ok((kind, Iter::new(attrs), line_number)).into()
167
4.02M
}
Unexecuted instantiation: gix_attributes::parse::parse_line
168
169
const BLANKS: &[u8] = b" \t\r";