Skip to main content

aws_types/
sdk_ua_metadata.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Customer-facing types for self-identifying additional framework metadata in the user agent.
7
8use aws_smithy_types::config_bag::{Storable, StoreAppend};
9use std::borrow::Cow;
10use std::error::Error;
11use std::fmt;
12
13/// Metadata about a software framework or third-party library that is being used with the SDK.
14///
15/// This is rendered into the user agent string (as `lib/{name}/{version}`) so that third-party
16/// libraries built on top of the AWS SDK can self-identify in requests they make.
17///
18/// The name and version may only have alphanumeric characters and any of these characters:
19/// ```text
20/// !#$%&'*+-.^_`|~
21/// ```
22/// Spaces are not allowed.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct FrameworkMetadata {
25    name: Cow<'static, str>,
26    version: Option<Cow<'static, str>>,
27}
28
29impl Storable for FrameworkMetadata {
30    type Storer = StoreAppend<Self>;
31}
32
33impl FrameworkMetadata {
34    /// Creates new `FrameworkMetadata`.
35    ///
36    /// This will return an [`InvalidFrameworkMetadata`] error if the given name or version doesn't
37    /// meet the character requirements. See [`FrameworkMetadata`] for details on these requirements.
38    pub fn new(
39        name: impl Into<Cow<'static, str>>,
40        version: Option<impl Into<Cow<'static, str>>>,
41    ) -> Result<Self, InvalidFrameworkMetadata> {
42        let name = name.into();
43        let version = version.map(Into::into);
44
45        if name.is_empty() {
46            return Err(InvalidFrameworkMetadata);
47        }
48        // Reject (do not sanitize) any character outside the permitted charset. This mirrors
49        // `AppName::new` and prevents header injection via untrusted framework metadata.
50        fn valid_character(c: char) -> bool {
51            match c {
52                _ if c.is_ascii_alphanumeric() => true,
53                '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`'
54                | '|' | '~' => true,
55                _ => false,
56            }
57        }
58        if !name.chars().all(valid_character) {
59            return Err(InvalidFrameworkMetadata);
60        }
61        if let Some(version) = &version {
62            if version.is_empty() {
63                return Err(InvalidFrameworkMetadata);
64            }
65            if !version.chars().all(valid_character) {
66                return Err(InvalidFrameworkMetadata);
67            }
68        }
69        Ok(Self { name, version })
70    }
71
72    /// Returns the framework name.
73    pub fn name(&self) -> &str {
74        &self.name
75    }
76
77    /// Returns the framework version, if set.
78    pub fn version(&self) -> Option<&str> {
79        self.version.as_deref()
80    }
81}
82
83impl fmt::Display for FrameworkMetadata {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        // framework-metadata = "lib/" name ["/" version]
86        match &self.version {
87            Some(version) => write!(f, "lib/{}/{}", self.name, version),
88            None => write!(f, "lib/{}", self.name),
89        }
90    }
91}
92
93/// Error for when framework metadata doesn't meet character requirements.
94///
95/// See [`FrameworkMetadata`] for details on these requirements.
96#[derive(Debug)]
97#[non_exhaustive]
98pub struct InvalidFrameworkMetadata;
99
100impl Error for InvalidFrameworkMetadata {}
101
102impl fmt::Display for InvalidFrameworkMetadata {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        write!(
105            f,
106            "The framework metadata name and version can only have alphanumeric characters, or \
107             any of '!' |  '#' |  '$' |  '%' |  '&' |  '\\'' |  '*' |  '+' |  '-' | \
108             '.' |  '^' |  '_' |  '`' |  '|' |  '~'"
109        )
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::FrameworkMetadata;
116
117    #[test]
118    fn valid_name_and_version() {
119        let md = FrameworkMetadata::new("some-framework", Some("1.0")).expect("valid");
120        assert_eq!("some-framework", md.name());
121        assert_eq!(Some("1.0"), md.version());
122    }
123
124    #[test]
125    fn valid_name_no_version() {
126        let md =
127            FrameworkMetadata::new("asdf1234ASDF!#$%&'*+-.^_`|~", None::<&str>).expect("valid");
128        assert_eq!(None, md.version());
129    }
130
131    #[test]
132    fn invalid_charset_name() {
133        assert!(FrameworkMetadata::new("foo bar", None::<&str>).is_err());
134        assert!(FrameworkMetadata::new("🚀", None::<&str>).is_err());
135    }
136
137    #[test]
138    fn invalid_charset_version() {
139        assert!(FrameworkMetadata::new("framework", Some("1 0")).is_err());
140        assert!(FrameworkMetadata::new("framework", Some("🚀")).is_err());
141    }
142
143    #[test]
144    fn empty_version_rejected() {
145        assert!(FrameworkMetadata::new("framework", Some("")).is_err());
146    }
147
148    #[test]
149    fn empty_name() {
150        assert!(FrameworkMetadata::new("", None::<&str>).is_err());
151    }
152
153    #[test]
154    fn rejects_header_injection_characters() {
155        // None of these may ever reach the user-agent header.
156        for bad in [
157            "a\r\nb",   // CRLF (header injection)
158            "a\nb",     // LF
159            "a\rb",     // CR
160            "a\tb",     // tab
161            "a b",      // space (separates UA tokens)
162            "a/b",      // slash (would forge an extra lib/.../... segment)
163            "a\u{0}b",  // NUL
164            "a\u{7f}b", // DEL
165        ] {
166            assert!(
167                FrameworkMetadata::new(bad, None::<&str>).is_err(),
168                "name {bad:?} should be rejected"
169            );
170            assert!(
171                FrameworkMetadata::new("framework", Some(bad)).is_err(),
172                "version {bad:?} should be rejected"
173            );
174        }
175    }
176
177    #[test]
178    fn accepts_every_allowed_symbol_in_version() {
179        let md =
180            FrameworkMetadata::new("framework", Some("1.0-rc.1+build_2~3")).expect("valid version");
181        assert_eq!(Some("1.0-rc.1+build_2~3"), md.version());
182    }
183
184    #[test]
185    fn clone_and_equality() {
186        let a = FrameworkMetadata::new("framework", Some("1.0")).unwrap();
187        let b = a.clone();
188        assert_eq!(a, b);
189        let c = FrameworkMetadata::new("framework", Some("2.0")).unwrap();
190        assert_ne!(a, c);
191        let d = FrameworkMetadata::new("framework", None::<&str>).unwrap();
192        assert_ne!(a, d);
193    }
194}