pacman_key/
validation.rs

1use crate::error::{Error, Result};
2
3/// Validates a keyring name before passing to subprocess.
4///
5/// Accepted formats:
6/// - Alphanumeric characters, hyphens, and underscores only
7/// - Must not be empty
8/// - Must not contain shell metacharacters
9///
10/// Returns the keyring name on success.
11pub fn validate_keyring_name(name: &str) -> Result<&str> {
12    if name.is_empty() {
13        return Err(Error::InvalidKeyringName {
14            name: name.to_string(),
15            reason: "keyring name cannot be empty".to_string(),
16        });
17    }
18
19    if !name
20        .chars()
21        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
22    {
23        return Err(Error::InvalidKeyringName {
24            name: name.to_string(),
25            reason:
26                "keyring name must contain only alphanumeric characters, hyphens, or underscores"
27                    .to_string(),
28        });
29    }
30
31    Ok(name)
32}
33
34/// Validates a key ID format before passing to subprocess.
35///
36/// Accepted formats:
37/// - 8 hex characters (short key ID, discouraged due to collisions)
38/// - 16 hex characters (long key ID)
39/// - 40 hex characters (full fingerprint, recommended)
40/// - Any of the above with "0x" prefix
41///
42/// Returns the normalized keyid (uppercase, no prefix) on success.
43pub fn validate_keyid(keyid: &str) -> Result<String> {
44    if keyid.is_empty() {
45        return Err(Error::InvalidKeyId {
46            keyid: keyid.to_string(),
47            reason: "key ID cannot be empty".to_string(),
48        });
49    }
50
51    let normalized = keyid
52        .strip_prefix("0x")
53        .or_else(|| keyid.strip_prefix("0X"))
54        .unwrap_or(keyid)
55        .to_uppercase();
56
57    if !normalized.chars().all(|c| c.is_ascii_hexdigit()) {
58        return Err(Error::InvalidKeyId {
59            keyid: keyid.to_string(),
60            reason: "key ID must contain only hexadecimal characters".to_string(),
61        });
62    }
63
64    match normalized.len() {
65        8 | 16 | 40 => Ok(normalized),
66        len => Err(Error::InvalidKeyId {
67            keyid: keyid.to_string(),
68            reason: format!("key ID must be 8, 16, or 40 hex characters (got {})", len),
69        }),
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_valid_keyring_names() {
79        assert_eq!(validate_keyring_name("archlinux").unwrap(), "archlinux");
80        assert_eq!(
81            validate_keyring_name("archlinuxarm").unwrap(),
82            "archlinuxarm"
83        );
84        assert_eq!(validate_keyring_name("arch-linux").unwrap(), "arch-linux");
85        assert_eq!(validate_keyring_name("arch_linux").unwrap(), "arch_linux");
86        assert_eq!(validate_keyring_name("manjaro").unwrap(), "manjaro");
87        assert_eq!(validate_keyring_name("test123").unwrap(), "test123");
88    }
89
90    #[test]
91    fn test_invalid_keyring_name_empty() {
92        let err = validate_keyring_name("").unwrap_err();
93        assert!(matches!(err, Error::InvalidKeyringName { .. }));
94    }
95
96    #[test]
97    fn test_invalid_keyring_name_special_chars() {
98        let err = validate_keyring_name("$(whoami)").unwrap_err();
99        assert!(matches!(err, Error::InvalidKeyringName { .. }));
100
101        let err = validate_keyring_name("arch;linux").unwrap_err();
102        assert!(matches!(err, Error::InvalidKeyringName { .. }));
103
104        let err = validate_keyring_name("arch&linux").unwrap_err();
105        assert!(matches!(err, Error::InvalidKeyringName { .. }));
106
107        let err = validate_keyring_name("arch|linux").unwrap_err();
108        assert!(matches!(err, Error::InvalidKeyringName { .. }));
109
110        let err = validate_keyring_name("arch`whoami`").unwrap_err();
111        assert!(matches!(err, Error::InvalidKeyringName { .. }));
112
113        let err = validate_keyring_name("arch linux").unwrap_err();
114        assert!(matches!(err, Error::InvalidKeyringName { .. }));
115
116        let err = validate_keyring_name("arch\nlinux").unwrap_err();
117        assert!(matches!(err, Error::InvalidKeyringName { .. }));
118    }
119
120    #[test]
121    fn test_valid_short_keyid() {
122        assert_eq!(validate_keyid("DEADBEEF").unwrap(), "DEADBEEF");
123        assert_eq!(validate_keyid("deadbeef").unwrap(), "DEADBEEF");
124    }
125
126    #[test]
127    fn test_valid_long_keyid() {
128        assert_eq!(
129            validate_keyid("786C63F330D7CB92").unwrap(),
130            "786C63F330D7CB92"
131        );
132    }
133
134    #[test]
135    fn test_valid_fingerprint() {
136        assert_eq!(
137            validate_keyid("ABAF11C65A2970B130ABE3C479BE3E4300411886").unwrap(),
138            "ABAF11C65A2970B130ABE3C479BE3E4300411886"
139        );
140    }
141
142    #[test]
143    fn test_valid_with_0x_prefix() {
144        assert_eq!(validate_keyid("0xDEADBEEF").unwrap(), "DEADBEEF");
145        assert_eq!(validate_keyid("0XDEADBEEF").unwrap(), "DEADBEEF");
146    }
147
148    #[test]
149    fn test_invalid_empty() {
150        let err = validate_keyid("").unwrap_err();
151        assert!(matches!(err, Error::InvalidKeyId { .. }));
152    }
153
154    #[test]
155    fn test_invalid_non_hex() {
156        let err = validate_keyid("DEADBEEG").unwrap_err();
157        assert!(matches!(err, Error::InvalidKeyId { .. }));
158    }
159
160    #[test]
161    fn test_invalid_wrong_length() {
162        let err = validate_keyid("DEADBE").unwrap_err();
163        assert!(matches!(err, Error::InvalidKeyId { .. }));
164    }
165
166    #[test]
167    fn test_invalid_contains_spaces() {
168        let err = validate_keyid("DEAD BEEF").unwrap_err();
169        assert!(matches!(err, Error::InvalidKeyId { .. }));
170    }
171
172}