gix-config 0.61.0

A git-config file parser and editor from the gitoxide project
Documentation
use bstr::{BStr, BString, ByteSlice};
use gix_error::ExnMessageResult;

use crate::parse::{Span, section::HeaderData};

impl HeaderData {
    /// Invalid section or subsection name bytes are stored as `input` in [`gix_error::Message::values`].
    /// After [wrapping](gix_error::Error::from_error()), inspect them with [metadata](gix_error::Error::metadata()).
    pub(crate) fn new_in(
        name: impl AsRef<str>,
        subsection: impl Into<Option<BString>>,
        backing: &mut Vec<u8>,
    ) -> ExnMessageResult<HeaderData> {
        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) -> ExnMessageResult<BString> {
    is_valid_subsection(name).then(|| name.into()).ok_or_else(|| {
        gix_error::validation("sub-section names must not contain newlines or null bytes")
            .with("input", name)
            .into()
    })
}

fn validated_name(name: &BStr) -> ExnMessageResult<BString> {
    name.iter()
        .all(|b| b.is_ascii_alphanumeric() || *b == b'-')
        .then(|| name.into())
        .ok_or_else(|| {
            gix_error::validation("section names can only be ascii, '-'")
                .with("input", name)
                .into()
        })
}

impl HeaderData {
    pub(crate) fn rebase(&mut self, offset: usize) -> ExnMessageResult {
        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<u8>) -> ExnMessageResult<HeaderData> {
        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"
        );
    }
}