gix_config/parse/section/
header.rs1use 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#[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 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
44pub 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 pub fn is_legacy(&self) -> bool {
65 self.separator.as_deref().is_some_and(|n| n == ".")
66 }
67
68 pub fn subsection_name(&self) -> Option<&BStr> {
75 self.subsection_name.as_deref()
76 }
77
78 pub fn name(&self) -> &BStr {
80 &self.name
81 }
82
83 #[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 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 #[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}