use std::borrow::Cow; use bstr::{BStr, ByteSlice}; use gix_error::{ErrorExt, ResultExt, ValidationError}; use crate::{AssignmentRef, Name, NameRef, StateRef}; /// The kind of attribute that was parsed. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Kind { /// A pattern to match paths against Pattern(gix_glob::Pattern), /// The name of the macro to define, always a valid attribute name Macro(Name), } /// An iterator over attribute assignments, parsed line by line. pub struct Lines<'a> { lines: bstr::Lines<'a>, line_no: usize, } /// An iterator over attribute assignments in a single line. pub struct Iter<'a> { attrs: std::slice::Split<'a, u8, fn(&u8) -> bool>, } impl<'a> Iter<'a> { /// Create a new instance to parse attribute assignments from `input`. pub fn new(input: &'a BStr) -> Self { Iter { attrs: input.split(is_blank as fn(&u8) -> bool), } } fn parse_attr(&self, attr: &'a [u8]) -> Result, gix_error::ValidationError> { let mut tokens = attr.splitn(2, |b| *b == b'='); let attr = tokens.next().expect("attr itself").as_bstr(); let possibly_value = tokens.next(); let (attr, state) = if attr.first() == Some(&b'-') { (&attr[1..], StateRef::Unset) } else if attr.first() == Some(&b'!') { (&attr[1..], StateRef::Unspecified) } else { (attr, possibly_value.map_or(StateRef::Set, StateRef::from_bytes)) }; Ok(AssignmentRef::new(check_attr(attr)?, state)) } } fn check_attr(attr: &BStr) -> Result, gix_error::ValidationError> { NameRef::try_from(attr).and_then(|name| { (!name.as_str().starts_with("builtin_")).then_some(name).ok_or_else(|| { gix_error::ValidationError::new_with_input("Attribute name uses the reserved 'builtin_' prefix", attr) }) }) } impl<'a> Iterator for Iter<'a> { type Item = Result, gix_error::ValidationError>; fn next(&mut self) -> Option { let attr = self.attrs.find(|a| !a.is_empty())?; self.parse_attr(attr).into() } } /// Instantiation impl<'a> Lines<'a> { /// Create a new instance to parse all attributes in all lines of the input `bytes`. pub fn new(bytes: &'a [u8]) -> Self { let bom = unicode_bom::Bom::from(bytes); Lines { lines: bytes[bom.len()..].lines(), line_no: 0, } } } impl<'a> Iterator for Lines<'a> { type Item = Result<(Kind, Iter<'a>, usize), gix_error::Exn>; fn next(&mut self) -> Option { fn skip_blanks(line: &BStr) -> &BStr { line.find_not_byteset(BLANKS).map_or(line, |pos| &line[pos..]) } for line in self.lines.by_ref() { self.line_no += 1; let line = skip_blanks(line.into()); if line.first() == Some(&b'#') { continue; } match parse_line(line, self.line_no) { None => continue, Some(res) => return Some(res), } } None } } fn parse_line( line: &BStr, line_number: usize, ) -> Option, usize), gix_error::Exn>> { if line.is_empty() { return None; } let unquoted = line .starts_with(b"\"") .then(|| gix_quote::ansi_c::undo(line).ok()) .flatten(); let (line, attrs): (Cow<'_, _>, _) = match unquoted { Some((unquoted, consumed)) => (unquoted, &line[consumed..]), None => line .find_byteset(BLANKS) .map(|pos| (line[..pos].as_bstr().into(), line[pos..].as_bstr())) .unwrap_or((line.into(), [].as_bstr())), }; let kind_res = match line.strip_prefix(b"[attr]").filter(|name| !name.is_empty()) { Some(macro_name) => check_attr(macro_name.into()) .or_raise(|| ValidationError::new(format!("Macro in line {line_number} has an invalid name"))) .map(|name| Kind::Macro(name.to_owned())), None => { let pattern = gix_glob::Pattern::from_bytes(line.as_ref())?; if pattern.mode.contains(gix_glob::pattern::Mode::NEGATIVE) { Err(ValidationError::new_with_input( format!(r"Line {line_number} has a negative pattern, for literal characters use \!"), line.as_ref(), ) .raise()) } else { Ok(Kind::Pattern(pattern)) } } }; let kind = match kind_res { Ok(kind) => kind, Err(err) => return Some(Err(err)), }; Ok((kind, Iter::new(attrs), line_number)).into() } fn is_blank(b: &u8) -> bool { BLANKS.contains(b) } const BLANKS: &[u8] = b" \t\r";