1use std::borrow::Cow;
2
3use bstr::{BStr, ByteSlice};
4
5use crate::{AssignmentRef, Name, NameRef, StateRef, name};
6
7#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub enum Kind {
11 Pattern(gix_glob::Pattern),
13 Macro(Name),
15}
16
17mod error {
18 use bstr::BString;
19 #[derive(thiserror::Error, Debug)]
21 #[expect(missing_docs)]
22 pub enum Error {
23 #[error(r"Line {line_number} has a negative pattern, for literal characters use \!: {line}")]
24 PatternNegation { line_number: usize, line: BString },
25 #[error("Attribute in line {line_number} has an invalid name: {attribute}")]
26 AttributeName { line_number: usize, attribute: BString },
27 #[error("Macro in line {line_number} has an invalid name: {macro_name}")]
28 MacroName { line_number: usize, macro_name: BString },
29 }
30}
31pub use error::Error;
32
33pub struct Lines<'a> {
35 lines: bstr::Lines<'a>,
36 line_no: usize,
37}
38
39pub struct Iter<'a> {
41 attrs: std::slice::Split<'a, u8, fn(&u8) -> bool>,
42}
43
44impl<'a> Iter<'a> {
45 pub fn new(input: &'a BStr) -> Self {
47 Iter {
48 attrs: input.split(is_blank as fn(&u8) -> bool),
49 }
50 }
51
52 fn parse_attr(&self, attr: &'a [u8]) -> Result<AssignmentRef<'a>, name::Error> {
53 let mut tokens = attr.splitn(2, |b| *b == b'=');
54 let attr = tokens.next().expect("attr itself").as_bstr();
55 let possibly_value = tokens.next();
56 let (attr, state) = if attr.first() == Some(&b'-') {
57 (&attr[1..], StateRef::Unset)
58 } else if attr.first() == Some(&b'!') {
59 (&attr[1..], StateRef::Unspecified)
60 } else {
61 (attr, possibly_value.map_or(StateRef::Set, StateRef::from_bytes))
62 };
63 Ok(AssignmentRef::new(check_attr(attr)?, state))
64 }
65}
66
67fn check_attr(attr: &BStr) -> Result<NameRef<'_>, name::Error> {
68 NameRef::try_from(attr).and_then(|name| {
69 (!name.as_str().starts_with("builtin_"))
70 .then_some(name)
71 .ok_or_else(|| name::Error { attribute: attr.into() })
72 })
73}
74
75impl<'a> Iterator for Iter<'a> {
76 type Item = Result<AssignmentRef<'a>, name::Error>;
77
78 fn next(&mut self) -> Option<Self::Item> {
79 let attr = self.attrs.find(|a| !a.is_empty())?;
80 self.parse_attr(attr).into()
81 }
82}
83
84impl<'a> Lines<'a> {
86 pub fn new(bytes: &'a [u8]) -> Self {
88 let bom = unicode_bom::Bom::from(bytes);
89 Lines {
90 lines: bytes[bom.len()..].lines(),
91 line_no: 0,
92 }
93 }
94}
95
96impl<'a> Iterator for Lines<'a> {
97 type Item = Result<(Kind, Iter<'a>, usize), Error>;
98
99 fn next(&mut self) -> Option<Self::Item> {
100 fn skip_blanks(line: &BStr) -> &BStr {
101 line.find_not_byteset(BLANKS).map_or(line, |pos| &line[pos..])
102 }
103 for line in self.lines.by_ref() {
104 self.line_no += 1;
105 let line = skip_blanks(line.into());
106 if line.first() == Some(&b'#') {
107 continue;
108 }
109 match parse_line(line, self.line_no) {
110 None => continue,
111 Some(res) => return Some(res),
112 }
113 }
114 None
115 }
116}
117
118fn parse_line(line: &BStr, line_number: usize) -> Option<Result<(Kind, Iter<'_>, usize), Error>> {
119 if line.is_empty() {
120 return None;
121 }
122
123 let unquoted = line
124 .starts_with(b"\"")
125 .then(|| gix_quote::ansi_c::undo(line).ok())
126 .flatten();
127 let (line, attrs): (Cow<'_, _>, _) = match unquoted {
128 Some((unquoted, consumed)) => (unquoted, &line[consumed..]),
129 None => line
130 .find_byteset(BLANKS)
131 .map(|pos| (line[..pos].as_bstr().into(), line[pos..].as_bstr()))
132 .unwrap_or((line.into(), [].as_bstr())),
133 };
134
135 let kind_res = match line.strip_prefix(b"[attr]").filter(|name| !name.is_empty()) {
136 Some(macro_name) => check_attr(macro_name.into())
137 .map_err(|err| Error::MacroName {
138 line_number,
139 macro_name: err.attribute,
140 })
141 .map(|name| Kind::Macro(name.to_owned())),
142 None => {
143 let pattern = gix_glob::Pattern::from_bytes(line.as_ref())?;
144 if pattern.mode.contains(gix_glob::pattern::Mode::NEGATIVE) {
145 Err(Error::PatternNegation {
146 line: line.into_owned(),
147 line_number,
148 })
149 } else {
150 Ok(Kind::Pattern(pattern))
151 }
152 }
153 };
154 let kind = match kind_res {
155 Ok(kind) => kind,
156 Err(err) => return Some(Err(err)),
157 };
158 Ok((kind, Iter::new(attrs), line_number)).into()
159}
160
161fn is_blank(b: &u8) -> bool {
162 BLANKS.contains(b)
163}
164
165const BLANKS: &[u8] = b" \t\r";