Skip to main content

alpm_types/
openpgp.rs

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