blob: ad1288f3fe01889a8c48339e09e4dc6da51f795f [file] [log] [blame]
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<Cow<'a, str>>,
subsection: impl Into<Option<Cow<'a, BStr>>>,
) -> Result<Header<'a>, 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<Cow<'_, BStr>, Error> {
is_valid_subsection(name.as_ref())
.then_some(name)
.ok_or(Error::InvalidSubSection)
}
fn validated_name(name: Cow<'_, BStr>) -> Result<Cow<'_, BStr>, Error> {
name.iter()
.all(|b| b.is_ascii_alphanumeric() || *b == b'-')
.then_some(name)
.ok_or(Error::InvalidName)
}
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<Header<'_>> 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<Header<'a>> for Event<'a> {
fn from(header: Header<'_>) -> Event<'_> {
Event::SectionHeader(header)
}
}
#[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"
);
}
}