use std::{borrow::Cow, fmt::Display}; use bstr::{BStr, BString, ByteSlice, ByteVec}; use crate::parse::{ section::{into_cow_bstr, Header, Name}, Event, }; /// The error returned by [`Header::new(…)`][super::Header::new()]. #[derive(Debug, PartialOrd, PartialEq, Eq, thiserror::Error)] #[allow(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, } impl<'a> Header<'a> { /// Instantiate a new header either with a section `name`, e.g. "core" serializing to `["core"]` /// or `[remote "origin"]` for `subsection` being "origin" and `name` being "remote". pub fn new( name: impl Into>, subsection: impl Into>>, ) -> Result, Error> { let name = Name(validated_name(into_cow_bstr(name.into()))?); if let Some(subsection_name) = subsection.into() { Ok(Header { name, separator: Some(Cow::Borrowed(" ".into())), subsection_name: Some(validated_subsection(subsection_name)?), }) } else { Ok(Header { name, separator: None, subsection_name: None, }) } } } /// Return true if `name` is valid as subsection name, like `origin` in `[remote "origin"]`. pub fn is_valid_subsection(name: &BStr) -> bool { name.find_byteset(b"\n\0").is_none() } fn validated_subsection(name: Cow<'_, BStr>) -> Result, Error> { is_valid_subsection(name.as_ref()) .then_some(name) .ok_or(Error::InvalidSubSection) } fn validated_name(name: Cow<'_, BStr>) -> Result, Error> { name.iter() .all(|b| b.is_ascii_alphanumeric() || *b == b'-') .then_some(name) .ok_or(Error::InvalidName) } #[cfg(test)] mod tests { use super::*; #[test] fn empty_header_names_are_legal() { assert!(Header::new("", None).is_ok(), "yes, git allows this, so do we"); } #[test] fn empty_header_sub_names_are_legal() { assert!( Header::new("remote", Some(Cow::Borrowed("".into()))).is_ok(), "yes, git allows this, so do we" ); } } impl Header<'_> { ///Return true if this is a header like `[legacy.subsection]`, or false otherwise. pub fn is_legacy(&self) -> bool { self.separator.as_deref().map_or(false, |n| n == ".") } /// Return the subsection name, if present, i.e. "origin" in `[remote "origin"]`. /// /// It is parsed without quotes, and with escapes folded /// into their resulting characters. /// Thus during serialization, escapes and quotes must be re-added. /// This makes it possible to use [`Event`] data for lookups directly. pub fn subsection_name(&self) -> Option<&BStr> { self.subsection_name.as_deref() } /// Return the name of the header, like "remote" in `[remote "origin"]`. pub fn name(&self) -> &BStr { &self.name } /// Serialize this type into a `BString` for convenience. /// /// Note that `to_string()` can also be used, but might not be lossless. #[must_use] pub fn to_bstring(&self) -> BString { let mut buf = Vec::new(); self.write_to(&mut buf).expect("io error impossible"); buf.into() } /// Stream ourselves to the given `out`, in order to reproduce this header mostly losslessly /// as it was parsed. pub fn write_to(&self, mut out: impl std::io::Write) -> std::io::Result<()> { out.write_all(b"[")?; out.write_all(&self.name)?; if let (Some(sep), Some(subsection)) = (&self.separator, &self.subsection_name) { let sep = sep.as_ref(); out.write_all(sep)?; if sep == "." { out.write_all(subsection.as_ref())?; } else { out.write_all(b"\"")?; out.write_all(escape_subsection(subsection.as_ref()).as_ref())?; out.write_all(b"\"")?; } } out.write_all(b"]") } /// Turn this instance into a fully owned one with `'static` lifetime. #[must_use] pub fn to_owned(&self) -> Header<'static> { Header { name: self.name.to_owned(), separator: self.separator.clone().map(|v| Cow::Owned(v.into_owned())), subsection_name: self.subsection_name.clone().map(|v| Cow::Owned(v.into_owned())), } } } fn escape_subsection(name: &BStr) -> Cow<'_, BStr> { if name.find_byteset(b"\\\"").is_none() { return name.into(); } let mut buf = Vec::with_capacity(name.len()); for b in name.iter().copied() { match b { b'\\' => buf.push_str(br#"\\"#), b'"' => buf.push_str(br#"\""#), _ => buf.push(b), } } BString::from(buf).into() } impl Display for Header<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Display::fmt(&self.to_bstring(), f) } } impl From> for BString { fn from(header: Header<'_>) -> Self { header.into() } } impl From<&Header<'_>> for BString { fn from(header: &Header<'_>) -> Self { header.to_bstring() } } impl<'a> From> for Event<'a> { fn from(header: Header<'_>) -> Event<'_> { Event::SectionHeader(header) } }