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: §ion.header,
287 events: §ion.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}