1use std::{
2 fmt::{Display, Formatter},
3 str::FromStr,
4 string::ToString,
5};
6
7use email_address::EmailAddress;
8use serde::{Deserialize, Serialize};
9use winnow::{
10 ModalResult,
11 Parser,
12 combinator::{cut_err, eof, seq},
13 error::{StrContext, StrContextValue},
14 token::take_till,
15};
16
17use crate::Error;
18
19#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
60pub enum OpenPGPIdentifier {
61 #[serde(rename = "openpgp_key_id")]
63 OpenPGPKeyId(OpenPGPKeyId),
64 #[serde(rename = "openpgp_v4_fingerprint")]
66 OpenPGPv4Fingerprint(OpenPGPv4Fingerprint),
67}
68
69impl FromStr for OpenPGPIdentifier {
70 type Err = Error;
71
72 fn from_str(s: &str) -> Result<Self, Self::Err> {
73 match s.parse::<OpenPGPv4Fingerprint>() {
74 Ok(fingerprint) => Ok(OpenPGPIdentifier::OpenPGPv4Fingerprint(fingerprint)),
75 Err(_) => match s.parse::<OpenPGPKeyId>() {
76 Ok(key_id) => Ok(OpenPGPIdentifier::OpenPGPKeyId(key_id)),
77 Err(e) => Err(e),
78 },
79 }
80 }
81}
82
83impl From<OpenPGPKeyId> for OpenPGPIdentifier {
84 fn from(key_id: OpenPGPKeyId) -> Self {
85 OpenPGPIdentifier::OpenPGPKeyId(key_id)
86 }
87}
88
89impl From<OpenPGPv4Fingerprint> for OpenPGPIdentifier {
90 fn from(fingerprint: OpenPGPv4Fingerprint) -> Self {
91 OpenPGPIdentifier::OpenPGPv4Fingerprint(fingerprint)
92 }
93}
94
95impl Display for OpenPGPIdentifier {
96 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
97 match self {
98 OpenPGPIdentifier::OpenPGPKeyId(key_id) => write!(f, "{key_id}"),
99 OpenPGPIdentifier::OpenPGPv4Fingerprint(fingerprint) => write!(f, "{fingerprint}"),
100 }
101 }
102}
103
104#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
140pub struct OpenPGPKeyId(String);
141
142impl OpenPGPKeyId {
143 pub fn new(key_id: String) -> Result<Self, Error> {
147 if key_id.len() == 16 && key_id.chars().all(|c| c.is_ascii_hexdigit()) {
148 Ok(Self(key_id.to_ascii_uppercase()))
149 } else {
150 Err(Error::InvalidOpenPGPKeyId(key_id))
151 }
152 }
153
154 pub fn as_str(&self) -> &str {
156 &self.0
157 }
158
159 pub fn into_inner(self) -> String {
161 self.0
162 }
163}
164
165impl FromStr for OpenPGPKeyId {
166 type Err = Error;
167
168 fn from_str(s: &str) -> Result<Self, Self::Err> {
177 Self::new(s.to_string())
178 }
179}
180
181impl Display for OpenPGPKeyId {
182 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
184 write!(f, "{}", self.0)
185 }
186}
187
188#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
233pub struct OpenPGPv4Fingerprint(String);
234
235impl OpenPGPv4Fingerprint {
236 pub fn new(fingerprint: String) -> Result<Self, Error> {
241 Self::from_str(&fingerprint)
242 }
243
244 pub fn as_str(&self) -> &str {
246 &self.0
247 }
248
249 pub fn into_inner(self) -> String {
251 self.0
252 }
253}
254
255impl FromStr for OpenPGPv4Fingerprint {
256 type Err = Error;
257
258 fn from_str(s: &str) -> Result<Self, Self::Err> {
268 let normalized = s.to_ascii_uppercase().replace(" ", "");
269
270 if !s.starts_with(' ')
271 && !s.ends_with(' ')
272 && normalized.len() == 40
273 && normalized.chars().all(|c| c.is_ascii_hexdigit())
274 {
275 Ok(Self(normalized))
276 } else {
277 Err(Error::InvalidOpenPGPv4Fingerprint)
278 }
279 }
280}
281
282impl Display for OpenPGPv4Fingerprint {
283 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
285 write!(f, "{}", self.as_str().to_ascii_uppercase())
286 }
287}
288
289#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
323pub struct Packager {
324 name: String,
325 email: EmailAddress,
326}
327
328impl Packager {
329 pub fn new(name: String, email: EmailAddress) -> Packager {
331 Packager { name, email }
332 }
333
334 pub fn name(&self) -> &str {
336 &self.name
337 }
338
339 pub fn email(&self) -> &EmailAddress {
341 &self.email
342 }
343
344 pub fn parser(input: &mut &str) -> ModalResult<Self> {
356 seq!(Self {
357 name: cut_err(take_till(1.., '<'))
359 .map(|s: &str| s.trim().to_string())
360 .context(StrContext::Label("packager name")),
361 _: cut_err('<').context(StrContext::Label("or missing opening delimiter '<' for email address")),
363 email: cut_err(
365 take_till(1.., '>')
366 .try_map(EmailAddress::from_str))
367 .context(StrContext::Label("Email address")
368 ),
369 _: cut_err('>').context(StrContext::Label("or missing closing delimiter '>' for email address")),
371 _: eof.context(StrContext::Expected(StrContextValue::Description("end of packager string"))),
372 })
373 .parse_next(input)
374 }
375}
376
377impl FromStr for Packager {
378 type Err = Error;
379 fn from_str(s: &str) -> Result<Packager, Self::Err> {
381 Ok(Self::parser.parse(s)?)
382 }
383}
384
385impl Display for Packager {
386 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
387 write!(fmt, "{} <{}>", self.name, self.email)
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use rstest::rstest;
394 use testresult::{TestError, TestResult};
395
396 use super::*;
397
398 #[rstest]
399 #[case("4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E")]
400 #[case("4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E")]
401 #[case("1234567890abcdef1234567890abcdef12345678")]
402 #[case("1234 5678 90ab cdef 1234 5678 90ab cdef 1234 5678")]
403 fn test_parse_openpgp_fingerprint(#[case] input: &str) -> Result<(), Error> {
404 input.parse::<OpenPGPv4Fingerprint>()?;
405 Ok(())
406 }
407
408 #[rstest]
409 #[case(
411 "A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8G9H0",
412 Err(Error::InvalidOpenPGPv4Fingerprint)
413 )]
414 #[case(
416 "1234567890ABCDEF1234567890ABCDEF1234567",
417 Err(Error::InvalidOpenPGPv4Fingerprint)
418 )]
419 #[case(
421 "1234567890ABCDEF1234567890ABCDEF1234567890",
422 Err(Error::InvalidOpenPGPv4Fingerprint)
423 )]
424 #[case(
426 " 4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E",
427 Err(Error::InvalidOpenPGPv4Fingerprint)
428 )]
429 #[case(
431 "4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E ",
432 Err(Error::InvalidOpenPGPv4Fingerprint)
433 )]
434 #[case("invalid", Err(Error::InvalidOpenPGPv4Fingerprint))]
436 fn test_parse_invalid_openpgp_fingerprint(
437 #[case] input: &str,
438 #[case] expected: Result<OpenPGPv4Fingerprint, Error>,
439 ) {
440 let result = input.parse::<OpenPGPv4Fingerprint>();
441 assert_eq!(result, expected);
442 }
443
444 #[rstest]
445 #[case("2F2670AC164DB36F")]
446 #[case("584A3EBFE705CDCD")]
447 fn test_parse_openpgp_key_id(#[case] input: &str) -> Result<(), Error> {
448 input.parse::<OpenPGPKeyId>()?;
449 Ok(())
450 }
451
452 #[test]
453 fn test_serialize_openpgp_key_id() -> TestResult {
454 let id = "584A3EBFE705CDCD".parse::<OpenPGPKeyId>()?;
455 let json = serde_json::to_string(&OpenPGPIdentifier::OpenPGPKeyId(id))?;
456 assert_eq!(r#"{"openpgp_key_id":"584A3EBFE705CDCD"}"#, json);
457
458 Ok(())
459 }
460
461 #[rstest]
462 #[case(
463 "1234567890abcdef1234567890abcdef12345678",
464 "1234567890ABCDEF1234567890ABCDEF12345678"
465 )]
466 #[case(
467 "1234 5678 90ab cdef 1234 5678 90ab cdef 1234 5678",
468 "1234567890ABCDEF1234567890ABCDEF12345678"
469 )]
470 fn test_serialize_openpgp_v4_fingerprint(
471 #[case] input: &str,
472 #[case] output: &str,
473 ) -> TestResult {
474 let print = input.parse::<OpenPGPv4Fingerprint>()?;
475 let json = serde_json::to_string(&OpenPGPIdentifier::OpenPGPv4Fingerprint(print))?;
476 assert_eq!(format!("{{\"openpgp_v4_fingerprint\":\"{output}\"}}"), json);
477
478 Ok(())
479 }
480
481 #[rstest]
482 #[case("1234567890ABCGH", Err(Error::InvalidOpenPGPKeyId("1234567890ABCGH".to_string())))]
484 #[case("1234567890ABCDE", Err(Error::InvalidOpenPGPKeyId("1234567890ABCDE".to_string())))]
486 #[case("1234567890ABCDEF0", Err(Error::InvalidOpenPGPKeyId("1234567890ABCDEF0".to_string())))]
488 #[case("invalid", Err(Error::InvalidOpenPGPKeyId("invalid".to_string())))]
490 fn test_parse_invalid_openpgp_key_id(
491 #[case] input: &str,
492 #[case] expected: Result<OpenPGPKeyId, Error>,
493 ) {
494 let result = input.parse::<OpenPGPKeyId>();
495 assert_eq!(result, expected);
496 }
497
498 #[rstest]
499 #[case(
500 "Foobar McFooface (The Third) <foobar@mcfooface.org>",
501 Packager{
502 name: "Foobar McFooface (The Third)".to_string(),
503 email: EmailAddress::from_str("foobar@mcfooface.org").unwrap()
504 }
505 )]
506 #[case(
507 "Foobar McFooface <foobar@mcfooface.org>",
508 Packager{
509 name: "Foobar McFooface".to_string(),
510 email: EmailAddress::from_str("foobar@mcfooface.org").unwrap()
511 }
512 )]
513 fn valid_packager(#[case] from_str: &str, #[case] packager: Packager) {
514 assert_eq!(Packager::from_str(from_str), Ok(packager));
515 }
516
517 #[rstest]
519 #[case::no_name("<foobar@mcfooface.org>", "invalid packager name")]
520 #[case::no_name_and_address_not_wrapped(
521 "foobar@mcfooface.org",
522 "invalid or missing opening delimiter '<' for email address"
523 )]
524 #[case::no_wrapped_address(
525 "Foobar McFooface",
526 "invalid or missing opening delimiter '<' for email address"
527 )]
528 #[case::two_wrapped_addresses(
529 "Foobar McFooface <foobar@mcfooface.org> <foobar@mcfoofacemcfooface.org>",
530 "expected end of packager string"
531 )]
532 #[case::address_without_local_part("Foobar McFooface <@mcfooface.org>", "Local part is empty")]
533 fn invalid_packager(#[case] packager: &str, #[case] expected_error: &str) -> TestResult {
534 let Err(err) = Packager::from_str(packager) else {
535 return Err(TestError::from(format!(
536 "Expected packager string to be invalid: {packager}"
537 )));
538 };
539
540 let error = err.to_string();
541 assert!(
542 error.contains(expected_error),
543 "Expected error:\n{error}\n\nto contain string:\n{expected_error}"
544 );
545
546 Ok(())
547 }
548
549 #[rstest]
550 #[case(
551 Packager::from_str("Foobar McFooface <foobar@mcfooface.org>").unwrap(),
552 "Foobar McFooface <foobar@mcfooface.org>"
553 )]
554 fn packager_format_string(#[case] packager: Packager, #[case] packager_str: &str) {
555 assert_eq!(packager_str, format!("{packager}"));
556 }
557
558 #[rstest]
559 #[case(Packager::from_str("Foobar McFooface <foobar@mcfooface.org>").unwrap(), "Foobar McFooface")]
560 fn packager_name(#[case] packager: Packager, #[case] name: &str) {
561 assert_eq!(name, packager.name());
562 }
563
564 #[rstest]
565 #[case(
566 Packager::from_str("Foobar McFooface <foobar@mcfooface.org>").unwrap(),
567 &EmailAddress::from_str("foobar@mcfooface.org").unwrap(),
568 )]
569 fn packager_email(#[case] packager: Packager, #[case] email: &EmailAddress) {
570 assert_eq!(email, packager.email());
571 }
572}