voa_config/config/technology/openpgp/
filter.rs1use std::{fmt::Display, str::FromStr};
4
5use addr::parse_domain_name;
6use const_hex::{decode_to_slice, encode};
7use serde::{Deserialize, Serialize};
8
9use crate::Error;
10
11#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
13pub struct DomainName(String);
14
15impl FromStr for DomainName {
16 type Err = Error;
17
18 fn from_str(s: &str) -> Result<Self, Self::Err> {
19 let name = parse_domain_name(s).map_err(|source| {
20 crate::config::technology::openpgp::Error::InvalidDomainName {
21 domain_name: s.to_string(),
22 message: source.to_string(),
23 }
24 })?;
25 if name.root().is_none() {
28 return Err(
29 crate::config::technology::openpgp::Error::InvalidDomainName {
30 domain_name: s.to_string(),
31 message: "The domain cannot be a top-level domain".to_string(),
32 }
33 .into(),
34 );
35 }
36
37 Ok(Self(name.to_string()))
38 }
39}
40
41impl Display for DomainName {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 write!(f, "{}", self.0)
44 }
45}
46
47#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
49#[serde(into = "String", try_from = "String")]
50pub enum OpenpgpFingerprint {
51 V4([u8; 20]),
53
54 V6([u8; 32]),
56}
57
58impl AsRef<[u8]> for OpenpgpFingerprint {
59 fn as_ref(&self) -> &[u8] {
60 match self {
61 Self::V4(bytes) => bytes,
62 Self::V6(bytes) => bytes,
63 }
64 }
65}
66
67impl From<OpenpgpFingerprint> for String {
68 fn from(value: OpenpgpFingerprint) -> Self {
69 value.to_string()
70 }
71}
72
73impl FromStr for OpenpgpFingerprint {
74 type Err = Error;
75
76 fn from_str(s: &str) -> Result<Self, Self::Err> {
77 match s.len() {
78 40 => {
79 let mut output = [0u8; 20];
80 decode_to_slice(s, &mut output).map_err(|source| {
81 crate::config::technology::openpgp::Error::InvalidOpenpgpFingerprint {
82 fingerprint: s.into(),
83 context: "creating an OpenPGP v4 fingerprint".to_string(),
84 source,
85 }
86 })?;
87 Ok(Self::V4(output))
88 }
89 64 => {
90 let mut output = [0u8; 32];
91 decode_to_slice(s, &mut output).map_err(|source| {
92 crate::config::technology::openpgp::Error::InvalidOpenpgpFingerprint {
93 fingerprint: s.into(),
94 context: "creating an OpenPGP v6 fingerprint".to_string(),
95 source,
96 }
97 })?;
98 Ok(Self::V6(output))
99 }
100 _ => Err(
101 crate::config::technology::openpgp::Error::InvalidOpenpgpFingerprintLength {
102 fingerprint: s.into(),
103 }
104 .into(),
105 ),
106 }
107 }
108}
109
110impl TryFrom<String> for OpenpgpFingerprint {
111 type Error = Error;
112
113 fn try_from(value: String) -> Result<Self, Self::Error> {
114 Self::from_str(&value)
115 }
116}
117
118impl Display for OpenpgpFingerprint {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 write!(
121 f,
122 "{}",
123 match self {
124 Self::V4(bytes) => encode(bytes),
125 Self::V6(bytes) => encode(bytes),
126 }
127 )
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use rstest::rstest;
134 use testresult::TestResult;
135
136 use super::*;
137
138 #[rstest]
139 #[case("example.org")]
140 #[case("sub.example.org")]
141 fn domain_name_from_str_succeeds(#[case] name: &str) -> TestResult {
142 let _domain = DomainName::from_str(name)?;
143 Ok(())
144 }
145
146 #[rstest]
147 #[case("example foo")]
148 #[case("fail")]
149 #[case("_")]
150 fn domain_name_from_str_fails(#[case] name: &str) -> TestResult {
151 match DomainName::from_str(name) {
152 Err(Error::OpenpgpTechnology(
153 crate::config::technology::openpgp::Error::InvalidDomainName { .. },
154 )) => {}
155 Err(error) => panic!(
156 "Expected to fail with an Error::InvalidDomainName, but failed with another error instead: {error}"
157 ),
158 Ok(name) => panic!(
159 "Expected to fail with an Error::InvalidDomainName, but succeeded instead: {name:?}"
160 ),
161 }
162
163 Ok(())
164 }
165
166 #[rstest]
167 #[case::v4("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15")]
168 #[case::v6("b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c")]
169 fn lowercase_openpgp_fingerprint_roundtrip_succeeds(#[case] input: &str) -> TestResult {
170 let fingerprint = OpenpgpFingerprint::from_str(input)?;
171 assert_eq!(fingerprint.to_string(), input);
172 Ok(())
173 }
174
175 #[rstest]
176 #[case::v4(
177 "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15",
178 &[241, 210, 210, 249, 36, 233, 134, 172, 134, 253, 247, 179, 108, 148, 188, 223, 50, 190, 236, 21]
179 )]
180 #[case::v6(
181 "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c",
182 &[
183 181, 187, 157, 128, 20, 160, 249, 177, 214, 30, 33, 231, 150, 215, 141, 204, 223, 19,
184 82, 242, 60, 211, 40, 18, 244, 133, 11, 135, 138, 228, 148, 76,
185 ]
186 )]
187 fn openpgp_fingerprint_as_ref_matches(
188 #[case] input: &str,
189 #[case] expected: &[u8],
190 ) -> TestResult {
191 let fingerprint = OpenpgpFingerprint::from_str(input)?;
192 assert_eq!(fingerprint.as_ref(), expected);
193 Ok(())
194 }
195
196 #[rstest]
197 #[case::wrong_chars_for_v4("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")]
198 #[case::wrong_chars_for_v6("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")]
199 fn openpgp_fingerprint_from_str_fails_on_chars(#[case] input: &str) -> TestResult {
200 match OpenpgpFingerprint::from_str(input) {
201 Err(Error::OpenpgpTechnology(
202 crate::config::technology::openpgp::Error::InvalidOpenpgpFingerprint { .. },
203 )) => {}
204 Err(error) => panic!(
205 "Expected to fail with an Error::InvalidOpenpgpFingerprint, but failed with another error instead: {error}"
206 ),
207 Ok(name) => panic!(
208 "Expected to fail with an Error::InvalidOpenpgpFingerprint, but succeeded instead: {name:?}"
209 ),
210 }
211
212 Ok(())
213 }
214
215 #[rstest]
216 #[case::too_long_for_v4("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15234234")]
217 #[case::too_short_for_v4("f1d2d2f924e986ac86fdf7b36c94bcdf32")]
218 #[case::too_long_for_v6(
219 "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c2234234"
220 )]
221 #[case::too_short_for_v6("b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878")]
222 fn openpgp_fingerprint_from_str_fails_on_length(#[case] input: &str) -> TestResult {
223 match OpenpgpFingerprint::from_str(input) {
224 Err(Error::OpenpgpTechnology(
225 crate::config::technology::openpgp::Error::InvalidOpenpgpFingerprintLength {
226 ..
227 },
228 )) => {}
229 Err(error) => panic!(
230 "Expected to fail with an Error::InvalidOpenpgpFingerprint, but failed with another error instead: {error}"
231 ),
232 Ok(name) => panic!(
233 "Expected to fail with an Error::InvalidOpenpgpFingerprint, but succeeded instead: {name:?}"
234 ),
235 }
236
237 Ok(())
238 }
239}