use std::{
fmt::{Display, Formatter},
str::FromStr,
string::ToString,
};
use base64::{Engine, prelude::BASE64_STANDARD};
use email_address::EmailAddress;
use fluent_i18n::t;
use serde::{Deserialize, Serialize};
use winnow::{
ModalResult,
Parser,
combinator::{cut_err, eof, seq},
error::{StrContext, StrContextValue},
token::take_till,
};
use crate::Error;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum OpenPGPIdentifier {
#[serde(rename = "openpgp_key_id")]
OpenPGPKeyId(OpenPGPKeyId),
#[serde(rename = "openpgp_v4_fingerprint")]
OpenPGPv4Fingerprint(OpenPGPv4Fingerprint),
}
impl FromStr for OpenPGPIdentifier {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.parse::<OpenPGPv4Fingerprint>() {
Ok(fingerprint) => Ok(OpenPGPIdentifier::OpenPGPv4Fingerprint(fingerprint)),
Err(_) => match s.parse::<OpenPGPKeyId>() {
Ok(key_id) => Ok(OpenPGPIdentifier::OpenPGPKeyId(key_id)),
Err(e) => Err(e),
},
}
}
}
impl From<OpenPGPKeyId> for OpenPGPIdentifier {
fn from(key_id: OpenPGPKeyId) -> Self {
OpenPGPIdentifier::OpenPGPKeyId(key_id)
}
}
impl From<OpenPGPv4Fingerprint> for OpenPGPIdentifier {
fn from(fingerprint: OpenPGPv4Fingerprint) -> Self {
OpenPGPIdentifier::OpenPGPv4Fingerprint(fingerprint)
}
}
impl Display for OpenPGPIdentifier {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
OpenPGPIdentifier::OpenPGPKeyId(key_id) => write!(f, "{key_id}"),
OpenPGPIdentifier::OpenPGPv4Fingerprint(fingerprint) => write!(f, "{fingerprint}"),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct OpenPGPKeyId(String);
impl OpenPGPKeyId {
pub fn new(key_id: String) -> Result<Self, Error> {
if key_id.len() == 16 && key_id.chars().all(|c| c.is_ascii_hexdigit()) {
Ok(Self(key_id.to_ascii_uppercase()))
} else {
Err(Error::InvalidOpenPGPKeyId(key_id))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl FromStr for OpenPGPKeyId {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s.to_string())
}
}
impl Display for OpenPGPKeyId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct OpenPGPv4Fingerprint(String);
impl OpenPGPv4Fingerprint {
pub fn new(fingerprint: String) -> Result<Self, Error> {
Self::from_str(&fingerprint)
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl FromStr for OpenPGPv4Fingerprint {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalized = s.to_ascii_uppercase().replace(" ", "");
if !s.starts_with(' ')
&& !s.ends_with(' ')
&& normalized.len() == 40
&& normalized.chars().all(|c| c.is_ascii_hexdigit())
{
Ok(Self(normalized))
} else {
Err(Error::InvalidOpenPGPv4Fingerprint)
}
}
}
impl Display for OpenPGPv4Fingerprint {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str().to_ascii_uppercase())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Base64OpenPGPSignature(String);
impl Base64OpenPGPSignature {
pub fn new(signature: String) -> Result<Self, Error> {
Self::from_str(&signature)
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl AsRef<str> for Base64OpenPGPSignature {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl FromStr for Base64OpenPGPSignature {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
BASE64_STANDARD
.decode(s)
.map_err(|_| Error::InvalidBase64Encoding {
expected_item: t!("error-invalid-base64-encoding-pgp-signature"),
})?
.to_vec();
Ok(Self(s.to_string()))
}
}
impl Display for Base64OpenPGPSignature {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Packager {
name: String,
email: EmailAddress,
}
impl Packager {
pub fn new(name: String, email: EmailAddress) -> Packager {
Packager { name, email }
}
pub fn name(&self) -> &str {
&self.name
}
pub fn email(&self) -> &EmailAddress {
&self.email
}
pub fn parser(input: &mut &str) -> ModalResult<Self> {
seq!(Self {
name: cut_err(take_till(1.., '<'))
.map(|s: &str| s.trim().to_string())
.context(StrContext::Label("packager name")),
_: cut_err('<').context(StrContext::Label("or missing opening delimiter '<' for email address")),
email: cut_err(
take_till(1.., '>')
.try_map(EmailAddress::from_str))
.context(StrContext::Label("Email address")
),
_: cut_err('>').context(StrContext::Label("or missing closing delimiter '>' for email address")),
_: eof.context(StrContext::Expected(StrContextValue::Description("end of packager string"))),
})
.parse_next(input)
}
}
impl FromStr for Packager {
type Err = Error;
fn from_str(s: &str) -> Result<Packager, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
impl Display for Packager {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
write!(fmt, "{} <{}>", self.name, self.email)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use testresult::TestResult;
use super::*;
#[rstest]
#[case("4A0C4DFFC02E1A7ED969ED231C2358A25A10D94E")]
#[case("4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E")]
#[case("1234567890abcdef1234567890abcdef12345678")]
#[case("1234 5678 90ab cdef 1234 5678 90ab cdef 1234 5678")]
fn test_parse_openpgp_fingerprint(#[case] input: &str) -> Result<(), Error> {
input.parse::<OpenPGPv4Fingerprint>()?;
Ok(())
}
#[rstest]
#[case(
"A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8G9H0",
Err(Error::InvalidOpenPGPv4Fingerprint)
)]
#[case(
"1234567890ABCDEF1234567890ABCDEF1234567",
Err(Error::InvalidOpenPGPv4Fingerprint)
)]
#[case(
"1234567890ABCDEF1234567890ABCDEF1234567890",
Err(Error::InvalidOpenPGPv4Fingerprint)
)]
#[case(
" 4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E",
Err(Error::InvalidOpenPGPv4Fingerprint)
)]
#[case(
"4A0C 4DFF C02E 1A7E D969 ED23 1C23 58A2 5A10 D94E ",
Err(Error::InvalidOpenPGPv4Fingerprint)
)]
#[case("invalid", Err(Error::InvalidOpenPGPv4Fingerprint))]
fn test_parse_invalid_openpgp_fingerprint(
#[case] input: &str,
#[case] expected: Result<OpenPGPv4Fingerprint, Error>,
) {
let result = input.parse::<OpenPGPv4Fingerprint>();
assert_eq!(result, expected);
}
#[rstest]
#[case("2F2670AC164DB36F")]
#[case("584A3EBFE705CDCD")]
fn test_parse_openpgp_key_id(#[case] input: &str) -> Result<(), Error> {
input.parse::<OpenPGPKeyId>()?;
Ok(())
}
#[test]
fn test_serialize_openpgp_key_id() -> TestResult {
let id = "584A3EBFE705CDCD".parse::<OpenPGPKeyId>()?;
let json = serde_json::to_string(&OpenPGPIdentifier::OpenPGPKeyId(id))?;
assert_eq!(r#"{"openpgp_key_id":"584A3EBFE705CDCD"}"#, json);
Ok(())
}
#[rstest]
#[case(
"1234567890abcdef1234567890abcdef12345678",
"1234567890ABCDEF1234567890ABCDEF12345678"
)]
#[case(
"1234 5678 90ab cdef 1234 5678 90ab cdef 1234 5678",
"1234567890ABCDEF1234567890ABCDEF12345678"
)]
fn test_serialize_openpgp_v4_fingerprint(
#[case] input: &str,
#[case] output: &str,
) -> TestResult {
let print = input.parse::<OpenPGPv4Fingerprint>()?;
let json = serde_json::to_string(&OpenPGPIdentifier::OpenPGPv4Fingerprint(print))?;
assert_eq!(format!("{{\"openpgp_v4_fingerprint\":\"{output}\"}}"), json);
Ok(())
}
#[rstest]
#[case("1234567890ABCGH", Err(Error::InvalidOpenPGPKeyId("1234567890ABCGH".to_string())))]
#[case("1234567890ABCDE", Err(Error::InvalidOpenPGPKeyId("1234567890ABCDE".to_string())))]
#[case("1234567890ABCDEF0", Err(Error::InvalidOpenPGPKeyId("1234567890ABCDEF0".to_string())))]
#[case("invalid", Err(Error::InvalidOpenPGPKeyId("invalid".to_string())))]
fn test_parse_invalid_openpgp_key_id(
#[case] input: &str,
#[case] expected: Result<OpenPGPKeyId, Error>,
) {
let result = input.parse::<OpenPGPKeyId>();
assert_eq!(result, expected);
}
#[rstest]
#[case("d2hhdCBhcmUgeW91IGxvb2tpbmcgZm9yPyA7LTsK")]
fn test_parse_openpgp_signature(#[case] input: &str) -> Result<(), Error> {
input.parse::<Base64OpenPGPSignature>()?;
Ok(())
}
#[rstest]
#[case(
"d2hhdCBhcmUge=W91IGxvb2tpbmcgZm9yPyA7LTsK",
Err(Error::InvalidBase64Encoding { expected_item: t!("error-invalid-base64-encoding-pgp-signature") })
)]
#[case("!@#$%^&*", Err(Error::InvalidBase64Encoding { expected_item: t!("error-invalid-base64-encoding-pgp-signature") }))]
#[case(
"iHUEABYKh9mi7GCIlMAP9ws/jU4WEbgE=",
Err(Error::InvalidBase64Encoding { expected_item: t!("error-invalid-base64-encoding-pgp-signature") })
)]
fn test_parse_invalid_openpgp_signature(
#[case] input: &str,
#[case] expected: Result<Base64OpenPGPSignature, Error>,
) {
let result = input.parse::<Base64OpenPGPSignature>();
assert_eq!(result, expected);
}
#[rstest]
#[case(
"Foobar McFooface (The Third) <foobar@mcfooface.org>",
Packager{
name: "Foobar McFooface (The Third)".to_string(),
email: EmailAddress::from_str("foobar@mcfooface.org").unwrap()
}
)]
#[case(
"Foobar McFooface <foobar@mcfooface.org>",
Packager{
name: "Foobar McFooface".to_string(),
email: EmailAddress::from_str("foobar@mcfooface.org").unwrap()
}
)]
fn valid_packager(#[case] from_str: &str, #[case] packager: Packager) {
assert_eq!(Packager::from_str(from_str), Ok(packager));
}
#[rstest]
#[case::no_name("<foobar@mcfooface.org>", "invalid packager name")]
#[case::no_name_and_address_not_wrapped(
"foobar@mcfooface.org",
"invalid or missing opening delimiter '<' for email address"
)]
#[case::no_wrapped_address(
"Foobar McFooface",
"invalid or missing opening delimiter '<' for email address"
)]
#[case::two_wrapped_addresses(
"Foobar McFooface <foobar@mcfooface.org> <foobar@mcfoofacemcfooface.org>",
"expected end of packager string"
)]
#[case::address_without_local_part("Foobar McFooface <@mcfooface.org>", "Local part is empty")]
fn invalid_packager(#[case] packager: &str, #[case] expected_error: &str) -> TestResult {
let Err(err) = Packager::from_str(packager) else {
panic!("Expected packager string to be invalid: {packager}");
};
let error = err.to_string();
assert!(
error.contains(expected_error),
"Expected error:\n{error}\n\nto contain string:\n{expected_error}"
);
Ok(())
}
#[rstest]
#[case(
Packager::from_str("Foobar McFooface <foobar@mcfooface.org>").unwrap(),
"Foobar McFooface <foobar@mcfooface.org>"
)]
fn packager_format_string(#[case] packager: Packager, #[case] packager_str: &str) {
assert_eq!(packager_str, format!("{packager}"));
}
#[rstest]
#[case(Packager::from_str("Foobar McFooface <foobar@mcfooface.org>").unwrap(), "Foobar McFooface")]
fn packager_name(#[case] packager: Packager, #[case] name: &str) {
assert_eq!(name, packager.name());
}
#[rstest]
#[case(
Packager::from_str("Foobar McFooface <foobar@mcfooface.org>").unwrap(),
&EmailAddress::from_str("foobar@mcfooface.org").unwrap(),
)]
fn packager_email(#[case] packager: Packager, #[case] email: &EmailAddress) {
assert_eq!(email, packager.email());
}
}