csp/
lib.rs

1//! This crate is a helper to quickly construct a CSP and then turn it into a
2//! String.
3//!
4//! This library can help you when you don't want to remember some weird
5//! formatting rules of CSP, and want to avoid typos. And it certainly can be
6//! handy if you need to re-use things, for example a list of sources (just
7//! `.clone()` them everywhere and you're good to go!).
8//!
9//! WARNING: this library does not care if you create invalid CSP rules, and
10//! happily allows them and turns them into Strings. But it does force you to
11//! use a typed structure, so it'll be harder to mess up than when manually
12//! writing CSP. Another thing that this crate does not do: It does not do any
13//! base64 or percent encoding or anything like that.
14//!
15//! # Example usage
16//! ```rust
17//! use csp::{CSP, Directive, Sources, Source};
18//!
19//! let csp = CSP::new()
20//!   .push(Directive::ImgSrc(
21//!     Sources::new_with(Source::Self_)
22//!       .push(Source::Host("https://*.example.org"))
23//!       .push(Source::Host("https://shields.io")),
24//!   ))
25//!   .push(Directive::ConnectSrc(
26//!     Sources::new()
27//!       .push(Source::Host("http://crates.io"))
28//!       .push(Source::Scheme("https"))
29//!       .push(Source::Self_),
30//!   ))
31//!   .push(Directive::StyleSrc(
32//!     Sources::new_with(Source::Self_).push(Source::UnsafeInline),
33//!   ))
34//!   .push(Directive::ObjectSrc(Sources::new()));
35//!
36//! let csp_header = "Content-Security-Policy: ".to_owned() + &csp.to_string();
37//! ```
38//! # Copyright notice for this crate's docs:
39//! Most of the comments for various CSP things are from [MDN](https://developer.mozilla.org/en-US/docs/MDN/About), so they licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/)
40//! So attribution of most of the docs goes to [Mozilla Contributors](https://developer.mozilla.org/en-US/docs/MDN/Writing_guidelines/contributors.txt).
41//!
42//! Please go to MDN to read up to date docs, as these ones might not be up to
43//! date.
44
45#![deny(clippy::all)]
46#![deny(unsafe_code)]
47#![deny(clippy::cargo)]
48#![warn(missing_docs)]
49#![deny(rustdoc::invalid_html_tags)]
50#![warn(clippy::pedantic)]
51#![warn(clippy::nursery)]
52
53use std::fmt;
54
55#[derive(Debug, Default, Clone)]
56/// The starting point for building a Content Security Policy.
57///
58/// You'll add [`Directive`] into this struct, and later on call `.to_string()`
59/// on it to get it as a header compatible string. Doesn't include
60/// content-security-policy: part in it though.
61///
62/// [`Directive`]: Directive
63pub struct CSP<'a>(Vec<Directive<'a>>);
64
65#[derive(Debug, Default, Clone)]
66/// A struct to give source(s) to a [`Directive`] which might require it.
67///
68/// # Example usage
69/// ```rust
70/// use csp::{Sources, Source};
71///
72/// let sources = Sources::new().push(Source::Self_).push(Source::Scheme("data"));
73///
74/// assert_eq!(sources.to_string(), "'self' data:");
75/// ```
76///
77/// [`Directive`]: Directive
78pub struct Sources<'a>(Vec<Source<'a>>);
79
80#[derive(Debug, Default, Clone, PartialEq, Eq)]
81/// Used for `PluginTypes` [`Directive`].
82///
83/// # Example usage
84/// ```rust
85/// let flash = csp::Plugins::new().push(("application", "x-shockwave-flash"));
86/// ```
87///  to get `application/x-shockwave-flash`
88///
89/// [`Directive`]: Directive
90pub struct Plugins<'a>(Vec<(&'a str, &'a str)>);
91
92#[derive(Debug, Default, Clone)]
93/// Used for `ReportUri` [`Directive`].
94///
95/// # Example usage
96/// ```rust
97/// let report_uris = csp::ReportUris::new().push("https://example.org/report");
98/// ```
99///
100/// [`Directive`]: Directive
101pub struct ReportUris<'a>(Vec<&'a str>);
102
103#[derive(Debug, Default, Clone)]
104/// Used for `Sandbox` [`Directive`].
105///
106/// [`Directive`]: Directive
107pub struct SandboxAllowedList(Vec<SandboxAllow>);
108
109#[derive(Debug, Clone)]
110/// Used for `RequireSriFor` [`Directive`].
111///
112/// [`Directive`]: Directive
113pub enum SriFor {
114  /// Requires SRI for scripts.
115  Script,
116  /// Requires SRI for style sheets.
117  Style,
118  /// Requires SRI for both, scripts and style sheets.
119  ScriptStyle,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123/// The source that a bunch of directives can have multiple of.
124///
125/// If nothing gets added, becomes 'none'.
126pub enum Source<'a> {
127  /// Internet hosts by name or IP address, as well as an optional URL scheme
128  /// and/or port number.
129  ///
130  /// The site's address may include an optional leading wildcard (the
131  /// asterisk character, '*'), and you may use a wildcard (again, '*') as the
132  /// port number, indicating that all legal ports are valid for the source.
133  /// Examples:
134  /// - `http://*.example.com`: Matches all attempts to load from any subdomain
135  ///   of example.com using the `http:` URL scheme.
136  /// - `mail.example.com:443`: Matches all attempts to access port 443 on
137  ///   mail.example.com.
138  /// - `https://store.example.com`: Matches all attempts to access
139  ///   store.example.com using https:.
140  Host(&'a str),
141  /// A schema such as 'http' or 'https'.
142  ///
143  ///  The colon is automatically added to the end. You can also specify data
144  /// schemas (not recommended).
145  /// - `data` Allows data: URIs to be used as a content source. This is
146  ///   insecure; an attacker can also inject arbitrary data: URIs. Use this
147  ///   sparingly and definitely not for scripts.
148  /// - `mediastream` Allows `mediastream:` URIs to be used as a content source.
149  /// - `blob` Allows `blob:` URIs to be used as a content source.
150  /// - `filesystem` Allows `filesystem:` URIs to be used as a content source.
151  Scheme(&'a str),
152  /// Refers to the origin from which the protected document is being served,
153  /// including the same URL scheme and port number.
154  ///
155  /// Some browsers specifically exclude `blob` and `filesystem` from source
156  /// directives. Sites needing to allow these content types can specify them
157  /// using the Data attribute.
158  Self_,
159  /// Allows the use of `eval()` and similar methods for creating code from
160  /// strings.
161  UnsafeEval,
162  /// Allows the compilation and instantiation of WebAssembly.
163  WasmUnsafeEval,
164  /// Allows to enable specific inline event handlers. If you only need to
165  /// allow inline event handlers and not inline `<script>` elements or
166  /// `javascript:` URLs, this is a safer method compared to using the
167  /// `unsafe-inline` expression.
168  UnsafeHashes,
169  /// Allows the use of inline resources, such as inline `<script>` elements,
170  /// javascript: URLs, inline event handlers, and inline <\style> elements.
171  UnsafeInline,
172  /// A whitelist for specific inline scripts using a cryptographic nonce
173  /// (number used once). The server must generate a unique nonce value each
174  /// time it transmits a policy. It is critical to provide an unguessable
175  /// nonce, as bypassing a resource’s policy is otherwise trivial. See unsafe
176  /// inline script for an example. Specifying nonce makes a modern browser
177  /// ignore `'unsafe-inline'` which could still be set for older browsers
178  /// without nonce support.
179  Nonce(&'a str),
180  /// A sha256, sha384 or sha512 hash of scripts or styles. The use of this
181  /// source consists of two portions separated by a dash: the encryption
182  /// algorithm used to create the hash and the base64-encoded hash of the
183  /// script or style. When generating the hash, don't include the `<script>`
184  /// or `<style>` tags and note that capitalization and whitespace matter,
185  /// including leading or trailing whitespace. See unsafe inline script for
186  /// an example. In CSP 2.0 this applied only to inline scripts. CSP 3.0
187  /// allows it in the case of `script-src` for external scripts.
188  Hash((&'a str, &'a str)),
189  /// The `strict-dynamic` source expression specifies that the trust
190  /// explicitly given to a script present in the markup, by accompanying it
191  /// with a nonce or a hash, shall be propagated to all the scripts loaded by
192  /// that root script. At the same time, any whitelist or source expressions
193  /// such as `'self'` or `'unsafe-inline'` will be ignored. See script-src
194  /// for an example.
195  StrictDynamic,
196  /// Requires a sample of the violating code to be included in the violation
197  /// report.
198  ReportSample,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
202/// Optionally used for the `Sandbox` directive. Not using it but using the
203/// sandbox directive disallows everything that you could allow with the
204/// optional values.
205pub enum SandboxAllow {
206  /// Allows for downloads to occur without a gesture from the user.
207  DownloadsWithoutUserActivation,
208  /// Allows the embedded browsing context to submit forms. If this keyword is
209  /// not used, this operation is not allowed.
210  Forms,
211  /// Allows the embedded browsing context to open modal windows.
212  Modals,
213  /// Allows the embedded browsing context to disable the ability to lock the
214  /// screen orientation.
215  OrientationLock,
216  /// Allows the embedded browsing context to use the Pointer Lock API.
217  PointerLock,
218  /// Allows popups (like from window.open, target="_blank", showModalDialog).
219  /// If this keyword is not used, that functionality will silently fail.
220  Popups,
221  /// Allows a sandboxed document to open new windows without forcing the
222  /// sandboxing flags upon them. This will allow, for example, a third-party
223  /// advertisement to be safely sandboxed without forcing the same
224  /// restrictions upon a landing page.
225  PopupsToEscapeSandbox,
226  /// Allows embedders to have control over whether an iframe can start a
227  /// presentation session.
228  Presentation,
229  /// Allows the content to be treated as being from its normal origin. If
230  /// this keyword is not used, the embedded content is treated as being from
231  /// a unique origin.
232  SameOrigin,
233  /// Allows the embedded browsing context to run scripts (but not create
234  /// pop-up windows). If this keyword is not used, this operation is not
235  /// allowed.
236  Scripts,
237  /// Lets the resource request access to the parent's storage capabilities
238  /// with the Storage Access API.
239  StorageAccessByUserActivation,
240  /// Allows the embedded browsing context to navigate (load) content to the
241  /// top-level browsing context. If this keyword is not used, this operation
242  /// is not allowed.
243  TopNavigation,
244  /// Lets the resource navigate the top-level browsing context, but only if
245  /// initiated by a user gesture.
246  TopNavigationByUserActivation,
247}
248
249#[derive(Debug, Clone)]
250/// A CSP directive.
251pub enum Directive<'a> {
252  /// Restricts the URLs which can be used in a document's `<base>` element.
253  ///
254  /// If this value is absent, then any URI is allowed. If this directive is
255  /// absent, the user agent will use the value in the `<base>` element.
256  BaseUri(Sources<'a>),
257  /// Prevents loading any assets using HTTP when the page is loaded using
258  /// HTTPS.
259  ///
260  ///All mixed content resource requests are blocked, including both active
261  /// and passive mixed content. This also applies to `<iframe>` documents,
262  /// ensuring the entire page is mixed content free.
263  /// The upgrade-insecure-requests directive is evaluated before
264  /// block-all-mixed-content and If the former is set, the latter is
265  /// effectively a no-op. It is recommended to set one directive or the other
266  /// – not both, unless you want to force HTTPS on older browsers that do not
267  /// force it after a redirect to HTTP.
268  BlockAllMixedContent,
269  /// Defines the valid sources for web workers and nested browsing contexts
270  /// loaded using elements such as `<frame>` and `<iframe>`.
271  ///
272  /// For workers, non-compliant requests are treated as fatal network errors
273  /// by the user agent.
274  ChildSrc(Sources<'a>),
275  /// restricts the URLs which can be loaded using script interfaces. The APIs
276  /// that are restricted are:
277  ///
278  /// - `<a>` ping,
279  /// - `Fetch`,
280  /// - `XMLHttpRequest`,
281  /// - `WebSocket`,
282  /// - `EventSource`,
283  /// - `Navigator.sendBeacon()`.
284  ///
285  /// Note: connect-src 'self' does not resolve to websocket schemas in all browsers, more info: <https://github.com/w3c/webappsec-csp/issues/7>
286  ConnectSrc(Sources<'a>),
287  /// Serves as a fallback for the other CSP fetch directives.
288  ///
289  /// For each of the following directives that are absent, the user agent
290  /// will look for the default-src directive and will use this value for it:
291  /// - child-src
292  /// - connect-src
293  /// - font-src
294  /// - frame-src
295  /// - img-src
296  /// - manifest-src
297  /// - media-src
298  /// - object-src
299  /// - prefetch-src
300  /// - script-src
301  /// - script-src-elem
302  /// - script-src-attr
303  /// - style-src
304  /// - style-src-elem
305  /// - style-src-attr
306  /// - worker-src
307  DefaultSrc(Sources<'a>),
308  /// Specifies valid sources for fonts loaded using @font-face.
309  FontSrc(Sources<'a>),
310  /// Restricts the URLs which can be used as the target of a form submissions
311  /// from a given context.
312  ///
313  /// Whether form-action should block redirects after a form submission is
314  /// debated and browser implementations of this aspect are inconsistent
315  /// (e.g. Firefox 57 doesn't block the redirects whereas Chrome 63 does).
316  FormAction(Sources<'a>),
317  /// specifies valid parents that may embed a page using `<frame>`, `<iframe>`,
318  /// `<object>`, `<embed>`, or `<applet>`.
319  ///
320  /// Setting this directive to 'none' is similar to X-Frame-Options: deny
321  /// (which is also supported in older browsers).
322  FrameAncestors(Sources<'a>),
323  /// Specifies valid sources for nested browsing contexts loading using
324  /// elements such as `<frame>` and `<iframe>`.
325  FrameSrc(Sources<'a>),
326  /// Specifies valid sources of images and favicons.
327  ImgSrc(Sources<'a>),
328  /// Specifies which manifest can be applied to the resource.
329  ManifestSrc(Sources<'a>),
330  /// Specifies valid sources for loading media using the `<audio>` and
331  /// `<video>` elements.
332  MediaSrc(Sources<'a>),
333  /// restricts the URLs to which a document can initiate navigations by any
334  /// means including `<form>` (if form-action is not specified), `<a>`,
335  /// window.location, window.open, etc.
336  ///
337  /// This is an enforcement on what navigations this document initiates not
338  /// on what this document is allowed to navigate to.
339  ///
340  /// Note: If the form-action directive is present, the navigate-to directive
341  /// will not act on navigations that are form submissions.
342  NavigateTo(Sources<'a>),
343  /// specifies valid sources for the `<object>`, `<embed>`, and `<applet>`
344  /// elements.
345  ///
346  /// To set allowed types for `<object>`, `<embed>`, and `<applet>` elements,
347  /// use the `PluginTypes` [`Directive`].
348  ///
349  /// Elements controlled by object-src are perhaps coincidentally considered
350  /// legacy HTML elements and aren't receiving new standardized features
351  /// (such as the security attributes sandbox or allow for `<iframe>`).
352  /// Therefore it is recommended to restrict this fetch-directive (e.g.
353  /// explicitly set object-src 'none' if possible).
354  ObjectSrc(Sources<'a>),
355  /// Restricts the set of plugins that can be embedded into a document by
356  /// limiting the types of resources which can be loaded.
357  ///
358  /// Instantiation of an `<embed>`, `<object>` or `<applet>` element will fail
359  /// if:
360  /// - the element to load does not declare a valid MIME type,
361  /// - the declared type does not match one of specified types in the
362  ///   plugin-types directive,
363  /// - the fetched resource does not match the declared type.
364  PluginTypes(Plugins<'a>),
365  /// Specifies valid resources that may be prefetched or prerendered.
366  PrefetchSrc(Sources<'a>),
367  /// Instructs the user agent to store reporting endpoints for an origin.
368  ///
369  /// ```text
370  /// Content-Security-Policy: ...; report-to groupname
371  /// ```
372  ///
373  /// The directive has no effect in and of itself, but only gains meaning in
374  /// combination with other directives.
375  ReportTo(&'a str),
376  /// Deprecated.
377  ///
378  /// Instructs the user agent to report attempts to violate the Content
379  /// Security Policy. These violation reports consist of JSON documents sent
380  /// via an HTTP POST request to the specified URI.
381  ///
382  /// This feature is no longer recommended. Though some browsers might still
383  /// support it, it may have already been removed from the relevant web
384  /// standards, may be in the process of being dropped, or may only be kept
385  /// for compatibility purposes. Avoid using it, and update existing code if
386  /// possible.
387  ///
388  /// Though the report-to directive is intended to replace the deprecated
389  /// report-uri directive, report-to isn’t supported in most browsers yet. So
390  /// for compatibility with current browsers while also adding forward
391  /// compatibility when browsers get report-to support, you can specify both
392  /// report-uri and report-to:
393  ///
394  /// > `Content-Security-Policy: ...; report-uri <https://endpoint.com>;
395  /// > report-to groupname`
396  ///
397  /// In browsers that support report-to, the report-uri directive will be
398  /// ignored.
399  ReportUri(ReportUris<'a>),
400  /// Instructs the client to require the use of Subresource Integrity for
401  /// scripts or styles on the page.
402  RequireSriFor(SriFor),
403  /// Enables a sandbox for the requested resource similar to the `<iframe>`
404  /// sandbox attribute.
405  ///
406  /// It applies restrictions to a page's actions including preventing popups,
407  /// preventing the execution of plugins and scripts, and enforcing a
408  /// same-origin policy.
409  ///
410  /// You can leave the [`SandboxAllowedList`] empty
411  /// (`SandboxAllowedList::new_empty()`) to disallow everything.
412  Sandbox(SandboxAllowedList),
413  /// Specifies valid sources for JavaScript.
414  ///
415  /// This includes not only URLs loaded directly into `<script>` elements, but
416  /// also things like inline script event handlers (onclick) and XSLT
417  /// stylesheets which can trigger script execution.
418  ScriptSrc(Sources<'a>),
419  /// Specifies valid sources for JavaScript.
420  ///
421  /// This includes not only URLs loaded directly into `<script>` elements, but
422  /// also things like inline script event handlers (onclick) and XSLT
423  /// stylesheets which can trigger script execution.
424  ScriptSrcAttr(Sources<'a>),
425  /// Specifies valid sources for JavaScript `<script>` elements, but not
426  /// inline script event handlers like onclick.
427  ScriptSrcElem(Sources<'a>),
428  /// specifies valid sources for stylesheets.
429  StyleSrc(Sources<'a>),
430  /// Specifies valid sources for inline styles applied to individual DOM
431  /// elements.
432  StyleSrcAttr(Sources<'a>),
433  /// Specifies valid sources for stylesheets `<style>` elements and `<link>`
434  /// elements with rel="stylesheet".
435  StyleSrcElem(Sources<'a>),
436  /// Instructs user agents to restrict usage of known DOM XSS sinks to a
437  /// predefined set of functions that only accept non-spoofable, typed values
438  /// in place of strings.
439  ///
440  /// This allows authors to define rules guarding writing values to the DOM
441  /// and thus reducing the DOM XSS attack surface to small, isolated parts of
442  /// the web application codebase, facilitating their monitoring and code
443  /// review. This directive declares a white-list of trusted type policy
444  /// names created with TrustedTypes.createPolicy from Trusted Types API.
445  TrustedTypes(Vec<&'a str>),
446  /// Instructs user agents to treat all of a site's insecure URLs (those
447  /// served over HTTP) as though they have been replaced with secure URLs
448  /// (those served over HTTPS).
449  ///
450  /// This directive is intended for web sites with large numbers of insecure
451  /// legacy URLs that need to be rewritten.
452  ///
453  /// The upgrade-insecure-requests directive is evaluated before
454  /// block-all-mixed-content and if it is set, the latter is effectively a
455  /// no-op. It is recommended to set either directive, but not both, unless
456  /// you want to force HTTPS on older browsers that do not force it after a
457  /// redirect to HTTP. The upgrade-insecure-requests directive will not
458  /// ensure that users visiting your site via links on third-party sites will
459  /// be upgraded to HTTPS for the top-level navigation and thus does not
460  /// replace the Strict-Transport-Security (HSTS) header, which should still
461  /// be set with an appropriate max-age to ensure that users are not subject
462  /// to SSL stripping attacks.
463  UpgradeInsecureRequests,
464  /// Instructs user agents to restrict usage of known DOM XSS sinks to a
465  /// predefined set of functions that only accept non-spoofable, typed values
466  /// in place of strings.
467  ///
468  /// This allows authors to define rules guarding writing values to the DOM
469  /// and thus reducing the DOM XSS attack surface to small, isolated parts of
470  /// the web application codebase, facilitating their monitoring and code
471  /// review. This directive declares a white-list of trusted type policy
472  /// names created with TrustedTypes.createPolicy from Trusted Types API.
473  WorkerSrc(Sources<'a>),
474}
475
476impl<'a> CSP<'a> {
477  #[must_use]
478  /// Creates a new empty CSP
479  pub fn new() -> Self {
480    Self::default()
481  }
482
483  #[must_use]
484  /// Creates a new CSP with a given directive
485  pub fn new_with(directive: Directive<'a>) -> Self {
486    Self(vec![directive])
487  }
488
489  #[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
490  #[allow(missing_docs)]
491  pub fn add_borrowed<'b>(&'b mut self, directive: Directive<'a>) -> &'b mut Self {
492    self.push_borrowed(directive);
493    self
494  }
495
496  /// Pushes a directive to the end of the borrowed CSP
497  pub fn push_borrowed<'b>(&'b mut self, directive: Directive<'a>) -> &'b mut Self {
498    self.0.push(directive);
499    self
500  }
501
502  #[allow(clippy::should_implement_trait)]
503  #[deprecated(since = "1.0.0", note = "please use `push` instead")]
504  #[must_use]
505  #[allow(missing_docs)]
506  pub fn add(self, directive: Directive<'a>) -> Self {
507    self.push(directive)
508  }
509
510  #[must_use]
511  /// Pushes a directive to the end of the CSP
512  pub fn push(mut self, directive: Directive<'a>) -> Self {
513    self.0.push(directive);
514    self
515  }
516
517  #[must_use]
518  /// Merge duplicate directives. For directives that carry repeated entries,
519  /// the entries are unioned (no duplicates); for other directives, the
520  /// *first* wins.
521  pub fn normalize(self) -> Self {
522    use Directive::*;
523    let mut out: Vec<Directive<'a>> = Vec::with_capacity(self.0.len());
524
525    for d in self.0 {
526      match d {
527        BaseUri(s) => {
528          if let Some(BaseUri(existing)) =
529            out.iter_mut().find(|x| matches!(x, BaseUri(_)))
530          {
531            existing.extend_unique(s);
532          } else {
533            out.push(BaseUri(s));
534          }
535        }
536        ChildSrc(s) => {
537          if let Some(ChildSrc(existing)) =
538            out.iter_mut().find(|x| matches!(x, ChildSrc(_)))
539          {
540            existing.extend_unique(s);
541          } else {
542            out.push(ChildSrc(s));
543          }
544        }
545        ConnectSrc(s) => {
546          if let Some(ConnectSrc(existing)) =
547            out.iter_mut().find(|x| matches!(x, ConnectSrc(_)))
548          {
549            existing.extend_unique(s);
550          } else {
551            out.push(ConnectSrc(s));
552          }
553        }
554        DefaultSrc(s) => {
555          if let Some(DefaultSrc(existing)) =
556            out.iter_mut().find(|x| matches!(x, DefaultSrc(_)))
557          {
558            existing.extend_unique(s);
559          } else {
560            out.push(DefaultSrc(s));
561          }
562        }
563        FontSrc(s) => {
564          if let Some(FontSrc(existing)) =
565            out.iter_mut().find(|x| matches!(x, FontSrc(_)))
566          {
567            existing.extend_unique(s);
568          } else {
569            out.push(FontSrc(s));
570          }
571        }
572        FormAction(s) => {
573          if let Some(FormAction(existing)) =
574            out.iter_mut().find(|x| matches!(x, FormAction(_)))
575          {
576            existing.extend_unique(s);
577          } else {
578            out.push(FormAction(s));
579          }
580        }
581        FrameAncestors(s) => {
582          if let Some(FrameAncestors(existing)) =
583            out.iter_mut().find(|x| matches!(x, FrameAncestors(_)))
584          {
585            existing.extend_unique(s);
586          } else {
587            out.push(FrameAncestors(s));
588          }
589        }
590        FrameSrc(s) => {
591          if let Some(FrameSrc(existing)) =
592            out.iter_mut().find(|x| matches!(x, FrameSrc(_)))
593          {
594            existing.extend_unique(s);
595          } else {
596            out.push(FrameSrc(s));
597          }
598        }
599        ImgSrc(s) => {
600          if let Some(ImgSrc(existing)) = out.iter_mut().find(|x| matches!(x, ImgSrc(_)))
601          {
602            existing.extend_unique(s);
603          } else {
604            out.push(ImgSrc(s));
605          }
606        }
607        ManifestSrc(s) => {
608          if let Some(ManifestSrc(existing)) =
609            out.iter_mut().find(|x| matches!(x, ManifestSrc(_)))
610          {
611            existing.extend_unique(s);
612          } else {
613            out.push(ManifestSrc(s));
614          }
615        }
616        MediaSrc(s) => {
617          if let Some(MediaSrc(existing)) =
618            out.iter_mut().find(|x| matches!(x, MediaSrc(_)))
619          {
620            existing.extend_unique(s);
621          } else {
622            out.push(MediaSrc(s));
623          }
624        }
625        NavigateTo(s) => {
626          if let Some(NavigateTo(existing)) =
627            out.iter_mut().find(|x| matches!(x, NavigateTo(_)))
628          {
629            existing.extend_unique(s);
630          } else {
631            out.push(NavigateTo(s));
632          }
633        }
634        ObjectSrc(s) => {
635          if let Some(ObjectSrc(existing)) =
636            out.iter_mut().find(|x| matches!(x, ObjectSrc(_)))
637          {
638            existing.extend_unique(s);
639          } else {
640            out.push(ObjectSrc(s));
641          }
642        }
643        PrefetchSrc(s) => {
644          if let Some(PrefetchSrc(existing)) =
645            out.iter_mut().find(|x| matches!(x, PrefetchSrc(_)))
646          {
647            existing.extend_unique(s);
648          } else {
649            out.push(PrefetchSrc(s));
650          }
651        }
652        ScriptSrc(s) => {
653          if let Some(ScriptSrc(existing)) =
654            out.iter_mut().find(|x| matches!(x, ScriptSrc(_)))
655          {
656            existing.extend_unique(s);
657          } else {
658            out.push(ScriptSrc(s));
659          }
660        }
661        ScriptSrcAttr(s) => {
662          if let Some(ScriptSrcAttr(existing)) =
663            out.iter_mut().find(|x| matches!(x, ScriptSrcAttr(_)))
664          {
665            existing.extend_unique(s);
666          } else {
667            out.push(ScriptSrcAttr(s));
668          }
669        }
670        ScriptSrcElem(s) => {
671          if let Some(ScriptSrcElem(existing)) =
672            out.iter_mut().find(|x| matches!(x, ScriptSrcElem(_)))
673          {
674            existing.extend_unique(s);
675          } else {
676            out.push(ScriptSrcElem(s));
677          }
678        }
679        StyleSrc(s) => {
680          if let Some(StyleSrc(existing)) =
681            out.iter_mut().find(|x| matches!(x, StyleSrc(_)))
682          {
683            existing.extend_unique(s);
684          } else {
685            out.push(StyleSrc(s));
686          }
687        }
688        StyleSrcAttr(s) => {
689          if let Some(StyleSrcAttr(existing)) =
690            out.iter_mut().find(|x| matches!(x, StyleSrcAttr(_)))
691          {
692            existing.extend_unique(s);
693          } else {
694            out.push(StyleSrcAttr(s));
695          }
696        }
697        StyleSrcElem(s) => {
698          if let Some(StyleSrcElem(existing)) =
699            out.iter_mut().find(|x| matches!(x, StyleSrcElem(_)))
700          {
701            existing.extend_unique(s);
702          } else {
703            out.push(StyleSrcElem(s));
704          }
705        }
706        WorkerSrc(s) => {
707          if let Some(WorkerSrc(existing)) =
708            out.iter_mut().find(|x| matches!(x, WorkerSrc(_)))
709          {
710            existing.extend_unique(s);
711          } else {
712            out.push(WorkerSrc(s));
713          }
714        }
715
716        Sandbox(allowed) => {
717          if let Some(Sandbox(existing)) =
718            out.iter_mut().find(|x| matches!(x, Sandbox(_)))
719          {
720            existing.extend_unique(allowed);
721          } else {
722            out.push(Sandbox(allowed));
723          }
724        }
725
726        PluginTypes(plugins) => {
727          if let Some(PluginTypes(existing)) =
728            out.iter_mut().find(|x| matches!(x, PluginTypes(_)))
729          {
730            existing.extend_unique(plugins);
731          } else {
732            out.push(PluginTypes(plugins));
733          }
734        }
735
736        TrustedTypes(types) => {
737          if let Some(TrustedTypes(existing)) =
738            out.iter_mut().find(|x| matches!(x, TrustedTypes(_)))
739          {
740            for t in types {
741              if !existing.contains(&t) {
742                existing.push(t);
743              }
744            }
745          } else {
746            out.push(TrustedTypes(types));
747          }
748        }
749
750        // For non sources cases, they can't be merged so we keep the *first* instance
751        // (same as the spec if directives are repeated)
752        directive @ (BlockAllMixedContent
753        | UpgradeInsecureRequests
754        | RequireSriFor(_)
755        | ReportUri(_)
756        | ReportTo(_)) => {
757          // check if directive already exists, skip adding this one if so.
758          if out
759            .iter()
760            .any(|x| core::mem::discriminant(x) == core::mem::discriminant(&directive))
761          {
762            continue;
763          }
764          out.push(directive);
765        }
766      }
767    }
768
769    CSP(out)
770  }
771}
772
773impl<'a> Sources<'a> {
774  #[must_use]
775  /// Creates a new empty Sources
776  pub const fn new() -> Self {
777    Self(vec![])
778  }
779
780  #[must_use]
781  /// Creates new Sources with a source
782  pub fn new_with(source: Source<'a>) -> Self {
783    Self(vec![source])
784  }
785
786  #[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
787  #[allow(missing_docs)]
788  pub fn add_borrowed<'b>(&'b mut self, source: Source<'a>) -> &'b mut Self {
789    self.push_borrowed(source);
790    self
791  }
792
793  /// Pushes a source to the end of the borrowed Sources
794  pub fn push_borrowed<'b>(&'b mut self, source: Source<'a>) -> &'b mut Self {
795    self.0.push(source);
796    self
797  }
798
799  #[allow(clippy::should_implement_trait)]
800  #[deprecated(since = "1.0.0", note = "please use `push` instead")]
801  #[must_use]
802  #[allow(missing_docs)]
803  pub fn add(self, source: Source<'a>) -> Self {
804    self.push(source)
805  }
806
807  #[must_use]
808  /// Pushes a source to the end of the Sources
809  pub fn push(mut self, source: Source<'a>) -> Self {
810    self.0.push(source);
811    self
812  }
813
814  /// Add all unique sources from `other` into `self`, preserving order.
815  pub fn extend_unique(&mut self, other: Self) {
816    for s in other.0 {
817      if !self.0.contains(&s) {
818        self.0.push(s);
819      }
820    }
821  }
822}
823
824impl<'a> Plugins<'a> {
825  #[must_use]
826  /// Creates a new Plugins with a plugin
827  pub fn new_with(plugin: (&'a str, &'a str)) -> Self {
828    Self(vec![plugin])
829  }
830
831  #[must_use]
832  /// Creates a new empty plugins
833  pub const fn new() -> Self {
834    Self(vec![])
835  }
836
837  #[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
838  #[allow(missing_docs)]
839  pub fn add_borrowed<'b>(&'b mut self, plugin: (&'a str, &'a str)) -> &'b mut Self {
840    self.push_borrowed(plugin);
841    self
842  }
843
844  /// Pushes a plugin to the end of the borrowed Plugins
845  pub fn push_borrowed<'b>(&'b mut self, plugin: (&'a str, &'a str)) -> &'b mut Self {
846    self.0.push(plugin);
847    self
848  }
849
850  #[allow(clippy::should_implement_trait)]
851  #[deprecated(since = "1.0.0", note = "please use `push` instead")]
852  #[must_use]
853  #[allow(missing_docs)]
854  pub fn add(self, plugin: (&'a str, &'a str)) -> Self {
855    self.push(plugin)
856  }
857
858  #[must_use]
859  /// Pushes a plugin to the end of the Plugins
860  pub fn push(mut self, plugin: (&'a str, &'a str)) -> Self {
861    self.0.push(plugin);
862    self
863  }
864
865  /// Add all unique sources from `other` into `self`, preserving order.
866  pub fn extend_unique(&mut self, other: Self) {
867    for s in other.0 {
868      if !self.0.contains(&s) {
869        self.0.push(s);
870      }
871    }
872  }
873}
874
875impl SandboxAllowedList {
876  #[must_use]
877  /// Creates a new `SandboxAllowedList` with only a certain sandbox allowance
878  pub fn new_with(sandbox_allow: SandboxAllow) -> Self {
879    Self(vec![sandbox_allow])
880  }
881
882  #[must_use]
883  /// Creates a new empty `SandboxAllowedList`
884  pub const fn new() -> Self {
885    Self(vec![])
886  }
887
888  #[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
889  #[allow(missing_docs)]
890  pub fn add_borrowed(&'_ mut self, sandbox_allow: SandboxAllow) -> &'_ mut Self {
891    self.push_borrowed(sandbox_allow);
892    self
893  }
894
895  /// Pushes a sandbox allow type to the end of the borrowed
896  /// `SandboxAllowedList`
897  pub fn push_borrowed(&'_ mut self, sandbox_allow: SandboxAllow) -> &'_ mut Self {
898    self.0.push(sandbox_allow);
899    self
900  }
901
902  #[allow(clippy::should_implement_trait)]
903  #[deprecated(since = "1.0.0", note = "please use `push` instead")]
904  #[must_use]
905  #[allow(missing_docs)]
906  pub fn add(self, sandbox_allow: SandboxAllow) -> Self {
907    self.push(sandbox_allow)
908  }
909
910  #[must_use]
911  /// Pushes a sandbox allow type to the end of the `SandboxAllowedList`
912  pub fn push(mut self, sandbox_allow: SandboxAllow) -> Self {
913    self.0.push(sandbox_allow);
914    self
915  }
916
917  /// Add all unique sources from `other` into `self`, preserving order.
918  pub fn extend_unique(&mut self, other: Self) {
919    for s in other.0 {
920      if !self.0.contains(&s) {
921        self.0.push(s);
922      }
923    }
924  }
925}
926
927impl<'a> ReportUris<'a> {
928  #[must_use]
929  /// Creates a new `ReportUris` with a certain uri
930  pub fn new_with(report_uri: &'a str) -> Self {
931    ReportUris(vec![report_uri])
932  }
933
934  #[must_use]
935  /// Creates a new empty `ReportUris`
936  pub const fn new() -> Self {
937    ReportUris(vec![])
938  }
939
940  #[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
941  #[allow(missing_docs)]
942  pub fn add_borrowed<'b>(&'b mut self, report_uri: &'a str) -> &'b mut Self {
943    self.push_borrowed(report_uri);
944    self
945  }
946
947  /// Pushes a report uri to the end of the borrowed `ReportUris`
948  pub fn push_borrowed<'b>(&'b mut self, report_uri: &'a str) -> &'b mut Self {
949    self.0.push(report_uri);
950    self
951  }
952
953  #[allow(clippy::should_implement_trait)]
954  #[deprecated(since = "1.0.0", note = "please use `push` instead")]
955  #[must_use]
956  #[allow(missing_docs)]
957  pub fn add(self, report_uri: &'a str) -> Self {
958    self.push(report_uri)
959  }
960
961  #[must_use]
962  /// Pushes a report uri to the end of the `ReportUris`
963  pub fn push(mut self, report_uri: &'a str) -> Self {
964    self.0.push(report_uri);
965    self
966  }
967}
968
969impl fmt::Display for Source<'_> {
970  fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
971    match self {
972      Self::Host(s) => write!(fmt, "{s}"),
973      Self::Scheme(s) => write!(fmt, "{s}:"),
974      Self::Self_ => write!(fmt, "'self'"),
975      Self::UnsafeEval => write!(fmt, "'unsafe-eval'"),
976      Self::WasmUnsafeEval => write!(fmt, "'wasm-unsafe-eval'"),
977      Self::UnsafeHashes => write!(fmt, "'unsafe-hashes'"),
978      Self::UnsafeInline => write!(fmt, "'unsafe-inline'"),
979      Self::Nonce(s) => write!(fmt, "'nonce-{s}'"),
980      Self::Hash((algo, hash)) => write!(fmt, "'{algo}-{hash}'"),
981      Self::StrictDynamic => write!(fmt, "'strict-dynamic'"),
982      Self::ReportSample => write!(fmt, "'report-sample'"),
983    }
984  }
985}
986
987impl fmt::Display for SandboxAllow {
988  fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
989    match self {
990      Self::DownloadsWithoutUserActivation => {
991        write!(fmt, "allow-downloads-without-user-activation")
992      }
993      Self::Forms => write!(fmt, "allow-forms"),
994      Self::Modals => write!(fmt, "allow-modals"),
995      Self::OrientationLock => write!(fmt, "allow-orientation-lock"),
996      Self::PointerLock => write!(fmt, "allow-pointer-lock"),
997      Self::Popups => write!(fmt, "allow-popups"),
998      Self::PopupsToEscapeSandbox => write!(fmt, "allow-popups-to-escape-sandbox"),
999      Self::Presentation => write!(fmt, "allow-presentation"),
1000      Self::SameOrigin => write!(fmt, "allow-same-origin"),
1001      Self::Scripts => write!(fmt, "allow-scripts"),
1002      Self::StorageAccessByUserActivation => {
1003        write!(fmt, "allow-storage-access-by-user-activation")
1004      }
1005      Self::TopNavigation => write!(fmt, "allow-top-navigation"),
1006      Self::TopNavigationByUserActivation => {
1007        write!(fmt, "allow-top-navigation-by-user-activation")
1008      }
1009    }
1010  }
1011}
1012
1013impl fmt::Display for SriFor {
1014  fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
1015    match self {
1016      Self::Script => write!(fmt, "script"),
1017      Self::Style => write!(fmt, "style"),
1018      Self::ScriptStyle => write!(fmt, "script style"),
1019    }
1020  }
1021}
1022
1023impl fmt::Display for Directive<'_> {
1024  fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
1025    match self {
1026      Self::BaseUri(s) => write!(fmt, "base-uri {s}"),
1027      Self::BlockAllMixedContent => write!(fmt, "block-all-mixed-content"),
1028      Self::ChildSrc(s) => write!(fmt, "child-src {s}"),
1029      Self::ConnectSrc(s) => write!(fmt, "connect-src {s}"),
1030      Self::DefaultSrc(s) => write!(fmt, "default-src {s}"),
1031      Self::FontSrc(s) => write!(fmt, "font-src {s}"),
1032      Self::FormAction(s) => write!(fmt, "form-action {s}"),
1033      Self::FrameAncestors(s) => write!(fmt, "frame-ancestors {s}"),
1034      Self::FrameSrc(s) => write!(fmt, "frame-src {s}"),
1035      Self::ImgSrc(s) => write!(fmt, "img-src {s}"),
1036      Self::ManifestSrc(s) => write!(fmt, "manifest-src {s}"),
1037      Self::MediaSrc(s) => write!(fmt, "media-src {s}"),
1038      Self::NavigateTo(s) => write!(fmt, "navigate-to {s}"),
1039      Self::ObjectSrc(s) => write!(fmt, "object-src {s}"),
1040      Self::PluginTypes(s) => write!(fmt, "plugin-types {s}"),
1041      Self::PrefetchSrc(s) => write!(fmt, "prefetch-src {s}"),
1042      Self::ReportTo(s) => write!(fmt, "report-to {s}"),
1043      Self::ReportUri(uris) => {
1044        if uris.0.is_empty() {
1045          return Ok(());
1046        }
1047        write!(fmt, "report-uri ")?;
1048
1049        for uri in &uris.0[0..uris.0.len() - 1] {
1050          write!(fmt, "{uri} ")?;
1051        }
1052
1053        let last = uris.0[uris.0.len() - 1];
1054        write!(fmt, "{last}")
1055      }
1056      Self::RequireSriFor(s) => write!(fmt, "require-sri-for {s}"),
1057      Self::Sandbox(s) => {
1058        if s.0.is_empty() {
1059          write!(fmt, "sandbox")
1060        } else {
1061          write!(fmt, "sandbox {s}")
1062        }
1063      }
1064      Self::ScriptSrc(s) => write!(fmt, "script-src {s}"),
1065      Self::ScriptSrcAttr(s) => write!(fmt, "script-src-attr {s}"),
1066      Self::ScriptSrcElem(s) => write!(fmt, "script-src-elem {s}"),
1067      Self::StyleSrc(s) => write!(fmt, "style-src {s}"),
1068      Self::StyleSrcAttr(s) => write!(fmt, "style-src-attr {s}"),
1069      Self::StyleSrcElem(s) => write!(fmt, "style-src-elem {s}"),
1070      Self::TrustedTypes(trusted_types) => {
1071        if trusted_types.is_empty() {
1072          return Ok(());
1073        }
1074        write!(fmt, "trusted-types ")?;
1075
1076        for trusted_type in &trusted_types[0..trusted_types.len() - 1] {
1077          write!(fmt, "{trusted_type} ")?;
1078        }
1079
1080        let last = trusted_types[trusted_types.len() - 1];
1081        write!(fmt, "{last}")
1082      }
1083      Self::UpgradeInsecureRequests => write!(fmt, "upgrade-insecure-requests"),
1084      Self::WorkerSrc(s) => write!(fmt, "worker-src {s}"),
1085    }
1086  }
1087}
1088
1089impl fmt::Display for Plugins<'_> {
1090  fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
1091    if self.0.is_empty() {
1092      return Ok(());
1093    }
1094
1095    for plugin in &self.0[0..self.0.len() - 1] {
1096      write!(fmt, "{}/{} ", plugin.0, plugin.1)?;
1097    }
1098
1099    let last = &self.0[self.0.len() - 1];
1100    write!(fmt, "{}/{}", last.0, last.1)
1101  }
1102}
1103
1104impl fmt::Display for Sources<'_> {
1105  fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
1106    if self.0.is_empty() {
1107      return write!(fmt, "'none'");
1108    }
1109
1110    for source in &self.0[0..self.0.len() - 1] {
1111      write!(fmt, "{source} ")?;
1112    }
1113
1114    let last = &self.0[self.0.len() - 1];
1115    write!(fmt, "{last}")
1116  }
1117}
1118
1119impl fmt::Display for SandboxAllowedList {
1120  fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
1121    if self.0.is_empty() {
1122      return Ok(());
1123    }
1124
1125    for directive in &self.0[0..self.0.len() - 1] {
1126      write!(fmt, "{directive} ")?;
1127    }
1128
1129    let last = &self.0[self.0.len() - 1];
1130    write!(fmt, "{last}")
1131  }
1132}
1133
1134impl fmt::Display for CSP<'_> {
1135  fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
1136    if self.0.is_empty() {
1137      return Ok(());
1138    }
1139
1140    for directive in &self.0[0..self.0.len() - 1] {
1141      write!(fmt, "{directive}; ")?;
1142    }
1143
1144    let last = &self.0[self.0.len() - 1];
1145    write!(fmt, "{last}")
1146  }
1147}
1148
1149#[cfg(test)]
1150mod tests {
1151  use super::*;
1152
1153  #[test]
1154  /// Tests combining different Directives and sources, and makes sure that
1155  /// spaces and semicolons are inserted correctly.
1156  fn large_csp() {
1157    let font_src = Source::Host("https://cdn.example.org");
1158
1159    let mut csp = CSP::new()
1160      .push(Directive::ImgSrc(
1161        Sources::new_with(Source::Self_)
1162          .push(Source::Scheme("https"))
1163          .push(Source::Host("http://shields.io")),
1164      ))
1165      .push(Directive::ConnectSrc(
1166        Sources::new().push(Source::Host("https://crates.io")).push(Source::Self_),
1167      ))
1168      .push(Directive::StyleSrc(
1169        Sources::new_with(Source::Self_)
1170          .push(Source::UnsafeInline)
1171          .push(font_src.clone()),
1172      ));
1173
1174    csp.push_borrowed(Directive::FontSrc(Sources::new_with(font_src)));
1175
1176    println!("{csp}");
1177
1178    let csp = csp.to_string();
1179
1180    assert_eq!(
1181      csp,
1182      "img-src 'self' https: http://shields.io; connect-src https://crates.io 'self'; style-src 'self' 'unsafe-inline' https://cdn.example.org; font-src https://cdn.example.org"
1183    );
1184  }
1185
1186  #[test]
1187  /// Tests all the possible source variations.
1188  fn all_sources() {
1189    let csp = CSP::new().push(Directive::ScriptSrc(
1190      Sources::new()
1191        .push(Source::Hash(("sha256", "1234a")))
1192        .push(Source::Nonce("5678b"))
1193        .push(Source::ReportSample)
1194        .push(Source::StrictDynamic)
1195        .push(Source::UnsafeEval)
1196        .push(Source::WasmUnsafeEval)
1197        .push(Source::UnsafeHashes)
1198        .push(Source::UnsafeInline)
1199        .push(Source::Scheme("data"))
1200        .push(Source::Host("https://example.org"))
1201        .push(Source::Self_),
1202    ));
1203
1204    assert_eq!(
1205      csp.to_string(),
1206      "script-src 'sha256-1234a' 'nonce-5678b' 'report-sample' 'strict-dynamic' 'unsafe-eval' 'wasm-unsafe-eval' 'unsafe-hashes' 'unsafe-inline' data: https://example.org 'self'"
1207    );
1208  }
1209
1210  #[test]
1211  fn empty_values() {
1212    let csp = CSP::new();
1213
1214    assert_eq!(csp.to_string(), "");
1215
1216    let csp = CSP::new().push(Directive::ImgSrc(Sources::new()));
1217
1218    assert_eq!(csp.to_string(), "img-src 'none'");
1219  }
1220
1221  #[test]
1222  fn sandbox() {
1223    let csp = CSP::new().push(Directive::Sandbox(SandboxAllowedList::new()));
1224
1225    assert_eq!(csp.to_string(), "sandbox");
1226
1227    let csp = CSP::new()
1228      .push(Directive::Sandbox(SandboxAllowedList::new().push(SandboxAllow::Scripts)));
1229
1230    assert_eq!(csp.to_string(), "sandbox allow-scripts");
1231    assert_eq!(
1232      csp.to_string(),
1233      "sandbox ".to_owned() + &SandboxAllow::Scripts.to_string()
1234    );
1235  }
1236
1237  #[test]
1238  fn special() {
1239    let mut csp = CSP::new();
1240    let sri_directive = Directive::RequireSriFor(SriFor::Script);
1241
1242    csp.push_borrowed(sri_directive);
1243
1244    assert_eq!(csp.to_string(), "require-sri-for script");
1245
1246    let csp = CSP::new_with(Directive::BlockAllMixedContent);
1247    assert_eq!(csp.to_string(), "block-all-mixed-content");
1248
1249    let csp = CSP::new_with(Directive::PluginTypes(
1250      Plugins::new().push(("application", "x-java-applet")),
1251    ));
1252    assert_eq!(csp.to_string(), "plugin-types application/x-java-applet");
1253
1254    let csp = CSP::new_with(Directive::ReportTo("endpoint-1"));
1255    assert_eq!(csp.to_string(), "report-to endpoint-1");
1256
1257    let csp = CSP::new_with(Directive::ReportUri(
1258      ReportUris::new_with("https://r1.example.org").push("https://r2.example.org"),
1259    ));
1260    assert_eq!(
1261      csp.to_string(),
1262      "report-uri https://r1.example.org https://r2.example.org"
1263    );
1264
1265    let csp = CSP::new_with(Directive::TrustedTypes(vec!["hello", "hello2"]));
1266    assert_eq!(csp.to_string(), "trusted-types hello hello2");
1267
1268    let csp = CSP::new_with(Directive::UpgradeInsecureRequests);
1269    assert_eq!(csp.to_string(), "upgrade-insecure-requests");
1270  }
1271
1272  #[test]
1273  fn empty_trusted_types() {
1274    let csp = CSP::new_with(Directive::TrustedTypes(vec![]));
1275    assert_eq!(csp.to_string(), "");
1276  }
1277
1278  #[test]
1279  fn empty_report_uri() {
1280    let csp = CSP::new_with(Directive::ReportUri(ReportUris::new()));
1281    assert_eq!(csp.to_string(), "");
1282  }
1283
1284  #[test]
1285  fn repeated_directive_normalization() {
1286    let mut csp = CSP::new();
1287
1288    csp.push_borrowed(Directive::ConnectSrc(Sources::new_with(Source::Self_)));
1289    csp.push_borrowed(Directive::ConnectSrc(Sources::new_with(Source::Host(
1290      "https://example.org",
1291    ))));
1292
1293    csp.push_borrowed(Directive::ScriptSrc(Sources::new_with(Source::Nonce("abc"))));
1294    csp.push_borrowed(Directive::ScriptSrc(Sources::new_with(Source::Self_)));
1295
1296    assert_eq!(
1297      csp.normalize().to_string(),
1298      "connect-src 'self' https://example.org; script-src 'nonce-abc' 'self'"
1299    );
1300  }
1301
1302  #[test]
1303  fn repeated_duplicate_directives_are_removed() {
1304    let mut csp = CSP::new();
1305
1306    csp.push_borrowed(Directive::ConnectSrc(Sources::new_with(Source::Self_)));
1307
1308    csp.push_borrowed(Directive::ConnectSrc(Sources::new_with(Source::Self_)));
1309
1310    assert_eq!(csp.normalize().to_string(), "connect-src 'self'");
1311  }
1312
1313  #[test]
1314  fn repeated_non_mergeable_directive_first_wins() {
1315    let mut csp = CSP::new();
1316
1317    csp.push_borrowed(Directive::RequireSriFor(SriFor::Script));
1318    csp.push_borrowed(Directive::RequireSriFor(SriFor::Style));
1319
1320    assert_eq!(csp.normalize().to_string(), "require-sri-for script");
1321  }
1322}