Skip to main content

rama_http_headers/common/content_security_policy/
source_list.rs

1use std::borrow::Cow;
2use std::fmt::{self, Write as _};
3
4use rama_net::Protocol;
5use rama_net::address::Domain;
6use rama_utils::macros::generate_set_and_with;
7
8use super::host_source::HostSource;
9use super::source_expression::{HashAlgorithm, SourceExpression};
10
11/// Ordered list of [`SourceExpression`]s — the value of one CSP
12/// directive.
13///
14/// Builder methods come in two flavours: `with_*` consumes `self` and
15/// returns `Self` for chaining; `set_*` / [`add`](SourceList::add)
16/// mutate in place.
17#[derive(Debug, Clone, PartialEq, Eq, Default)]
18pub struct SourceList {
19    sources: Vec<SourceExpression>,
20}
21
22impl SourceList {
23    /// Empty list — for directives like `upgrade-insecure-requests`
24    /// that carry no source value.
25    #[must_use]
26    pub const fn empty() -> Self {
27        Self {
28            sources: Vec::new(),
29        }
30    }
31
32    /// `'none'`-only list. Per spec must not be combined with any
33    /// other source.
34    #[must_use]
35    pub fn none() -> Self {
36        Self {
37            sources: vec![SourceExpression::None],
38        }
39    }
40
41    /// `'self'`-only list. Equivalent to
42    /// `SourceList::empty().with_self_keyword()`.
43    #[must_use]
44    pub fn self_origin() -> Self {
45        Self {
46            sources: vec![SourceExpression::SelfOrigin],
47        }
48    }
49
50    /// Borrow the underlying sources in declared (emit) order.
51    pub fn as_slice(&self) -> &[SourceExpression] {
52        &self.sources
53    }
54
55    /// Iterate sources in declared (emit) order.
56    pub fn iter(&self) -> std::slice::Iter<'_, SourceExpression> {
57        self.sources.iter()
58    }
59
60    /// Push any source expression and return `Self`.
61    #[must_use]
62    pub fn with(mut self, expr: SourceExpression) -> Self {
63        self.sources.push(expr);
64        self
65    }
66
67    /// Push any source expression in place.
68    pub fn add(&mut self, expr: SourceExpression) -> &mut Self {
69        self.sources.push(expr);
70        self
71    }
72
73    // ---- keyword-only convenience hatches ------------------------------
74    generate_set_and_with! {
75        /// Append the `'self'` keyword.
76        pub fn self_keyword(mut self) -> Self {
77            self.sources.push(SourceExpression::SelfOrigin);
78            self
79        }
80    }
81    generate_set_and_with! {
82        /// Append the `'unsafe-inline'` keyword.
83        pub fn unsafe_inline(mut self) -> Self {
84            self.sources.push(SourceExpression::UnsafeInline);
85            self
86        }
87    }
88    generate_set_and_with! {
89        /// Append the `'unsafe-eval'` keyword.
90        pub fn unsafe_eval(mut self) -> Self {
91            self.sources.push(SourceExpression::UnsafeEval);
92            self
93        }
94    }
95    generate_set_and_with! {
96        /// Append the `'strict-dynamic'` keyword.
97        pub fn strict_dynamic(mut self) -> Self {
98            self.sources.push(SourceExpression::StrictDynamic);
99            self
100        }
101    }
102    generate_set_and_with! {
103        /// Append the `'wasm-unsafe-eval'` keyword.
104        pub fn wasm_unsafe_eval(mut self) -> Self {
105            self.sources.push(SourceExpression::WasmUnsafeEval);
106            self
107        }
108    }
109    generate_set_and_with! {
110        /// Append the `'report-sample'` keyword.
111        pub fn report_sample(mut self) -> Self {
112            self.sources.push(SourceExpression::ReportSample);
113            self
114        }
115    }
116    generate_set_and_with! {
117        /// Append the `*` wildcard.
118        pub fn wildcard(mut self) -> Self {
119            self.sources.push(SourceExpression::Wildcard);
120            self
121        }
122    }
123
124    // ---- common scheme shortcuts ---------------------------------------
125    generate_set_and_with! {
126        /// Append the `data:` scheme.
127        pub fn data(mut self) -> Self {
128            self.sources
129                .push(SourceExpression::Scheme(Protocol::from_static("data")));
130            self
131        }
132    }
133    generate_set_and_with! {
134        /// Append the `blob:` scheme.
135        pub fn blob(mut self) -> Self {
136            self.sources
137                .push(SourceExpression::Scheme(Protocol::from_static("blob")));
138            self
139        }
140    }
141
142    // ---- typed parametrised hatches ------------------------------------
143    generate_set_and_with! {
144        /// Append a [`Protocol`] as a scheme source (`<scheme>:`).
145        pub fn scheme(mut self, scheme: Protocol) -> Self {
146            self.sources.push(SourceExpression::Scheme(scheme));
147            self
148        }
149    }
150    generate_set_and_with! {
151        /// Append a [`HostSource`] (or anything convertible into one —
152        /// `Domain`, a `&str` via [`HostSource::try_parse`] etc.).
153        pub fn host(mut self, host: impl Into<HostSource>) -> Self {
154            self.sources.push(SourceExpression::Host(host.into()));
155            self
156        }
157    }
158    generate_set_and_with! {
159        /// Append a bare-domain host source (no scheme, port, or path).
160        pub fn domain(mut self, domain: Domain) -> Self {
161            self.sources
162                .push(SourceExpression::Host(HostSource::new(domain)));
163            self
164        }
165    }
166    generate_set_and_with! {
167        /// Append a `'nonce-<base64>'` source.
168        pub fn nonce(mut self, nonce: impl Into<Cow<'static, str>>) -> Self {
169            self.sources.push(SourceExpression::Nonce(nonce.into()));
170            self
171        }
172    }
173    generate_set_and_with! {
174        /// Append a `'<algo>-<base64>'` source.
175        pub fn hash(mut self, algorithm: HashAlgorithm, value: impl Into<Cow<'static, str>>) -> Self {
176            self.sources.push(SourceExpression::Hash {
177                algorithm,
178                value: value.into(),
179            });
180            self
181        }
182    }
183}
184
185impl fmt::Display for SourceList {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        for (i, src) in self.sources.iter().enumerate() {
188            if i > 0 {
189                f.write_char(' ')?;
190            }
191            src.fmt(f)?;
192        }
193        Ok(())
194    }
195}
196
197impl<T: Into<SourceExpression>> FromIterator<T> for SourceList {
198    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
199        Self {
200            sources: iter.into_iter().map(Into::into).collect(),
201        }
202    }
203}
204
205impl From<SourceExpression> for SourceList {
206    fn from(expr: SourceExpression) -> Self {
207        Self {
208            sources: vec![expr],
209        }
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn renders_space_separated() {
219        let list = SourceList::self_origin()
220            .with_data()
221            .with_host(Domain::from_static("example.com"));
222        assert_eq!(list.to_string(), "'self' data: example.com");
223    }
224
225    #[test]
226    fn none_renders_just_none() {
227        assert_eq!(SourceList::none().to_string(), "'none'");
228    }
229
230    #[test]
231    fn empty_renders_empty() {
232        assert_eq!(SourceList::empty().to_string(), "");
233    }
234
235    #[test]
236    fn set_and_add_mutate_in_place() {
237        let mut list = SourceList::empty();
238        list.set_self_keyword().set_unsafe_inline();
239        assert_eq!(list.to_string(), "'self' 'unsafe-inline'");
240        list.add(SourceExpression::Wildcard);
241        assert_eq!(list.to_string(), "'self' 'unsafe-inline' *");
242    }
243
244    #[test]
245    fn builds_from_iter() {
246        let list: SourceList = [SourceExpression::SelfOrigin, SourceExpression::Wildcard]
247            .into_iter()
248            .collect();
249        assert_eq!(list.to_string(), "'self' *");
250    }
251
252    #[test]
253    fn host_helper_accepts_string_and_domain_and_host_source() {
254        let from_str = SourceList::empty()
255            .with_host(HostSource::try_parse("https://raw.githubusercontent.com").unwrap());
256        assert_eq!(from_str.to_string(), "https://raw.githubusercontent.com");
257
258        let from_domain = SourceList::empty().with_domain(Domain::from_static("example.com"));
259        assert_eq!(from_domain.to_string(), "example.com");
260
261        let from_built = SourceList::empty().with_host(
262            HostSource::new(Domain::from_static("example.com"))
263                .with_scheme(Protocol::HTTPS)
264                .with_port(8443),
265        );
266        assert_eq!(from_built.to_string(), "https://example.com:8443");
267    }
268
269    #[test]
270    fn scheme_helper_accepts_typed_protocol() {
271        let list = SourceList::empty().with_scheme(Protocol::HTTPS);
272        assert_eq!(list.to_string(), "https:");
273    }
274
275    #[test]
276    fn hash_helper_emits_canonical_form() {
277        let list = SourceList::empty().with_hash(HashAlgorithm::Sha256, "abc");
278        assert_eq!(list.to_string(), "'sha256-abc'");
279    }
280}