#
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}