git-config 0.16.2

Please use `gix-<thiscrate>` instead ('git' -> 'gix')
Documentation
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)
}

#[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<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)
    }
}