use nom::{
IResult,
bytes::complete::{tag, take_while_m_n},
character::is_hex_digit,
combinator::map_res,
multi::separated_list1,
};
use std::{borrow::Cow, fmt::Debug, str::from_utf8};
use url::Url;
#[derive(Debug, Clone)]
pub struct UnverifiedAssetLink<'a> {
package_name: Cow<'a, str>,
sha256_cert_fingerprint: Vec<u8>,
host: Cow<'a, str>,
asset_link_url: Url,
}
impl<'a> UnverifiedAssetLink<'a> {
pub fn new(
package_name: impl Into<Cow<'a, str>>,
sha256_cert_fingerprint: &str,
host: impl Into<Cow<'a, str>>,
asset_link_url: Url,
) -> Result<Self, ValidationError> {
if !valid_asset_link_url(&asset_link_url) {
return Err(ValidationError::InvalidAssetLinkUrl);
}
let host = host.into();
valid_fingerprint(sha256_cert_fingerprint).map(|sha256_cert_fingerprint| Self {
package_name: package_name.into(),
sha256_cert_fingerprint,
host,
asset_link_url,
})
}
pub fn sha256_cert_fingerprint(&self) -> &[u8] {
self.sha256_cert_fingerprint.as_slice()
}
pub fn package_name(&self) -> &str {
&self.package_name
}
pub fn host(&self) -> &str {
&self.host
}
pub fn asset_link_url(&self) -> Url {
self.asset_link_url.clone()
}
}
#[derive(Debug)]
pub enum ValidationError {
ParseFailed(String),
InvalidLength,
InvalidAssetLinkUrl,
}
impl<T> From<nom::Err<nom::error::Error<T>>> for ValidationError {
fn from(value: nom::Err<nom::error::Error<T>>) -> Self {
let code_msg = value.map(|err| format!("{:?}", err.code));
let message = match code_msg {
nom::Err::Incomplete(_) => "Parsing incomplete".to_owned(),
nom::Err::Error(msg) => format!("Parsing error: {msg}"),
nom::Err::Failure(msg) => format!("Parsing failure: {msg}"),
};
ValidationError::ParseFailed(message)
}
}
pub fn valid_fingerprint(fingerprint: &str) -> Result<Vec<u8>, ValidationError> {
#[derive(Debug)]
enum HexError {
Utf8,
ParseInt,
}
fn parse_fingerprint(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
separated_list1(
tag(":"),
map_res(
take_while_m_n(2, 2, |c| is_hex_digit(c) && !c.is_ascii_lowercase()),
|hex| {
u8::from_str_radix(from_utf8(hex).map_err(|_| HexError::Utf8)?, 16)
.map_err(|_| HexError::ParseInt)
},
),
)(input)
}
let (left, parsed) = parse_fingerprint(fingerprint.as_bytes())?;
(left.is_empty() && parsed.len() == 32)
.then_some(parsed)
.ok_or(ValidationError::InvalidLength)
}
fn valid_asset_link_url(url: &Url) -> bool {
url.scheme() == "https" && url.path() == "/.well-known/assetlinks.json"
}
#[cfg(test)]
mod test;