use bstr::{BStr, BString, ByteSlice}; use crate::parse::{Span, section::HeaderData}; /// The error returned when creating a section header. #[derive(Debug, PartialOrd, PartialEq, Eq, thiserror::Error)] #[expect(missing_docs)] pub enum Error { #[error("section names can only be ascii, '-'")] InvalidName, #[error("sub-section names must not contain newlines or null bytes")] InvalidSubSection, #[error(transparent)] Span(#[from] crate::parse::span::Error), } impl HeaderData { pub(crate) fn new_in( name: impl AsRef, subsection: impl Into>, backing: &mut Vec, ) -> Result { let name = validated_name(name.as_ref().as_bytes().as_bstr())?; let name = Span::append(backing, &name)?; let (separator, subsection_name) = match subsection.into() { Some(subsection_name) => { let subsection_name = validated_subsection(subsection_name.as_ref())?; let mut raw = Vec::with_capacity(subsection_name.len()); write_escaped_subsection(subsection_name.as_bstr(), &mut raw).expect("writing to memory cannot fail"); let raw_span = Span::append(backing, &raw)?; let subsection_name = if raw.as_slice() == subsection_name.as_slice() { crate::parse::MaybeDecoded::raw(raw_span) } else { crate::parse::MaybeDecoded::decoded(raw_span, subsection_name) }; (Some(Span::append(backing, b" ")?), Some(subsection_name)) } None => (None, None), }; Ok(HeaderData { name, separator, subsection_name, }) } } /// Return true if `name` is valid as subsection name, like `origin` in `[remote "origin"]`. pub fn is_valid_subsection(name: impl crate::AsBStr) -> bool { name.as_bstr().find_byteset(b"\n\0").is_none() } fn validated_subsection(name: &BStr) -> Result { is_valid_subsection(name) .then(|| name.into()) .ok_or(Error::InvalidSubSection) } fn validated_name(name: &BStr) -> Result { name.iter() .all(|b| b.is_ascii_alphanumeric() || *b == b'-') .then(|| name.into()) .ok_or(Error::InvalidName) } impl HeaderData { pub(crate) fn rebase(&mut self, offset: usize) -> Result<(), crate::parse::span::Error> { self.name.rebase(offset)?; if let Some(separator) = &mut self.separator { separator.rebase(offset)?; } if let Some(subsection_name) = &mut self.subsection_name { subsection_name.rebase(offset)?; } Ok(()) } pub(crate) fn copy_to_backing_in( &self, source: &[u8], target: &mut Vec, ) -> Result { Ok(HeaderData { name: self.name.copy_to_backing_in(source, target)?, separator: self .separator .as_ref() .map(|bytes| bytes.copy_to_backing_in(source, target)) .transpose()?, subsection_name: self .subsection_name .as_ref() .map(|name| name.copy_to_backing_in(source, target)) .transpose()?, }) } pub(crate) fn write_to_in(&self, backing: &[u8], mut out: impl std::io::Write) -> std::io::Result<()> { out.write_all(b"[")?; out.write_all(self.name.as_slice_in(backing))?; if let (Some(sep), Some(subsection)) = (&self.separator, &self.subsection_name) { out.write_all(sep.as_slice_in(backing))?; if sep.as_slice_in(backing) == b"." { out.write_all(subsection.raw_span().as_slice_in(backing))?; } else { out.write_all(b"\"")?; out.write_all(subsection.raw_span().as_slice_in(backing))?; out.write_all(b"\"")?; } } out.write_all(b"]") } } pub(crate) fn write_escaped_subsection(name: &BStr, mut out: impl std::io::Write) -> std::io::Result<()> { for b in name.iter().copied() { match b { b'\\' => out.write_all(br"\\")?, b'"' => out.write_all(br#"\""#)?, _ => out.write_all(&[b])?, } } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn empty_header_names_are_legal() { assert!( HeaderData::new_in("", None, &mut Vec::new()).is_ok(), "yes, git allows this, so do we" ); } #[test] fn empty_header_sub_names_are_legal() { assert!( HeaderData::new_in("remote", Some("".into()), &mut Vec::new()).is_ok(), "yes, git allows this, so do we" ); } }