use smallvec::SmallVec; use crate::{ parse, parse::{Event, EventRef, SectionData}, }; /// Events that precede the first section in a configuration file. pub(crate) type FrontMatterEvents = SmallVec<[Event; 8]>; /// A `git-config` file parser. /// /// This parser exposes low-level syntactic events from a `git-config` file. /// Generally speaking, you'll want to use [`File`] as it wraps /// around the parser to provide a higher-level abstraction to a `git-config` /// file, including querying, modifying, and updating values. /// /// This parser guarantees that the events emitted are sufficient to /// reconstruct a `git-config` file identical to the source `git-config` /// when writing it. /// /// # Differences between a `.ini` parser /// /// While the `git-config` format closely resembles the [`.ini` file format], /// there are subtle differences that make them incompatible. For one, the file /// format is not well defined, and there exists no formal specification to /// adhere to. /// /// For concrete examples, some notable differences are: /// - `git-config` sections permit subsections via either a quoted string /// (`[some-section "subsection"]`) or via the deprecated dot notation /// (`[some-section.subsection]`). Successful parsing these section names is not /// well defined in typical `.ini` parsers. This parser will handle these cases /// perfectly. /// - Comment markers are not strictly defined either. This parser will always /// and only handle a semicolon or octothorpe (also known as a hash or number /// sign). /// - Global properties before the first section are accepted for compatibility /// with Git, even though they are uncommon in `.gitconfig` files. /// - Only `\t`, `\n`, `\b` `\\` are valid escape characters. /// - Quoted and semi-quoted values will be parsed (but quotes will be included /// in event outputs). An example of a semi-quoted value is `5"hello world"`, /// which should be interpreted as `5hello world` after /// [normalization][crate::value::normalize()]. /// - Line continuations via a `\` character is supported (inside or outside of quotes) /// - Whitespace handling similarly follows the `git-config` specification as /// closely as possible, where excess whitespace after a non-quoted value are /// trimmed, and line continuations onto a new line with excess spaces are kept. /// - Only equal signs (optionally padded by spaces) are valid name/value /// delimiters. /// /// Note that things such as case-sensitivity or duplicate sections are /// _not_ handled. This parser is a low level _syntactic_ interpreter /// and higher level wrappers around this parser should handle _semantic_ values. /// This also means /// that string-like values are not interpreted. For example, `hello"world"` /// would be read at a high level as `helloworld` but this parser will return /// the former instead, with the extra quotes. This is because it is not the /// responsibility of the parser to interpret these values, and doing so would /// necessarily require a copy, which this parser avoids. /// /// # Trait Implementations /// /// - This struct does _not_ implement [`FromStr`] due to lifetime /// constraints implied on the required `from_str` method. Instead, it provides /// [`From<&'_ str>`]. /// /// # Idioms /// /// If you do want to use this parser, there are some idioms that may help you /// with interpreting sequences of events. /// /// ## `Value` events do not immediately follow `Key` events /// /// Consider the following `git-config` example: /// /// ```text /// [core] /// autocrlf = input /// ``` /// /// Because this parser guarantees near-perfect reconstruction, there are many /// non-significant events that occur in addition to the ones you may expect: /// /// ``` /// # use gix_config::parse::{EventRef, Events}; /// # let events = Events::from_str("[core]\n autocrlf = input")?; /// assert_eq!(events.iter().collect::>(), vec![ /// EventRef::SectionHeader { /// name: "core".into(), /// separator: None, /// subsection_name: None, /// }, /// EventRef::Newline("\n".into()), /// EventRef::Whitespace(" ".into()), /// EventRef::SectionValueName("autocrlf".into()), /// EventRef::Whitespace(" ".into()), /// EventRef::KeyValueSeparator, /// EventRef::Whitespace(" ".into()), /// EventRef::Value("input".into()), /// ]); /// # Ok::<_, gix_config::parse::Error>(()) /// ``` /// /// In particular, [`EventRef::SectionValueName`] and [`EventRef::Value`] are separated by two /// [`EventRef::Whitespace`] events around an [`EventRef::KeyValueSeparator`]. If the config instead /// had `autocrlf=input`, those whitespace events would not be present. /// /// ## `KeyValueSeparator` event is not guaranteed to emit /// /// Consider the following `git-config` example: /// /// ```text /// [core] /// autocrlf /// ``` /// /// This is a valid config with a `autocrlf` key having an implicit `true` /// value. This means that there is not a `=` separating the key and value, /// which means that the corresponding event won't appear either: /// /// ``` /// # use gix_config::parse::Events; /// # let section_data = "[core]\n autocrlf"; /// # let events = Events::from_str(section_data)?; /// # assert_eq!( /// # events.iter().map(|event| event.to_string()).collect::>(), /// # vec!["[core]", "\n", " ", "autocrlf", ""] /// # ); /// # Ok::<_, gix_config::parse::Error>(()) /// ``` /// /// ## Quoted values are not unquoted /// /// Consider the following `git-config` example: /// /// ```text /// [core] /// autocrlf=true"" /// filemode=fa"lse" /// ``` /// /// Both these events, when fully processed, should normally be `true` and /// `false`. However, because this parser preserves the original event stream, we cannot process /// partially quoted values, such as the `false` example. As a result, to /// maintain consistency, the parser will just take all values as literals. The /// relevant event stream emitted is thus emitted as: /// /// ``` /// # use gix_config::parse::Events; /// # let section_data = "[core]\nautocrlf=true\"\"\nfilemode=fa\"lse\""; /// # let events = Events::from_str(section_data)?; /// # assert_eq!( /// # events.iter().map(|event| event.to_string()).collect::>(), /// # vec!["[core]", "\n", "autocrlf", "=", r#"true"""#, "\n", "filemode", "=", r#"fa"lse""#] /// # ); /// # Ok::<_, gix_config::parse::Error>(()) /// ``` /// /// ## Whitespace after line continuations are part of the value /// /// Consider the following `git-config` example: /// /// ```text /// [some-section] /// file=a\ /// c /// ``` /// /// Because how `git-config` treats continuations, the whitespace preceding `c` /// are in fact part of the value of `file`. The fully interpreted key/value /// pair is actually `file=a c`. As a result, the parser will provide this /// split value accordingly: /// /// ``` /// # use gix_config::parse::Events; /// # let section_data = "[some-section]\nfile=a\\\n c"; /// # let events = Events::from_str(section_data)?; /// # assert_eq!( /// # events.iter().map(|event| event.to_string()).collect::>(), /// # vec!["[some-section]", "\n", "file", "=", "a\\", "\n", " c"] /// # ); /// # Ok::<_, gix_config::parse::Error>(()) /// ``` /// /// [`File`]: crate::File /// [`.ini` file format]: https://en.wikipedia.org/wiki/INI_file /// [`git`'s documentation]: https://git-scm.com/docs/git-config#_configuration_file /// [`FromStr`]: std::str::FromStr /// [`From<&'_ str>`]: std::convert::From #[derive(Clone, Debug, Default)] pub struct Events { pub(crate) backing: Vec, /// Events seen before the first section. pub(crate) frontmatter: FrontMatterEvents, /// All parsed sections. pub(crate) sections: Vec, } impl Events { /// Attempt to parse the provided bytes. /// /// Inputs larger than [`u32::MAX`] bytes are rejected because event spans use 32-bit offsets. /// /// Use `filter` to only include those events for which it returns true. pub fn from_bytes(input: &[u8], filter: Option) -> bool>) -> Result { let mut header = None; let mut events = Vec::with_capacity(256); let mut frontmatter = FrontMatterEvents::default(); let mut sections = Vec::new(); // The parser emits offsets into the caller's input. Copy it only after successful parsing to // make the returned events self-contained without allocating on parse errors. parse::from_bytes::from_bytes(input, &mut |e: Event| match e { Event::SectionHeader(next_header) => { match header.take() { None => { frontmatter = std::mem::take(&mut events).into_iter().collect(); } Some(prev_header) => { #[expect( clippy::drain_collect, reason = "Keep the scratch vector's allocation for parsing the next section." )] let section_events = events.drain(..).collect(); sections.push(parse::SectionData { header: prev_header, events: section_events, }); } } header = Some(match Event::SectionHeader(next_header) { Event::SectionHeader(h) => h, _ => unreachable!("BUG: event type changed"), }); } event => { if filter.is_none_or(|f| f(event.as_ref_in(input))) { events.push(event); } } })?; match header { None => { frontmatter = events.into_iter().collect(); } Some(prev_header) => { sections.push(parse::SectionData { header: prev_header, events: std::mem::take(&mut events), }); } } Ok(Events { backing: input.to_vec(), frontmatter, sections, }) } /// Attempt to parse the provided `input` string. /// /// Prefer the [`from_bytes()`](Self::from_bytes()) method if UTF8 encoding /// isn't guaranteed. #[expect( clippy::should_implement_trait, reason = "the method has domain-specific semantics despite sharing a standard trait method name" )] pub fn from_str(input: &str) -> Result { Self::from_bytes(input.as_bytes(), None) } /// Return all contained events as borrowed views. pub fn iter(&self) -> impl Iterator> + '_ { self.frontmatter().chain(self.sections().flat_map(SectionRef::iter)) } /// Return all events before the first section as borrowed views. pub fn frontmatter(&self) -> impl Iterator> + '_ { self.frontmatter.iter().map(move |event| event.as_ref_in(&self.backing)) } /// Return all parsed sections as borrowed views. pub fn sections(&self) -> impl Iterator> + '_ { self.sections.iter().map(move |section| SectionRef { header: §ion.header, events: §ion.events, backing: &self.backing, }) } } impl TryFrom<&str> for Events { type Error = parse::Error; fn try_from(value: &str) -> Result { Self::from_str(value) } } impl TryFrom<&[u8]> for Events { type Error = parse::Error; fn try_from(value: &[u8]) -> Result { Events::from_bytes(value, None) } } /// A borrowed view of a parsed section. #[derive(Copy, Clone, Debug)] pub struct SectionRef<'a> { header: &'a parse::section::HeaderData, events: &'a [Event], backing: &'a [u8], } impl<'a> SectionRef<'a> { /// Return the section header as an event view. pub fn header(&self) -> EventRef<'a> { EventRef::SectionHeader { name: self.header.name.as_bstr_in(self.backing), separator: self .header .separator .as_ref() .map(|separator| separator.as_bstr_in(self.backing)), subsection_name: self .header .subsection_name .as_ref() .map(|subsection_name| subsection_name.value_in(self.backing)), } } /// Return the events contained in this section body. pub fn body(self) -> impl Iterator> + 'a { self.events.iter().map(move |event| event.as_ref_in(self.backing)) } /// Return the complete event stream for this section, including its header. pub fn iter(self) -> impl Iterator> + 'a { std::iter::once(self.header()).chain(self.body()) } }