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 rstest::rstest;
488    use testresult::TestResult;
489
490    use super::*;
491
492    #[rstest]
493    #[case("4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E")]
494    #[case("4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E")]
495    #[case("1234567890abcdef1234567890abcdef12345678")]
496    #[case("1234 5678 90ab cdef 1234 5678 90ab cdef 1234 5678")]
497    fn test_parse_openpgp_fingerprint(#[case] input: &str) -> Result<(), Error> {
498        input.parse::<OpenPGPv4Fingerprint>()?;
499        Ok(())
500    }
501
502    #[rstest]
503    // Contains non-hex characters 'G' and 'H'
504    #[case(
505        "A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8G9H0",
506        Err(Error::InvalidOpenPGPv4Fingerprint)
507    )]
508    // Less than 40 characters
509    #[case(
510        "1234567890ABCDEF1234567890ABCDEF1234567",
511        Err(Error::InvalidOpenPGPv4Fingerprint)
512    )]
513    // More than 40 characters
514    #[case(
515        "1234567890ABCDEF1234567890ABCDEF1234567890",
516        Err(Error::InvalidOpenPGPv4Fingerprint)
517    )]
518    // Starts with whitespace
519    #[case(
520        " 4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E",
521        Err(Error::InvalidOpenPGPv4Fingerprint)
522    )]
523    // Ends with whitespace
524    #[case(
525        "4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E ",
526        Err(Error::InvalidOpenPGPv4Fingerprint)
527    )]
528    // Just invalid
529    #[case("invalid", Err(Error::InvalidOpenPGPv4Fingerprint))]
530    fn test_parse_invalid_openpgp_fingerprint(
531        #[case] input: &str,
532        #[case] expected: Result<OpenPGPv4Fingerprint, Error>,
533    ) {
534        let result = input.parse::<OpenPGPv4Fingerprint>();
535        assert_eq!(result, expected);
536    }
537
538    #[rstest]
539    #[case("2F2670AC164DB36F")]
540    #[case("584A3EBFE705CDCD")]
541    fn test_parse_openpgp_key_id(#[case] input: &str) -> Result<(), Error> {
542        input.parse::<OpenPGPKeyId>()?;
543        Ok(())
544    }
545
546    #[test]
547    fn test_serialize_openpgp_key_id() -> TestResult {
548        let id = "584A3EBFE705CDCD".parse::<OpenPGPKeyId>()?;
549        let json = serde_json::to_string(&OpenPGPIdentifier::OpenPGPKeyId(id))?;
550        assert_eq!(r#"{"openpgp_key_id":"584A3EBFE705CDCD"}"#, json);
551
552        Ok(())
553    }
554
555    #[rstest]
556    #[case(
557        "1234567890abcdef1234567890abcdef12345678",
558        "1234567890ABCDEF1234567890ABCDEF12345678"
559    )]
560    #[case(
561        "1234 5678 90ab cdef 1234 5678 90ab cdef 1234 5678",
562        "1234567890ABCDEF1234567890ABCDEF12345678"
563    )]
564    fn test_serialize_openpgp_v4_fingerprint(
565        #[case] input: &str,
566        #[case] output: &str,
567    ) -> TestResult {
568        let print = input.parse::<OpenPGPv4Fingerprint>()?;
569        let json = serde_json::to_string(&OpenPGPIdentifier::OpenPGPv4Fingerprint(print))?;
570        assert_eq!(format!("{{\"openpgp_v4_fingerprint\":\"{output}\"}}"), json);
571
572        Ok(())
573    }
574
575    #[rstest]
576    // Contains non-hex characters 'G' and 'H'
577    #[case("1234567890ABCGH", Err(Error::InvalidOpenPGPKeyId("1234567890ABCGH".to_string())))]
578    // Less than 16 characters
579    #[case("1234567890ABCDE", Err(Error::InvalidOpenPGPKeyId("1234567890ABCDE".to_string())))]
580    // More than 16 characters
581    #[case("1234567890ABCDEF0", Err(Error::InvalidOpenPGPKeyId("1234567890ABCDEF0".to_string())))]
582    // Just invalid
583    #[case("invalid", Err(Error::InvalidOpenPGPKeyId("invalid".to_string())))]
584    fn test_parse_invalid_openpgp_key_id(
585        #[case] input: &str,
586        #[case] expected: Result<OpenPGPKeyId, Error>,
587    ) {
588        let result = input.parse::<OpenPGPKeyId>();
589        assert_eq!(result, expected);
590    }
591
592    #[rstest]
593    #[case("d2hhdCBhcmUgeW91IGxvb2tpbmcgZm9yPyA7LTsK")]
594    fn test_parse_openpgp_signature(#[case] input: &str) -> Result<(), Error> {
595        input.parse::<Base64OpenPGPSignature>()?;
596        Ok(())
597    }
598
599    #[rstest]
600    // "=" in the middle
601    #[case(
602        "d2hhdCBhcmUge=W91IGxvb2tpbmcgZm9yPyA7LTsK",
603        Err(Error::InvalidBase64Encoding { expected_item: t!("error-invalid-base64-encoding-pgp-signature") })
604    )]
605    // invalid characters
606    #[case("!@#$%^&*", Err(Error::InvalidBase64Encoding { expected_item: t!("error-invalid-base64-encoding-pgp-signature") }))]
607    // just invalid
608    #[case(
609        "iHUEABYKh9mi7GCIlMAP9ws/jU4WEbgE=",
610        Err(Error::InvalidBase64Encoding { expected_item: t!("error-invalid-base64-encoding-pgp-signature") })
611    )]
612    fn test_parse_invalid_openpgp_signature(
613        #[case] input: &str,
614        #[case] expected: Result<Base64OpenPGPSignature, Error>,
615    ) {
616        let result = input.parse::<Base64OpenPGPSignature>();
617        assert_eq!(result, expected);
618    }
619
620    #[rstest]
621    #[case(
622        "Foobar McFooface (The Third) <foobar@mcfooface.org>",
623        Packager{
624            name: "Foobar McFooface (The Third)".to_string(),
625            email: EmailAddress::from_str("foobar@mcfooface.org").unwrap()
626        }
627    )]
628    #[case(
629        "Foobar McFooface <foobar@mcfooface.org>",
630        Packager{
631            name: "Foobar McFooface".to_string(),
632            email: EmailAddress::from_str("foobar@mcfooface.org").unwrap()
633        }
634    )]
635    fn valid_packager(#[case] from_str: &str, #[case] packager: Packager) {
636        assert_eq!(Packager::from_str(from_str), Ok(packager));
637    }
638
639    /// Test that invalid packager expressions are detected as such and throw the expected error.
640    #[rstest]
641    #[case::no_name("<foobar@mcfooface.org>", "invalid packager name")]
642    #[case::no_name_and_address_not_wrapped(
643        "foobar@mcfooface.org",
644        "invalid or missing opening delimiter '<' for email address"
645    )]
646    #[case::no_wrapped_address(
647        "Foobar McFooface",
648        "invalid or missing opening delimiter '<' for email address"
649    )]
650    #[case::two_wrapped_addresses(
651        "Foobar McFooface <foobar@mcfooface.org> <foobar@mcfoofacemcfooface.org>",
652        "expected end of packager string"
653    )]
654    #[case::address_without_local_part("Foobar McFooface <@mcfooface.org>", "Local part is empty")]
655    fn invalid_packager(#[case] packager: &str, #[case] expected_error: &str) -> TestResult {
656        let Err(err) = Packager::from_str(packager) else {
657            panic!("Expected packager string to be invalid: {packager}");
658        };
659
660        let error = err.to_string();
661        assert!(
662            error.contains(expected_error),
663            "Expected error:\n{error}\n\nto contain string:\n{expected_error}"
664        );
665
666        Ok(())
667    }
668
669    #[rstest]
670    #[case(
671        Packager::from_str("Foobar McFooface <foobar@mcfooface.org>").unwrap(),
672        "Foobar McFooface <foobar@mcfooface.org>"
673    )]
674    fn packager_format_string(#[case] packager: Packager, #[case] packager_str: &str) {
675        assert_eq!(packager_str, format!("{packager}"));
676    }
677
678    #[rstest]
679    #[case(Packager::from_str("Foobar McFooface <foobar@mcfooface.org>").unwrap(), "Foobar McFooface")]
680    fn packager_name(#[case] packager: Packager, #[case] name: &str) {
681        assert_eq!(name, packager.name());
682    }
683
684    #[rstest]
685    #[case(
686        Packager::from_str("Foobar McFooface <foobar@mcfooface.org>").unwrap(),
687        &EmailAddress::from_str("foobar@mcfooface.org").unwrap(),
688    )]
689    fn packager_email(#[case] packager: Packager, #[case] email: &EmailAddress) {
690        assert_eq!(email, packager.email());
691    }
692}