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 exactly 40 uppercase hexadecimal characters.
192///
193/// [OpenPGP v4 fingerprint]: https://openpgp.dev/book/certificates.html#fingerprint
194///
195/// ## Note
196///
197/// This type supports constructing from both uppercase and lowercase hexadecimal characters but
198/// guarantees to return the fingerprint in uppercase.
199///
200/// ## Examples
201///
202/// ```
203/// use std::str::FromStr;
204///
205/// use alpm_types::{Error, OpenPGPv4Fingerprint};
206///
207/// # fn main() -> Result<(), alpm_types::Error> {
208/// // Create OpenPGPv4Fingerprint from a valid OpenPGP v4 fingerprint
209/// let key = OpenPGPv4Fingerprint::from_str("4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E")?;
210/// assert_eq!(key.as_str(), "4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E");
211///
212/// // Attempting to create a OpenPGPv4Fingerprint from an invalid fingerprint will fail
213/// assert!(OpenPGPv4Fingerprint::from_str("INVALIDKEY").is_err());
214///
215/// // Format as String
216/// assert_eq!(
217///     format!("{}", key),
218///     "4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E"
219/// );
220/// # Ok(())
221/// # }
222/// ```
223#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
224pub struct OpenPGPv4Fingerprint(String);
225
226impl OpenPGPv4Fingerprint {
227    /// Creates a new `OpenPGPv4Fingerprint` instance
228    ///
229    /// See [`OpenPGPv4Fingerprint::from_str`] for more information on how the OpenPGP v4
230    /// fingerprint is validated.
231    pub fn new(fingerprint: String) -> Result<Self, Error> {
232        Self::from_str(&fingerprint)
233    }
234
235    /// Returns a reference to the inner OpenPGP v4 fingerprint as a `&str`.
236    pub fn as_str(&self) -> &str {
237        &self.0
238    }
239
240    /// Consumes the `OpenPGPv4Fingerprint` and returns the inner `String`.
241    pub fn into_inner(self) -> String {
242        self.0
243    }
244}
245
246impl FromStr for OpenPGPv4Fingerprint {
247    type Err = Error;
248
249    /// Creates a new `OpenPGPv4Fingerprint` instance after validating that it follows the correct
250    /// format.
251    ///
252    /// A valid OpenPGP v4 fingerprint should be exactly 40 characters long and consist only
253    /// of digits (`0-9`) and hexadecimal letters (`A-F`).
254    ///
255    /// # Errors
256    ///
257    /// Returns an error if the OpenPGP v4 fingerprint is not valid.
258    fn from_str(s: &str) -> Result<Self, Self::Err> {
259        if s.len() == 40 && s.chars().all(|c| c.is_ascii_hexdigit()) {
260            Ok(Self(s.to_ascii_uppercase()))
261        } else {
262            Err(Error::InvalidOpenPGPv4Fingerprint)
263        }
264    }
265}
266
267impl Display for OpenPGPv4Fingerprint {
268    /// Converts the `OpenPGPv4Fingerprint` to a uppercase `String`.
269    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
270        write!(f, "{}", self.as_str().to_ascii_uppercase())
271    }
272}
273
274/// A packager of a package
275///
276/// A `Packager` is represented by a User ID (e.g. `"Foobar McFooFace <foobar@mcfooface.org>"`).
277/// Internally this struct wraps a `String` for the name and an `EmailAddress` for a valid email
278/// address.
279///
280/// ## Examples
281/// ```
282/// use std::str::FromStr;
283///
284/// use alpm_types::{Error, Packager};
285///
286/// # fn main() -> Result<(), alpm_types::Error> {
287/// // create Packager from &str
288/// let packager = Packager::from_str("Foobar McFooface <foobar@mcfooface.org>")?;
289///
290/// // get name
291/// assert_eq!("Foobar McFooface", packager.name());
292///
293/// // get email
294/// assert_eq!("foobar@mcfooface.org", packager.email().to_string());
295///
296/// // get email domain
297/// assert_eq!("mcfooface.org", packager.email().domain());
298///
299/// // format as String
300/// assert_eq!(
301///     "Foobar McFooface <foobar@mcfooface.org>",
302///     format!("{}", packager)
303/// );
304/// # Ok(())
305/// # }
306/// ```
307#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
308pub struct Packager {
309    name: String,
310    email: EmailAddress,
311}
312
313impl Packager {
314    /// Create a new Packager
315    pub fn new(name: String, email: EmailAddress) -> Packager {
316        Packager { name, email }
317    }
318
319    /// Return the name of the Packager
320    pub fn name(&self) -> &str {
321        &self.name
322    }
323
324    /// Return the email of the Packager
325    pub fn email(&self) -> &EmailAddress {
326        &self.email
327    }
328
329    /// Parses a [`Packager`] from a string slice.
330    ///
331    /// Consumes all of its input.
332    ///
333    /// # Examples
334    ///
335    /// See [`Self::from_str`] for code examples.
336    ///
337    /// # Errors
338    ///
339    /// Returns an error if `input` does not represent a valid [`Packager`].
340    pub fn parser(input: &mut &str) -> ModalResult<Self> {
341        seq!(Self {
342            // The name that precedes the email address
343            name: cut_err(take_till(1.., '<'))
344                .map(|s: &str| s.trim().to_string())
345                .context(StrContext::Label("packager name")),
346            // The '<' delimiter that marks the start of the email string
347            _: cut_err('<').context(StrContext::Label("or missing opening delimiter '<' for email address")),
348            // The email address, which is validated by the EmailAddress struct.
349            email: cut_err(
350                take_till(1.., '>')
351                    .try_map(EmailAddress::from_str))
352                    .context(StrContext::Label("Email address")
353                ),
354            // The '>' delimiter that marks the end of the email string
355            _: cut_err('>').context(StrContext::Label("or missing closing delimiter '>' for email address")),
356            _: eof.context(StrContext::Expected(StrContextValue::Description("end of packager string"))),
357        })
358        .parse_next(input)
359    }
360}
361
362impl FromStr for Packager {
363    type Err = Error;
364    /// Create a Packager from a string
365    fn from_str(s: &str) -> Result<Packager, Self::Err> {
366        Ok(Self::parser.parse(s)?)
367    }
368}
369
370impl Display for Packager {
371    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
372        write!(fmt, "{} <{}>", self.name, self.email)
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use rstest::rstest;
379    use testresult::{TestError, TestResult};
380
381    use super::*;
382
383    #[rstest]
384    #[case("4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E")]
385    #[case("1234567890abcdef1234567890abcdef12345678")]
386    fn test_parse_openpgp_fingerprint(#[case] input: &str) -> Result<(), Error> {
387        input.parse::<OpenPGPv4Fingerprint>()?;
388        Ok(())
389    }
390
391    #[rstest]
392    // Contains non-hex characters 'G' and 'H'
393    #[case(
394        "A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8G9H0",
395        Err(Error::InvalidOpenPGPv4Fingerprint)
396    )]
397    // Less than 40 characters
398    #[case(
399        "1234567890ABCDEF1234567890ABCDEF1234567",
400        Err(Error::InvalidOpenPGPv4Fingerprint)
401    )]
402    // More than 40 characters
403    #[case(
404        "1234567890ABCDEF1234567890ABCDEF1234567890",
405        Err(Error::InvalidOpenPGPv4Fingerprint)
406    )]
407    // Just invalid
408    #[case("invalid", Err(Error::InvalidOpenPGPv4Fingerprint))]
409    fn test_parse_invalid_openpgp_fingerprint(
410        #[case] input: &str,
411        #[case] expected: Result<OpenPGPv4Fingerprint, Error>,
412    ) {
413        let result = input.parse::<OpenPGPv4Fingerprint>();
414        assert_eq!(result, expected);
415    }
416
417    #[rstest]
418    #[case("2F2670AC164DB36F")]
419    #[case("584A3EBFE705CDCD")]
420    fn test_parse_openpgp_key_id(#[case] input: &str) -> Result<(), Error> {
421        input.parse::<OpenPGPKeyId>()?;
422        Ok(())
423    }
424
425    #[test]
426    fn test_serialize_openpgp_key_id() -> TestResult {
427        let id = "584A3EBFE705CDCD".parse::<OpenPGPKeyId>()?;
428        let json = serde_json::to_string(&OpenPGPIdentifier::OpenPGPKeyId(id))?;
429        assert_eq!(r#"{"openpgp_key_id":"584A3EBFE705CDCD"}"#, json);
430
431        Ok(())
432    }
433
434    #[test]
435    fn test_serialize_openpgp_v4_fingerprint() -> TestResult {
436        let print = "1234567890abcdef1234567890abcdef12345678".parse::<OpenPGPv4Fingerprint>()?;
437        let json = serde_json::to_string(&OpenPGPIdentifier::OpenPGPv4Fingerprint(print))?;
438        assert_eq!(
439            r#"{"openpgp_v4_fingerprint":"1234567890ABCDEF1234567890ABCDEF12345678"}"#,
440            json
441        );
442
443        Ok(())
444    }
445
446    #[rstest]
447    // Contains non-hex characters 'G' and 'H'
448    #[case("1234567890ABCGH", Err(Error::InvalidOpenPGPKeyId("1234567890ABCGH".to_string())))]
449    // Less than 16 characters
450    #[case("1234567890ABCDE", Err(Error::InvalidOpenPGPKeyId("1234567890ABCDE".to_string())))]
451    // More than 16 characters
452    #[case("1234567890ABCDEF0", Err(Error::InvalidOpenPGPKeyId("1234567890ABCDEF0".to_string())))]
453    // Just invalid
454    #[case("invalid", Err(Error::InvalidOpenPGPKeyId("invalid".to_string())))]
455    fn test_parse_invalid_openpgp_key_id(
456        #[case] input: &str,
457        #[case] expected: Result<OpenPGPKeyId, Error>,
458    ) {
459        let result = input.parse::<OpenPGPKeyId>();
460        assert_eq!(result, expected);
461    }
462
463    #[rstest]
464    #[case(
465        "Foobar McFooface (The Third) <foobar@mcfooface.org>",
466        Packager{
467            name: "Foobar McFooface (The Third)".to_string(),
468            email: EmailAddress::from_str("foobar@mcfooface.org").unwrap()
469        }
470    )]
471    #[case(
472        "Foobar McFooface <foobar@mcfooface.org>",
473        Packager{
474            name: "Foobar McFooface".to_string(),
475            email: EmailAddress::from_str("foobar@mcfooface.org").unwrap()
476        }
477    )]
478    fn valid_packager(#[case] from_str: &str, #[case] packager: Packager) {
479        assert_eq!(Packager::from_str(from_str), Ok(packager));
480    }
481
482    /// Test that invalid packager expressions are detected as such and throw the expected error.
483    #[rstest]
484    #[case::no_name("<foobar@mcfooface.org>", "invalid packager name")]
485    #[case::no_name_and_address_not_wrapped(
486        "foobar@mcfooface.org",
487        "invalid or missing opening delimiter '<' for email address"
488    )]
489    #[case::no_wrapped_address(
490        "Foobar McFooface",
491        "invalid or missing opening delimiter '<' for email address"
492    )]
493    #[case::two_wrapped_addresses(
494        "Foobar McFooface <foobar@mcfooface.org> <foobar@mcfoofacemcfooface.org>",
495        "expected end of packager string"
496    )]
497    #[case::address_without_local_part("Foobar McFooface <@mcfooface.org>", "Local part is empty")]
498    fn invalid_packager(#[case] packager: &str, #[case] expected_error: &str) -> TestResult {
499        let Err(err) = Packager::from_str(packager) else {
500            return Err(TestError::from(format!(
501                "Expected packager string to be invalid: {packager}"
502            )));
503        };
504
505        let error = err.to_string();
506        assert!(
507            error.contains(expected_error),
508            "Expected error:\n{error}\n\nto contain string:\n{expected_error}"
509        );
510
511        Ok(())
512    }
513
514    #[rstest]
515    #[case(
516        Packager::from_str("Foobar McFooface <foobar@mcfooface.org>").unwrap(),
517        "Foobar McFooface <foobar@mcfooface.org>"
518    )]
519    fn packager_format_string(#[case] packager: Packager, #[case] packager_str: &str) {
520        assert_eq!(packager_str, format!("{packager}"));
521    }
522
523    #[rstest]
524    #[case(Packager::from_str("Foobar McFooface <foobar@mcfooface.org>").unwrap(), "Foobar McFooface")]
525    fn packager_name(#[case] packager: Packager, #[case] name: &str) {
526        assert_eq!(name, packager.name());
527    }
528
529    #[rstest]
530    #[case(
531        Packager::from_str("Foobar McFooface <foobar@mcfooface.org>").unwrap(),
532        &EmailAddress::from_str("foobar@mcfooface.org").unwrap(),
533    )]
534    fn packager_email(#[case] packager: Packager, #[case] email: &EmailAddress) {
535        assert_eq!(email, packager.email());
536    }
537}