1use chrono::NaiveDate;
2use tracing::debug;
3
4use crate::error::Result;
5use crate::types::{Key, KeyType, KeyValidity, Signature};
6
7pub fn parse_keys(output: &str) -> Result<Vec<Key>> {
8 let mut keys = Vec::new();
9 let mut current_key: Option<KeyBuilder> = None;
10
11 for line in output.lines() {
12 let fields: Vec<&str> = line.split(':').collect();
13 if fields.is_empty() {
14 continue;
15 }
16
17 match fields[0] {
18 "pub" => {
19 if let Some(builder) = current_key.take() {
20 if let Some(key) = builder.build() {
21 keys.push(key);
22 } else {
23 debug!("skipping key: missing required fields (fingerprint or key_type)");
24 }
25 }
26 current_key = Some(KeyBuilder::from_pub_fields(&fields));
27 }
28 "fpr" if current_key.is_some() => {
29 if let Some(ref mut builder) = current_key
30 && builder.fingerprint.is_none()
31 && fields.len() > 9
32 {
33 builder.fingerprint = Some(fields[9].to_string());
34 }
35 }
36 "uid" if current_key.is_some() => {
37 if let Some(ref mut builder) = current_key
38 && builder.uid.is_none()
39 && fields.len() > 9
40 {
41 builder.uid = Some(fields[9].to_string());
42 }
43 }
44 "sub" | "ssb" | "uat" | "rev" | "tru" => {
45 debug!(
46 record_type = fields[0],
47 "skipping unhandled GPG record type"
48 );
49 }
50 _ if !fields[0].is_empty() => {
51 debug!(record_type = fields[0], "skipping unknown GPG record type");
52 }
53 _ => {}
54 }
55 }
56
57 if let Some(builder) = current_key {
58 if let Some(key) = builder.build() {
59 keys.push(key);
60 } else {
61 debug!("skipping final key: missing required fields (fingerprint or key_type)");
62 }
63 }
64
65 Ok(keys)
66}
67
68pub fn parse_signatures(output: &str) -> Result<Vec<Signature>> {
69 let mut signatures = Vec::new();
70
71 for line in output.lines() {
72 let fields: Vec<&str> = line.split(':').collect();
73 if fields.is_empty() || fields[0] != "sig" {
74 continue;
75 }
76
77 if fields.len() > 9 {
78 let keyid = fields.get(4).unwrap_or(&"");
79 if keyid.is_empty() {
80 debug!("skipping signature with empty keyid");
81 continue;
82 }
83
84 signatures.push(Signature {
85 keyid: keyid.to_string(),
86 created: fields.get(5).and_then(|s| parse_timestamp(s)),
87 expires: fields.get(6).and_then(|s| parse_timestamp(s)),
88 uid: fields.get(9).unwrap_or(&"").to_string(),
89 sig_class: fields.get(10).unwrap_or(&"").to_string(),
90 });
91 }
92 }
93
94 Ok(signatures)
95}
96
97fn parse_timestamp(s: &str) -> Option<NaiveDate> {
98 if s.is_empty() {
99 return None;
100 }
101 s.parse::<i64>()
102 .ok()
103 .and_then(|ts| chrono::DateTime::from_timestamp(ts, 0))
104 .map(|dt| dt.date_naive())
105}
106
107fn parse_algorithm(code: &str) -> String {
108 match code {
109 "1" => "RSA".to_string(),
110 "2" => "RSA".to_string(),
111 "3" => "RSA".to_string(),
112 "16" => "Elgamal".to_string(),
113 "17" => "DSA".to_string(),
114 "18" => "ECDH".to_string(),
115 "19" => "ECDSA".to_string(),
116 "20" => "Elgamal".to_string(),
117 "22" => "EdDSA".to_string(),
118 _ => format!("ALG{}", code),
119 }
120}
121
122#[derive(Default)]
123struct KeyBuilder {
124 fingerprint: Option<String>,
125 uid: Option<String>,
126 created: Option<NaiveDate>,
127 expires: Option<NaiveDate>,
128 validity: KeyValidity,
129 key_type: Option<KeyType>,
130}
131
132impl KeyBuilder {
133 fn from_pub_fields(fields: &[&str]) -> Self {
134 let mut builder = Self::default();
135
136 if fields.len() > 1 {
137 builder.validity = fields[1]
138 .chars()
139 .next()
140 .map(KeyValidity::from_gpg_char)
141 .unwrap_or_default();
142 }
143
144 if fields.len() > 2 {
145 let bits = fields[2].parse().unwrap_or(0);
146 let algorithm = fields
147 .get(3)
148 .map(|s| parse_algorithm(s))
149 .unwrap_or_default();
150 builder.key_type = Some(KeyType { algorithm, bits });
151 }
152
153 if fields.len() > 5 {
154 builder.created = parse_timestamp(fields[5]);
155 }
156
157 if fields.len() > 6 {
158 builder.expires = parse_timestamp(fields[6]);
159 }
160
161 builder
162 }
163
164 fn build(self) -> Option<Key> {
165 Some(Key {
166 fingerprint: self.fingerprint?,
167 uid: self.uid.unwrap_or_default(),
168 created: self.created,
169 expires: self.expires,
170 validity: self.validity,
171 key_type: self.key_type?,
172 })
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 const SAMPLE_KEY_OUTPUT: &str = r#"pub:f:4096:1:4AA4767BBC9C4B1D:1409337986:1725177586::-:::scSC::::::23::0:
181fpr:::::::::6645B0A8C7005E78DB1D7864F99FFE0FEAE999BD:
182uid:f::::1409337986::2CAEDC6E92DD5AF0E9A7C7C44E08C3C7A9E26BE4::Arch Linux ARM Build System <builder@archlinuxarm.org>::::::::::0:
183sub:f:4096:1:B31FB30B04D73EB0:1409337986:1725177586:::::s::::::23:
184fpr:::::::::BAE40BD8DC8BDAAA11DCFF68B31FB30B04D73EB0:
185pub:u:4096:1:786C63F330D7CB92:1568815794:::-:::scSC::::::23::0:
186fpr:::::::::ABAF11C65A2970B130ABE3C479BE3E4300411886:
187uid:u::::1568815794::F64689C4BF20D8BB2C66F7AD22DCE8C8C4B42E69::Levente Polyak <anthraxx@archlinux.org>::::::::::0:"#;
188
189 #[test]
190 fn test_parse_keys() {
191 let keys = parse_keys(SAMPLE_KEY_OUTPUT).unwrap();
192 assert_eq!(keys.len(), 2);
193
194 assert_eq!(
195 keys[0].fingerprint,
196 "6645B0A8C7005E78DB1D7864F99FFE0FEAE999BD"
197 );
198 assert_eq!(
199 keys[0].uid,
200 "Arch Linux ARM Build System <builder@archlinuxarm.org>"
201 );
202 assert_eq!(keys[0].validity, KeyValidity::Full);
203 assert_eq!(keys[0].key_type.bits, 4096);
204
205 assert_eq!(
206 keys[1].fingerprint,
207 "ABAF11C65A2970B130ABE3C479BE3E4300411886"
208 );
209 assert!(keys[1].uid.contains("Levente Polyak"));
210 assert_eq!(keys[1].validity, KeyValidity::Ultimate);
211 }
212
213 #[test]
214 fn test_parse_empty() {
215 let keys = parse_keys("").unwrap();
216 assert!(keys.is_empty());
217 }
218
219 #[test]
220 fn test_parse_expired_key() {
221 let output = r#"pub:e:4096:1:DEADBEEF12345678:1400000000:1500000000::-:::scSC::::::23::0:
222fpr:::::::::AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:
223uid:e::::1400000000::HASH::Expired User <expired@example.org>::::::::::0:"#;
224
225 let keys = parse_keys(output).unwrap();
226 assert_eq!(keys.len(), 1);
227 assert_eq!(keys[0].validity, KeyValidity::Expired);
228 assert!(keys[0].expires.is_some());
229 }
230
231 #[test]
232 fn test_parse_revoked_key() {
233 let output = r#"pub:r:4096:1:DEADBEEF12345678:1400000000:::-:::scSC::::::23::0:
234fpr:::::::::BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB:
235uid:r::::1400000000::HASH::Revoked User <revoked@example.org>::::::::::0:"#;
236
237 let keys = parse_keys(output).unwrap();
238 assert_eq!(keys.len(), 1);
239 assert_eq!(keys[0].validity, KeyValidity::Revoked);
240 }
241
242 #[test]
243 fn test_parse_key_without_uid() {
244 let output = r#"pub:f:4096:1:DEADBEEF12345678:1400000000:::-:::scSC::::::23::0:
245fpr:::::::::CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC:
246sub:f:4096:1:SUBKEYID12345678:1400000000:::::s::::::23:"#;
247
248 let keys = parse_keys(output).unwrap();
249 assert_eq!(keys.len(), 1);
250 assert_eq!(
251 keys[0].fingerprint,
252 "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
253 );
254 assert!(keys[0].uid.is_empty());
255 }
256
257 #[test]
258 fn test_parse_eddsa_key() {
259 let output = r#"pub:f:256:22:EDDSA12345678901:1600000000:::-:::scSC::::::23::0:
260fpr:::::::::DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD:
261uid:f::::1600000000::HASH::EdDSA User <eddsa@example.org>::::::::::0:"#;
262
263 let keys = parse_keys(output).unwrap();
264 assert_eq!(keys.len(), 1);
265 assert_eq!(keys[0].key_type.algorithm, "EdDSA");
266 assert_eq!(keys[0].key_type.bits, 256);
267 }
268
269 const SAMPLE_SIGNATURE_OUTPUT: &str = r#"pub:f:4096:1:4AA4767BBC9C4B1D:1409337986:1725177586::-:::scSC::::::23::0:
270sig:::1:4AA4767BBC9C4B1D:1409337986::::Arch Linux ARM Build System <builder@archlinuxarm.org>:13x:::::2:
271sig:::1:786C63F330D7CB92:1568815800::::Levente Polyak <anthraxx@archlinux.org>:10x:::::2:"#;
272
273 #[test]
274 fn test_parse_signatures() {
275 let sigs = parse_signatures(SAMPLE_SIGNATURE_OUTPUT).unwrap();
276 assert_eq!(sigs.len(), 2);
277
278 assert_eq!(sigs[0].keyid, "4AA4767BBC9C4B1D");
279 assert!(sigs[0].uid.contains("builder@archlinuxarm.org"));
280
281 assert_eq!(sigs[1].keyid, "786C63F330D7CB92");
282 assert!(sigs[1].uid.contains("anthraxx"));
283 }
284
285 #[test]
286 fn test_parse_signatures_empty() {
287 let sigs = parse_signatures("").unwrap();
288 assert!(sigs.is_empty());
289 }
290
291 #[test]
292 fn test_parse_signature_with_expiry() {
293 let output =
294 "sig:::1:KEYID123456789:1600000000:1700000000:::Signer <sign@example.org>:10x:::::2:";
295 let sigs = parse_signatures(output).unwrap();
296
297 assert_eq!(sigs.len(), 1);
298 assert!(sigs[0].created.is_some());
299 assert!(sigs[0].expires.is_some());
300 }
301
302 #[test]
303 fn test_parse_malformed_pub_line_too_few_fields() {
304 let output = "pub:f:4096";
305 let keys = parse_keys(output).unwrap();
306 assert!(keys.is_empty());
307 }
308
309 #[test]
310 fn test_parse_missing_fingerprint() {
311 let output = r#"pub:f:4096:1:DEADBEEF12345678:1400000000:::-:::scSC::::::23::0:
312uid:f::::1400000000::HASH::User without fingerprint <user@example.org>::::::::::0:"#;
313 let keys = parse_keys(output).unwrap();
314 assert!(keys.is_empty());
315 }
316
317 #[test]
318 fn test_parse_garbage_input() {
319 let output = "this is not gpg output\nneither is this";
320 let keys = parse_keys(output).unwrap();
321 assert!(keys.is_empty());
322 }
323
324 #[test]
325 fn test_parse_mixed_valid_and_invalid() {
326 let output = r#"garbage line
327pub:f:4096:1:VALIDKEY12345678:1400000000:::-:::scSC::::::23::0:
328fpr:::::::::AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:
329uid:f::::1400000000::HASH::Valid User <valid@example.org>::::::::::0:
330more garbage
331pub:broken:line
332fpr:not:enough:fields"#;
333 let keys = parse_keys(output).unwrap();
334 assert_eq!(keys.len(), 1);
335 assert_eq!(keys[0].uid, "Valid User <valid@example.org>");
336 }
337
338 #[test]
339 fn test_parse_signature_malformed() {
340 let output = "sig:too:few";
341 let sigs = parse_signatures(output).unwrap();
342 assert!(sigs.is_empty());
343 }
344
345 #[test]
346 fn test_parse_signature_empty_keyid_skipped() {
347 let output = "sig:::1::1600000000::::Signer <sign@example.org>:10x:::::2:";
348 let sigs = parse_signatures(output).unwrap();
349 assert!(sigs.is_empty());
350 }
351
352 #[test]
353 fn test_parse_signature_mixed_valid_and_empty_keyid() {
354 let output = r#"sig:::1:VALIDKEYID123456:1600000000::::Valid Signer <valid@example.org>:10x:::::2:
355sig:::1::1600000000::::Empty KeyID Signer <empty@example.org>:10x:::::2:
356sig:::1:ANOTHERKEYID1234:1600000000::::Another Signer <another@example.org>:10x:::::2:"#;
357 let sigs = parse_signatures(output).unwrap();
358 assert_eq!(sigs.len(), 2);
359 assert_eq!(sigs[0].keyid, "VALIDKEYID123456");
360 assert_eq!(sigs[1].keyid, "ANOTHERKEYID1234");
361 }
362
363 #[test]
364 fn test_parse_key_with_unhandled_record_types() {
365 let output = r#"pub:f:4096:1:DEADBEEF12345678:1400000000:::-:::scSC::::::23::0:
366fpr:::::::::EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE:
367uid:f::::1400000000::HASH::User <user@example.org>::::::::::0:
368sub:f:4096:1:SUBKEYID12345678:1400000000:::::s::::::23:
369rev:::::1400000000::::User <user@example.org>:20::0:
370tru::1:1400000000:0:3:1:5"#;
371 let keys = parse_keys(output).unwrap();
372 assert_eq!(keys.len(), 1);
373 assert_eq!(
374 keys[0].fingerprint,
375 "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE"
376 );
377 }
378
379 #[test]
380 fn test_parse_timestamp_invalid() {
381 assert!(parse_timestamp("not_a_number").is_none());
382 assert!(parse_timestamp("").is_none());
383 }
384
385 #[test]
386 fn test_parse_timestamp_valid() {
387 use chrono::Datelike;
388 let date = parse_timestamp("1609459200");
389 assert!(date.is_some());
390 let d = date.unwrap();
391 assert_eq!(d.year(), 2021);
392 assert_eq!(d.month(), 1);
393 assert_eq!(d.day(), 1);
394 }
395}