Skip to main content

radicle_git_metadata/commit/
headers.rs

1use core::fmt;
2use std::borrow::Cow;
3
4const BEGIN_SSH: &str = "-----BEGIN SSH SIGNATURE-----\n";
5const BEGIN_PGP: &str = "-----BEGIN PGP SIGNATURE-----\n";
6
7/// A collection of headers stored in [`super::CommitData`].
8///
9/// Note: these do not include `tree`, `parent`, `author`, and `committer`.
10#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
11pub struct Headers(pub(super) Vec<(String, String)>);
12
13/// A `gpgsig` signature stored in [`super::CommitData`].
14#[derive(Debug)]
15pub enum Signature<'a> {
16    /// A PGP signature, i.e. starts with `-----BEGIN PGP SIGNATURE-----`.
17    Pgp(Cow<'a, str>),
18    /// A SSH signature, i.e. starts with `-----BEGIN SSH SIGNATURE-----`.
19    Ssh(Cow<'a, str>),
20}
21
22impl<'a> Signature<'a> {
23    fn from_str(s: &'a str) -> Result<Self, UnknownScheme> {
24        if s.starts_with(BEGIN_SSH) {
25            Ok(Signature::Ssh(Cow::Borrowed(s)))
26        } else if s.starts_with(BEGIN_PGP) {
27            Ok(Signature::Pgp(Cow::Borrowed(s)))
28        } else {
29            Err(UnknownScheme)
30        }
31    }
32}
33
34impl fmt::Display for Signature<'_> {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            Signature::Pgp(pgp) => f.write_str(pgp.as_ref()),
38            Signature::Ssh(ssh) => f.write_str(ssh.as_ref()),
39        }
40    }
41}
42
43pub struct UnknownScheme;
44
45impl Headers {
46    pub fn new() -> Self {
47        Headers(Vec::new())
48    }
49
50    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
51        self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
52    }
53
54    pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
55        self.iter()
56            .filter_map(move |(k, v)| (k == name).then_some(v))
57    }
58
59    pub fn signatures(&self) -> impl Iterator<Item = Signature<'_>> + '_ {
60        self.0.iter().filter_map(|(k, v)| {
61            if k == "gpgsig" {
62                Signature::from_str(v).ok()
63            } else {
64                None
65            }
66        })
67    }
68
69    /// Push a header to the end of the headers section.
70    pub fn push(&mut self, name: &str, value: &str) {
71        self.0.push((name.to_owned(), value.trim().to_owned()));
72    }
73
74    pub(crate) fn strip_signatures(&mut self) {
75        self.0.retain(|(key, _)| key != "gpgsig");
76    }
77}
78
79#[derive(Debug, thiserror::Error)]
80pub enum ParseError {
81    #[error("missing tree")]
82    MissingTree,
83    #[error("invalid tree")]
84    InvalidTree,
85    #[error("invalid format")]
86    InvalidFormat,
87    #[error("invalid parent")]
88    InvalidParent,
89    #[error("invalid header")]
90    InvalidHeader,
91    #[error("invalid author")]
92    InvalidAuthor,
93    #[error("missing author")]
94    MissingAuthor,
95    #[error("invalid committer")]
96    InvalidCommitter,
97    #[error("missing committer")]
98    MissingCommitter,
99}
100
101pub fn parse_commit_header<
102    Tree: std::str::FromStr,
103    Parent: std::str::FromStr,
104    Signature: std::str::FromStr,
105>(
106    header: &str,
107) -> Result<(Tree, Vec<Parent>, Signature, Signature, Headers), ParseError> {
108    let mut lines = header.lines();
109
110    let tree = match lines.next() {
111        Some(tree) => tree
112            .strip_prefix("tree ")
113            .map(Tree::from_str)
114            .transpose()
115            .map_err(|_| ParseError::InvalidTree)?
116            .ok_or(ParseError::MissingTree)?,
117        None => return Err(ParseError::MissingTree),
118    };
119
120    let mut parents = Vec::new();
121    let mut author: Option<Signature> = None;
122    let mut committer: Option<Signature> = None;
123    let mut headers = Headers::new();
124
125    for line in lines {
126        // Check if a signature is still being parsed
127        if let Some(rest) = line.strip_prefix(' ') {
128            let value: &mut String = headers
129                .0
130                .last_mut()
131                .map(|(_, v)| v)
132                .ok_or(ParseError::InvalidFormat)?;
133            value.push('\n');
134            value.push_str(rest);
135            continue;
136        }
137
138        if let Some((name, value)) = line.split_once(' ') {
139            match name {
140                "parent" => parents.push(
141                    value
142                        .parse::<Parent>()
143                        .map_err(|_| ParseError::InvalidParent)?,
144                ),
145                "author" => {
146                    author = Some(
147                        value
148                            .parse::<Signature>()
149                            .map_err(|_| ParseError::InvalidAuthor)?,
150                    )
151                }
152                "committer" => {
153                    committer = Some(
154                        value
155                            .parse::<Signature>()
156                            .map_err(|_| ParseError::InvalidCommitter)?,
157                    )
158                }
159                _ => headers.push(name, value),
160            }
161            continue;
162        }
163    }
164
165    Ok((
166        tree,
167        parents,
168        author.ok_or(ParseError::MissingAuthor)?,
169        committer.ok_or(ParseError::MissingCommitter)?,
170        headers,
171    ))
172}