use aws_smithy_types::config_bag::{Storable, StoreAppend};
use std::borrow::Cow;
use std::error::Error;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FrameworkMetadata {
name: Cow<'static, str>,
version: Option<Cow<'static, str>>,
}
impl Storable for FrameworkMetadata {
type Storer = StoreAppend<Self>;
}
impl FrameworkMetadata {
pub fn new(
name: impl Into<Cow<'static, str>>,
version: Option<impl Into<Cow<'static, str>>>,
) -> Result<Self, InvalidFrameworkMetadata> {
let name = name.into();
let version = version.map(Into::into);
if name.is_empty() {
return Err(InvalidFrameworkMetadata);
}
fn valid_character(c: char) -> bool {
match c {
_ if c.is_ascii_alphanumeric() => true,
'!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`'
| '|' | '~' => true,
_ => false,
}
}
if !name.chars().all(valid_character) {
return Err(InvalidFrameworkMetadata);
}
if let Some(version) = &version {
if version.is_empty() {
return Err(InvalidFrameworkMetadata);
}
if !version.chars().all(valid_character) {
return Err(InvalidFrameworkMetadata);
}
}
Ok(Self { name, version })
}
pub fn name(&self) -> &str {
&self.name
}
pub fn version(&self) -> Option<&str> {
self.version.as_deref()
}
}
impl fmt::Display for FrameworkMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.version {
Some(version) => write!(f, "lib/{}/{}", self.name, version),
None => write!(f, "lib/{}", self.name),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct InvalidFrameworkMetadata;
impl Error for InvalidFrameworkMetadata {}
impl fmt::Display for InvalidFrameworkMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"The framework metadata name and version can only have alphanumeric characters, or \
any of '!' | '#' | '$' | '%' | '&' | '\\'' | '*' | '+' | '-' | \
'.' | '^' | '_' | '`' | '|' | '~'"
)
}
}
#[cfg(test)]
mod tests {
use super::FrameworkMetadata;
#[test]
fn valid_name_and_version() {
let md = FrameworkMetadata::new("some-framework", Some("1.0")).expect("valid");
assert_eq!("some-framework", md.name());
assert_eq!(Some("1.0"), md.version());
}
#[test]
fn valid_name_no_version() {
let md =
FrameworkMetadata::new("asdf1234ASDF!#$%&'*+-.^_`|~", None::<&str>).expect("valid");
assert_eq!(None, md.version());
}
#[test]
fn invalid_charset_name() {
assert!(FrameworkMetadata::new("foo bar", None::<&str>).is_err());
assert!(FrameworkMetadata::new("🚀", None::<&str>).is_err());
}
#[test]
fn invalid_charset_version() {
assert!(FrameworkMetadata::new("framework", Some("1 0")).is_err());
assert!(FrameworkMetadata::new("framework", Some("🚀")).is_err());
}
#[test]
fn empty_version_rejected() {
assert!(FrameworkMetadata::new("framework", Some("")).is_err());
}
#[test]
fn empty_name() {
assert!(FrameworkMetadata::new("", None::<&str>).is_err());
}
#[test]
fn rejects_header_injection_characters() {
for bad in [
"a\r\nb", "a\nb", "a\rb", "a\tb", "a b", "a/b", "a\u{0}b", "a\u{7f}b", ] {
assert!(
FrameworkMetadata::new(bad, None::<&str>).is_err(),
"name {bad:?} should be rejected"
);
assert!(
FrameworkMetadata::new("framework", Some(bad)).is_err(),
"version {bad:?} should be rejected"
);
}
}
#[test]
fn accepts_every_allowed_symbol_in_version() {
let md =
FrameworkMetadata::new("framework", Some("1.0-rc.1+build_2~3")).expect("valid version");
assert_eq!(Some("1.0-rc.1+build_2~3"), md.version());
}
#[test]
fn clone_and_equality() {
let a = FrameworkMetadata::new("framework", Some("1.0")).unwrap();
let b = a.clone();
assert_eq!(a, b);
let c = FrameworkMetadata::new("framework", Some("2.0")).unwrap();
assert_ne!(a, c);
let d = FrameworkMetadata::new("framework", None::<&str>).unwrap();
assert_ne!(a, d);
}
}