Skip to main content

gix_config/parse/
events.rs

1use smallvec::SmallVec;
2
3use crate::{
4    parse,
5    parse::{Event, EventRef, SectionData},
6};
7
8/// Events that precede the first section in a configuration file.
9pub(crate) type FrontMatterEvents = SmallVec<[Event; 8]>;
10
11/// A `git-config` file parser.
12///
13/// This parser exposes low-level syntactic events from a `git-config` file.
14/// Generally speaking, you'll want to use [`File`] as it wraps
15/// around the parser to provide a higher-level abstraction to a `git-config`
16/// file, including querying, modifying, and updating values.
17///
18/// This parser guarantees that the events emitted are sufficient to
19/// reconstruct a `git-config` file identical to the source `git-config`
20/// when writing it.
21///
22/// # Differences between a `.ini` parser
23///
24/// While the `git-config` format closely resembles the [`.ini` file format],
25/// there are subtle differences that make them incompatible. For one, the file
26/// format is not well defined, and there exists no formal specification to
27/// adhere to.
28///
29/// For concrete examples, some notable differences are:
30/// - `git-config` sections permit subsections via either a quoted string
31///   (`[some-section "subsection"]`) or via the deprecated dot notation
32///   (`[some-section.subsection]`). Successful parsing these section names is not
33///   well defined in typical `.ini` parsers. This parser will handle these cases
34///   perfectly.
35/// - Comment markers are not strictly defined either. This parser will always
36///   and only handle a semicolon or octothorpe (also known as a hash or number
37///   sign).
38/// - Global properties before the first section are accepted for compatibility
39///   with Git, even though they are uncommon in `.gitconfig` files.
40/// - Only `\t`, `\n`, `\b` `\\` are valid escape characters.
41/// - Quoted and semi-quoted values will be parsed (but quotes will be included
42///   in event outputs). An example of a semi-quoted value is `5"hello world"`,
43///   which should be interpreted as `5hello world` after
44///   [normalization][crate::value::normalize()].
45/// - Line continuations via a `\` character is supported (inside or outside of quotes)
46/// - Whitespace handling similarly follows the `git-config` specification as
47///   closely as possible, where excess whitespace after a non-quoted value are
48///   trimmed, and line continuations onto a new line with excess spaces are kept.
49/// - Only equal signs (optionally padded by spaces) are valid name/value
50///   delimiters.
51///
52/// Note that things such as case-sensitivity or duplicate sections are
53/// _not_ handled. This parser is a low level _syntactic_ interpreter
54/// and higher level wrappers around this parser should handle _semantic_ values.
55/// This also means
56/// that string-like values are not interpreted. For example, `hello"world"`
57/// would be read at a high level as `helloworld` but this parser will return
58/// the former instead, with the extra quotes. This is because it is not the
59/// responsibility of the parser to interpret these values, and doing so would
60/// necessarily require a copy, which this parser avoids.
61///
62/// # Trait Implementations
63///
64/// - This struct does _not_ implement [`FromStr`] due to lifetime
65///   constraints implied on the required `from_str` method. Instead, it provides
66///   [`From<&'_ str>`].
67///
68/// # Idioms
69///
70/// If you do want to use this parser, there are some idioms that may help you
71/// with interpreting sequences of events.
72///
73/// ## `Value` events do not immediately follow `Key` events
74///
75/// Consider the following `git-config` example:
76///
77/// ```text
78/// [core]
79///   autocrlf = input
80/// ```
81///
82/// Because this parser guarantees near-perfect reconstruction, there are many
83/// non-significant events that occur in addition to the ones you may expect:
84///
85/// ```
86/// # use gix_config::parse::{EventRef, Events};
87/// # let events = Events::from_str("[core]\n  autocrlf = input")?;
88/// assert_eq!(events.iter().collect::<Vec<_>>(), vec![
89///     EventRef::SectionHeader {
90///         name: "core".into(),
91///         separator: None,
92///         subsection_name: None,
93///     },
94///     EventRef::Newline("\n".into()),
95///     EventRef::Whitespace("  ".into()),
96///     EventRef::SectionValueName("autocrlf".into()),
97///     EventRef::Whitespace(" ".into()),
98///     EventRef::KeyValueSeparator,
99///     EventRef::Whitespace(" ".into()),
100///     EventRef::Value("input".into()),
101/// ]);
102/// # Ok::<_, gix_config::parse::Error>(())
103/// ```
104///
105/// In particular, [`EventRef::SectionValueName`] and [`EventRef::Value`] are separated by two
106/// [`EventRef::Whitespace`] events around an [`EventRef::KeyValueSeparator`]. If the config instead
107/// had `autocrlf=input`, those whitespace events would not be present.
108///
109/// ## `KeyValueSeparator` event is not guaranteed to emit
110///
111/// Consider the following `git-config` example:
112///
113/// ```text
114/// [core]
115///   autocrlf
116/// ```
117///
118/// This is a valid config with a `autocrlf` key having an implicit `true`
119/// value. This means that there is not a `=` separating the key and value,
120/// which means that the corresponding event won't appear either:
121///
122/// ```
123/// # use gix_config::parse::Events;
124/// # let section_data = "[core]\n  autocrlf";
125/// # let events = Events::from_str(section_data)?;
126/// # assert_eq!(
127/// #     events.iter().map(|event| event.to_string()).collect::<Vec<_>>(),
128/// #     vec!["[core]", "\n", "  ", "autocrlf", ""]
129/// # );
130/// # Ok::<_, gix_config::parse::Error>(())
131/// ```
132///
133/// ## Quoted values are not unquoted
134///
135/// Consider the following `git-config` example:
136///
137/// ```text
138/// [core]
139/// autocrlf=true""
140/// filemode=fa"lse"
141/// ```
142///
143/// Both these events, when fully processed, should normally be `true` and
144/// `false`. However, because this parser preserves the original event stream, we cannot process
145/// partially quoted values, such as the `false` example. As a result, to
146/// maintain consistency, the parser will just take all values as literals. The
147/// relevant event stream emitted is thus emitted as:
148///
149/// ```
150/// # use gix_config::parse::Events;
151/// # let section_data = "[core]\nautocrlf=true\"\"\nfilemode=fa\"lse\"";
152/// # let events = Events::from_str(section_data)?;
153/// # assert_eq!(
154/// #     events.iter().map(|event| event.to_string()).collect::<Vec<_>>(),
155/// #     vec!["[core]", "\n", "autocrlf", "=", r#"true"""#, "\n", "filemode", "=", r#"fa"lse""#]
156/// # );
157/// # Ok::<_, gix_config::parse::Error>(())
158/// ```
159///
160/// ## Whitespace after line continuations are part of the value
161///
162/// Consider the following `git-config` example:
163///
164/// ```text
165/// [some-section]
166/// file=a\
167///     c
168/// ```
169///
170/// Because how `git-config` treats continuations, the whitespace preceding `c`
171/// are in fact part of the value of `file`. The fully interpreted key/value
172/// pair is actually `file=a    c`. As a result, the parser will provide this
173/// split value accordingly:
174///
175/// ```
176/// # use gix_config::parse::Events;
177/// # let section_data = "[some-section]\nfile=a\\\n    c";
178/// # let events = Events::from_str(section_data)?;
179/// # assert_eq!(
180/// #     events.iter().map(|event| event.to_string()).collect::<Vec<_>>(),
181/// #     vec!["[some-section]", "\n", "file", "=", "a\\", "\n", "    c"]
182/// # );
183/// # Ok::<_, gix_config::parse::Error>(())
184/// ```
185///
186/// [`File`]: crate::File
187/// [`.ini` file format]: https://en.wikipedia.org/wiki/INI_file
188/// [`git`'s documentation]: https://git-scm.com/docs/git-config#_configuration_file
189/// [`FromStr`]: std::str::FromStr
190/// [`From<&'_ str>`]: std::convert::From
191#[derive(Clone, Debug, Default)]
192pub struct Events {
193    pub(crate) backing: Vec<u8>,
194    /// Events seen before the first section.
195    pub(crate) frontmatter: FrontMatterEvents,
196    /// All parsed sections.
197    pub(crate) sections: Vec<SectionData>,
198}
199
200impl Events {
201    /// Attempt to parse the provided bytes.
202    ///
203    /// Inputs larger than [`u32::MAX`] bytes are rejected because event spans use 32-bit offsets.
204    ///
205    /// Use `filter` to only include those events for which it returns true.
206    pub fn from_bytes(input: &[u8], filter: Option<fn(EventRef<'_>) -> bool>) -> Result<Events, parse::Error> {
207        let mut header = None;
208        let mut events = Vec::with_capacity(256);
209        let mut frontmatter = FrontMatterEvents::default();
210        let mut sections = Vec::new();
211        // The parser emits offsets into the caller's input. Copy it only after successful parsing to
212        // make the returned events self-contained without allocating on parse errors.
213        parse::from_bytes::from_bytes(input, &mut |e: Event| match e {
214            Event::SectionHeader(next_header) => {
215                match header.take() {
216                    None => {
217                        frontmatter = std::mem::take(&mut events).into_iter().collect();
218                    }
219                    Some(prev_header) => {
220                        #[expect(
221                            clippy::drain_collect,
222                            reason = "Keep the scratch vector's allocation for parsing the next section."
223                        )]
224                        let section_events = events.drain(..).collect();
225                        sections.push(parse::SectionData {
226                            header: prev_header,
227                            events: section_events,
228                        });
229                    }
230                }
231                header = Some(match Event::SectionHeader(next_header) {
232                    Event::SectionHeader(h) => h,
233                    _ => unreachable!("BUG: event type changed"),
234                });
235            }
236            event => {
237                if filter.is_none_or(|f| f(event.as_ref_in(input))) {
238                    events.push(event);
239                }
240            }
241        })?;
242
243        match header {
244            None => {
245                frontmatter = events.into_iter().collect();
246            }
247            Some(prev_header) => {
248                sections.push(parse::SectionData {
249                    header: prev_header,
250                    events: std::mem::take(&mut events),
251                });
252            }
253        }
254        Ok(Events {
255            backing: input.to_vec(),
256            frontmatter,
257            sections,
258        })
259    }
260
261    /// Attempt to parse the provided `input` string.
262    ///
263    /// Prefer the [`from_bytes()`](Self::from_bytes()) method if UTF8 encoding
264    /// isn't guaranteed.
265    #[expect(
266        clippy::should_implement_trait,
267        reason = "the method has domain-specific semantics despite sharing a standard trait method name"
268    )]
269    pub fn from_str(input: &str) -> Result<Events, parse::Error> {
270        Self::from_bytes(input.as_bytes(), None)
271    }
272
273    /// Return all contained events as borrowed views.
274    pub fn iter(&self) -> impl Iterator<Item = EventRef<'_>> + '_ {
275        self.frontmatter().chain(self.sections().flat_map(SectionRef::iter))
276    }
277
278    /// Return all events before the first section as borrowed views.
279    pub fn frontmatter(&self) -> impl Iterator<Item = EventRef<'_>> + '_ {
280        self.frontmatter.iter().map(move |event| event.as_ref_in(&self.backing))
281    }
282
283    /// Return all parsed sections as borrowed views.
284    pub fn sections(&self) -> impl Iterator<Item = SectionRef<'_>> + '_ {
285        self.sections.iter().map(move |section| SectionRef {
286            header: &section.header,
287            events: &section.events,
288            backing: &self.backing,
289        })
290    }
291}
292
293impl TryFrom<&str> for Events {
294    type Error = parse::Error;
295
296    fn try_from(value: &str) -> Result<Self, Self::Error> {
297        Self::from_str(value)
298    }
299}
300
301impl TryFrom<&[u8]> for Events {
302    type Error = parse::Error;
303
304    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
305        Events::from_bytes(value, None)
306    }
307}
308
309/// A borrowed view of a parsed section.
310#[derive(Copy, Clone, Debug)]
311pub struct SectionRef<'a> {
312    header: &'a parse::section::HeaderData,
313    events: &'a [Event],
314    backing: &'a [u8],
315}
316
317impl<'a> SectionRef<'a> {
318    /// Return the section header as an event view.
319    pub fn header(&self) -> EventRef<'a> {
320        EventRef::SectionHeader {
321            name: self.header.name.as_bstr_in(self.backing),
322            separator: self
323                .header
324                .separator
325                .as_ref()
326                .map(|separator| separator.as_bstr_in(self.backing)),
327            subsection_name: self
328                .header
329                .subsection_name
330                .as_ref()
331                .map(|subsection_name| subsection_name.value_in(self.backing)),
332        }
333    }
334
335    /// Return the events contained in this section body.
336    pub fn body(self) -> impl Iterator<Item = EventRef<'a>> + 'a {
337        self.events.iter().map(move |event| event.as_ref_in(self.backing))
338    }
339
340    /// Return the complete event stream for this section, including its header.
341    pub fn iter(self) -> impl Iterator<Item = EventRef<'a>> + 'a {
342        std::iter::once(self.header()).chain(self.body())
343    }
344}