pacman_key/
parse.rs

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}