radicle_git_metadata/commit/
headers.rs1use 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#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
11pub struct Headers(pub(super) Vec<(String, String)>);
12
13#[derive(Debug)]
15pub enum Signature<'a> {
16 Pgp(Cow<'a, str>),
18 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 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 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}