#![deny(clippy::all)]
#![deny(unsafe_code)]
#![deny(clippy::cargo)]
#![warn(missing_docs)]
#![deny(rustdoc::invalid_html_tags)]
#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
use std::fmt;
#[derive(Debug, Default, Clone)]
pub struct CSP<'a>(Vec<Directive<'a>>);
#[derive(Debug, Default, Clone)]
pub struct Sources<'a>(Vec<Source<'a>>);
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Plugins<'a>(Vec<(&'a str, &'a str)>);
#[derive(Debug, Default, Clone)]
pub struct ReportUris<'a>(Vec<&'a str>);
#[derive(Debug, Default, Clone)]
pub struct SandboxAllowedList(Vec<SandboxAllow>);
#[derive(Debug, Clone)]
pub enum SriFor {
Script,
Style,
ScriptStyle,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Source<'a> {
Host(&'a str),
Scheme(&'a str),
Self_,
UnsafeEval,
WasmUnsafeEval,
UnsafeHashes,
UnsafeInline,
Nonce(&'a str),
Hash((&'a str, &'a str)),
StrictDynamic,
ReportSample,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SandboxAllow {
DownloadsWithoutUserActivation,
Forms,
Modals,
OrientationLock,
PointerLock,
Popups,
PopupsToEscapeSandbox,
Presentation,
SameOrigin,
Scripts,
StorageAccessByUserActivation,
TopNavigation,
TopNavigationByUserActivation,
}
#[derive(Debug, Clone)]
pub enum Directive<'a> {
BaseUri(Sources<'a>),
BlockAllMixedContent,
ChildSrc(Sources<'a>),
ConnectSrc(Sources<'a>),
DefaultSrc(Sources<'a>),
FontSrc(Sources<'a>),
FormAction(Sources<'a>),
FrameAncestors(Sources<'a>),
FrameSrc(Sources<'a>),
ImgSrc(Sources<'a>),
ManifestSrc(Sources<'a>),
MediaSrc(Sources<'a>),
NavigateTo(Sources<'a>),
ObjectSrc(Sources<'a>),
PluginTypes(Plugins<'a>),
PrefetchSrc(Sources<'a>),
ReportTo(&'a str),
ReportUri(ReportUris<'a>),
RequireSriFor(SriFor),
Sandbox(SandboxAllowedList),
ScriptSrc(Sources<'a>),
ScriptSrcAttr(Sources<'a>),
ScriptSrcElem(Sources<'a>),
StyleSrc(Sources<'a>),
StyleSrcAttr(Sources<'a>),
StyleSrcElem(Sources<'a>),
TrustedTypes(Vec<&'a str>),
UpgradeInsecureRequests,
WorkerSrc(Sources<'a>),
}
impl<'a> CSP<'a> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn new_with(directive: Directive<'a>) -> Self {
Self(vec![directive])
}
#[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
#[allow(missing_docs)]
pub fn add_borrowed<'b>(&'b mut self, directive: Directive<'a>) -> &'b mut Self {
self.push_borrowed(directive);
self
}
pub fn push_borrowed<'b>(&'b mut self, directive: Directive<'a>) -> &'b mut Self {
self.0.push(directive);
self
}
#[allow(clippy::should_implement_trait)]
#[deprecated(since = "1.0.0", note = "please use `push` instead")]
#[must_use]
#[allow(missing_docs)]
pub fn add(self, directive: Directive<'a>) -> Self {
self.push(directive)
}
#[must_use]
pub fn push(mut self, directive: Directive<'a>) -> Self {
self.0.push(directive);
self
}
#[must_use]
pub fn normalize(self) -> Self {
use Directive::*;
let mut out: Vec<Directive<'a>> = Vec::with_capacity(self.0.len());
for d in self.0 {
match d {
BaseUri(s) => {
if let Some(BaseUri(existing)) =
out.iter_mut().find(|x| matches!(x, BaseUri(_)))
{
existing.extend_unique(s);
} else {
out.push(BaseUri(s));
}
}
ChildSrc(s) => {
if let Some(ChildSrc(existing)) =
out.iter_mut().find(|x| matches!(x, ChildSrc(_)))
{
existing.extend_unique(s);
} else {
out.push(ChildSrc(s));
}
}
ConnectSrc(s) => {
if let Some(ConnectSrc(existing)) =
out.iter_mut().find(|x| matches!(x, ConnectSrc(_)))
{
existing.extend_unique(s);
} else {
out.push(ConnectSrc(s));
}
}
DefaultSrc(s) => {
if let Some(DefaultSrc(existing)) =
out.iter_mut().find(|x| matches!(x, DefaultSrc(_)))
{
existing.extend_unique(s);
} else {
out.push(DefaultSrc(s));
}
}
FontSrc(s) => {
if let Some(FontSrc(existing)) =
out.iter_mut().find(|x| matches!(x, FontSrc(_)))
{
existing.extend_unique(s);
} else {
out.push(FontSrc(s));
}
}
FormAction(s) => {
if let Some(FormAction(existing)) =
out.iter_mut().find(|x| matches!(x, FormAction(_)))
{
existing.extend_unique(s);
} else {
out.push(FormAction(s));
}
}
FrameAncestors(s) => {
if let Some(FrameAncestors(existing)) =
out.iter_mut().find(|x| matches!(x, FrameAncestors(_)))
{
existing.extend_unique(s);
} else {
out.push(FrameAncestors(s));
}
}
FrameSrc(s) => {
if let Some(FrameSrc(existing)) =
out.iter_mut().find(|x| matches!(x, FrameSrc(_)))
{
existing.extend_unique(s);
} else {
out.push(FrameSrc(s));
}
}
ImgSrc(s) => {
if let Some(ImgSrc(existing)) = out.iter_mut().find(|x| matches!(x, ImgSrc(_)))
{
existing.extend_unique(s);
} else {
out.push(ImgSrc(s));
}
}
ManifestSrc(s) => {
if let Some(ManifestSrc(existing)) =
out.iter_mut().find(|x| matches!(x, ManifestSrc(_)))
{
existing.extend_unique(s);
} else {
out.push(ManifestSrc(s));
}
}
MediaSrc(s) => {
if let Some(MediaSrc(existing)) =
out.iter_mut().find(|x| matches!(x, MediaSrc(_)))
{
existing.extend_unique(s);
} else {
out.push(MediaSrc(s));
}
}
NavigateTo(s) => {
if let Some(NavigateTo(existing)) =
out.iter_mut().find(|x| matches!(x, NavigateTo(_)))
{
existing.extend_unique(s);
} else {
out.push(NavigateTo(s));
}
}
ObjectSrc(s) => {
if let Some(ObjectSrc(existing)) =
out.iter_mut().find(|x| matches!(x, ObjectSrc(_)))
{
existing.extend_unique(s);
} else {
out.push(ObjectSrc(s));
}
}
PrefetchSrc(s) => {
if let Some(PrefetchSrc(existing)) =
out.iter_mut().find(|x| matches!(x, PrefetchSrc(_)))
{
existing.extend_unique(s);
} else {
out.push(PrefetchSrc(s));
}
}
ScriptSrc(s) => {
if let Some(ScriptSrc(existing)) =
out.iter_mut().find(|x| matches!(x, ScriptSrc(_)))
{
existing.extend_unique(s);
} else {
out.push(ScriptSrc(s));
}
}
ScriptSrcAttr(s) => {
if let Some(ScriptSrcAttr(existing)) =
out.iter_mut().find(|x| matches!(x, ScriptSrcAttr(_)))
{
existing.extend_unique(s);
} else {
out.push(ScriptSrcAttr(s));
}
}
ScriptSrcElem(s) => {
if let Some(ScriptSrcElem(existing)) =
out.iter_mut().find(|x| matches!(x, ScriptSrcElem(_)))
{
existing.extend_unique(s);
} else {
out.push(ScriptSrcElem(s));
}
}
StyleSrc(s) => {
if let Some(StyleSrc(existing)) =
out.iter_mut().find(|x| matches!(x, StyleSrc(_)))
{
existing.extend_unique(s);
} else {
out.push(StyleSrc(s));
}
}
StyleSrcAttr(s) => {
if let Some(StyleSrcAttr(existing)) =
out.iter_mut().find(|x| matches!(x, StyleSrcAttr(_)))
{
existing.extend_unique(s);
} else {
out.push(StyleSrcAttr(s));
}
}
StyleSrcElem(s) => {
if let Some(StyleSrcElem(existing)) =
out.iter_mut().find(|x| matches!(x, StyleSrcElem(_)))
{
existing.extend_unique(s);
} else {
out.push(StyleSrcElem(s));
}
}
WorkerSrc(s) => {
if let Some(WorkerSrc(existing)) =
out.iter_mut().find(|x| matches!(x, WorkerSrc(_)))
{
existing.extend_unique(s);
} else {
out.push(WorkerSrc(s));
}
}
Sandbox(allowed) => {
if let Some(Sandbox(existing)) =
out.iter_mut().find(|x| matches!(x, Sandbox(_)))
{
existing.extend_unique(allowed);
} else {
out.push(Sandbox(allowed));
}
}
PluginTypes(plugins) => {
if let Some(PluginTypes(existing)) =
out.iter_mut().find(|x| matches!(x, PluginTypes(_)))
{
existing.extend_unique(plugins);
} else {
out.push(PluginTypes(plugins));
}
}
TrustedTypes(types) => {
if let Some(TrustedTypes(existing)) =
out.iter_mut().find(|x| matches!(x, TrustedTypes(_)))
{
for t in types {
if !existing.contains(&t) {
existing.push(t);
}
}
} else {
out.push(TrustedTypes(types));
}
}
directive @ (BlockAllMixedContent
| UpgradeInsecureRequests
| RequireSriFor(_)
| ReportUri(_)
| ReportTo(_)) => {
if out
.iter()
.any(|x| core::mem::discriminant(x) == core::mem::discriminant(&directive))
{
continue;
}
out.push(directive);
}
}
}
CSP(out)
}
}
impl<'a> Sources<'a> {
#[must_use]
pub const fn new() -> Self {
Self(vec![])
}
#[must_use]
pub fn new_with(source: Source<'a>) -> Self {
Self(vec![source])
}
#[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
#[allow(missing_docs)]
pub fn add_borrowed<'b>(&'b mut self, source: Source<'a>) -> &'b mut Self {
self.push_borrowed(source);
self
}
pub fn push_borrowed<'b>(&'b mut self, source: Source<'a>) -> &'b mut Self {
self.0.push(source);
self
}
#[allow(clippy::should_implement_trait)]
#[deprecated(since = "1.0.0", note = "please use `push` instead")]
#[must_use]
#[allow(missing_docs)]
pub fn add(self, source: Source<'a>) -> Self {
self.push(source)
}
#[must_use]
pub fn push(mut self, source: Source<'a>) -> Self {
self.0.push(source);
self
}
pub fn extend_unique(&mut self, other: Self) {
for s in other.0 {
if !self.0.contains(&s) {
self.0.push(s);
}
}
}
}
impl<'a> Plugins<'a> {
#[must_use]
pub fn new_with(plugin: (&'a str, &'a str)) -> Self {
Self(vec![plugin])
}
#[must_use]
pub const fn new() -> Self {
Self(vec![])
}
#[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
#[allow(missing_docs)]
pub fn add_borrowed<'b>(&'b mut self, plugin: (&'a str, &'a str)) -> &'b mut Self {
self.push_borrowed(plugin);
self
}
pub fn push_borrowed<'b>(&'b mut self, plugin: (&'a str, &'a str)) -> &'b mut Self {
self.0.push(plugin);
self
}
#[allow(clippy::should_implement_trait)]
#[deprecated(since = "1.0.0", note = "please use `push` instead")]
#[must_use]
#[allow(missing_docs)]
pub fn add(self, plugin: (&'a str, &'a str)) -> Self {
self.push(plugin)
}
#[must_use]
pub fn push(mut self, plugin: (&'a str, &'a str)) -> Self {
self.0.push(plugin);
self
}
pub fn extend_unique(&mut self, other: Self) {
for s in other.0 {
if !self.0.contains(&s) {
self.0.push(s);
}
}
}
}
impl SandboxAllowedList {
#[must_use]
pub fn new_with(sandbox_allow: SandboxAllow) -> Self {
Self(vec![sandbox_allow])
}
#[must_use]
pub const fn new() -> Self {
Self(vec![])
}
#[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
#[allow(missing_docs)]
pub fn add_borrowed(&'_ mut self, sandbox_allow: SandboxAllow) -> &'_ mut Self {
self.push_borrowed(sandbox_allow);
self
}
pub fn push_borrowed(&'_ mut self, sandbox_allow: SandboxAllow) -> &'_ mut Self {
self.0.push(sandbox_allow);
self
}
#[allow(clippy::should_implement_trait)]
#[deprecated(since = "1.0.0", note = "please use `push` instead")]
#[must_use]
#[allow(missing_docs)]
pub fn add(self, sandbox_allow: SandboxAllow) -> Self {
self.push(sandbox_allow)
}
#[must_use]
pub fn push(mut self, sandbox_allow: SandboxAllow) -> Self {
self.0.push(sandbox_allow);
self
}
pub fn extend_unique(&mut self, other: Self) {
for s in other.0 {
if !self.0.contains(&s) {
self.0.push(s);
}
}
}
}
impl<'a> ReportUris<'a> {
#[must_use]
pub fn new_with(report_uri: &'a str) -> Self {
ReportUris(vec![report_uri])
}
#[must_use]
pub const fn new() -> Self {
ReportUris(vec![])
}
#[deprecated(since = "1.0.0", note = "please use `push_borrowed` instead")]
#[allow(missing_docs)]
pub fn add_borrowed<'b>(&'b mut self, report_uri: &'a str) -> &'b mut Self {
self.push_borrowed(report_uri);
self
}
pub fn push_borrowed<'b>(&'b mut self, report_uri: &'a str) -> &'b mut Self {
self.0.push(report_uri);
self
}
#[allow(clippy::should_implement_trait)]
#[deprecated(since = "1.0.0", note = "please use `push` instead")]
#[must_use]
#[allow(missing_docs)]
pub fn add(self, report_uri: &'a str) -> Self {
self.push(report_uri)
}
#[must_use]
pub fn push(mut self, report_uri: &'a str) -> Self {
self.0.push(report_uri);
self
}
}
impl fmt::Display for Source<'_> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Host(s) => write!(fmt, "{s}"),
Self::Scheme(s) => write!(fmt, "{s}:"),
Self::Self_ => write!(fmt, "'self'"),
Self::UnsafeEval => write!(fmt, "'unsafe-eval'"),
Self::WasmUnsafeEval => write!(fmt, "'wasm-unsafe-eval'"),
Self::UnsafeHashes => write!(fmt, "'unsafe-hashes'"),
Self::UnsafeInline => write!(fmt, "'unsafe-inline'"),
Self::Nonce(s) => write!(fmt, "'nonce-{s}'"),
Self::Hash((algo, hash)) => write!(fmt, "'{algo}-{hash}'"),
Self::StrictDynamic => write!(fmt, "'strict-dynamic'"),
Self::ReportSample => write!(fmt, "'report-sample'"),
}
}
}
impl fmt::Display for SandboxAllow {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::DownloadsWithoutUserActivation => {
write!(fmt, "allow-downloads-without-user-activation")
}
Self::Forms => write!(fmt, "allow-forms"),
Self::Modals => write!(fmt, "allow-modals"),
Self::OrientationLock => write!(fmt, "allow-orientation-lock"),
Self::PointerLock => write!(fmt, "allow-pointer-lock"),
Self::Popups => write!(fmt, "allow-popups"),
Self::PopupsToEscapeSandbox => write!(fmt, "allow-popups-to-escape-sandbox"),
Self::Presentation => write!(fmt, "allow-presentation"),
Self::SameOrigin => write!(fmt, "allow-same-origin"),
Self::Scripts => write!(fmt, "allow-scripts"),
Self::StorageAccessByUserActivation => {
write!(fmt, "allow-storage-access-by-user-activation")
}
Self::TopNavigation => write!(fmt, "allow-top-navigation"),
Self::TopNavigationByUserActivation => {
write!(fmt, "allow-top-navigation-by-user-activation")
}
}
}
}
impl fmt::Display for SriFor {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Script => write!(fmt, "script"),
Self::Style => write!(fmt, "style"),
Self::ScriptStyle => write!(fmt, "script style"),
}
}
}
impl fmt::Display for Directive<'_> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::BaseUri(s) => write!(fmt, "base-uri {s}"),
Self::BlockAllMixedContent => write!(fmt, "block-all-mixed-content"),
Self::ChildSrc(s) => write!(fmt, "child-src {s}"),
Self::ConnectSrc(s) => write!(fmt, "connect-src {s}"),
Self::DefaultSrc(s) => write!(fmt, "default-src {s}"),
Self::FontSrc(s) => write!(fmt, "font-src {s}"),
Self::FormAction(s) => write!(fmt, "form-action {s}"),
Self::FrameAncestors(s) => write!(fmt, "frame-ancestors {s}"),
Self::FrameSrc(s) => write!(fmt, "frame-src {s}"),
Self::ImgSrc(s) => write!(fmt, "img-src {s}"),
Self::ManifestSrc(s) => write!(fmt, "manifest-src {s}"),
Self::MediaSrc(s) => write!(fmt, "media-src {s}"),
Self::NavigateTo(s) => write!(fmt, "navigate-to {s}"),
Self::ObjectSrc(s) => write!(fmt, "object-src {s}"),
Self::PluginTypes(s) => write!(fmt, "plugin-types {s}"),
Self::PrefetchSrc(s) => write!(fmt, "prefetch-src {s}"),
Self::ReportTo(s) => write!(fmt, "report-to {s}"),
Self::ReportUri(uris) => {
if uris.0.is_empty() {
return Ok(());
}
write!(fmt, "report-uri ")?;
for uri in &uris.0[0..uris.0.len() - 1] {
write!(fmt, "{uri} ")?;
}
let last = uris.0[uris.0.len() - 1];
write!(fmt, "{last}")
}
Self::RequireSriFor(s) => write!(fmt, "require-sri-for {s}"),
Self::Sandbox(s) => {
if s.0.is_empty() {
write!(fmt, "sandbox")
} else {
write!(fmt, "sandbox {s}")
}
}
Self::ScriptSrc(s) => write!(fmt, "script-src {s}"),
Self::ScriptSrcAttr(s) => write!(fmt, "script-src-attr {s}"),
Self::ScriptSrcElem(s) => write!(fmt, "script-src-elem {s}"),
Self::StyleSrc(s) => write!(fmt, "style-src {s}"),
Self::StyleSrcAttr(s) => write!(fmt, "style-src-attr {s}"),
Self::StyleSrcElem(s) => write!(fmt, "style-src-elem {s}"),
Self::TrustedTypes(trusted_types) => {
if trusted_types.is_empty() {
return Ok(());
}
write!(fmt, "trusted-types ")?;
for trusted_type in &trusted_types[0..trusted_types.len() - 1] {
write!(fmt, "{trusted_type} ")?;
}
let last = trusted_types[trusted_types.len() - 1];
write!(fmt, "{last}")
}
Self::UpgradeInsecureRequests => write!(fmt, "upgrade-insecure-requests"),
Self::WorkerSrc(s) => write!(fmt, "worker-src {s}"),
}
}
}
impl fmt::Display for Plugins<'_> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if self.0.is_empty() {
return Ok(());
}
for plugin in &self.0[0..self.0.len() - 1] {
write!(fmt, "{}/{} ", plugin.0, plugin.1)?;
}
let last = &self.0[self.0.len() - 1];
write!(fmt, "{}/{}", last.0, last.1)
}
}
impl fmt::Display for Sources<'_> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if self.0.is_empty() {
return write!(fmt, "'none'");
}
for source in &self.0[0..self.0.len() - 1] {
write!(fmt, "{source} ")?;
}
let last = &self.0[self.0.len() - 1];
write!(fmt, "{last}")
}
}
impl fmt::Display for SandboxAllowedList {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if self.0.is_empty() {
return Ok(());
}
for directive in &self.0[0..self.0.len() - 1] {
write!(fmt, "{directive} ")?;
}
let last = &self.0[self.0.len() - 1];
write!(fmt, "{last}")
}
}
impl fmt::Display for CSP<'_> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if self.0.is_empty() {
return Ok(());
}
for directive in &self.0[0..self.0.len() - 1] {
write!(fmt, "{directive}; ")?;
}
let last = &self.0[self.0.len() - 1];
write!(fmt, "{last}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn large_csp() {
let font_src = Source::Host("https://cdn.example.org");
let mut csp = CSP::new()
.push(Directive::ImgSrc(
Sources::new_with(Source::Self_)
.push(Source::Scheme("https"))
.push(Source::Host("http://shields.io")),
))
.push(Directive::ConnectSrc(
Sources::new().push(Source::Host("https://crates.io")).push(Source::Self_),
))
.push(Directive::StyleSrc(
Sources::new_with(Source::Self_)
.push(Source::UnsafeInline)
.push(font_src.clone()),
));
csp.push_borrowed(Directive::FontSrc(Sources::new_with(font_src)));
println!("{csp}");
let csp = csp.to_string();
assert_eq!(
csp,
"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"
);
}
#[test]
fn all_sources() {
let csp = CSP::new().push(Directive::ScriptSrc(
Sources::new()
.push(Source::Hash(("sha256", "1234a")))
.push(Source::Nonce("5678b"))
.push(Source::ReportSample)
.push(Source::StrictDynamic)
.push(Source::UnsafeEval)
.push(Source::WasmUnsafeEval)
.push(Source::UnsafeHashes)
.push(Source::UnsafeInline)
.push(Source::Scheme("data"))
.push(Source::Host("https://example.org"))
.push(Source::Self_),
));
assert_eq!(
csp.to_string(),
"script-src 'sha256-1234a' 'nonce-5678b' 'report-sample' 'strict-dynamic' 'unsafe-eval' 'wasm-unsafe-eval' 'unsafe-hashes' 'unsafe-inline' data: https://example.org 'self'"
);
}
#[test]
fn empty_values() {
let csp = CSP::new();
assert_eq!(csp.to_string(), "");
let csp = CSP::new().push(Directive::ImgSrc(Sources::new()));
assert_eq!(csp.to_string(), "img-src 'none'");
}
#[test]
fn sandbox() {
let csp = CSP::new().push(Directive::Sandbox(SandboxAllowedList::new()));
assert_eq!(csp.to_string(), "sandbox");
let csp = CSP::new()
.push(Directive::Sandbox(SandboxAllowedList::new().push(SandboxAllow::Scripts)));
assert_eq!(csp.to_string(), "sandbox allow-scripts");
assert_eq!(
csp.to_string(),
"sandbox ".to_owned() + &SandboxAllow::Scripts.to_string()
);
}
#[test]
fn special() {
let mut csp = CSP::new();
let sri_directive = Directive::RequireSriFor(SriFor::Script);
csp.push_borrowed(sri_directive);
assert_eq!(csp.to_string(), "require-sri-for script");
let csp = CSP::new_with(Directive::BlockAllMixedContent);
assert_eq!(csp.to_string(), "block-all-mixed-content");
let csp = CSP::new_with(Directive::PluginTypes(
Plugins::new().push(("application", "x-java-applet")),
));
assert_eq!(csp.to_string(), "plugin-types application/x-java-applet");
let csp = CSP::new_with(Directive::ReportTo("endpoint-1"));
assert_eq!(csp.to_string(), "report-to endpoint-1");
let csp = CSP::new_with(Directive::ReportUri(
ReportUris::new_with("https://r1.example.org").push("https://r2.example.org"),
));
assert_eq!(
csp.to_string(),
"report-uri https://r1.example.org https://r2.example.org"
);
let csp = CSP::new_with(Directive::TrustedTypes(vec!["hello", "hello2"]));
assert_eq!(csp.to_string(), "trusted-types hello hello2");
let csp = CSP::new_with(Directive::UpgradeInsecureRequests);
assert_eq!(csp.to_string(), "upgrade-insecure-requests");
}
#[test]
fn empty_trusted_types() {
let csp = CSP::new_with(Directive::TrustedTypes(vec![]));
assert_eq!(csp.to_string(), "");
}
#[test]
fn empty_report_uri() {
let csp = CSP::new_with(Directive::ReportUri(ReportUris::new()));
assert_eq!(csp.to_string(), "");
}
#[test]
fn repeated_directive_normalization() {
let mut csp = CSP::new();
csp.push_borrowed(Directive::ConnectSrc(Sources::new_with(Source::Self_)));
csp.push_borrowed(Directive::ConnectSrc(Sources::new_with(Source::Host(
"https://example.org",
))));
csp.push_borrowed(Directive::ScriptSrc(Sources::new_with(Source::Nonce("abc"))));
csp.push_borrowed(Directive::ScriptSrc(Sources::new_with(Source::Self_)));
assert_eq!(
csp.normalize().to_string(),
"connect-src 'self' https://example.org; script-src 'nonce-abc' 'self'"
);
}
#[test]
fn repeated_duplicate_directives_are_removed() {
let mut csp = CSP::new();
csp.push_borrowed(Directive::ConnectSrc(Sources::new_with(Source::Self_)));
csp.push_borrowed(Directive::ConnectSrc(Sources::new_with(Source::Self_)));
assert_eq!(csp.normalize().to_string(), "connect-src 'self'");
}
#[test]
fn repeated_non_mergeable_directive_first_wins() {
let mut csp = CSP::new();
csp.push_borrowed(Directive::RequireSriFor(SriFor::Script));
csp.push_borrowed(Directive::RequireSriFor(SriFor::Style));
assert_eq!(csp.normalize().to_string(), "require-sri-for script");
}
}