Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle-git-metadata src commit headers.rs
use core::fmt;
use std::borrow::Cow;

const BEGIN_SSH: &str = "-----BEGIN SSH SIGNATURE-----\n";
const BEGIN_PGP: &str = "-----BEGIN PGP SIGNATURE-----\n";

/// A collection of headers stored in [`super::CommitData`].
///
/// Note: these do not include `tree`, `parent`, `author`, and `committer`.
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Headers(pub(super) Vec<(String, String)>);

/// A `gpgsig` signature stored in [`super::CommitData`].
#[derive(Debug)]
pub enum Signature<'a> {
    /// A PGP signature, i.e. starts with `-----BEGIN PGP SIGNATURE-----`.
    Pgp(Cow<'a, str>),
    /// A SSH signature, i.e. starts with `-----BEGIN SSH SIGNATURE-----`.
    Ssh(Cow<'a, str>),
}

impl<'a> Signature<'a> {
    fn from_str(s: &'a str) -> Result<Self, UnknownScheme> {
        if s.starts_with(BEGIN_SSH) {
            Ok(Signature::Ssh(Cow::Borrowed(s)))
        } else if s.starts_with(BEGIN_PGP) {
            Ok(Signature::Pgp(Cow::Borrowed(s)))
        } else {
            Err(UnknownScheme)
        }
    }
}

impl fmt::Display for Signature<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Signature::Pgp(pgp) => f.write_str(pgp.as_ref()),
            Signature::Ssh(ssh) => f.write_str(ssh.as_ref()),
        }
    }
}

pub struct UnknownScheme;

impl Headers {
    pub fn new() -> Self {
        Headers(Vec::new())
    }

    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
        self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
    }

    pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
        self.iter()
            .filter_map(move |(k, v)| (k == name).then_some(v))
    }

    pub fn signatures(&self) -> impl Iterator<Item = Signature<'_>> + '_ {
        self.0.iter().filter_map(|(k, v)| {
            if k == "gpgsig" {
                Signature::from_str(v).ok()
            } else {
                None
            }
        })
    }

    /// Push a header to the end of the headers section.
    pub fn push(&mut self, name: &str, value: &str) {
        self.0.push((name.to_owned(), value.trim().to_owned()));
    }

    pub(crate) fn strip_signatures(&mut self) {
        self.0.retain(|(key, _)| key != "gpgsig");
    }
}

#[derive(Debug, thiserror::Error)]
pub enum ParseError {
    #[error("missing tree")]
    MissingTree,
    #[error("invalid tree")]
    InvalidTree,
    #[error("invalid format")]
    InvalidFormat,
    #[error("invalid parent")]
    InvalidParent,
    #[error("invalid header")]
    InvalidHeader,
    #[error("invalid author")]
    InvalidAuthor,
    #[error("missing author")]
    MissingAuthor,
    #[error("invalid committer")]
    InvalidCommitter,
    #[error("missing committer")]
    MissingCommitter,
}

pub fn parse_commit_header<
    Tree: std::str::FromStr,
    Parent: std::str::FromStr,
    Signature: std::str::FromStr,
>(
    header: &str,
) -> Result<(Tree, Vec<Parent>, Signature, Signature, Headers), ParseError> {
    let mut lines = header.lines();

    let tree = match lines.next() {
        Some(tree) => tree
            .strip_prefix("tree ")
            .map(Tree::from_str)
            .transpose()
            .map_err(|_| ParseError::InvalidTree)?
            .ok_or(ParseError::MissingTree)?,
        None => return Err(ParseError::MissingTree),
    };

    let mut parents = Vec::new();
    let mut author: Option<Signature> = None;
    let mut committer: Option<Signature> = None;
    let mut headers = Headers::new();

    for line in lines {
        // Check if a signature is still being parsed
        if let Some(rest) = line.strip_prefix(' ') {
            let value: &mut String = headers
                .0
                .last_mut()
                .map(|(_, v)| v)
                .ok_or(ParseError::InvalidFormat)?;
            value.push('\n');
            value.push_str(rest);
            continue;
        }

        if let Some((name, value)) = line.split_once(' ') {
            match name {
                "parent" => parents.push(
                    value
                        .parse::<Parent>()
                        .map_err(|_| ParseError::InvalidParent)?,
                ),
                "author" => {
                    author = Some(
                        value
                            .parse::<Signature>()
                            .map_err(|_| ParseError::InvalidAuthor)?,
                    )
                }
                "committer" => {
                    committer = Some(
                        value
                            .parse::<Signature>()
                            .map_err(|_| ParseError::InvalidCommitter)?,
                    )
                }
                _ => headers.push(name, value),
            }
            continue;
        }
    }

    Ok((
        tree,
        parents,
        author.ok_or(ParseError::MissingAuthor)?,
        committer.ok_or(ParseError::MissingCommitter)?,
        headers,
    ))
}