rama_http_headers/common/content_security_policy/
source_list.rs1use 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#[derive(Debug, Clone, PartialEq, Eq, Default)]
18pub struct SourceList {
19 sources: Vec<SourceExpression>,
20}
21
22impl SourceList {
23 #[must_use]
26 pub const fn empty() -> Self {
27 Self {
28 sources: Vec::new(),
29 }
30 }
31
32 #[must_use]
35 pub fn none() -> Self {
36 Self {
37 sources: vec![SourceExpression::None],
38 }
39 }
40
41 #[must_use]
44 pub fn self_origin() -> Self {
45 Self {
46 sources: vec![SourceExpression::SelfOrigin],
47 }
48 }
49
50 pub fn as_slice(&self) -> &[SourceExpression] {
52 &self.sources
53 }
54
55 pub fn iter(&self) -> std::slice::Iter<'_, SourceExpression> {
57 self.sources.iter()
58 }
59
60 #[must_use]
62 pub fn with(mut self, expr: SourceExpression) -> Self {
63 self.sources.push(expr);
64 self
65 }
66
67 pub fn add(&mut self, expr: SourceExpression) -> &mut Self {
69 self.sources.push(expr);
70 self
71 }
72
73 generate_set_and_with! {
75 pub fn self_keyword(mut self) -> Self {
77 self.sources.push(SourceExpression::SelfOrigin);
78 self
79 }
80 }
81 generate_set_and_with! {
82 pub fn unsafe_inline(mut self) -> Self {
84 self.sources.push(SourceExpression::UnsafeInline);
85 self
86 }
87 }
88 generate_set_and_with! {
89 pub fn unsafe_eval(mut self) -> Self {
91 self.sources.push(SourceExpression::UnsafeEval);
92 self
93 }
94 }
95 generate_set_and_with! {
96 pub fn strict_dynamic(mut self) -> Self {
98 self.sources.push(SourceExpression::StrictDynamic);
99 self
100 }
101 }
102 generate_set_and_with! {
103 pub fn wasm_unsafe_eval(mut self) -> Self {
105 self.sources.push(SourceExpression::WasmUnsafeEval);
106 self
107 }
108 }
109 generate_set_and_with! {
110 pub fn report_sample(mut self) -> Self {
112 self.sources.push(SourceExpression::ReportSample);
113 self
114 }
115 }
116 generate_set_and_with! {
117 pub fn wildcard(mut self) -> Self {
119 self.sources.push(SourceExpression::Wildcard);
120 self
121 }
122 }
123
124 generate_set_and_with! {
126 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 pub fn blob(mut self) -> Self {
136 self.sources
137 .push(SourceExpression::Scheme(Protocol::from_static("blob")));
138 self
139 }
140 }
141
142 generate_set_and_with! {
144 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 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 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 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 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}