gix_config/parse/section/
header.rs

1use std::{borrow::Cow, fmt::Display};
2
3use bstr::{BStr, BString, ByteSlice, ByteVec};
4
5use crate::parse::{
6    section::{into_cow_bstr, Header, Name},
7    Event,
8};
9
10/// The error returned by [`Header::new(…)`][super::Header::new()].
11#[derive(Debug, PartialOrd, PartialEq, Eq, thiserror::Error)]
12#[allow(missing_docs)]
13pub enum Error {
14    #[error("section names can only be ascii, '-'")]
15    InvalidName,
16    #[error("sub-section names must not contain newlines or null bytes")]
17    InvalidSubSection,
18}
19
20impl<'a> Header<'a> {
21    /// Instantiate a new header either with a section `name`, e.g. "core" serializing to `["core"]`
22    /// or `[remote "origin"]` for `subsection` being "origin" and `name` being "remote".
23    pub fn new(
24        name: impl Into<Cow<'a, str>>,
25        subsection: impl Into<Option<Cow<'a, BStr>>>,
26    ) -> Result<Header<'a>, Error> {
27        let name = Name(validated_name(into_cow_bstr(name.into()))?);
28        if let Some(subsection_name) = subsection.into() {
29            Ok(Header {
30                name,
31                separator: Some(Cow::Borrowed(" ".into())),
32                subsection_name: Some(validated_subsection(subsection_name)?),
33            })
34        } else {
35            Ok(Header {
36                name,
37                separator: None,
38                subsection_name: None,
39            })
40        }
41    }
42}
43
44/// Return true if `name` is valid as subsection name, like `origin` in `[remote "origin"]`.
45pub fn is_valid_subsection(name: &BStr) -> bool {
46    name.find_byteset(b"\n\0").is_none()
47}
48
49fn validated_subsection(name: Cow<'_, BStr>) -> Result<Cow<'_, BStr>, Error> {
50    is_valid_subsection(name.as_ref())
51        .then_some(name)
52        .ok_or(Error::InvalidSubSection)
53}
54
55fn validated_name(name: Cow<'_, BStr>) -> Result<Cow<'_, BStr>, Error> {
56    name.iter()
57        .all(|b| b.is_ascii_alphanumeric() || *b == b'-')
58        .then_some(name)
59        .ok_or(Error::InvalidName)
60}
61
62impl Header<'_> {
63    ///Return true if this is a header like `[legacy.subsection]`, or false otherwise.
64    pub fn is_legacy(&self) -> bool {
65        self.separator.as_deref().is_some_and(|n| n == ".")
66    }
67
68    /// Return the subsection name, if present, i.e. "origin" in `[remote "origin"]`.
69    ///
70    /// It is parsed without quotes, and with escapes folded
71    /// into their resulting characters.
72    /// Thus during serialization, escapes and quotes must be re-added.
73    /// This makes it possible to use [`Event`] data for lookups directly.
74    pub fn subsection_name(&self) -> Option<&BStr> {
75        self.subsection_name.as_deref()
76    }
77
78    /// Return the name of the header, like "remote" in `[remote "origin"]`.
79    pub fn name(&self) -> &BStr {
80        &self.name
81    }
82
83    /// Serialize this type into a `BString` for convenience.
84    ///
85    /// Note that `to_string()` can also be used, but might not be lossless.
86    #[must_use]
87    pub fn to_bstring(&self) -> BString {
88        let mut buf = Vec::new();
89        self.write_to(&mut buf).expect("io error impossible");
90        buf.into()
91    }
92
93    /// Stream ourselves to the given `out`, in order to reproduce this header mostly losslessly
94    /// as it was parsed.
95    pub fn write_to(&self, mut out: impl std::io::Write) -> std::io::Result<()> {
96        out.write_all(b"[")?;
97        out.write_all(&self.name)?;
98
99        if let (Some(sep), Some(subsection)) = (&self.separator, &self.subsection_name) {
100            let sep = sep.as_ref();
101            out.write_all(sep)?;
102            if sep == "." {
103                out.write_all(subsection.as_ref())?;
104            } else {
105                out.write_all(b"\"")?;
106                out.write_all(escape_subsection(subsection.as_ref()).as_ref())?;
107                out.write_all(b"\"")?;
108            }
109        }
110
111        out.write_all(b"]")
112    }
113
114    /// Turn this instance into a fully owned one with `'static` lifetime.
115    #[must_use]
116    pub fn to_owned(&self) -> Header<'static> {
117        Header {
118            name: self.name.to_owned(),
119            separator: self.separator.clone().map(|v| Cow::Owned(v.into_owned())),
120            subsection_name: self.subsection_name.clone().map(|v| Cow::Owned(v.into_owned())),
121        }
122    }
123}
124
125fn escape_subsection(name: &BStr) -> Cow<'_, BStr> {
126    if name.find_byteset(b"\\\"").is_none() {
127        return name.into();
128    }
129    let mut buf = Vec::with_capacity(name.len());
130    for b in name.iter().copied() {
131        match b {
132            b'\\' => buf.push_str(br"\\"),
133            b'"' => buf.push_str(br#"\""#),
134            _ => buf.push(b),
135        }
136    }
137    BString::from(buf).into()
138}
139
140impl Display for Header<'_> {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        Display::fmt(&self.to_bstring(), f)
143    }
144}
145
146impl From<Header<'_>> for BString {
147    fn from(header: Header<'_>) -> Self {
148        header.to_bstring()
149    }
150}
151
152impl From<&Header<'_>> for BString {
153    fn from(header: &Header<'_>) -> Self {
154        header.to_bstring()
155    }
156}
157
158impl<'a> From<Header<'a>> for Event<'a> {
159    fn from(header: Header<'_>) -> Event<'_> {
160        Event::SectionHeader(header)
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn empty_header_names_are_legal() {
170        assert!(Header::new("", None).is_ok(), "yes, git allows this, so do we");
171    }
172
173    #[test]
174    fn empty_header_sub_names_are_legal() {
175        assert!(
176            Header::new("remote", Some(Cow::Borrowed("".into()))).is_ok(),
177            "yes, git allows this, so do we"
178        );
179    }
180}