alpm_types/
openpgp.rs

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/// An OpenPGP key identifier.
20///
21/// The `OpenPGPIdentifier` enum represents a valid OpenPGP identifier, which can be either an
22/// OpenPGP Key ID or an OpenPGP v4 fingerprint.
23///
24/// This type wraps an [`OpenPGPKeyId`] and an [`OpenPGPv4Fingerprint`] and provides a unified
25/// interface for both.
26///
27/// ## Examples
28///
29/// ```
30/// use std::str::FromStr;
31///
32/// use alpm_types::{Error, OpenPGPIdentifier, OpenPGPKeyId, OpenPGPv4Fingerprint};
33/// # fn main() -> Result<(), alpm_types::Error> {
34/// // Create a OpenPGPIdentifier from a valid OpenPGP v4 fingerprint
35/// let key = OpenPGPIdentifier::from_str("4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E")?;
36/// assert_eq!(
37///     key,
38///     OpenPGPIdentifier::OpenPGPv4Fingerprint(OpenPGPv4Fingerprint::from_str(
39///         "4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E"
40///     )?)
41/// );
42/// assert_eq!(key.to_string(), "4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E");
43/// assert_eq!(
44///     key,
45///     OpenPGPv4Fingerprint::from_str("4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E")?.into()
46/// );
47///
48/// // Create a OpenPGPIdentifier from a valid OpenPGP Key ID
49/// let key = OpenPGPIdentifier::from_str("2F2670AC164DB36F")?;
50/// assert_eq!(
51///     key,
52///     OpenPGPIdentifier::OpenPGPKeyId(OpenPGPKeyId::from_str("2F2670AC164DB36F")?)
53/// );
54/// assert_eq!(key.to_string(), "2F2670AC164DB36F");
55/// assert_eq!(key, OpenPGPKeyId::from_str("2F2670AC164DB36F")?.into());
56/// # Ok(())
57/// # }
58/// ```
59#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
60pub enum OpenPGPIdentifier {
61    /// An OpenPGP Key ID.
62    #[serde(rename = "openpgp_key_id")]
63    OpenPGPKeyId(OpenPGPKeyId),
64    /// An OpenPGP v4 fingerprint.
65    #[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/// An OpenPGP Key ID.
105///
106/// The `OpenPGPKeyId` type wraps a `String` representing an [OpenPGP Key ID],
107/// ensuring that it consists of exactly 16 uppercase hexadecimal characters.
108///
109/// [OpenPGP Key ID]: https://openpgp.dev/book/glossary.html#term-Key-ID
110///
111/// ## Note
112///
113/// - This type supports constructing from both uppercase and lowercase hexadecimal characters but
114///   guarantees to return the key ID in uppercase.
115///
116/// - The usage of this type is highly discouraged as the keys may not be unique. This will lead to
117///   a linting error in the future.
118///
119/// ## Examples
120///
121/// ```
122/// use std::str::FromStr;
123///
124/// use alpm_types::{Error, OpenPGPKeyId};
125///
126/// # fn main() -> Result<(), alpm_types::Error> {
127/// // Create OpenPGPKeyId from a valid key ID
128/// let key = OpenPGPKeyId::from_str("2F2670AC164DB36F")?;
129/// assert_eq!(key.as_str(), "2F2670AC164DB36F");
130///
131/// // Attempting to create an OpenPGPKeyId from an invalid key ID will fail
132/// assert!(OpenPGPKeyId::from_str("INVALIDKEYID").is_err());
133///
134/// // Format as String
135/// assert_eq!(format!("{key}"), "2F2670AC164DB36F");
136/// # Ok(())
137/// # }
138/// ```
139#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
140pub struct OpenPGPKeyId(String);
141
142impl OpenPGPKeyId {
143    /// Creates a new `OpenPGPKeyId` instance.
144    ///
145    /// See [`OpenPGPKeyId::from_str`] for more information on how the OpenPGP Key ID is validated.
146    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    /// Returns a reference to the inner OpenPGP Key ID as a `&str`.
155    pub fn as_str(&self) -> &str {
156        &self.0
157    }
158
159    /// Consumes the `OpenPGPKeyId` and returns the inner `String`.
160    pub fn into_inner(self) -> String {
161        self.0
162    }
163}
164
165impl FromStr for OpenPGPKeyId {
166    type Err = Error;
167
168    /// Creates a new `OpenPGPKeyId` instance after validating that it follows the correct format.
169    ///
170    /// A valid OpenPGP Key ID should be exactly 16 characters long and consist only
171    /// of digits (`0-9`) and hexadecimal letters (`A-F`).
172    ///
173    /// # Errors
174    ///
175    /// Returns an error if the OpenPGP Key ID is not valid.
176    fn from_str(s: &str) -> Result<Self, Self::Err> {
177        Self::new(s.to_string())
178    }
179}
180
181impl Display for OpenPGPKeyId {
182    /// Converts the `OpenPGPKeyId` to an uppercase `String`.
183    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
184        write!(f, "{}", self.0)
185    }
186}
187
188/// An OpenPGP v4 fingerprint.
189///
190/// The `OpenPGPv4Fingerprint` type wraps a `String` representing an [OpenPGP v4 fingerprint],
191/// ensuring that it consists of 40 uppercase hexadecimal characters with optional whitespace
192/// separators.
193///
194/// [OpenPGP v4 fingerprint]: https://openpgp.dev/book/certificates.html#fingerprint
195///
196/// ## Note
197///
198/// - This type supports constructing from both uppercase and lowercase hexadecimal characters, with
199///   and without whitespace separators, but guarantees to return the fingerprint in uppercase and
200///   with no whitespaces.
201///
202/// - Whitespaces are only allowed between hexadecimal characters, not at the start or end of the
203///   fingerprint.
204///
205/// ## Examples
206///
207/// ```
208/// use std::str::FromStr;
209///
210/// use alpm_types::{Error, OpenPGPv4Fingerprint};
211///
212/// # fn main() -> Result<(), alpm_types::Error> {
213/// // Create OpenPGPv4Fingerprint from a valid OpenPGP v4 fingerprint
214/// let key = OpenPGPv4Fingerprint::from_str("4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E")?;
215/// assert_eq!(key.as_str(), "4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E");
216///
217/// // Space separated fingerprint is also valid
218/// let key = OpenPGPv4Fingerprint::from_str("4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E")?;
219/// assert_eq!(key.as_str(), "4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E");
220///
221/// // Attempting to create a OpenPGPv4Fingerprint from an invalid fingerprint will fail
222/// assert!(OpenPGPv4Fingerprint::from_str("INVALIDKEY").is_err());
223///
224/// // Format as String
225/// assert_eq!(
226///     format!("{}", key),
227///     "4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E"
228/// );
229/// # Ok(())
230/// # }
231/// ```
232#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
233pub struct OpenPGPv4Fingerprint(String);
234
235impl OpenPGPv4Fingerprint {
236    /// Creates a new `OpenPGPv4Fingerprint` instance
237    ///
238    /// See [`OpenPGPv4Fingerprint::from_str`] for more information on how the OpenPGP v4
239    /// fingerprint is validated.
240    pub fn new(fingerprint: String) -> Result<Self, Error> {
241        Self::from_str(&fingerprint)
242    }
243
244    /// Returns a reference to the inner OpenPGP v4 fingerprint as a `&str`.
245    pub fn as_str(&self) -> &str {
246        &self.0
247    }
248
249    /// Consumes the `OpenPGPv4Fingerprint` and returns the inner `String`.
250    pub fn into_inner(self) -> String {
251        self.0
252    }
253}
254
255impl FromStr for OpenPGPv4Fingerprint {
256    type Err = Error;
257
258    /// Creates a new `OpenPGPv4Fingerprint` instance after validating that it follows the correct
259    /// format.
260    ///
261    /// A valid OpenPGP v4 fingerprint should be a 40 characters long string of digits (`0-9`)
262    /// and hexadecimal letters (`A-F`) optionally separated by whitespaces.
263    ///
264    /// # Errors
265    ///
266    /// Returns an error if the OpenPGP v4 fingerprint is not valid.
267    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    /// Converts the `OpenPGPv4Fingerprint` to a uppercase `String`.
284    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
285        write!(f, "{}", self.as_str().to_ascii_uppercase())
286    }
287}
288
289/// A packager of a package
290///
291/// A `Packager` is represented by a User ID (e.g. `"Foobar McFooFace <foobar@mcfooface.org>"`).
292/// Internally this struct wraps a `String` for the name and an `EmailAddress` for a valid email
293/// address.
294///
295/// ## Examples
296/// ```
297/// use std::str::FromStr;
298///
299/// use alpm_types::{Error, Packager};
300///
301/// # fn main() -> Result<(), alpm_types::Error> {
302/// // create Packager from &str
303/// let packager = Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?;
304///
305/// // get name
306/// assert_eq!("Foobar McFooface", packager.name());
307///
308/// // get email
309/// assert_eq!("foobar@mcfooface.org", packager.email().to_string());
310///
311/// // get email domain
312/// assert_eq!("mcfooface.org", packager.email().domain());
313///
314/// // format as String
315/// assert_eq!(
316///     "Foobar McFooface <foobar@mcfooface.org>",
317///     format!("{}", packager)
318/// );
319/// # Ok(())
320/// # }
321/// ```
322#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
323pub struct Packager {
324    name: String,
325    email: EmailAddress,
326}
327
328impl Packager {
329    /// Create a new Packager
330    pub fn new(name: String, email: EmailAddress) -> Packager {
331        Packager { name, email }
332    }
333
334    /// Return the name of the Packager
335    pub fn name(&self) -> &str {
336        &self.name
337    }
338
339    /// Return the email of the Packager
340    pub fn email(&self) -> &EmailAddress {
341        &self.email
342    }
343
344    /// Parses a [`Packager`] from a string slice.
345    ///
346    /// Consumes all of its input.
347    ///
348    /// # Examples
349    ///
350    /// See [`Self::from_str`] for code examples.
351    ///
352    /// # Errors
353    ///
354    /// Returns an error if `input` does not represent a valid [`Packager`].
355    pub fn parser(input: &mut &str) -> ModalResult<Self> {
356        seq!(Self {
357            // The name that precedes the email address
358            name: cut_err(take_till(1.., '<'))
359                .map(|s: &str| s.trim().to_string())
360                .context(StrContext::Label("packager name")),
361            // The '<' delimiter that marks the start of the email string
362            _: cut_err('<').context(StrContext::Label("or missing opening delimiter '<' for email address")),
363            // The email address, which is validated by the EmailAddress struct.
364            email: cut_err(
365                take_till(1.., '>')
366                    .try_map(EmailAddress::from_str))
367                    .context(StrContext::Label("Email address")
368                ),
369            // The '>' delimiter that marks the end of the email string
370            _: 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    /// Create a Packager from a string
380    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    // Contains non-hex characters 'G' and 'H'
410    #[case(
411        "A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8G9H0",
412        Err(Error::InvalidOpenPGPv4Fingerprint)
413    )]
414    // Less than 40 characters
415    #[case(
416        "1234567890ABCDEF1234567890ABCDEF1234567",
417        Err(Error::InvalidOpenPGPv4Fingerprint)
418    )]
419    // More than 40 characters
420    #[case(
421        "1234567890ABCDEF1234567890ABCDEF1234567890",
422        Err(Error::InvalidOpenPGPv4Fingerprint)
423    )]
424    // Starts with whitespace
425    #[case(
426        " 4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E",
427        Err(Error::InvalidOpenPGPv4Fingerprint)
428    )]
429    // Ends with whitespace
430    #[case(
431        "4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E ",
432        Err(Error::InvalidOpenPGPv4Fingerprint)
433    )]
434    // Just invalid
435    #[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    // Contains non-hex characters 'G' and 'H'
483    #[case("1234567890ABCGH", Err(Error::InvalidOpenPGPKeyId("1234567890ABCGH".to_string())))]
484    // Less than 16 characters
485    #[case("1234567890ABCDE", Err(Error::InvalidOpenPGPKeyId("1234567890ABCDE".to_string())))]
486    // More than 16 characters
487    #[case("1234567890ABCDEF0", Err(Error::InvalidOpenPGPKeyId("1234567890ABCDEF0".to_string())))]
488    // Just invalid
489    #[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    /// Test that invalid packager expressions are detected as such and throw the expected error.
518    #[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}