Skip to main content

gix_config/parse/section/
header.rs

1use bstr::{BStr, BString, ByteSlice};
2
3use crate::parse::{Span, section::HeaderData};
4
5/// The error returned when creating a section header.
6#[derive(Debug, PartialOrd, PartialEq, Eq, thiserror::Error)]
7#[expect(missing_docs)]
8pub enum Error {
9    #[error("section names can only be ascii, '-'")]
10    InvalidName,
11    #[error("sub-section names must not contain newlines or null bytes")]
12    InvalidSubSection,
13    #[error(transparent)]
14    Span(#[from] crate::parse::span::Error),
15}
16
17impl HeaderData {
18    pub(crate) fn new_in(
19        name: impl AsRef<str>,
20        subsection: impl Into<Option<BString>>,
21        backing: &mut Vec<u8>,
22    ) -> Result<HeaderData, Error> {
23        let name = validated_name(name.as_ref().as_bytes().as_bstr())?;
24        let name = Span::append(backing, &name)?;
25        let (separator, subsection_name) = match subsection.into() {
26            Some(subsection_name) => {
27                let subsection_name = validated_subsection(subsection_name.as_ref())?;
28                let mut raw = Vec::with_capacity(subsection_name.len());
29                write_escaped_subsection(subsection_name.as_bstr(), &mut raw).expect("writing to memory cannot fail");
30                let raw_span = Span::append(backing, &raw)?;
31                let subsection_name = if raw.as_slice() == subsection_name.as_slice() {
32                    crate::parse::MaybeDecoded::raw(raw_span)
33                } else {
34                    crate::parse::MaybeDecoded::decoded(raw_span, subsection_name)
35                };
36                (Some(Span::append(backing, b" ")?), Some(subsection_name))
37            }
38            None => (None, None),
39        };
40        Ok(HeaderData {
41            name,
42            separator,
43            subsection_name,
44        })
45    }
46}
47
48/// Return true if `name` is valid as subsection name, like `origin` in `[remote "origin"]`.
49pub fn is_valid_subsection(name: impl crate::AsBStr) -> bool {
50    name.as_bstr().find_byteset(b"\n\0").is_none()
51}
52
53fn validated_subsection(name: &BStr) -> Result<BString, Error> {
54    is_valid_subsection(name)
55        .then(|| name.into())
56        .ok_or(Error::InvalidSubSection)
57}
58
59fn validated_name(name: &BStr) -> Result<BString, Error> {
60    name.iter()
61        .all(|b| b.is_ascii_alphanumeric() || *b == b'-')
62        .then(|| name.into())
63        .ok_or(Error::InvalidName)
64}
65
66impl HeaderData {
67    pub(crate) fn rebase(&mut self, offset: usize) -> Result<(), crate::parse::span::Error> {
68        self.name.rebase(offset)?;
69        if let Some(separator) = &mut self.separator {
70            separator.rebase(offset)?;
71        }
72        if let Some(subsection_name) = &mut self.subsection_name {
73            subsection_name.rebase(offset)?;
74        }
75        Ok(())
76    }
77
78    pub(crate) fn copy_to_backing_in(
79        &self,
80        source: &[u8],
81        target: &mut Vec<u8>,
82    ) -> Result<HeaderData, crate::parse::span::Error> {
83        Ok(HeaderData {
84            name: self.name.copy_to_backing_in(source, target)?,
85            separator: self
86                .separator
87                .as_ref()
88                .map(|bytes| bytes.copy_to_backing_in(source, target))
89                .transpose()?,
90            subsection_name: self
91                .subsection_name
92                .as_ref()
93                .map(|name| name.copy_to_backing_in(source, target))
94                .transpose()?,
95        })
96    }
97
98    pub(crate) fn write_to_in(&self, backing: &[u8], mut out: impl std::io::Write) -> std::io::Result<()> {
99        out.write_all(b"[")?;
100        out.write_all(self.name.as_slice_in(backing))?;
101
102        if let (Some(sep), Some(subsection)) = (&self.separator, &self.subsection_name) {
103            out.write_all(sep.as_slice_in(backing))?;
104            if sep.as_slice_in(backing) == b"." {
105                out.write_all(subsection.raw_span().as_slice_in(backing))?;
106            } else {
107                out.write_all(b"\"")?;
108                out.write_all(subsection.raw_span().as_slice_in(backing))?;
109                out.write_all(b"\"")?;
110            }
111        }
112
113        out.write_all(b"]")
114    }
115}
116
117pub(crate) fn write_escaped_subsection(name: &BStr, mut out: impl std::io::Write) -> std::io::Result<()> {
118    for b in name.iter().copied() {
119        match b {
120            b'\\' => out.write_all(br"\\")?,
121            b'"' => out.write_all(br#"\""#)?,
122            _ => out.write_all(&[b])?,
123        }
124    }
125    Ok(())
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn empty_header_names_are_legal() {
134        assert!(
135            HeaderData::new_in("", None, &mut Vec::new()).is_ok(),
136            "yes, git allows this, so do we"
137        );
138    }
139
140    #[test]
141    fn empty_header_sub_names_are_legal() {
142        assert!(
143            HeaderData::new_in("remote", Some("".into()), &mut Vec::new()).is_ok(),
144            "yes, git allows this, so do we"
145        );
146    }
147}