Skip to main content

alpm_types/
openpgp.rs

1use std::{
2    fmt::{Display, Formatter},
3    str::FromStr,
4    string::ToString,
5};
6
7use alpm_parsers::prelude::*;
8use base64::{Engine, prelude::BASE64_STANDARD};
9use email_address::EmailAddress;
10use fluent_i18n::t;
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13#[cfg(feature = "serde")]
14use serde_with::DeserializeFromStr;
15use winnow::{
16    ascii::space0,
17    combinator::{alt, not, peek, repeat_till},
18    error::ErrMode,
19    token::any,
20};
21
22use crate::Error;
23
24/// An OpenPGP key identifier.
25///
26/// The `OpenPGPIdentifier` enum represents a valid OpenPGP identifier, which can be either an
27/// OpenPGP Key ID or an OpenPGP v4 fingerprint.
28///
29/// This type wraps an [`OpenPGPKeyId`] and an [`OpenPGPv4Fingerprint`] and provides a unified
30/// interface for both.
31///
32/// ## Examples
33///
34/// ```
35/// use std::str::FromStr;
36///
37/// use alpm_types::{Error, OpenPGPIdentifier, OpenPGPKeyId, OpenPGPv4Fingerprint};
38/// # fn main() -> Result<(), alpm_types::Error> {
39/// // Create a OpenPGPIdentifier from a valid OpenPGP v4 fingerprint
40/// let key = OpenPGPIdentifier::from_str("4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E")?;
41/// assert_eq!(
42///     key,
43///     OpenPGPIdentifier::OpenPGPv4Fingerprint(OpenPGPv4Fingerprint::from_str(
44///         "4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E"
45///     )?)
46/// );
47/// assert_eq!(key.to_string(), "4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E");
48/// assert_eq!(
49///     key,
50///     OpenPGPv4Fingerprint::from_str("4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E")?.into()
51/// );
52///
53/// // Create a OpenPGPIdentifier from a valid OpenPGP Key ID
54/// let key = OpenPGPIdentifier::from_str("2F2670AC164DB36F")?;
55/// assert_eq!(
56///     key,
57///     OpenPGPIdentifier::OpenPGPKeyId(OpenPGPKeyId::from_str("2F2670AC164DB36F")?)
58/// );
59/// assert_eq!(key.to_string(), "2F2670AC164DB36F");
60/// assert_eq!(key, OpenPGPKeyId::from_str("2F2670AC164DB36F")?.into());
61/// # Ok(())
62/// # }
63/// ```
64#[derive(Clone, Debug, Eq, PartialEq)]
65#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
66pub enum OpenPGPIdentifier {
67    /// An OpenPGP Key ID.
68    #[cfg_attr(feature = "serde", serde(rename = "openpgp_key_id"))]
69    OpenPGPKeyId(OpenPGPKeyId),
70    /// An OpenPGP v4 fingerprint.
71    #[cfg_attr(feature = "serde", serde(rename = "openpgp_v4_fingerprint"))]
72    OpenPGPv4Fingerprint(OpenPGPv4Fingerprint),
73}
74
75impl FromStr for OpenPGPIdentifier {
76    type Err = Error;
77
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        match s.parse::<OpenPGPv4Fingerprint>() {
80            Ok(fingerprint) => Ok(OpenPGPIdentifier::OpenPGPv4Fingerprint(fingerprint)),
81            Err(_) => match s.parse::<OpenPGPKeyId>() {
82                Ok(key_id) => Ok(OpenPGPIdentifier::OpenPGPKeyId(key_id)),
83                Err(e) => Err(e),
84            },
85        }
86    }
87}
88
89impl From<OpenPGPKeyId> for OpenPGPIdentifier {
90    fn from(key_id: OpenPGPKeyId) -> Self {
91        OpenPGPIdentifier::OpenPGPKeyId(key_id)
92    }
93}
94
95impl From<OpenPGPv4Fingerprint> for OpenPGPIdentifier {
96    fn from(fingerprint: OpenPGPv4Fingerprint) -> Self {
97        OpenPGPIdentifier::OpenPGPv4Fingerprint(fingerprint)
98    }
99}
100
101impl Display for OpenPGPIdentifier {
102    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
103        match self {
104            OpenPGPIdentifier::OpenPGPKeyId(key_id) => write!(f, "{key_id}"),
105            OpenPGPIdentifier::OpenPGPv4Fingerprint(fingerprint) => write!(f, "{fingerprint}"),
106        }
107    }
108}
109
110/// An OpenPGP Key ID.
111///
112/// The `OpenPGPKeyId` type wraps a `String` representing an [OpenPGP Key ID],
113/// ensuring that it consists of exactly 16 uppercase hexadecimal characters.
114///
115/// [OpenPGP Key ID]: https://openpgp.dev/book/glossary.html#term-Key-ID
116///
117/// ## Note
118///
119/// - This type supports constructing from both uppercase and lowercase hexadecimal characters but
120///   guarantees to return the key ID in uppercase.
121///
122/// - The usage of this type is highly discouraged as the keys may not be unique. This will lead to
123///   a linting error in the future.
124///
125/// ## Examples
126///
127/// ```
128/// use std::str::FromStr;
129///
130/// use alpm_types::{Error, OpenPGPKeyId};
131///
132/// # fn main() -> Result<(), alpm_types::Error> {
133/// // Create OpenPGPKeyId from a valid key ID
134/// let key = OpenPGPKeyId::from_str("2F2670AC164DB36F")?;
135/// assert_eq!(key.as_str(), "2F2670AC164DB36F");
136///
137/// // Attempting to create an OpenPGPKeyId from an invalid key ID will fail
138/// assert!(OpenPGPKeyId::from_str("INVALIDKEYID").is_err());
139///
140/// // Format as String
141/// assert_eq!(format!("{key}"), "2F2670AC164DB36F");
142/// # Ok(())
143/// # }
144/// ```
145#[derive(Clone, Debug, Eq, PartialEq)]
146#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
147pub struct OpenPGPKeyId(String);
148
149impl OpenPGPKeyId {
150    /// Creates a new `OpenPGPKeyId` instance.
151    ///
152    /// See [`OpenPGPKeyId::from_str`] for more information on how the OpenPGP Key ID is validated.
153    pub fn new(key_id: String) -> Result<Self, Error> {
154        if key_id.len() == 16 && key_id.chars().all(|c| c.is_ascii_hexdigit()) {
155            Ok(Self(key_id.to_ascii_uppercase()))
156        } else {
157            Err(Error::InvalidOpenPGPKeyId(key_id))
158        }
159    }
160
161    /// Returns a reference to the inner OpenPGP Key ID as a `&str`.
162    pub fn as_str(&self) -> &str {
163        &self.0
164    }
165
166    /// Consumes the `OpenPGPKeyId` and returns the inner `String`.
167    pub fn into_inner(self) -> String {
168        self.0
169    }
170}
171
172impl FromStr for OpenPGPKeyId {
173    type Err = Error;
174
175    /// Creates a new `OpenPGPKeyId` instance after validating that it follows the correct format.
176    ///
177    /// A valid OpenPGP Key ID should be exactly 16 characters long and consist only
178    /// of digits (`0-9`) and hexadecimal letters (`A-F`).
179    ///
180    /// # Errors
181    ///
182    /// Returns an error if the OpenPGP Key ID is not valid.
183    fn from_str(s: &str) -> Result<Self, Self::Err> {
184        Self::new(s.to_string())
185    }
186}
187
188impl Display for OpenPGPKeyId {
189    /// Converts the `OpenPGPKeyId` to an uppercase `String`.
190    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
191        write!(f, "{}", self.0)
192    }
193}
194
195/// An OpenPGP v4 fingerprint.
196///
197/// The `OpenPGPv4Fingerprint` type wraps a `String` representing an [OpenPGP v4 fingerprint],
198/// ensuring that it consists of 40 uppercase hexadecimal characters with optional whitespace
199/// separators.
200///
201/// [OpenPGP v4 fingerprint]: https://openpgp.dev/book/certificates.html#fingerprint
202///
203/// ## Note
204///
205/// - This type supports constructing from both uppercase and lowercase hexadecimal characters, with
206///   and without whitespace separators, but guarantees to return the fingerprint in uppercase and
207///   with no whitespaces.
208///
209/// - Whitespaces are only allowed between hexadecimal characters, not at the start or end of the
210///   fingerprint.
211///
212/// ## Examples
213///
214/// ```
215/// use std::str::FromStr;
216///
217/// use alpm_types::{Error, OpenPGPv4Fingerprint};
218///
219/// # fn main() -> Result<(), alpm_types::Error> {
220/// // Create OpenPGPv4Fingerprint from a valid OpenPGP v4 fingerprint
221/// let key = OpenPGPv4Fingerprint::from_str("4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E")?;
222/// assert_eq!(key.as_str(), "4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E");
223///
224/// // Space separated fingerprint is also valid
225/// let key = OpenPGPv4Fingerprint::from_str("4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E")?;
226/// assert_eq!(key.as_str(), "4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E");
227///
228/// // Attempting to create a OpenPGPv4Fingerprint from an invalid fingerprint will fail
229/// assert!(OpenPGPv4Fingerprint::from_str("INVALIDKEY").is_err());
230///
231/// // Format as String
232/// assert_eq!(
233///     format!("{}", key),
234///     "4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E"
235/// );
236/// # Ok(())
237/// # }
238/// ```
239#[derive(Clone, Debug, Eq, PartialEq)]
240#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
241pub struct OpenPGPv4Fingerprint(String);
242
243impl OpenPGPv4Fingerprint {
244    /// Creates a new `OpenPGPv4Fingerprint` instance
245    ///
246    /// See [`OpenPGPv4Fingerprint::from_str`] for more information on how the OpenPGP v4
247    /// fingerprint is validated.
248    pub fn new(fingerprint: String) -> Result<Self, Error> {
249        Self::from_str(&fingerprint)
250    }
251
252    /// Returns a reference to the inner OpenPGP v4 fingerprint as a `&str`.
253    pub fn as_str(&self) -> &str {
254        &self.0
255    }
256
257    /// Consumes the `OpenPGPv4Fingerprint` and returns the inner `String`.
258    pub fn into_inner(self) -> String {
259        self.0
260    }
261}
262
263impl FromStr for OpenPGPv4Fingerprint {
264    type Err = Error;
265
266    /// Creates a new `OpenPGPv4Fingerprint` instance after validating that it follows the correct
267    /// format.
268    ///
269    /// A valid OpenPGP v4 fingerprint should be a 40 characters long string of digits (`0-9`)
270    /// and hexadecimal letters (`A-F`) optionally separated by whitespaces.
271    ///
272    /// # Errors
273    ///
274    /// Returns an error if the OpenPGP v4 fingerprint is not valid.
275    fn from_str(s: &str) -> Result<Self, Self::Err> {
276        let normalized = s.to_ascii_uppercase().replace(" ", "");
277
278        if !s.starts_with(' ')
279            && !s.ends_with(' ')
280            && normalized.len() == 40
281            && normalized.chars().all(|c| c.is_ascii_hexdigit())
282        {
283            Ok(Self(normalized))
284        } else {
285            Err(Error::InvalidOpenPGPv4Fingerprint)
286        }
287    }
288}
289
290impl Display for OpenPGPv4Fingerprint {
291    /// Converts the `OpenPGPv4Fingerprint` to a uppercase `String`.
292    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
293        write!(f, "{}", self.as_str().to_ascii_uppercase())
294    }
295}
296
297/// A base64 encoded OpenPGP detached signature.
298///
299/// Wraps a [`String`] representing a [base64] encoded [OpenPGP detached signature]
300/// ensuring it consists of valid [base64] characters.
301///
302/// ## Examples
303///
304/// ```
305/// use std::str::FromStr;
306///
307/// use alpm_types::{Error, Base64OpenPGPSignature};
308///
309/// # fn main() -> Result<(), alpm_types::Error> {
310/// // Create Base64OpenPGPSignature from a valid base64 String
311/// let sig = Base64OpenPGPSignature::from_str("iHUEABYKAB0WIQRizHP4hOUpV7L92IObeih9mi7GCAUCaBZuVAAKCRCbeih9mi7GCIlMAP9ws/jU4f580ZRQlTQKvUiLbAZOdcB7mQQj83hD1Nc/GwD/WIHhO1/OQkpMERejUrLo3AgVmY3b4/uGhx9XufWEbgE=")?;
312///
313/// // Attempting to create a Base64OpenPGPSignature from an invalid base64 String will fail
314/// assert!(Base64OpenPGPSignature::from_str("!@#$^&*").is_err());
315///
316/// // Format as String
317/// assert_eq!(
318///     format!("{}", sig),
319///     "iHUEABYKAB0WIQRizHP4hOUpV7L92IObeih9mi7GCAUCaBZuVAAKCRCbeih9mi7GCIlMAP9ws/jU4f580ZRQlTQKvUiLbAZOdcB7mQQj83hD1Nc/GwD/WIHhO1/OQkpMERejUrLo3AgVmY3b4/uGhx9XufWEbgE="
320/// );
321/// # Ok(())
322/// # }
323/// ```
324///
325/// [base64]: https://en.wikipedia.org/wiki/Base64
326/// [OpenPGP detached signature]: https://openpgp.dev/book/signing_data.html#detached-signatures
327#[derive(Clone, Debug, Eq, PartialEq)]
328#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
329pub struct Base64OpenPGPSignature(String);
330
331impl Base64OpenPGPSignature {
332    /// Creates a new [`Base64OpenPGPSignature`] instance.
333    ///
334    /// See [`Base64OpenPGPSignature::from_str`] for more information on how the OpenPGP signature
335    /// is validated.
336    pub fn new(signature: String) -> Result<Self, Error> {
337        Self::from_str(&signature)
338    }
339
340    /// Returns a reference to the inner OpenPGP signature as a `&str`.
341    pub fn as_str(&self) -> &str {
342        &self.0
343    }
344
345    /// Consumes the [`Base64OpenPGPSignature`] and returns the inner [`String`].
346    pub fn into_inner(self) -> String {
347        self.0
348    }
349}
350
351impl AsRef<str> for Base64OpenPGPSignature {
352    fn as_ref(&self) -> &str {
353        self.as_str()
354    }
355}
356
357impl FromStr for Base64OpenPGPSignature {
358    type Err = Error;
359
360    /// Creates a new [`Base64OpenPGPSignature`] instance after validating that it follows the
361    /// correct format.
362    ///
363    /// A valid [OpenPGP signature] should consist only of [base64] characters (A-Z, a-z, 0-9, +, /)
364    /// and may include padding characters (=) at the end.
365    ///
366    /// # Errors
367    ///
368    /// Returns an error if the OpenPGP signature is not valid.
369    ///
370    /// [base64]: https://en.wikipedia.org/wiki/Base64
371    /// [OpenPGP signature]: https://openpgp.dev/book/signing_data.html#detached-signatures
372    fn from_str(s: &str) -> Result<Self, Self::Err> {
373        BASE64_STANDARD
374            .decode(s)
375            .map_err(|_| Error::InvalidBase64Encoding {
376                expected_item: t!("error-invalid-base64-encoding-pgp-signature"),
377            })?
378            .to_vec();
379        Ok(Self(s.to_string()))
380    }
381}
382
383impl Display for Base64OpenPGPSignature {
384    /// Converts the [`Base64OpenPGPSignature`] to a [`String`].
385    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
386        write!(f, "{}", self.0)
387    }
388}
389
390/// A packager of a package
391///
392/// A `Packager` is represented by a User ID (e.g. `"Foobar McFooFace <foobar@mcfooface.org>"`).
393/// Internally this struct wraps a `String` for the name and an `EmailAddress` for a valid email
394/// address.
395///
396/// ## Examples
397/// ```
398/// use std::str::FromStr;
399///
400/// use alpm_types::{Error, Packager};
401///
402/// # fn main() -> Result<(), alpm_types::Error> {
403/// // create Packager from &str
404/// let packager = Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?;
405///
406/// // get name
407/// assert_eq!("Foobar McFooface", packager.name());
408///
409/// // get email
410/// assert_eq!("foobar@mcfooface.org", packager.email().to_string());
411///
412/// // get email domain
413/// assert_eq!("mcfooface.org", packager.email().domain());
414///
415/// // format as String
416/// assert_eq!(
417///     "Foobar McFooface <foobar@mcfooface.org>",
418///     format!("{}", packager)
419/// );
420/// # Ok(())
421/// # }
422/// ```
423#[derive(Clone, Debug, Eq, PartialEq)]
424#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
425pub struct Packager {
426    name: String,
427    email: EmailAddress,
428}
429
430impl Packager {
431    /// Create a new Packager
432    pub fn new(name: String, email: EmailAddress) -> Packager {
433        Packager { name, email }
434    }
435
436    /// Return the name of the Packager
437    pub fn name(&self) -> &str {
438        &self.name
439    }
440
441    /// Return the email of the Packager
442    pub fn email(&self) -> &EmailAddress {
443        &self.email
444    }
445}
446
447impl ParserUntil for Packager {
448    /// Parses a [`Packager`] from a string slice.
449    ///
450    /// # Errors
451    ///
452    /// Returns an error if `input` does not represent a valid [`Packager`].
453    fn parser_until<'a, P>(delimiter: P) -> impl Parser<Input<'a>, Self, ErrMode<ParseStack<'a>>>
454    where
455        P: Parser<Input<'a>, &'a str, ErrMode<ParseStack<'a>>>,
456    {
457        // Define the actual parser closure.
458        // The delimiter is moved into the closure and borrowed via `by_ref()` on each call.
459        let mut delimiter_parser = delimiter;
460        let parser = move |input: &mut Input<'a>| -> PResult<'a, Self> {
461            // This format is a bit opaque and may include leading whitespaces.
462            // Consume any of those so we don't have to guess about them later on.
463            space0.parse_next(input)?;
464
465            // Make sure the first character isn't a `<`, which may happen if the packager name is
466            // missing.
467            not("<")
468                .expected_text("a packager name")
469                .parse_next(input)?;
470
471            // The name that precedes the email address
472            let name = repeat_till::<_, _, (), _, _, _, _>(
473                1..,
474                any,
475                peek(alt(("<", delimiter_parser.by_ref()))),
476            )
477            .take()
478            .map(|name: &str| name.trim().to_string())
479            .layer("packager name")
480            .parse_next(input)?;
481
482            // The '<' delimiter that marks the start of the email string
483            '<'.expected_text("opening delimiter '<'")
484                .layer("Email address")
485                .parse_next(input)?;
486
487            // The email address, which is validated by the EmailAddress struct.
488            let email = repeat_till::<_, _, (), _, _, _, _>(
489                1..,
490                any,
491                peek(alt((">", delimiter_parser.by_ref()))),
492            )
493            .take()
494            .try_map(EmailAddress::from_str)
495            .layer("email address")
496            .parse_next(input)?;
497
498            // The '>' delimiter that marks the end of the email string
499            '>'.expected_text("closing delimiter '>' of packager email address")
500                .layer("Email address")
501                .parse_next(input)?;
502
503            peek(delimiter_parser.by_ref())
504                .label("packager: unexpected trailing content")
505                .expected_text("end of input.")
506                .parse_next(input)?;
507
508            Ok(Self { name, email })
509        };
510
511        parser.layer("alpm packager")
512    }
513}
514
515impl FromStr for Packager {
516    type Err = Error;
517    /// Creates a [`Packager`] from a string slice.
518    ///
519    /// Delegates to [`Packager::parser_until`].
520    ///
521    /// # Errors
522    ///
523    /// Returns an error if [`Packager::parser_until`] fails.
524    fn from_str(s: &str) -> Result<Packager, Self::Err> {
525        Ok(Self::parser_until_eof.parse(Input::new(s))?)
526    }
527}
528
529impl Display for Packager {
530    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
531        write!(fmt, "{} <{}>", self.name, self.email)
532    }
533}
534
535#[cfg(test)]
536mod tests {
537    use insta::assert_snapshot;
538    use rstest::rstest;
539    use testresult::TestResult;
540
541    use super::*;
542    use crate::configure_insta;
543
544    #[rstest]
545    #[case("4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E")]
546    #[case("4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E")]
547    #[case("1234567890abcdef1234567890abcdef12345678")]
548    #[case("1234 5678 90ab cdef 1234 5678 90ab cdef 1234 5678")]
549    fn test_parse_openpgp_fingerprint(#[case] input: &str) -> Result<(), Error> {
550        input.parse::<OpenPGPv4Fingerprint>()?;
551        Ok(())
552    }
553
554    #[rstest]
555    // Contains non-hex characters 'G' and 'H'
556    #[case("A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8G9H0")]
557    // Less than 40 characters
558    #[case("1234567890ABCDEF1234567890ABCDEF1234567")]
559    // More than 40 characters
560    #[case("1234567890ABCDEF1234567890ABCDEF1234567890")]
561    // Starts with whitespace
562    #[case(" 4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E")]
563    // Ends with whitespace
564    #[case("4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E ")]
565    // Just invalid
566    #[case("invalid")]
567    fn openpgpv4_fingerprint_from_str_fails_on_invalid_input(#[case] input: &str) -> TestResult {
568        let Err(Error::InvalidOpenPGPv4Fingerprint) = OpenPGPv4Fingerprint::from_str(input) else {
569            panic!("Expected to fail with Error::InvalidOpenPGPv4Fingerprint");
570        };
571
572        Ok(())
573    }
574
575    /// Make sure that invalid OpenPGP v4 fingerprints don't deserialize.
576    #[cfg(feature = "serde")]
577    #[rstest]
578    #[case("A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8G9H0")]
579    #[case("invalid")]
580    fn openpgp_fingerprint_deserialize_error(#[case] input: &str) {
581        let Err(serde_json::Error { .. }) =
582            serde_json::from_str::<OpenPGPv4Fingerprint>(&format!("\"{input}\""))
583        else {
584            panic!("'{input}' erroneously deserialized as an OpenPGPv4Fingerprint")
585        };
586    }
587
588    #[rstest]
589    #[case("2F2670AC164DB36F")]
590    #[case("584A3EBFE705CDCD")]
591    fn test_parse_openpgp_key_id(#[case] input: &str) -> Result<(), Error> {
592        input.parse::<OpenPGPKeyId>()?;
593        Ok(())
594    }
595
596    #[cfg(feature = "serde")]
597    #[test]
598    fn test_serialize_openpgp_key_id() -> TestResult {
599        let id = "584A3EBFE705CDCD".parse::<OpenPGPKeyId>()?;
600        let json = serde_json::to_string(&OpenPGPIdentifier::OpenPGPKeyId(id))?;
601        assert_eq!(r#"{"openpgp_key_id":"584A3EBFE705CDCD"}"#, json);
602
603        Ok(())
604    }
605
606    #[cfg(feature = "serde")]
607    #[rstest]
608    #[case(
609        "1234567890abcdef1234567890abcdef12345678",
610        "1234567890ABCDEF1234567890ABCDEF12345678"
611    )]
612    #[case(
613        "1234 5678 90ab cdef 1234 5678 90ab cdef 1234 5678",
614        "1234567890ABCDEF1234567890ABCDEF12345678"
615    )]
616    fn test_serialize_openpgp_v4_fingerprint(
617        #[case] input: &str,
618        #[case] output: &str,
619    ) -> TestResult {
620        let print = input.parse::<OpenPGPv4Fingerprint>()?;
621        let json = serde_json::to_string(&OpenPGPIdentifier::OpenPGPv4Fingerprint(print))?;
622        assert_eq!(format!("{{\"openpgp_v4_fingerprint\":\"{output}\"}}"), json);
623
624        Ok(())
625    }
626
627    /// Ensures, that [`OpenPGPKeyId::from_str`] fails on invalid input.
628    #[rstest]
629    // Contains non-hex characters 'G' and 'H'
630    #[case("1234567890ABCGH")]
631    // Less than 16 characters
632    #[case("1234567890ABCDE")]
633    // More than 16 characters
634    #[case("1234567890ABCDEF0")]
635    // Just invalid
636    #[case("invalid")]
637    fn test_parse_invalid_openpgp_key_id(#[case] input: &str) {
638        let Err(Error::InvalidOpenPGPKeyId(invalid_input)) = OpenPGPKeyId::from_str(input) else {
639            panic!("Expected to fail with Error::InvalidOpenPGPKeyId");
640        };
641
642        assert_eq!(invalid_input, input);
643    }
644
645    /// Make sure that invalid OpenPGP key IDs don't deserialize.
646    #[cfg(feature = "serde")]
647    #[rstest]
648    #[case("1234567890ABCGH")]
649    #[case("invalid")]
650    fn openpgp_key_id_deserialize_error(#[case] input: &str) {
651        let Err(serde_json::Error { .. }) =
652            serde_json::from_str::<OpenPGPKeyId>(&format!("\"{input}\""))
653        else {
654            panic!("'{input}' erroneously deserialized as an OpenPGPKeyId")
655        };
656    }
657
658    #[rstest]
659    #[case("d2hhdCBhcmUgeW91IGxvb2tpbmcgZm9yPyA7LTsK")]
660    fn test_parse_openpgp_signature(#[case] input: &str) -> Result<(), Error> {
661        input.parse::<Base64OpenPGPSignature>()?;
662        Ok(())
663    }
664
665    /// Ensures, that [`Base64OpenPGPSignature::from_str`] fails on invalid input.
666    #[rstest]
667    // "=" in the middle
668    #[case("d2hhdCBhcmUge=W91IGxvb2tpbmcgZm9yPyA7LTsK")]
669    // invalid characters
670    #[case("!@#$%^&*")]
671    // just invalid
672    #[case("iHUEABYKh9mi7GCIlMAP9ws/jU4WEbgE=")]
673    fn base64_openpgp_signature_from_str_fails_on_invalid_input(#[case] input: &str) {
674        let Err(Error::InvalidBase64Encoding { .. }) = Base64OpenPGPSignature::from_str(input)
675        else {
676            panic!("Expected to fail with Error::InvalidBase64Encoding");
677        };
678    }
679
680    /// Make sure that invalid base64 encoded OpenPGP signatures don't deserialize.
681    #[cfg(feature = "serde")]
682    #[rstest]
683    #[case("d2hhdCBhcmUge=W91IGxvb2tpbmcgZm9yPyA7LTsK")]
684    #[case("!@#$%^&*")]
685    fn openpgp_signature_deserialize_error(#[case] input: &str) {
686        let Err(serde_json::Error { .. }) =
687            serde_json::from_str::<Base64OpenPGPSignature>(&format!("\"{input}\""))
688        else {
689            panic!("'{input}' erroneously deserialized as a Base64OpenPGPSignature")
690        };
691    }
692
693    #[rstest]
694    #[case(
695        "Foobar McFooface (The Third) <foobar@mcfooface.org>",
696        Packager{
697            name: "Foobar McFooface (The Third)".to_string(),
698            email: EmailAddress::from_str("foobar@mcfooface.org")?
699        }
700    )]
701    #[case(
702        "Foobar McFooface <foobar@mcfooface.org>",
703        Packager{
704            name: "Foobar McFooface".to_string(),
705            email: EmailAddress::from_str("foobar@mcfooface.org")?
706        }
707    )]
708    fn valid_packager(#[case] from_str: &str, #[case] packager: Packager) -> TestResult {
709        assert_eq!(Packager::from_str(from_str)?, packager);
710
711        Ok(())
712    }
713
714    /// Test that invalid packager expressions are detected as such and throw the expected error.
715    #[rstest]
716    #[case::no_name("<foobar@mcfooface.org>")]
717    #[case::no_name_and_address_not_wrapped("foobar@mcfooface.org")]
718    #[case::no_wrapped_address("Foobar McFooface")]
719    #[case::two_wrapped_addresses(
720        "Foobar McFooface <foobar@mcfooface.org> <foobar@mcfoofacemcfooface.org>"
721    )]
722    #[case::address_without_local_part("Foobar McFooface <@mcfooface.org>")]
723    fn invalid_packager(#[case] input: &str) -> TestResult {
724        let (test_name, _guard) = configure_insta();
725
726        let Err(err_msg) = Packager::from_str(input) else {
727            panic!("'{input}' erroneously parsed as a Package")
728        };
729
730        assert_snapshot!(test_name, err_msg.to_string());
731
732        Ok(())
733    }
734
735    #[rstest]
736    #[case(
737        Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
738        "Foobar McFooface <foobar@mcfooface.org>"
739    )]
740    fn packager_format_string(
741        #[case] packager: Packager,
742        #[case] packager_str: &str,
743    ) -> TestResult {
744        assert_eq!(packager_str, format!("{packager}"));
745
746        Ok(())
747    }
748
749    #[rstest]
750    #[case(Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?, "Foobar McFooface")]
751    fn packager_name(#[case] packager: Packager, #[case] name: &str) -> TestResult {
752        assert_eq!(name, packager.name());
753
754        Ok(())
755    }
756
757    #[rstest]
758    #[case(
759        Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?,
760        &EmailAddress::from_str("foobar@mcfooface.org")?,
761    )]
762    fn packager_email(#[case] packager: Packager, #[case] email: &EmailAddress) -> TestResult {
763        assert_eq!(email, packager.email());
764
765        Ok(())
766    }
767}