1use std::borrow::Cow;
2
3use bstr::{BStr, ByteSlice};
4use kstring::KStringRef;
5
6use crate::{name, AssignmentRef, Name, NameRef, StateRef};
7
8#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub enum Kind {
12 Pattern(gix_glob::Pattern),
14 Macro(Name),
16}
17
18mod error {
19 use bstr::BString;
20 #[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}
34pub use error::Error;
35
36pub struct Lines<'a> {
38 lines: bstr::Lines<'a>,
39 line_no: usize,
40}
41
42pub struct Iter<'a> {
44 attrs: bstr::Fields<'a>,
45}
46
47impl<'a> Iter<'a> {
48 pub fn new(input: &'a BStr) -> Self {
50 Iter { attrs: input.fields() }
51 }
52
53 fn parse_attr(&self, attr: &'a [u8]) -> Result<AssignmentRef<'a>, name::Error> {
54 let mut tokens = attr.splitn(2, |b| *b == b'=');
55 let attr = tokens.next().expect("attr itself").as_bstr();
56 let possibly_value = tokens.next();
57 let (attr, state) = if attr.first() == Some(&b'-') {
58 (&attr[1..], StateRef::Unset)
59 } else if attr.first() == Some(&b'!') {
60 (&attr[1..], StateRef::Unspecified)
61 } else {
62 (attr, possibly_value.map_or(StateRef::Set, StateRef::from_bytes))
63 };
64 Ok(AssignmentRef::new(check_attr(attr)?, state))
65 }
66}
67
68fn 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 attr_valid(attr)
79 .then(|| NameRef(KStringRef::from_ref(attr.to_str().expect("no illformed utf8"))))
80 .ok_or_else(|| name::Error { attribute: attr.into() })
81}
82
83impl<'a> Iterator for Iter<'a> {
84 type Item = Result<AssignmentRef<'a>, name::Error>;
85
86 fn next(&mut self) -> Option<Self::Item> {
87 let attr = self.attrs.next().filter(|a| !a.is_empty())?;
88 self.parse_attr(attr).into()
89 }
90}
91
92impl<'a> Lines<'a> {
94 pub fn new(bytes: &'a [u8]) -> Self {
96 let bom = unicode_bom::Bom::from(bytes);
97 Lines {
98 lines: bytes[bom.len()..].lines(),
99 line_no: 0,
100 }
101 }
102}
103
104impl<'a> Iterator for Lines<'a> {
105 type Item = Result<(Kind, Iter<'a>, usize), Error>;
106
107 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 for line in self.lines.by_ref() {
112 self.line_no += 1;
113 let line = skip_blanks(line.into());
114 if line.first() == Some(&b'#') {
115 continue;
116 }
117 match parse_line(line, self.line_no) {
118 None => continue,
119 Some(res) => return Some(res),
120 }
121 }
122 None
123 }
124}
125
126fn parse_line(line: &BStr, line_number: usize) -> Option<Result<(Kind, Iter<'_>, usize), Error>> {
127 if line.is_empty() {
128 return None;
129 }
130
131 let (line, attrs): (Cow<'_, _>, _) = if line.starts_with(b"\"") {
132 let (unquoted, consumed) = match gix_quote::ansi_c::undo(line) {
133 Ok(res) => res,
134 Err(err) => return Some(Err(err.into())),
135 };
136 (unquoted, &line[consumed..])
137 } else {
138 line.find_byteset(BLANKS)
139 .map(|pos| (line[..pos].as_bstr().into(), line[pos..].as_bstr()))
140 .unwrap_or((line.into(), [].as_bstr()))
141 };
142
143 let kind_res = match line.strip_prefix(b"[attr]") {
144 Some(macro_name) => check_attr(macro_name.into())
145 .map_err(|err| Error::MacroName {
146 line_number,
147 macro_name: err.attribute,
148 })
149 .map(|name| Kind::Macro(name.to_owned())),
150 None => {
151 let pattern = gix_glob::Pattern::from_bytes(line.as_ref())?;
152 if pattern.mode.contains(gix_glob::pattern::Mode::NEGATIVE) {
153 Err(Error::PatternNegation {
154 line: line.into_owned(),
155 line_number,
156 })
157 } else {
158 Ok(Kind::Pattern(pattern))
159 }
160 }
161 };
162 let kind = match kind_res {
163 Ok(kind) => kind,
164 Err(err) => return Some(Err(err)),
165 };
166 Ok((kind, Iter::new(attrs), line_number)).into()
167}
168
169const BLANKS: &[u8] = b" \t\r";