content_security_policy/
lib.rs

1/*!
2Parse and validate Web [Content-Security-Policy level 3](https://www.w3.org/TR/CSP/)
3
4# Example
5
6```rust
7extern crate content_security_policy;
8use content_security_policy::*;
9fn main() {
10    let csp_list = CspList::parse("script-src *.notriddle.com", PolicySource::Header, PolicyDisposition::Enforce);
11    let (check_result, _) = csp_list.should_request_be_blocked(&Request {
12        url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
13        origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
14        redirect_count: 0,
15        destination: Destination::Script,
16        initiator: Initiator::None,
17        nonce: String::new(),
18        integrity_metadata: String::new(),
19        parser_metadata: ParserMetadata::None,
20    });
21    assert_eq!(check_result, CheckResult::Allowed);
22    let (check_result, _) = csp_list.should_request_be_blocked(&Request {
23        url: Url::parse("https://www.evil.example/script.js").unwrap(),
24        origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
25        redirect_count: 0,
26        destination: Destination::Script,
27        initiator: Initiator::None,
28        nonce: String::new(),
29        integrity_metadata: String::new(),
30        parser_metadata: ParserMetadata::None,
31    });
32    assert_eq!(check_result, CheckResult::Blocked);
33}
34```
35*/
36
37#![forbid(unsafe_code)]
38
39pub extern crate url;
40pub extern crate percent_encoding;
41
42pub(crate) mod text_util;
43pub mod sandboxing_directive;
44
45pub use url::{Origin, Position, Url};
46#[cfg(feature = "serde")] use serde::{Deserialize, Serialize};
47use sha2::Digest;
48use std::borrow::{Borrow, Cow};
49use std::cmp;
50use std::fmt::{self, Display, Formatter};
51use std::str::FromStr;
52use text_util::{
53    strip_leading_and_trailing_ascii_whitespace,
54    split_ascii_whitespace,
55    split_commas,
56    ascii_case_insensitive_match,
57    collect_a_sequence_of_non_ascii_white_space_code_points,
58};
59use sandboxing_directive::{SandboxingFlagSet, parse_a_sandboxing_directive};
60use MatchResult::Matches;
61use MatchResult::DoesNotMatch;
62use std::collections::HashSet;
63use regex::Regex;
64use once_cell::sync::Lazy;
65
66fn scheme_is_network(scheme: &str) -> bool {
67    scheme == "ftp" || scheme_is_httpx(scheme)
68}
69
70fn scheme_is_httpx(scheme: &str) -> bool {
71    scheme == "http" || scheme == "https"
72}
73
74/**
75A single parsed content security policy.
76
77https://www.w3.org/TR/CSP/#content-security-policy-object
78*/
79#[derive(Clone, Debug)]
80#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
81pub struct Policy {
82    pub directive_set: Vec<Directive>,
83    pub disposition: PolicyDisposition,
84    pub source: PolicySource,
85}
86
87impl Display for Policy {
88    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
89        for (i, directive) in self.directive_set.iter().enumerate() {
90            if i != 0 {
91                write!(f, "; ")?;
92            }
93            <Directive as Display>::fmt(directive, f)?;
94        }
95        Ok(())
96    }
97}
98
99impl Policy {
100    pub fn is_valid(&self) -> bool {
101        self.directive_set.iter().all(Directive::is_valid) &&
102            self.directive_set.iter().map(|d| d.name.clone()).collect::<HashSet<_>>().len() == self.directive_set.len() &&
103            !self.directive_set.is_empty()
104    }
105    /// https://www.w3.org/TR/CSP/#parse-serialized-policy
106    pub fn parse(serialized: &str, source: PolicySource, disposition: PolicyDisposition) -> Policy {
107        let mut policy = Policy {
108            directive_set: Vec::new(),
109            source, disposition,
110        };
111        // Rust's str::split corresponds to a WHATWG "strict split"
112        for token in serialized.split(';') {
113            let token = strip_leading_and_trailing_ascii_whitespace(token);
114            if token.is_empty() { continue };
115            let (directive_name, token) =
116                collect_a_sequence_of_non_ascii_white_space_code_points(token);
117            let mut directive_name = directive_name.to_owned();
118            directive_name.make_ascii_lowercase();
119            if policy.contains_a_directive_whose_name_is(&directive_name) {
120                continue;
121            }
122            let directive_value = split_ascii_whitespace(token).map(String::from).collect();
123            policy.directive_set.push(Directive {
124                name: directive_name,
125                value: directive_value,
126            });
127        }
128        policy
129    }
130    pub fn contains_a_directive_whose_name_is(&self, directive_name: &str) -> bool {
131        self.directive_set.iter().any(|d| d.name == directive_name)
132    }
133    /// https://www.w3.org/TR/CSP/#does-request-violate-policy
134    pub fn does_request_violate_policy(&self, request: &Request) -> Violates {
135        if request.initiator == Initiator::Prefetch {
136            return self.does_resource_hint_violate_policy(request);
137        }
138
139        let mut violates = Violates::DoesNotViolate;
140        for directive in &self.directive_set {
141            let result = directive.pre_request_check(request, self);
142            if result == CheckResult::Blocked {
143                violates = Violates::Directive(directive.clone());
144            }
145        }
146        violates
147    }
148
149    /// https://www.w3.org/TR/CSP/#does-resource-hint-violate-policy
150    pub fn does_resource_hint_violate_policy(&self, request: &Request) -> Violates {
151        let default_directive = &self.directive_set.iter()
152            .find(|x| x.name == "default-src");
153
154        if default_directive.is_none() {
155            return Violates::DoesNotViolate;
156        }
157
158        for directive in &self.directive_set {
159            let result = directive.pre_request_check(request, self);
160            if result == CheckResult::Allowed {
161                return Violates::DoesNotViolate;
162            }
163        }
164
165        return Violates::Directive(default_directive.unwrap().clone());
166    }
167}
168
169#[derive(Clone, Debug)]
170#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
171/// https://www.w3.org/TR/CSP/#csp-list
172pub struct CspList(pub Vec<Policy>);
173
174impl Display for CspList {
175    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
176        for (i, directive) in self.0.iter().enumerate() {
177            if i != 0 {
178                write!(f, ",")?;
179            }
180            <Policy as Display>::fmt(directive, f)?;
181        }
182        Ok(())
183    }
184}
185
186/// https://www.w3.org/TR/trusted-types/#trusted-types-csp-directive
187static TRUSTED_POLICY_SOURCE_GRAMMAR: Lazy<Regex> =
188    Lazy::new(|| Regex::new(r#"^[0-9a-zA-Z\-\#=_\/@\.%]+$"#).unwrap());
189
190impl CspList {
191    pub fn is_valid(&self) -> bool {
192        self.0.iter().all(Policy::is_valid)
193    }
194    /// https://www.w3.org/TR/CSP/#contains-a-header-delivered-content-security-policy
195    pub fn contains_a_header_delivered_content_security_policy(&self) -> bool {
196        self.0.iter().any(|policy| policy.source == PolicySource::Header)
197    }
198    /// https://www.w3.org/TR/CSP/#parse-serialized-policy-list
199    pub fn parse(list: &str, source: PolicySource, disposition: PolicyDisposition) -> CspList {
200        let mut policies = Vec::new();
201        for token in split_commas(list) {
202            let policy = Policy::parse(token, source, disposition);
203            if policy.directive_set.is_empty() { continue };
204            policies.push(policy)
205        }
206        CspList(policies)
207    }
208    pub fn append(&mut self, mut other: CspList) {
209        self.0.append(&mut other.0)
210    }
211    pub fn push(&mut self, policy: Policy) {
212        self.0.push(policy)
213    }
214    /**
215    Given a request, this algorithm reports violations based on client’s "report only" policies.
216
217    https://www.w3.org/TR/CSP/#report-for-request
218    */
219    pub fn report_violations_for_request(&self, request: &Request)
220        -> Vec<Violation> {
221        let mut violations = Vec::new();
222        for policy in &self.0 {
223            if policy.disposition == PolicyDisposition::Enforce { continue };
224            let violates = policy.does_request_violate_policy(request);
225            if let Violates::Directive(directive) = violates {
226                let resource = ViolationResource::Url(request.url.clone());
227                violations.push(Violation {
228                    resource,
229                    directive: Directive {
230                        name: get_the_effective_directive_for_request(request).to_owned(),
231                        value: directive.value.clone(),
232                    },
233                    policy: policy.clone(),
234                });
235            }
236        }
237        violations
238    }
239    /**
240    Given a request, this algorithm returns Blocked or Allowed and reports violations based on
241    request’s client’s Content Security Policy.
242
243    https://www.w3.org/TR/CSP/#should-block-request
244    */
245    pub fn should_request_be_blocked(&self, request: &Request) -> (CheckResult, Vec<Violation>) {
246        let mut result = CheckResult::Allowed;
247        let mut violations = Vec::new();
248        for policy in &self.0 {
249            if policy.disposition == PolicyDisposition::Report { continue };
250            let violates = policy.does_request_violate_policy(request);
251            if let Violates::Directive(directive) = violates {
252                result = CheckResult::Blocked;
253                let resource = ViolationResource::Url(request.url.clone());
254                violations.push(Violation {
255                    resource,
256                    directive: Directive {
257                        name: get_the_effective_directive_for_request(request).to_owned(),
258                        value: directive.value.clone(),
259                    },
260                    policy: policy.clone(),
261                });
262            }
263        }
264        (result, violations)
265    }
266    /**
267    Given a response and a request, this algorithm returns Blocked or Allowed, and reports
268    violations based on request’s client’s Content Security Policy.
269
270    https://www.w3.org/TR/CSP/#should-block-response
271    */
272    pub fn should_response_to_request_be_blocked(&self, request: &Request, response: &Response)
273        -> (CheckResult, Vec<Violation>) {
274        // Step 1. Let CSP list be request’s policy container’s CSP list.
275        // step 2. Let result be "Allowed".
276        let mut result = CheckResult::Allowed;
277        let mut violations = Vec::new();
278        // Step 3. For each policy of CSP list:
279        for policy in &self.0 {
280            // Step 3.1. For each directive of policy:
281            for directive in &policy.directive_set {
282                // Step 3.1.1. If the result of executing directive’s post-request check is "Blocked", then:
283                if directive.post_request_check(request, response, policy) == CheckResult::Blocked {
284                    // Step 3.1.1.1. Execute §5.5 Report a violation on the result of executing
285                    // §2.4.2 Create a violation object for request, and policy. on request, and policy.
286                    violations.push(Violation {
287                        resource: ViolationResource::Url(request.url.clone()),
288                        directive: Directive {
289                            name: get_the_effective_directive_for_request(request).to_owned(),
290                            value: directive.value.clone(),
291                        },
292                        policy: policy.clone(),
293                    });
294                    // Step 3.1.1.2. If policy’s disposition is "enforce", then set result to "Blocked".
295                    if policy.disposition == PolicyDisposition::Enforce {
296                        result = CheckResult::Blocked;
297                    }
298                }
299            }
300        }
301        (result, violations)
302    }
303    /// https://www.w3.org/TR/CSP/#should-block-inline
304    pub fn should_elements_inline_type_behavior_be_blocked(&self, element: &Element, type_: InlineCheckType, source: &str) -> (CheckResult, Vec<Violation>) {
305        use CheckResult::*;
306        let mut result = Allowed;
307        let mut violations = Vec::new();
308        for policy in &self.0 {
309            for directive in &policy.directive_set {
310                if directive.inline_check(element, type_, policy, source) == Allowed {
311                    continue;
312                }
313                let sample = if directive.value.iter().any(|t| &t[..] == "'report-sample'") {
314                    let max_length = cmp::min(40, source.len());
315                    Some(source[0..max_length].to_owned())
316                } else {
317                    None
318                };
319                let violation = Violation {
320                    resource: ViolationResource::Inline{ sample },
321                    directive: Directive {
322                        name: get_the_effective_directive_for_inline_checks(type_).to_owned(),
323                        value: directive.value.clone(),
324                    },
325                    policy: policy.clone(),
326                };
327                violations.push(violation);
328                if policy.disposition == PolicyDisposition::Enforce {
329                    result = Blocked;
330                }
331            }
332        }
333        (result, violations)
334    }
335    /**
336    https://www.w3.org/TR/CSP/#allow-base-for-document
337
338    Note that, while this algoritm is defined as operating on a document, the only property it
339    actually uses is the document's CSP List. So this function operates on that.
340    */
341    pub fn is_base_allowed_for_document(&self, base: &Url, self_origin: &Origin) -> (CheckResult, Vec<Violation>) {
342        use CheckResult::*;
343        let mut violations = Vec::new();
344        for policy in &self.0 {
345            let directive = policy.directive_set.iter().find(|directive| directive.name == "base-uri");
346            if let Some(directive) = directive {
347                if SourceList(&directive.value).does_url_match_source_list_in_origin_with_redirect_count(base, &self_origin, 0) == DoesNotMatch {
348                    let violation = Violation {
349                        directive: directive.clone(),
350                        resource: ViolationResource::Inline { sample: None },
351                        policy: policy.clone(),
352                    };
353                    violations.push(violation);
354                    if policy.disposition == PolicyDisposition::Enforce {
355                        return (Blocked, violations);
356                    }
357                }
358            }
359        }
360        return (Allowed, violations);
361    }
362    /**
363    https://w3c.github.io/trusted-types/dist/spec/#should-block-create-policy
364
365    Note that, while this algoritm is defined as operating on a global object, the only property it
366    actually uses is the global's CSP List. So this function operates on that.
367    */
368    pub fn is_trusted_type_policy_creation_allowed(&self, policy_name: &str, created_policy_names: &[&str]) -> (CheckResult, Vec<Violation>) {
369        use CheckResult::*;
370        // Step 1: Let result be "Allowed".
371        let mut result = Allowed;
372        let mut violations = Vec::new();
373        // Step 2: For each policy in global’s CSP list:
374        for policy in &self.0 {
375            // Step 2.1: Let createViolation be false.
376            let mut create_violation = false;
377            // Step 2.2: If policy’s directive set does not contain a directive which name is "trusted-types", skip to the next policy.
378            let directive = policy.directive_set.iter().find(|directive| directive.name == "trusted-types");
379            // Step 2.3: Let directive be the policy’s directive set’s directive which name is "trusted-types"
380            if let Some(directive) = directive {
381                // Step 2.4: If directive’s value only contains a tt-keyword which is a match for a value 'none', set createViolation to true.
382                if directive.value.len() == 1 && directive.value.contains(&"'none'".to_string()) {
383                    create_violation = true;
384                }
385                // Step 2.5: If createdPolicyNames contains policyName and directive’s value does not contain a tt-keyword
386                // which is a match for a value 'allow-duplicates', set createViolation to true.
387                if created_policy_names.contains(&policy_name) && !directive.value.iter().any(|v| v == "'allow-duplicates'") {
388                    create_violation = true;
389                }
390                // Step 2.6: If directive’s value does not contain a tt-policy-name, which value is policyName,
391                // and directive’s value does not contain a tt-wildcard, set createViolation to true.
392                if !(TRUSTED_POLICY_SOURCE_GRAMMAR.is_match(&policy_name) && (directive.value.iter().any(|p| p == policy_name) || directive.value.iter().any(|v| v == "*"))) {
393                    create_violation = true;
394                }
395                // Step 2.7: If createViolation is false, skip to the next policy.
396                if !create_violation {
397                    continue;
398                }
399                let max_length = cmp::min(40, policy_name.len());
400                // Step 2.10: Set violation’s sample to the substring of policyName, containing its first 40 characters.
401                let sample = policy_name[0..max_length].to_owned();
402                // Step 2.8: Let violation be the result of executing Create a violation object for global, policy,
403                // and directive on global, policy and "trusted-types"
404                let violation = Violation {
405                    directive: directive.clone(),
406                    // Step 2.9: Set violation’s resource to "trusted-types-policy".
407                    resource: ViolationResource::TrustedTypePolicy {
408                        // Step 2.10: Set violation’s sample to the substring of policyName, containing its first 40 characters.
409                        sample,
410                    },
411                    policy: policy.clone(),
412                };
413                // Step 2.11: Execute Report a violation on violation.
414                violations.push(violation);
415                // Step 2.12: If policy’s disposition is "enforce", then set result to "Blocked".
416                if policy.disposition == PolicyDisposition::Enforce {
417                    result = Blocked
418                }
419            }
420        }
421        return (result, violations);
422    }
423    /**
424    https://w3c.github.io/trusted-types/dist/spec/#abstract-opdef-does-sink-type-require-trusted-types
425
426    Note that, while this algoritm is defined as operating on a global object, the only property it
427    actually uses is the global's CSP List. So this function operates on that.
428    */
429    pub fn does_sink_type_require_trusted_types(&self, sink_group: &str, include_report_only_policies: bool) -> bool {
430        let sink_group = &sink_group.to_owned();
431        // Step 1: For each policy in global’s CSP list:
432        for policy in &self.0 {
433            // Step 1.1: If policy’s directive set does not contain a directive whose name is "require-trusted-types-for", skip to the next policy.
434            let directive = policy.directive_set.iter().find(|directive| directive.name == "require-trusted-types-for");
435            // Step 1.2: Let directive be the policy’s directive set’s directive whose name is "require-trusted-types-for"
436            if let Some(directive) = directive {
437                // Step 1.3: If directive’s value does not contain a trusted-types-sink-group which is a match for sinkGroup, skip to the next policy.
438                if !directive.value.contains(sink_group) {
439                    continue;
440                }
441                // Step 1.4: Let enforced be true if policy’s disposition is "enforce", and false otherwise.
442                let enforced = policy.disposition == PolicyDisposition::Enforce;
443                // Step 1.5: If enforced is true, return true.
444                if enforced {
445                    return true;
446                }
447                // Step 1.6: If includeReportOnlyPolicies is true, return true.
448                if include_report_only_policies {
449                    return true;
450                }
451            }
452        }
453        // Step 2: Return false.
454        false
455    }
456    /**
457    https://w3c.github.io/trusted-types/dist/spec/#should-block-sink-type-mismatch
458
459    Note that, while this algoritm is defined as operating on a global object, the only property it
460    actually uses is the global's CSP List. So this function operates on that.
461    */
462    pub fn should_sink_type_mismatch_violation_be_blocked_by_csp(&self, sink: &str, sink_group: &str, source: &str) -> (CheckResult, Vec<Violation>) {
463        use CheckResult::*;
464        let sink_group = &sink_group.to_owned();
465        // Step 1: Let result be "Allowed".
466        let mut result = Allowed;
467        let mut violations = Vec::new();
468        // Step 2: Let sample be source.
469        let mut sample = source;
470        // Step 3: If sink is "Function", then:
471        if sink == "Function" {
472            // Step 3.1: If sample starts with "function anonymous", strip that from sample.
473            if sample.starts_with("function anonymous") {
474                sample = &sample[18..];
475                // Step 3.2: Otherwise if sample starts with "async function anonymous", strip that from sample.
476            } else if sample.starts_with("async function anonymous") {
477                sample = &sample[24..];
478                // Step 3.3: Otherwise if sample starts with "function* anonymous", strip that from sample.
479            } else if sample.starts_with("function* anonymous") {
480                sample = &sample[19..];
481                // Step 3.4: Otherwise if sample starts with "async function* anonymous", strip that from sample.
482            } else if sample.starts_with("async function* anonymous") {
483                sample = &sample[25..];
484            }
485        }
486        // Step 4: For each policy in global’s CSP list:
487        for policy in &self.0 {
488            // Step 4.1: If policy’s directive set does not contain a directive whose name is "require-trusted-types-for", skip to the next policy.
489            let directive = policy.directive_set.iter().find(|directive| directive.name == "require-trusted-types-for");
490            // Step 4.2: Let directive be the policy’s directive set’s directive whose name is "require-trusted-types-for"
491            let Some(directive) = directive else { continue };
492            // Step 4.3: If directive’s value does not contain a trusted-types-sink-group which is a match for sinkGroup, skip to the next policy.
493            if !directive.value.contains(sink_group) {
494                continue;
495            }
496            // Step 4.6: Let trimmedSample be the substring of sample, containing its first 40 characters.
497            let max_length = cmp::min(40, sample.len());
498            let trimmed_sample = sample[0..max_length].to_owned();
499            // Step 4.4: Let violation be the result of executing Create a violation object for global, policy,
500            // and directive on global, policy and "require-trusted-types-for"
501            violations.push(Violation {
502                // Step 4.5: Set violation’s resource to "trusted-types-sink".
503                resource: ViolationResource::TrustedTypeSink {
504                    // Step 4.7: Set violation’s sample to be the result of concatenating the list « sink, trimmedSample « using "|" as a separator.
505                    sample: sink.to_owned() + "|" + &trimmed_sample,
506                },
507                directive: directive.clone(),
508                policy: policy.clone(),
509            });
510            // Step 4.9: If policy’s disposition is "enforce", then set result to "Blocked".
511            if policy.disposition == PolicyDisposition::Enforce {
512                result = Blocked
513            }
514        }
515        // Step 2: Return false.
516        (result, violations)
517    }
518    /// <https://html.spec.whatwg.org/multipage/#csp-derived-sandboxing-flags>
519    pub fn get_sandboxing_flag_set_for_document(&self) -> Option<SandboxingFlagSet> {
520        // Step 1. Let directives be an empty ordered set.
521        // Step 2. For each policy in cspList:
522        self.0
523            .iter()
524            .flat_map(|policy| {
525                policy.directive_set
526                    .iter()
527                    // Step 4. Let directive be directives[directives's size − 1].
528                    .rev()
529                    // Step 2.2. If policy's directive set contains a directive whose name is "sandbox",
530                    // then append that directive to directives.
531                    .find(|directive| directive.name == "sandbox")
532                    .and_then(|directive| directive.get_sandboxing_flag_set_for_document(policy))
533            })
534            // Step 3. If directives is empty, then return an empty sandboxing flag set.
535            .next()
536    }
537    /// https://www.w3.org/TR/CSP/#can-compile-strings
538    pub fn is_js_evaluation_allowed(&self, source: &str) -> (CheckResult, Vec<Violation>) {
539        let mut result = CheckResult::Allowed;
540        let mut violations = Vec::new();
541        // Step 5: For each policy of global’s CSP list:
542        for policy in &self.0 {
543            // Step 5.1: Let source-list be null.
544            let directive = policy.directive_set
545                .iter()
546                // Step 5.2: If policy contains a directive whose name is "script-src",
547                // then set source-list to that directive’s value.
548                .find(|directive| directive.name == "script-src")
549                // Step 5.2: Otherwise if policy contains a directive whose name is "default-src",
550                // then set source-list to that directive’s value.
551                .or_else(|| policy.directive_set.iter().find(|directive| directive.name == "default-src"));
552            // Step 5.3: If source-list is not null:
553            let Some(directive) = directive else { continue };
554            let source_list = SourceList(&directive.value);
555            if source_list.does_a_source_list_allow_js_evaluation() == AllowResult::Allows {
556                continue;
557            }
558            // Step 5.3.1: Let trustedTypesRequired be the result of executing
559            // Does sink type require trusted types?, with realm, 'script', and false.
560            let trusted_types_required = self.does_sink_type_require_trusted_types("'script'", false);
561            // Step 5.3.2: If trustedTypesRequired is true and source-list contains a source expression
562            // which is an ASCII case-insensitive match for the string "'trusted-types-eval'", then skip the following steps.
563            if trusted_types_required && directive.value.iter().any(|t| ascii_case_insensitive_match(&t[..], "'trusted-types-eval'")) {
564                continue;
565            }
566            // Step 5.3.3: If source-list contains a source expression which is
567            // an ASCII case-insensitive match for the string "'unsafe-eval'", then skip the following steps.
568            if directive.value.iter().any(|t| ascii_case_insensitive_match(&t[..], "'unsafe-eval'")) {
569                continue;
570            }
571            // Step 5.3.6: If source-list contains the expression "'report-sample'",
572            // then set violation’s sample to the substring of sourceString containing its first 40 characters.
573            let sample = if directive.value.iter().any(|t| &t[..] == "'report-sample'") {
574                let max_length = cmp::min(40, source.len());
575                Some(source[0..max_length].to_owned())
576            } else {
577                None
578            };
579            // Step 5.3.4: Let violation be the result of executing Create a violation object for global, policy,
580            // and directive on global, policy and "require-trusted-types-for"
581            violations.push(Violation {
582                // Step 5.3.5: Set violation’s resource to "eval".
583                resource: ViolationResource::Eval { sample },
584                directive: directive.clone(),
585                policy: policy.clone(),
586            });
587            // Step 5.3.8: If policy’s disposition is "enforce", then set result to "Blocked".
588            if policy.disposition == PolicyDisposition::Enforce {
589                result = CheckResult::Blocked
590            }
591        }
592        (result, violations)
593    }
594    /// https://www.w3.org/TR/CSP/#can-compile-wasm-bytes
595    pub fn is_wasm_evaluation_allowed(&self) -> (CheckResult, Vec<Violation>) {
596        let mut result = CheckResult::Allowed;
597        let mut violations = Vec::new();
598        // Step 3: For each policy of global’s CSP list:
599        for policy in &self.0 {
600            // Step 3.1: Let source-list be null.
601            let directive = policy.directive_set
602                .iter()
603                // Step 3.2: If policy contains a directive whose name is "script-src",
604                // then set source-list to that directive’s value.
605                .find(|directive| directive.name == "script-src")
606                // Step 3.2: Otherwise if policy contains a directive whose name is "default-src",
607                // then set source-list to that directive’s value.
608                .or_else(|| policy.directive_set.iter().find(|directive| directive.name == "default-src"));
609            let Some(directive) = directive else { continue };
610            let source_list = SourceList(&directive.value);
611            // Step 3.3: If source-list is non-null, and does not contain a source expression
612            // which is an ASCII case-insensitive match for the string "'unsafe-eval'",
613            // and does not contain a source expression which is an ASCII case-insensitive
614            // match for the string "'wasm-unsafe-eval'", then:
615            if source_list.does_a_source_list_allow_wasm_evaluation() == AllowResult::Allows {
616                continue;
617            }
618            // Step 3.3.1: Let violation be the result of executing § 2.4.1 Create a violation
619            // object for global, policy, and directive on global, policy, and "script-src".
620            violations.push(Violation {
621                // Step 5.3.5: Set violation’s resource to "wasm-eval".
622                resource: ViolationResource::WasmEval,
623                directive: directive.clone(),
624                policy: policy.clone(),
625            });
626            // Step 3.3.4: If policy’s disposition is "enforce", then set result to "Blocked".
627            if policy.disposition == PolicyDisposition::Enforce {
628                result = CheckResult::Blocked
629            }
630        }
631        (result, violations)
632    }
633    /// <https://w3c.github.io/webappsec-csp/#should-block-navigation-request>
634    ///
635    /// Here, `url_processor` is a callback to process trusted types (if applicable).
636    /// In case the Trusted Types algorithm returns an Error, return a None. Otherwise
637    /// return a Some with the string as provided by the policy.
638    ///
639    /// If trusted types are not applicable, then the `url_processor` can look like this:
640    /// ```rust
641    /// |s: &str| Some(s.to_owned());
642    /// ```
643    pub fn should_navigation_request_be_blocked<TrustedTypesUrlProcessor>(
644        &self, request: &mut Request, navigation_check_type: NavigationCheckType, url_processor: TrustedTypesUrlProcessor) -> (CheckResult, Vec<Violation>)
645    where
646        TrustedTypesUrlProcessor: Fn(&str) -> Option<String>
647    {
648        // Step 1: Let result be "Allowed".
649        let mut result = CheckResult::Allowed;
650        let mut violations = Vec::new();
651        // Step 2: For each policy of navigation request’s policy container’s CSP list:
652        for policy in &self.0 {
653            // Step 2.1: For each directive of policy:
654            for directive in &policy.directive_set {
655                // Step 2.1.1: If directive’s pre-navigation check returns "Allowed"
656                // when executed upon navigation request, type, and policy skip to the next directive.
657                if directive.pre_navigation_check(request, navigation_check_type, &url_processor, policy) == CheckResult::Allowed {
658                    continue;
659                }
660                // Step 2.1.2: Otherwise, let violation be the result of executing
661                // § 2.4.1 Create a violation object for global, policy, and directive
662                // on navigation request’s client’s global object, policy, and directive’s name.
663                violations.push(Violation {
664                    // Step 2.1.3: Set violation’s resource to navigation request’s URL.
665                    resource: ViolationResource::Url(request.url.clone()),
666                    directive: Directive {
667                        name: get_the_effective_directive_for_request(request).to_owned(),
668                        value: directive.value.clone(),
669                    },
670                    policy: policy.clone(),
671                });
672                // Step 2.1.5: If policy’s disposition is "enforce", then set result to "Blocked".
673                if policy.disposition == PolicyDisposition::Enforce {
674                    result = CheckResult::Blocked;
675                }
676            }
677        }
678        // Step 3: If result is "Allowed", and if navigation request’s current URL’s scheme is javascript:
679        if result == CheckResult::Allowed && request.url.scheme() == "javascript" {
680            // Step 3.1: For each policy of navigation request’s policy container’s CSP list:
681            for policy in &self.0 {
682                // Step 3.1.1: For each directive of policy:
683                for directive in &policy.directive_set {
684                    // Step 3.1.1.2: If directive’s inline check returns "Allowed" when executed upon null,
685                    // "navigation" and navigation request’s current URL, skip to the next directive.
686                    if directive.inline_check(&Element { nonce: None }, InlineCheckType::Navigation, policy, request.url.as_str()) == CheckResult::Allowed {
687                        continue;
688                    }
689                    // Step 3.1.1.3: Otherwise, let violation be the result of executing
690                    // § 2.4.1 Create a violation object for global, policy, and directive
691                    // on navigation request’s client’s global object, policy, and directive’s name.
692                    violations.push(Violation {
693                        // Step 3.1.1.4: Set violation’s resource to navigation request’s URL.
694                        resource: ViolationResource::Inline { sample: None },
695                        directive: Directive {
696                            // Step 3.1.1.1: Let directive-name be the result of executing
697                            // § 6.8.2 Get the effective directive for inline checks on type.
698                            name: get_the_effective_directive_for_inline_checks(InlineCheckType::Navigation).to_owned(),
699                            value: directive.value.clone(),
700                        },
701                        policy: policy.clone(),
702                    });
703                    // Step 3.1.1.6: If policy’s disposition is "enforce", then set result to "Blocked".
704                    if policy.disposition == PolicyDisposition::Enforce {
705                        result = CheckResult::Blocked;
706                    }
707                }
708            }
709        }
710        (result, violations)
711    }
712}
713
714#[derive(Clone, Debug)]
715pub struct Element<'a> {
716    /// When there is no nonce, populate this member with `None`.
717    ///
718    /// When the element is not [nonceable], also populate it with `None`.
719    ///
720    /// [nonceable]: https://www.w3.org/TR/CSP/#is-element-nonceable
721    pub nonce: Option<Cow<'a, str>>,
722}
723
724/**
725The valid values for type are "script", "script attribute", "style", and "style attribute".
726
727https://www.w3.org/TR/CSP/#should-block-inline
728*/
729#[derive(Clone, Copy, Debug, Eq, PartialEq)]
730pub enum InlineCheckType {
731    Script,
732    ScriptAttribute,
733    Style,
734    StyleAttribute,
735    Navigation,
736}
737
738/**
739The valid values for type are "form-submission" and "other".
740
741https://w3c.github.io/webappsec-csp/#directive-pre-navigation-check
742*/
743#[derive(Clone, Copy, Debug, Eq, PartialEq)]
744pub enum NavigationCheckType {
745    FormSubmission,
746    Other,
747}
748
749/**
750request to be validated
751
752https://fetch.spec.whatwg.org/#concept-request
753*/
754#[derive(Clone, Debug)]
755pub struct Request {
756    pub url: Url,
757    pub origin: Origin,
758    pub redirect_count: u32,
759    pub destination: Destination,
760    pub initiator: Initiator,
761    pub nonce: String,
762    pub integrity_metadata: String,
763    pub parser_metadata: ParserMetadata,
764}
765
766#[derive(Clone, Copy, Debug, Eq, PartialEq)]
767pub enum ParserMetadata {
768    ParserInserted,
769    NotParserInserted,
770    None,
771}
772
773#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
774#[derive(Clone, Copy, Debug, Eq, PartialEq)]
775pub enum Initiator {
776    Download,
777    ImageSet,
778    Manifest,
779    Prefetch,
780    Prerender,
781    Fetch,
782    Xslt,
783    None,
784}
785
786#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
787#[derive(Clone, Copy, Debug, Eq, PartialEq)]
788pub enum Destination {
789    None,
790    Audio,
791    AudioWorklet,
792    Document,
793    Embed,
794    Font,
795    Frame,
796    IFrame,
797    Image,
798    Json,
799    Manifest,
800    Object,
801    PaintWorklet,
802    Report,
803    Script,
804    ServiceWorker,
805    SharedWorker,
806    Style,
807    Track,
808    Video,
809    WebIdentity,
810    Worker,
811    Xslt,
812}
813
814pub struct InvalidDestination;
815
816impl FromStr for Destination {
817    type Err = InvalidDestination;
818
819    fn from_str(s: &str) -> Result<Self, Self::Err> {
820        let destination = match s {
821            "" => Self::None,
822            "audio" => Self::Audio,
823            "audioworklet" => Self::AudioWorklet,
824            "document" => Self::Document,
825            "embed" => Self::Embed,
826            "font" => Self::Font,
827            "frame" => Self::Frame,
828            "iframe" => Self::IFrame,
829            "image" => Self::Image,
830            "json" => Self::Json,
831            "manifest" => Self::Manifest,
832            "object" => Self::Object,
833            "paintworklet" => Self::PaintWorklet,
834            "report" => Self::Report,
835            "script" => Self::Script,
836            "serviceworker" => Self::ServiceWorker,
837            "sharedworker" => Self::SharedWorker,
838            "style" => Self::Style,
839            "track" => Self::Track,
840            "video" => Self::Video,
841            "webidentity" => Self::WebIdentity,
842            "worker" => Self::Worker,
843            "xslt" => Self::Xslt,
844            _ => return Err(InvalidDestination),
845        };
846
847        Ok(destination)
848    }
849}
850
851impl Destination {
852    /// https://fetch.spec.whatwg.org/#request-destination-script-like
853    pub fn is_script_like(self) -> bool {
854        use Destination::*;
855        matches!(self, AudioWorklet | PaintWorklet | Script | ServiceWorker | SharedWorker | Worker | Xslt)
856    }
857
858    pub const fn as_str(&self) -> &'static str {
859        match self {
860            Self::None => "",
861            Self::Audio => "audio",
862            Self::AudioWorklet => "audioworklet",
863            Self::Document => "document",
864            Self::Embed => "embed",
865            Self::Font => "font",
866            Self::Frame => "frame",
867            Self::IFrame => "iframe",
868            Self::Image => "image",
869            Self::Json => "json",
870            Self::Manifest => "manifest",
871            Self::Object => "object",
872            Self::PaintWorklet => "paintworklet",
873            Self::Report => "report",
874            Self::Script => "script",
875            Self::ServiceWorker => "serviceworker",
876            Self::SharedWorker => "sharedworker",
877            Self::Style => "style",
878            Self::Track => "track",
879            Self::Video => "video",
880            Self::WebIdentity => "webidentity",
881            Self::Worker => "worker",
882            Self::Xslt => "xslt",
883        }
884    }
885}
886
887/**
888response to be validated
889https://fetch.spec.whatwg.org/#concept-response
890*/
891#[derive(Clone, Debug)]
892pub struct Response {
893    pub url: Url,
894    pub redirect_count: u32,
895}
896
897/**
898violation information
899
900https://www.w3.org/TR/CSP/#violation
901*/
902#[derive(Clone, Debug)]
903#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
904pub struct Violation {
905    pub resource: ViolationResource,
906    pub directive: Directive,
907    pub policy: Policy,
908}
909
910/**
911violation information
912
913https://www.w3.org/TR/CSP/#violation
914*/
915#[derive(Clone, Debug)]
916#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
917pub enum ViolationResource {
918    Url(Url),
919    Inline {
920        sample: Option<String>,
921    },
922    TrustedTypePolicy {
923        sample: String,
924    },
925    TrustedTypeSink {
926        sample: String,
927    },
928    Eval {
929        sample: Option<String>,
930    },
931    WasmEval,
932}
933
934/**
935Many algorithms are allowed to return either "Allowed" or "Blocked".
936The spec describes these as strings.
937*/
938#[derive(Clone, Debug, Eq, PartialEq)]
939#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
940pub enum CheckResult {
941    Allowed,
942    Blocked,
943}
944
945/**
946https://www.w3.org/TR/CSP/#does-request-violate-policy
947*/
948#[derive(Clone, Debug, Eq, PartialEq)]
949#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
950pub enum Violates {
951    DoesNotViolate,
952    Directive(Directive),
953}
954
955/// https://www.w3.org/TR/CSP/#policy-disposition
956#[derive(Clone, Copy, Debug, Eq, PartialEq)]
957#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
958pub enum PolicyDisposition {
959    Enforce,
960    Report,
961}
962
963/// https://www.w3.org/TR/CSP/#policy-source
964#[derive(Clone, Copy, Debug, Eq, PartialEq)]
965#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
966pub enum PolicySource {
967    Header,
968    Meta,
969}
970
971/// https://www.w3.org/TR/CSP/#directives
972#[derive(Clone, Debug, Eq, PartialEq)]
973#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
974pub struct Directive {
975    pub name: String,
976    pub value: Vec<String>,
977}
978
979impl Display for Directive {
980    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
981        <str as Display>::fmt(&self.name[..], f)?;
982        write!(f, " ")?;
983        for (i, token) in self.value.iter().enumerate() {
984            if i != 0 {
985                write!(f, " ")?;
986            }
987            <str as Display>::fmt(&token[..], f)?;
988        }
989        Ok(())
990    }
991}
992
993impl Directive {
994    /// https://www.w3.org/TR/CSP/#serialized-directive
995    pub fn is_valid(&self) -> bool {
996        DIRECTIVE_NAME_GRAMMAR.is_match(&self.name) &&
997            self.value.iter().all(|t| DIRECTIVE_VALUE_TOKEN_GRAMMAR.is_match(&t[..]))
998    }
999    /// https://www.w3.org/TR/CSP/#directive-pre-request-check
1000    pub fn pre_request_check(&self, request: &Request, policy: &Policy) -> CheckResult {
1001        use CheckResult::*;
1002        match &self.name[..] {
1003            "child-src" => {
1004                let name = get_the_effective_directive_for_request(request);
1005                if !should_fetch_directive_execute(name, "child-src", policy) {
1006                    return Allowed;
1007                }
1008                (Directive {
1009                    name: String::from(name),
1010                    value: self.value.clone(),
1011                }).pre_request_check(request, policy)
1012            },
1013            "connect-src" => {
1014                let name = get_the_effective_directive_for_request(request);
1015                if !should_fetch_directive_execute(name, "connect-src", policy) {
1016                    return Allowed;
1017                }
1018                if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1019                    return Blocked;
1020                }
1021                Allowed
1022            }
1023            "default-src" => {
1024                let name = get_the_effective_directive_for_request(request);
1025                if !should_fetch_directive_execute(name, "default-src", policy) {
1026                    return Allowed;
1027                }
1028                (Directive {
1029                    name: String::from(name),
1030                    value: self.value.clone(),
1031                }).pre_request_check(request, policy)
1032            }
1033            "font-src" => {
1034                let name = get_the_effective_directive_for_request(request);
1035                if !should_fetch_directive_execute(name, "font-src", policy) {
1036                    return Allowed;
1037                }
1038                if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1039                    return Blocked;
1040                }
1041                Allowed
1042            }
1043            "frame-src" => {
1044                let name = get_the_effective_directive_for_request(request);
1045                if !should_fetch_directive_execute(name, "frame-src", policy) {
1046                    return Allowed;
1047                }
1048                if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1049                    return Blocked;
1050                }
1051                Allowed
1052            }
1053            "img-src" => {
1054                let name = get_the_effective_directive_for_request(request);
1055                if !should_fetch_directive_execute(name, "img-src", policy) {
1056                    return Allowed;
1057                }
1058                if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1059                    return Blocked;
1060                }
1061                Allowed
1062            }
1063            "manifest-src" => {
1064                let name = get_the_effective_directive_for_request(request);
1065                if !should_fetch_directive_execute(name, "manifest-src", policy) {
1066                    return Allowed;
1067                }
1068                if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1069                    return Blocked;
1070                }
1071                Allowed
1072            }
1073            "media-src" => {
1074                let name = get_the_effective_directive_for_request(request);
1075                if !should_fetch_directive_execute(name, "media-src", policy) {
1076                    return Allowed;
1077                }
1078                if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1079                    return Blocked;
1080                }
1081                Allowed
1082            }
1083            "object-src" => {
1084                let name = get_the_effective_directive_for_request(request);
1085                if !should_fetch_directive_execute(name, "object-src", policy) {
1086                    return Allowed;
1087                }
1088                if SourceList(&self.value[..]).does_request_match_source_list(request) == DoesNotMatch {
1089                    return Blocked;
1090                }
1091                Allowed
1092            }
1093            "script-src" => {
1094                let name = get_the_effective_directive_for_request(request);
1095                if !should_fetch_directive_execute(name, "script-src", policy) {
1096                    return Allowed;
1097                }
1098                script_directives_prerequest_check(request, self)
1099            }
1100            "script-src-elem" => {
1101                let name = get_the_effective_directive_for_request(request);
1102                if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1103                    return Allowed;
1104                }
1105                script_directives_prerequest_check(request, self)
1106            }
1107            "style-src" => {
1108                let name = get_the_effective_directive_for_request(request);
1109                if !should_fetch_directive_execute(name, "style-src", policy) {
1110                    return Allowed;
1111                }
1112                let source_list = SourceList(&self.value);
1113                if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1114                    return Allowed;
1115                }
1116                if source_list.does_request_match_source_list(request) == DoesNotMatch {
1117                    return Blocked;
1118                }
1119                Allowed
1120            }
1121            "style-src-elem" => {
1122                let name = get_the_effective_directive_for_request(request);
1123                if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1124                    return Allowed;
1125                }
1126                let source_list = SourceList(&self.value);
1127                if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1128                    return Allowed;
1129                }
1130                if source_list.does_request_match_source_list(request) == DoesNotMatch {
1131                    return Blocked;
1132                }
1133                Allowed
1134            }
1135            "worker-src" => {
1136                let name = get_the_effective_directive_for_request(request);
1137                if !should_fetch_directive_execute(name, "worker-src", policy) {
1138                    return Allowed;
1139                }
1140                let source_list = SourceList(&self.value);
1141                if source_list.does_request_match_source_list(request) == DoesNotMatch {
1142                    return Blocked;
1143                }
1144                Allowed
1145            }
1146            _ => Allowed,
1147        }
1148    }
1149    /// https://www.w3.org/TR/CSP/#directive-post-request-check
1150    pub fn post_request_check(&self, request: &Request, response: &Response, policy: &Policy) -> CheckResult {
1151        use CheckResult::*;
1152        match &self.name[..] {
1153            "child-src" => {
1154                let name = get_the_effective_directive_for_request(request);
1155                if !should_fetch_directive_execute(name, "child-src", policy) {
1156                    return Allowed;
1157                }
1158                Directive {
1159                    name: name.to_owned(),
1160                    value: self.value.clone()
1161                }.post_request_check(request, response, policy)
1162            }
1163            "connect-src" => {
1164                let name = get_the_effective_directive_for_request(request);
1165                if !should_fetch_directive_execute(name, "connect-src", policy) {
1166                    return Allowed;
1167                }
1168                let source_list = SourceList(&self.value);
1169                if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1170                    return Blocked;
1171                }
1172                Allowed
1173            }
1174            "default-src" => {
1175                let name = get_the_effective_directive_for_request(request);
1176                if !should_fetch_directive_execute(name, "default-src", policy) {
1177                    return Allowed;
1178                }
1179                Directive {
1180                    name: name.to_owned(),
1181                    value: self.value.clone(),
1182                }.post_request_check(request, response, policy)
1183            }
1184            "font-src" => {
1185                let name = get_the_effective_directive_for_request(request);
1186                if !should_fetch_directive_execute(name, "font-src", policy) {
1187                    return Allowed;
1188                }
1189                let source_list = SourceList(&self.value);
1190                if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1191                    return Blocked;
1192                }
1193                Allowed
1194            }
1195            "frame-src" => {
1196                let name = get_the_effective_directive_for_request(request);
1197                if !should_fetch_directive_execute(name, "frame-src", policy) {
1198                    return Allowed;
1199                }
1200                let source_list = SourceList(&self.value);
1201                if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1202                    return Blocked;
1203                }
1204                Allowed
1205            }
1206            "img-src" => {
1207                let name = get_the_effective_directive_for_request(request);
1208                if !should_fetch_directive_execute(name, "img-src", policy) {
1209                    return Allowed;
1210                }
1211                let source_list = SourceList(&self.value);
1212                if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1213                    return Blocked;
1214                }
1215                Allowed
1216            }
1217            "manifest-src" => {
1218                let name = get_the_effective_directive_for_request(request);
1219                if !should_fetch_directive_execute(name, "manifest-src", policy) {
1220                    return Allowed;
1221                }
1222                let source_list = SourceList(&self.value);
1223                if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1224                    return Blocked;
1225                }
1226                Allowed
1227            }
1228            "media-src" => {
1229                let name = get_the_effective_directive_for_request(request);
1230                if !should_fetch_directive_execute(name, "media-src", policy) {
1231                    return Allowed;
1232                }
1233                let source_list = SourceList(&self.value);
1234                if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1235                    return Blocked;
1236                }
1237                Allowed
1238            }
1239            "object-src" => {
1240                let name = get_the_effective_directive_for_request(request);
1241                if !should_fetch_directive_execute(name, "object-src", policy) {
1242                    return Allowed;
1243                }
1244                let source_list = SourceList(&self.value);
1245                if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1246                    return Blocked;
1247                }
1248                Allowed
1249            }
1250            "script-src" => {
1251                let name = get_the_effective_directive_for_request(request);
1252                if !should_fetch_directive_execute(name, "script-src", policy) {
1253                    return Allowed;
1254                }
1255                script_directives_postrequest_check(request, response, self)
1256            }
1257            "script-src-elem" => {
1258                let name = get_the_effective_directive_for_request(request);
1259                if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1260                    return Allowed;
1261                }
1262                script_directives_postrequest_check(request, response, self)
1263            }
1264            "style-src" => {
1265                let name = get_the_effective_directive_for_request(request);
1266                if !should_fetch_directive_execute(name, "style-src", policy) {
1267                    return Allowed;
1268                }
1269                let source_list = SourceList(&self.value);
1270                if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1271                    return Allowed;
1272                }
1273                if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1274                    return Blocked;
1275                }
1276                Allowed
1277            }
1278            "style-src-elem" => {
1279                let name = get_the_effective_directive_for_request(request);
1280                if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1281                    return Allowed;
1282                }
1283                let source_list = SourceList(&self.value);
1284                if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1285                    return Allowed;
1286                }
1287                if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1288                    return Blocked;
1289                }
1290                Allowed
1291            }
1292            "worker-src" => {
1293                let name = get_the_effective_directive_for_request(request);
1294                if !should_fetch_directive_execute(name, "worker-src", policy) {
1295                    return Allowed;
1296                }
1297                let source_list = SourceList(&self.value);
1298                if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1299                    return Blocked;
1300                }
1301                Allowed
1302            }
1303            _ => Allowed,
1304        }
1305    }
1306    /// https://www.w3.org/TR/CSP/#directive-inline-check
1307    pub fn inline_check(&self, element: &Element, type_: InlineCheckType, policy: &Policy, source: &str) -> CheckResult {
1308        use CheckResult::*;
1309        match &self.name[..] {
1310            "default-src" => {
1311                let name = get_the_effective_directive_for_inline_checks(type_);
1312                if !should_fetch_directive_execute(name, "default-src", policy) {
1313                    return Allowed;
1314                }
1315                Directive {
1316                    name: name.to_owned(),
1317                    value: self.value.clone()
1318                }.inline_check(element, type_, policy, source)
1319            }
1320            "script-src" => {
1321                let name = get_the_effective_directive_for_inline_checks(type_);
1322                if !should_fetch_directive_execute(name, "script-src", policy) {
1323                    return Allowed;
1324                }
1325                let source_list = SourceList(&self.value);
1326                if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1327                    return Blocked;
1328                }
1329                Allowed
1330            }
1331            "script-src-elem" => {
1332                let name = get_the_effective_directive_for_inline_checks(type_);
1333                if !should_fetch_directive_execute(name, "script-src-elem", policy) {
1334                    return Allowed;
1335                }
1336                let source_list = SourceList(&self.value);
1337                if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1338                    return Blocked;
1339                }
1340                Allowed
1341            }
1342            "script-src-attr" => {
1343                let name = get_the_effective_directive_for_inline_checks(type_);
1344                if !should_fetch_directive_execute(name, "script-src-attr", policy) {
1345                    return Allowed;
1346                }
1347                let source_list = SourceList(&self.value);
1348                if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1349                    return Blocked;
1350                }
1351                Allowed
1352            }
1353            "style-src" => {
1354                let name = get_the_effective_directive_for_inline_checks(type_);
1355                if !should_fetch_directive_execute(name, "style-src", policy) {
1356                    return Allowed;
1357                }
1358                let source_list = SourceList(&self.value);
1359                if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1360                    return Blocked;
1361                }
1362                Allowed
1363            }
1364            "style-src-elem" => {
1365                let name = get_the_effective_directive_for_inline_checks(type_);
1366                if !should_fetch_directive_execute(name, "style-src-elem", policy) {
1367                    return Allowed;
1368                }
1369                let source_list = SourceList(&self.value);
1370                if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1371                    return Blocked;
1372                }
1373                Allowed
1374            }
1375            "style-src-attr" => {
1376                let name = get_the_effective_directive_for_inline_checks(type_);
1377                if !should_fetch_directive_execute(name, "style-src-attr", policy) {
1378                    return Allowed;
1379                }
1380                let source_list = SourceList(&self.value);
1381                if source_list.does_element_match_source_list_for_type_and_source(element, type_, source) == DoesNotMatch {
1382                    return Blocked;
1383                }
1384                Allowed
1385            }
1386            _ => Allowed
1387        }
1388    }
1389    /// <https://html.spec.whatwg.org/multipage/#csp-derived-sandboxing-flags>
1390    pub fn get_sandboxing_flag_set_for_document(&self, policy: &Policy) -> Option<SandboxingFlagSet> {
1391        debug_assert!(&self.name[..] == "sandbox");
1392        // Step 2.1. If policy's disposition is not "enforce", then continue.
1393        if policy.disposition != PolicyDisposition::Enforce {
1394            None
1395        } else {
1396            // Step 5. Return the result of parsing the sandboxing directive directive.
1397            Some(parse_a_sandboxing_directive(&self.value[..]))
1398        }
1399    }
1400    /// <https://w3c.github.io/webappsec-csp/#directive-pre-navigation-check>
1401    pub fn pre_navigation_check<TrustedTypesUrlProcessor>(
1402        &self, request: &mut Request, type_: NavigationCheckType, url_processor: &TrustedTypesUrlProcessor, _policy: &Policy) -> CheckResult
1403    where
1404        TrustedTypesUrlProcessor: Fn(&str) -> Option<String>
1405    {
1406        use CheckResult::*;
1407        match &self.name[..] {
1408            // <https://w3c.github.io/webappsec-csp/#form-action-pre-navigate>
1409            "form-action" => {
1410                // Step 2: If navigation type is "form-submission":
1411                if type_ == NavigationCheckType::FormSubmission {
1412                    let source_list = SourceList(&self.value);
1413                    // Step 2.1: If the result of executing § 6.7.2.5 Does request match source list? on request,
1414                    // this directive’s value, and a policy, is "Does Not Match", return "Blocked".
1415                    if source_list.does_request_match_source_list(request) == DoesNotMatch {
1416                        return Blocked;
1417                    }
1418                }
1419                // Step 3: Return "Allowed".
1420                Allowed
1421            },
1422            // <https://www.w3.org/TR/trusted-types/#require-trusted-types-for-pre-navigation-check>
1423            "require-trusted-types-for" => {
1424                let url = &request.url;
1425                // Step 1. If request’s url’s scheme is not "javascript", return "Allowed" and abort further steps.
1426                if url.scheme() != "javascript" {
1427                    return Allowed;
1428                }
1429                // Step 2. Let urlString be the result of running the URL serializer on request’s url.
1430                //
1431                // Already done when creating Request
1432                // Step 3. Let encodedScriptSource be the result of removing the leading "javascript:" from urlString.
1433                let encoded_script_source = &url[Position::AfterScheme..][1..];
1434                // Step 4. Let convertedScriptSource be the result of executing Process value with a default policy algorithm
1435                // If that algorithm threw an error or convertedScriptSource is not a TrustedScript object,
1436                // return "Blocked" and abort further steps.
1437                let Some(converted_script_source) = url_processor(encoded_script_source) else {
1438                    return Blocked;
1439                };
1440                // Step 5. Set urlString to be the result of prepending "javascript:" to stringified convertedScriptSource.
1441                let url_string = "javascript:".to_owned() + &converted_script_source;
1442                // Step 6. Let newURL be the result of running the URL parser on urlString.
1443                // If the parser returns a failure, return "Blocked" and abort further steps.
1444                let Ok(new_url) = Url::parse(&url_string) else {
1445                    return Blocked;
1446                };
1447                // Step 7. Set request’s url to newURL.
1448                request.url = new_url;
1449                // Step 8. Return "Allowed".
1450                Allowed
1451            }
1452            _ => Allowed,
1453        }
1454    }
1455}
1456
1457/// https://www.w3.org/TR/CSP/#effective-directive-for-inline-check
1458fn get_the_effective_directive_for_inline_checks(type_: InlineCheckType) -> &'static str {
1459    use InlineCheckType::*;
1460    match type_ {
1461        Script | Navigation => "script-src-elem",
1462        ScriptAttribute => "script-src-attr",
1463        Style => "style-src-elem",
1464        StyleAttribute => "style-src-attr",
1465    }
1466}
1467
1468/// <https://www.w3.org/TR/CSP/#script-pre-request>
1469fn script_directives_prerequest_check(request: &Request, directive: &Directive) -> CheckResult {
1470    use CheckResult::*;
1471    // Step 1. If request’s destination is script-like:
1472    if request_is_script_like(request) {
1473        let source_list = SourceList(&directive.value[..]);
1474        // Step 1.1. If the result of executing § 6.7.2.3 Does nonce match source list? on
1475        // request’s cryptographic nonce metadata and this directive’s value is "Matches", return "Allowed".
1476        if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1477            return Allowed;
1478        }
1479        // Step 1.2. If the result of executing § 6.7.2.4 Does integrity metadata match source list? on
1480        // request’s integrity metadata and this directive’s value is "Matches", return "Allowed".
1481        if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata) == Matches {
1482            return Allowed;
1483        }
1484        // Step 1.3. If directive’s value contains a source expression that is an
1485        // ASCII case-insensitive match for the "'strict-dynamic'" keyword-source:
1486        if directive.value.iter().any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'")) {
1487            // Step 1.3.1. If the request’s parser metadata is "parser-inserted", return "Blocked".
1488            if request.parser_metadata == ParserMetadata::ParserInserted {
1489                return Blocked;
1490            }
1491            // Otherwise, return "Allowed".
1492            return Allowed;
1493        }
1494
1495        // Step 1.4. If the result of executing § 6.7.2.5 Does request match source list? on
1496        // request, directive’s value, and policy, is "Does Not Match", return "Blocked".
1497        if source_list.does_request_match_source_list(request) == DoesNotMatch {
1498            return Blocked;
1499        }
1500    }
1501    // Step 2. Return "Allowed".
1502    Allowed
1503}
1504
1505/// https://www.w3.org/TR/CSP/#script-post-request
1506fn script_directives_postrequest_check(request: &Request, response: &Response, directive: &Directive) -> CheckResult {
1507    use CheckResult::*;
1508    // Step 1. If request’s destination is script-like:
1509    if request_is_script_like(request) {
1510        // Step 1.1. Call potentially report hash with response, request, directive and policy.
1511        // TODO
1512        let source_list = SourceList(&directive.value[..]);
1513        // Step 1.2. If the result of executing § 6.7.2.3 Does nonce match source list? on
1514        // request’s cryptographic nonce metadata and this directive’s value is "Matches", return "Allowed".
1515        if source_list.does_nonce_match_source_list(&request.nonce) == Matches {
1516            return Allowed;
1517        }
1518        // Step 1.3. If the result of executing § 6.7.2.4 Does integrity metadata match source list? on
1519        // request’s integrity metadata and this directive’s value is "Matches", return "Allowed".
1520        if source_list.does_integrity_metadata_match_source_list(&request.integrity_metadata) == Matches {
1521            return Allowed;
1522        }
1523        // Step 1.4. If directive’s value contains "'strict-dynamic'":
1524        if directive.value.iter().any(|ex| ascii_case_insensitive_match(ex, "'strict-dynamic'")) {
1525            // Step 1.4.1. If the request’s parser metadata is "parser-inserted", return "Blocked".
1526            if request.parser_metadata == ParserMetadata::ParserInserted {
1527                return Blocked;
1528            }
1529            // Otherwise, return "Allowed".
1530            return Allowed;
1531        }
1532        // Step 1.5. If the result of executing § 6.7.2.6 Does response to request match source list? on
1533        // response, request, directive’s value, and policy, is "Does Not Match", return "Blocked".
1534        if source_list.does_response_to_request_match_source_list(request, response) == DoesNotMatch {
1535            return Blocked;
1536        }
1537    }
1538    // Step 2. Return "Allowed".
1539    Allowed
1540}
1541
1542/// https://fetch.spec.whatwg.org/#request-destination-script-like
1543fn request_is_script_like(request: &Request) -> bool {
1544    request.destination.is_script_like()
1545}
1546
1547/// https://www.w3.org/TR/CSP/#should-directive-execute
1548fn should_fetch_directive_execute(effective_directive_name: &str, directive_name: &str, policy: &Policy) -> bool {
1549    let directive_fallback_list = get_fetch_directive_fallback_list(effective_directive_name);
1550    for fallback_directive in directive_fallback_list {
1551        if directive_name == *fallback_directive {
1552            return true;
1553        }
1554        if policy.contains_a_directive_whose_name_is(fallback_directive) {
1555            return false;
1556        }
1557    }
1558    false
1559}
1560
1561/// https://www.w3.org/TR/CSP/#directive-fallback-list
1562fn get_fetch_directive_fallback_list(directive_name: &str) -> &'static [&'static str] {
1563    match directive_name {
1564        "script-src-elem" => &["script-src-elem", "script-src", "default-src"],
1565        "script-src-attr" => &["script-src-attr", "script-src", "default-src"],
1566        "style-src-elem"  => &["style-src-elem", "style-src", "default-src"],
1567        "style-src-attr"  => &["style-src-attr", "style-src", "default-src"],
1568        "worker-src"      => &["worker-src", "child-src", "script-src", "default-src"],
1569        "connect-src"     => &["connect-src", "default-src"],
1570        "manifest-src"    => &["manifest-src", "default-src"],
1571        "object-src"      => &["object-src", "default-src"],
1572        "frame-src"       => &["frame-src", "child-src", "default-src"],
1573        "media-src"       => &["media-src", "default-src"],
1574        "font-src"        => &["font-src", "default-src"],
1575        "img-src"         => &["img-src", "default-src"],
1576        _                 => &[],
1577    }
1578}
1579
1580/// https://www.w3.org/TR/CSP/#effective-directive-for-a-request
1581fn get_the_effective_directive_for_request(request: &Request) -> &'static str {
1582    use Initiator::*;
1583    use Destination::*;
1584    // Step 1: If request’s initiator is "prefetch" or "prerender", return default-src.
1585    if request.initiator == Prefetch || request.initiator == Prerender {
1586        return "default-src";
1587    }
1588    // Step 2: Switch on request’s destination, and execute the associated steps:
1589    match request.destination {
1590        Destination::Manifest => "manifest-src",
1591        Object | Embed => "object-src",
1592        Frame | IFrame => "frame-src",
1593        Audio | Track | Video => "media-src",
1594        Font => "font-src",
1595        Image => "img-src",
1596        Style => "style-src-elem",
1597        Script | Destination::Xslt | AudioWorklet | PaintWorklet => "script-src-elem",
1598        ServiceWorker | SharedWorker | Worker => "worker-src",
1599        Json | WebIdentity => "connect-src",
1600        Report => "",
1601        // Step 3: Return connect-src.
1602        _ => "connect-src",
1603    }
1604}
1605
1606/// https://www.w3.org/TR/CSP/#match-element-to-source-list
1607#[derive(Clone, Debug, Eq, PartialEq)]
1608pub enum MatchResult {
1609    Matches,
1610    DoesNotMatch,
1611}
1612
1613/// https://www.w3.org/TR/CSP/#grammardef-directive-name
1614static DIRECTIVE_NAME_GRAMMAR: Lazy<Regex> =
1615    Lazy::new(|| Regex::new(r#"^[0-9a-z\-]+$"#).unwrap());
1616/// https://www.w3.org/TR/CSP/#grammardef-directive-value
1617static DIRECTIVE_VALUE_TOKEN_GRAMMAR: Lazy<Regex> =
1618    Lazy::new(|| Regex::new(r#"^[\u{21}-\u{2B}\u{2D}-\u{3A}\u{3C}-\u{7E}]+$"#).unwrap());
1619/// https://www.w3.org/TR/CSP/#grammardef-nonce-source
1620static NONCE_SOURCE_GRAMMAR: Lazy<Regex> =
1621    Lazy::new(|| Regex::new(r#"^'nonce-(?P<n>[a-zA-Z0-9\+/\-_]+=*)'$"#).unwrap());
1622static NONE_SOURCE_GRAMMAR: Lazy<Regex> =
1623    Lazy::new(|| Regex::new(r#"^'none'$"#).unwrap());
1624/// https://www.w3.org/TR/CSP/#grammardef-scheme-source
1625static SCHEME_SOURCE_GRAMMAR: Lazy<Regex> =
1626    Lazy::new(|| Regex::new(r#"^(?P<scheme>[a-zA-Z][a-zA-Z0-9\+\-\.]*):$"#).unwrap());
1627/// https://www.w3.org/TR/CSP/#grammardef-host-source
1628static HOST_SOURCE_GRAMMAR: Lazy<Regex> =
1629    Lazy::new(|| Regex::new(r#"^((?P<scheme>[a-zA-Z][a-zA-Z0-9\+\-\.]*)://)?(?P<host>\*|(\*\.)?[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*)(?P<port>:(\*|[0-9]+))?(?P<path>/([:@%!\$&'\(\)\*\+,;=0-9a-zA-Z\-\._~]+)?(/[:@%!\$&'\(\)\*\+,;=0-9a-zA-Z\-\._~]*)*)?$"#).unwrap());
1630/// https://www.w3.org/TR/CSP/#grammardef-hash-source
1631static HASH_SOURCE_GRAMMAR: Lazy<Regex> =
1632    Lazy::new(|| Regex::new(r#"^'(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)'$"#).unwrap());
1633
1634/// https://www.w3.org/TR/CSP/#framework-directive-source-list
1635#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1636struct SourceList<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item=&'a U>>(I);
1637
1638impl<'a, U: 'a + ?Sized + Borrow<str>, I: Clone + IntoIterator<Item=&'a U>> SourceList<'a, U, I> {
1639    /// https://www.w3.org/TR/CSP/#match-nonce-to-source-list
1640    fn does_nonce_match_source_list(&self, nonce: &str) -> MatchResult {
1641        if nonce.is_empty() { return DoesNotMatch };
1642        for expression in self.0.clone().into_iter() {
1643            if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression.borrow()) {
1644                if let Some(captured_nonce) = captures.name("n") {
1645                    if nonce == captured_nonce.as_str() {
1646                        return Matches;
1647                    }
1648                }
1649            }
1650        }
1651        DoesNotMatch
1652    }
1653    /// https://www.w3.org/TR/CSP/#match-integrity-metadata-to-source-list
1654    fn does_integrity_metadata_match_source_list(&self, integrity_metadata: &str) -> MatchResult {
1655        // Step 2: Let integrity expressions be the set of source expressions in source list that match the hash-source grammar.
1656        let integrity_expressions: Vec<HashFunction> = self.0.clone().into_iter()
1657            .filter_map(|expression| {
1658                if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression.borrow()) {
1659                    if let (Some(algorithm), Some(value)) = (captures.name("algorithm").and_then(|a| HashAlgorithm::from_name(a.as_str())), captures.name("value")) {
1660                        return Some(HashFunction{ algorithm, value: String::from(value.as_str()) });
1661                    }
1662                }
1663                None
1664            })
1665            .collect();
1666        // Step 3: If integrity expressions is empty, return "Does Not Match".
1667        if integrity_expressions.is_empty() {
1668            return DoesNotMatch;
1669        }
1670        // Step 4: Let integrity sources be the result of executing the algorithm defined in SRI § 3.3.3 Parse metadata. on integrity metadata.
1671        let integrity_sources = parse_subresource_integrity_metadata(integrity_metadata);
1672        match integrity_sources {
1673            // Step 5: If integrity sources is "no metadata" or an empty set, return "Does Not Match".
1674            SubresourceIntegrityMetadata::NoMetadata => DoesNotMatch,
1675            SubresourceIntegrityMetadata::IntegritySources(integrity_sources) => {
1676                if integrity_sources.is_empty() {
1677                    return DoesNotMatch;
1678                }
1679                // Step 6: For each source of integrity sources:
1680                for source in &integrity_sources {
1681                    // Step 6.1: If integrity expressions does not contain a source expression whose hash-algorithm
1682                    // is an ASCII case-insensitive match for source’s hash-algorithm,
1683                    // and whose base64-value is identical to source’s base64-value, return "Does Not Match".
1684                    //
1685                    // Note that the case-insensitivy is already handled in HashAlgorithm::from_name and therefore
1686                    // we can do a simple equals check here for both algorithm and value.
1687                    if !integrity_expressions.iter().any(|ex| ex == source) {
1688                        return DoesNotMatch;
1689                    }
1690                }
1691                // Step 7: Return "Matches".
1692                Matches
1693            }
1694        }
1695    }
1696    /// https://www.w3.org/TR/CSP/#match-request-to-source-list
1697    fn does_request_match_source_list(&self, request: &Request) -> MatchResult {
1698        self.does_url_match_source_list_in_origin_with_redirect_count(
1699            &request.url,
1700            &request.origin,
1701            request.redirect_count,
1702        )
1703    }
1704    /// https://www.w3.org/TR/CSP/#match-url-to-source-list
1705    fn does_url_match_source_list_in_origin_with_redirect_count(
1706        &self,
1707        url: &Url,
1708        origin: &Origin,
1709        redirect_count: u32,
1710    ) -> MatchResult {
1711        for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1712            if NONE_SOURCE_GRAMMAR.is_match(expression) { continue };
1713            let result = does_url_match_expression_in_origin_with_redirect_count(
1714                url,
1715                expression,
1716                origin,
1717                redirect_count
1718            );
1719            if result == Matches {
1720                return Matches;
1721            }
1722        }
1723        DoesNotMatch
1724    }
1725    /// https://www.w3.org/TR/CSP/#match-element-to-source-list
1726    fn does_element_match_source_list_for_type_and_source(
1727        &self,
1728        element: &Element,
1729        type_: InlineCheckType,
1730        source: &str,
1731    ) -> MatchResult {
1732        if self.does_a_source_list_allow_all_inline_behavior_for_type(type_) == AllowResult::Allows {
1733            return Matches;
1734        }
1735        if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style {
1736            if let Some(nonce) = element.nonce.as_ref() {
1737                for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1738                    if let Some(captures) = NONCE_SOURCE_GRAMMAR.captures(expression) {
1739                        if let Some(captured_nonce) = captures.name("n") {
1740                            if nonce == captured_nonce.as_str() {
1741                                return Matches;
1742                            }
1743                        }
1744                    }
1745                }
1746            }
1747        }
1748        let mut unsafe_hashes = false;
1749        for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1750            if ascii_case_insensitive_match(expression, "'unsafe-hashes'") {
1751                unsafe_hashes = true;
1752                break;
1753            }
1754        }
1755        if type_ == InlineCheckType::Script || type_ == InlineCheckType::Style || unsafe_hashes {
1756            for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1757                if let Some(captures) = HASH_SOURCE_GRAMMAR.captures(expression) {
1758                    if let (Some(algorithm), Some(value)) = (captures.name("algorithm").and_then(|a| HashAlgorithm::from_name(a.as_str())), captures.name("value")) {
1759                        let actual = algorithm.apply(source);
1760                        let expected = value.as_str().replace('-', "+").replace('_', "/");
1761                        if actual == expected {
1762                            return Matches;
1763                        }
1764                    }
1765                }
1766            }
1767        }
1768        DoesNotMatch
1769    }
1770    /// https://www.w3.org/TR/CSP/#allow-all-inline
1771    fn does_a_source_list_allow_all_inline_behavior_for_type(&self, type_: InlineCheckType) -> AllowResult {
1772        use InlineCheckType::*;
1773        let mut allow_all_inline = false;
1774        for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1775            if HASH_SOURCE_GRAMMAR.is_match(expression) || NONCE_SOURCE_GRAMMAR.is_match(expression) {
1776                return AllowResult::DoesNotAllow;
1777            }
1778            if (type_ == Script || type_ == ScriptAttribute || type_ == Navigation) && expression == "'strict-dynamic'" {
1779                return AllowResult::DoesNotAllow;
1780            }
1781            if ascii_case_insensitive_match(expression, "'unsafe-inline'") {
1782                allow_all_inline = true;
1783            }
1784        }
1785        if allow_all_inline {
1786            AllowResult::Allows
1787        } else {
1788            AllowResult::DoesNotAllow
1789        }
1790    }
1791    /// https://www.w3.org/TR/CSP/#match-response-to-source-list
1792    fn does_response_to_request_match_source_list(
1793        &self,
1794        request: &Request,
1795        response: &Response) -> MatchResult {
1796        self.does_url_match_source_list_in_origin_with_redirect_count(
1797            &response.url,
1798            &request.origin,
1799            response.redirect_count,
1800        )
1801    }
1802    /// https://www.w3.org/TR/CSP/#can-compile-strings
1803    fn does_a_source_list_allow_js_evaluation(&self) -> AllowResult {
1804        for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1805            // Step 5.3: If source-list contains a source expression which is an ASCII case-insensitive match
1806            // for the string "'unsafe-eval'", then skip the following steps.
1807            if ascii_case_insensitive_match(expression, "'unsafe-eval'") {
1808                return AllowResult::Allows;
1809            }
1810        }
1811        AllowResult::DoesNotAllow
1812    }
1813    /// https://www.w3.org/TR/CSP/#can-compile-wasm-bytes
1814    fn does_a_source_list_allow_wasm_evaluation(&self) -> AllowResult {
1815        for expression in self.0.clone().into_iter().map(Borrow::borrow) {
1816            if ascii_case_insensitive_match(expression, "'unsafe-eval'") ||
1817                ascii_case_insensitive_match(expression, "'wasm-unsafe-eval'") {
1818                return AllowResult::Allows;
1819            }
1820        }
1821        AllowResult::DoesNotAllow
1822    }
1823}
1824
1825/// https://www.w3.org/TR/CSP/#allow-all-inline
1826#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1827enum AllowResult {
1828    Allows,
1829    DoesNotAllow,
1830}
1831
1832/// https://www.w3.org/TR/CSP/#match-url-to-source-expression
1833fn does_url_match_expression_in_origin_with_redirect_count(
1834    url: &Url,
1835    expression: &str,
1836    origin: &Origin,
1837    redirect_count: u32,
1838) -> MatchResult {
1839    let url_scheme = url.scheme();
1840    if expression == "*" {
1841        if scheme_is_network(url_scheme) {
1842            return Matches;
1843        }
1844        return origin_scheme_part_match(origin, url_scheme);
1845    }
1846    if let Some(captures) = SCHEME_SOURCE_GRAMMAR.captures(expression) {
1847        if let Some(expression_scheme) = captures.name("scheme") {
1848            return scheme_part_match(expression_scheme.as_str(), url_scheme);
1849        }
1850        // It should not be possible to match HOST_SOURCE_GRAMMAR without having a scheme part
1851        return DoesNotMatch;
1852    }
1853    if let Some(captures) = HOST_SOURCE_GRAMMAR.captures(expression) {
1854        let expr_has_scheme_part = if let Some(expression_scheme) = captures.name("scheme") {
1855            if scheme_part_match(expression_scheme.as_str(), url_scheme) != Matches {
1856                return DoesNotMatch;
1857            }
1858            true
1859        } else {
1860            false
1861        };
1862        let url_host = if let Some(url_host) = url.host() {
1863            url_host
1864        } else {
1865            return DoesNotMatch;
1866        };
1867        if !expr_has_scheme_part &&
1868            origin_scheme_part_match(origin, url.scheme()) != Matches {
1869            return DoesNotMatch;
1870        }
1871        if let Some(expression_host) = captures.name("host") {
1872            if host_part_match(expression_host.as_str(), &url_host.to_string()) != Matches {
1873                return DoesNotMatch;
1874            }
1875        } else {
1876            // It should not be possible to match HOST_SOURCE_GRAMMAR without having a host part
1877            return DoesNotMatch;
1878        }
1879        // Skip the first byte of the port capture to avoid the `:`.
1880        let port_part = captures.name("port").map(|port| &port.as_str()[1..]).unwrap_or("");
1881        let url_port = url_port(url);
1882        if port_part_match(port_part, &url_port[..], url.scheme()) != Matches {
1883            return DoesNotMatch;
1884        }
1885        let path_part = captures.name("path").map(|path_part| path_part.as_str()).unwrap_or("");
1886        if path_part != "/" && redirect_count == 0 {
1887            let path = url.path();
1888            if path_part_match(path_part, path) != Matches {
1889                return DoesNotMatch;
1890            }
1891        }
1892        return Matches;
1893    }
1894    if ascii_case_insensitive_match(expression, "'self'") {
1895        if *origin == url.origin() {
1896            return Matches;
1897        }
1898        if let Origin::Tuple(scheme, host, port) = origin {
1899            let hosts_are_the_same = Some(host) == url.host().map(|p| p.to_owned()).as_ref();
1900            let ports_are_the_same = Some(*port) == url.port();
1901            let origins_port_is_default_for_scheme = Some(*port) == default_port(scheme);
1902            let url_port_is_default_port_for_scheme = url.port() == default_port(scheme)
1903                && default_port(scheme).is_some();
1904            let ports_are_default = url_port_is_default_port_for_scheme && origins_port_is_default_for_scheme;
1905            if hosts_are_the_same
1906                && (ports_are_the_same || ports_are_default)
1907                && ((url_scheme == "https" || url_scheme == "wss")
1908                        || (scheme == "http" && (url_scheme == "http" || url_scheme == "ws"))) {
1909                return Matches;
1910            }
1911        }
1912    }
1913    DoesNotMatch
1914}
1915
1916/// https://www.w3.org/TR/CSP/#match-hosts
1917fn host_part_match(pattern: &str, host: &str) -> MatchResult {
1918    debug_assert!(!host.is_empty());
1919    // Step 1. If host is not a domain, return "Does Not Match".
1920    if host.is_empty() {
1921        return DoesNotMatch;
1922    }
1923    if pattern.as_bytes()[0] == b'*' {
1924        // Step 2. If pattern is "*", return "Matches".
1925        if pattern.len() == 1 {
1926            return Matches;
1927        }
1928        // Step 3. If pattern starts with "*.":
1929        if pattern.as_bytes()[1] == b'.' {
1930            // Step 3.1 Let remaining be pattern with the leading U+002A (*) removed and ASCII lowercased.
1931            let remaining_pattern = &pattern[1..];
1932            if remaining_pattern.len() > host.len() {
1933                return DoesNotMatch;
1934            }
1935            let remaining_host = &host[(host.len()-remaining_pattern.len())..];
1936            debug_assert_eq!(remaining_host.len(), remaining_pattern.len());
1937            // Step 3.2. If host to ASCII lowercase ends with remaining, then return "Matches".
1938            if ascii_case_insensitive_match(remaining_pattern, remaining_host) {
1939                return Matches;
1940            }
1941            // Step 3.3 Return "Does Not Match".
1942            return DoesNotMatch;
1943        }
1944    }
1945    // Step 4. If pattern is not an ASCII case-insensitive match for host, return "Does Not Match".
1946    if !ascii_case_insensitive_match(pattern, host) {
1947        return DoesNotMatch;
1948    }
1949    static IPV4_ADDRESS_RULE: Lazy<Regex> =
1950        Lazy::new(|| Regex::new(r#"([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])"#).unwrap());
1951    if IPV4_ADDRESS_RULE.is_match(pattern) && pattern != "127.0.0.1" {
1952        return DoesNotMatch;
1953    }
1954    // The spec uses the phrase "if A is an IPv6 address", without giving specific instructions on
1955    // how to tell if this is the case. In URLs, IPv6 addresses start with `[`, so let's go with that.
1956    // See https://url.spec.whatwg.org/#host-parsing
1957    if pattern.as_bytes()[0] == b'[' {
1958        return DoesNotMatch;
1959    }
1960    // Step 5. Return "Matches".
1961    Matches
1962}
1963
1964/// https://www.w3.org/TR/CSP/#match-ports
1965fn port_part_match(port_a: &str, port_b: &str, scheme_b: &str) -> MatchResult {
1966    if port_a.is_empty() {
1967        if port_b == default_port_str(scheme_b) {
1968            return Matches;
1969        } else {
1970            return DoesNotMatch;
1971        }
1972    }
1973    if port_a == "*" {
1974        return Matches;
1975    }
1976    if port_a == port_b {
1977        return Matches;
1978    }
1979    if port_b.is_empty() {
1980        if port_a == default_port_str(scheme_b) {
1981            return Matches;
1982        } else {
1983            return DoesNotMatch;
1984        }
1985    }
1986    DoesNotMatch
1987}
1988
1989/// https://www.w3.org/TR/CSP/#match-paths
1990fn path_part_match(path_a: &str, path_b: &str) -> MatchResult {
1991    if path_a.is_empty() {
1992        return Matches;
1993    }
1994    if path_a == "/" && path_b.is_empty() {
1995        return Matches;
1996    }
1997    let exact_match = path_a.as_bytes()[path_a.len()-1] != b'/';
1998    let (mut path_list_a, path_list_b): (Vec<&str>, Vec<&str>) =
1999        (path_a.split('/').collect(), path_b.split('/').collect());
2000    if path_list_a.len() > path_list_b.len() {
2001        return DoesNotMatch;
2002    }
2003    if exact_match && path_list_a.len() != path_list_b.len() {
2004        return DoesNotMatch;
2005    }
2006    if !exact_match {
2007        debug_assert_eq!(path_list_a[path_list_a.len()-1], "");
2008        path_list_a.pop();
2009    }
2010    let mut piece_b_iter = path_list_b.iter();
2011    for piece_a in &path_list_a {
2012        let piece_b = piece_b_iter.next().unwrap();
2013        let piece_a: Vec<u8> = percent_encoding::percent_decode(piece_a.as_bytes()).collect();
2014        let piece_b: Vec<u8> = percent_encoding::percent_decode(piece_b.as_bytes()).collect();
2015        if piece_a != piece_b {
2016            return DoesNotMatch;
2017        }
2018    }
2019    Matches
2020}
2021
2022fn url_port(url: &Url) -> String {
2023    match url.port() {
2024        Some(num) => num.to_string(),
2025        None => default_port_str(url.scheme()).to_string(),
2026    }
2027}
2028
2029fn default_port_str(scheme: &str) -> &'static str {
2030    match scheme {
2031        "ftp" => "21",
2032        "gopher" => "70",
2033        "http" => "80",
2034        "https" => "443",
2035        "ws" => "80",
2036        "wss" => "443",
2037        _ => "",
2038    }
2039}
2040
2041fn default_port(scheme: &str) -> Option<u16> {
2042    Some(match scheme {
2043        "ftp" => 21,
2044        "gopher" => 70,
2045        "http" => 80,
2046        "https" => 443,
2047        "ws" => 80,
2048        "wss" => 443,
2049        _ => return None,
2050    })
2051}
2052
2053fn origin_scheme_part_match(a: &Origin, b: &str) -> MatchResult {
2054    if let Origin::Tuple(scheme, _host, _port) = a {
2055        scheme_part_match(&scheme[..], b)
2056    } else {
2057        DoesNotMatch
2058    }
2059}
2060
2061/// https://www.w3.org/TR/CSP/#match-schemes
2062fn scheme_part_match(a: &str, b: &str) -> MatchResult {
2063    let a = a.to_ascii_lowercase();
2064    let b = b.to_ascii_lowercase();
2065    match (&a[..], &b[..]) {
2066        _ if a == b => Matches,
2067        ("http", "https") |
2068        ("ws", "wss") |
2069        ("wss", "https") => Matches,
2070        _ => DoesNotMatch,
2071    }
2072}
2073
2074#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2075pub enum HashAlgorithm {
2076    Sha256,
2077    Sha384,
2078    Sha512,
2079}
2080
2081impl HashAlgorithm {
2082    pub fn from_name(name: &str) -> Option<HashAlgorithm> {
2083        use HashAlgorithm::*;
2084        match name {
2085            "sha256" | "Sha256" | "sHa256" | "shA256" | "SHa256" | "ShA256" | "sHA256" | "SHA256" => Some(Sha256),
2086            "sha384" | "Sha384" | "sHa384" | "shA384" | "SHa384" | "ShA384" | "sHA384" | "SHA384" => Some(Sha384),
2087            "sha512" | "Sha512" | "sHa512" | "shA512" | "SHa512" | "ShA512" | "sHA512" | "SHA512" => Some(Sha512),
2088            _ => None,
2089        }
2090    }
2091    pub fn apply(self, value: &str) -> String {
2092        use base64::Engine as _;
2093        let bytes = value.as_bytes();
2094        let standard = base64::engine::general_purpose::STANDARD;
2095        match self {
2096            HashAlgorithm::Sha256 => standard.encode(sha2::Sha256::digest(bytes)),
2097            HashAlgorithm::Sha384 => standard.encode(sha2::Sha384::digest(bytes)),
2098            HashAlgorithm::Sha512 => standard.encode(sha2::Sha512::digest(bytes)),
2099        }
2100    }
2101}
2102
2103/// https://www.w3.org/TR/SRI/#integrity-metadata
2104#[derive(Clone, Debug, Eq, PartialEq)]
2105pub struct HashFunction {
2106    algorithm: HashAlgorithm,
2107    value: String,
2108    // The spec defines a third member, options, but defines no values.
2109}
2110
2111/// https://www.w3.org/TR/SRI/#parse-metadata
2112#[derive(Clone, Debug, Eq, PartialEq)]
2113pub enum SubresourceIntegrityMetadata {
2114    NoMetadata,
2115    IntegritySources(Vec<HashFunction>)
2116}
2117
2118/// https://www.w3.org/TR/SRI/#the-integrity-attribute
2119/// This corresponds to the "hash-expression" grammar.
2120static SUBRESOURCE_METADATA_GRAMMAR: Lazy<Regex> =
2121    Lazy::new(|| Regex::new(r#"(?P<algorithm>[sS][hH][aA](256|384|512))-(?P<value>[a-zA-Z0-9\+/\-_]+=*)"#).unwrap());
2122
2123/// https://www.w3.org/TR/SRI/#parse-metadata
2124pub fn parse_subresource_integrity_metadata(string: &str) -> SubresourceIntegrityMetadata {
2125    let mut result = Vec::new();
2126    let mut empty = true;
2127    for token in split_ascii_whitespace(string) {
2128        empty = false;
2129        if let Some(captures) = SUBRESOURCE_METADATA_GRAMMAR.captures(token) {
2130            if let (Some(algorithm), Some(value)) = (captures.name("algorithm").and_then(|a| HashAlgorithm::from_name(a.as_str())), captures.name("value")) {
2131                result.push(HashFunction{ algorithm, value: String::from(value.as_str()) });
2132            }
2133        }
2134    }
2135    if empty {
2136        SubresourceIntegrityMetadata::NoMetadata
2137    } else {
2138        SubresourceIntegrityMetadata::IntegritySources(result)
2139    }
2140}
2141
2142#[cfg(test)]
2143mod test {
2144    use super::*;
2145    #[test]
2146    fn empty_directive_is_not_valid() {
2147        let d = Directive {
2148            name: String::new(),
2149            value: Vec::new(),
2150        };
2151        assert!(!d.is_valid());
2152    }
2153    #[test]
2154    pub fn duplicate_policy_is_not_valid() {
2155        let d = Directive {
2156            name: "test".to_owned(),
2157            value: vec!["test".to_owned()],
2158        };
2159        let p = Policy {
2160            directive_set: vec![d.clone(), d.clone()],
2161            disposition: PolicyDisposition::Enforce,
2162            source: PolicySource::Header,
2163        };
2164        assert!(!p.is_valid());
2165    }
2166    #[test]
2167    pub fn basic_policy_is_valid() {
2168        let p = Policy::parse("script-src notriddle.com", PolicySource::Header, PolicyDisposition::Enforce);
2169        assert!(p.is_valid());
2170    }
2171    #[test]
2172    pub fn policy_with_empty_directive_set_is_not_valid() {
2173        let p = Policy {
2174            directive_set: vec![],
2175            disposition: PolicyDisposition::Enforce,
2176            source: PolicySource::Header,
2177        };
2178        assert!(!p.is_valid());
2179    }
2180
2181    #[test]
2182    pub fn prefetch_request_does_not_violate_policy() {
2183        let request = Request {
2184            url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
2185            origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
2186            redirect_count: 0,
2187            destination: Destination::Script,
2188            initiator: Initiator::Prefetch,
2189            nonce: String::new(),
2190            integrity_metadata: String::new(),
2191            parser_metadata: ParserMetadata::None,
2192        };
2193
2194        let p = Policy::parse("child-src 'self'", PolicySource::Header, PolicyDisposition::Enforce);
2195
2196        let violation_result = p.does_request_violate_policy(&request);
2197
2198        assert!(violation_result == Violates::DoesNotViolate);
2199    }
2200
2201    #[test]
2202    pub fn prefetch_request_violates_policy() {
2203        let request = Request {
2204            url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
2205            origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
2206            redirect_count: 0,
2207            destination: Destination::ServiceWorker,
2208            initiator: Initiator::None,
2209            nonce: String::new(),
2210            integrity_metadata: String::new(),
2211            parser_metadata: ParserMetadata::None,
2212        };
2213
2214        let p = Policy::parse("default-src 'none'; script-src 'self' ", PolicySource::Header, PolicyDisposition::Enforce);
2215
2216        let violation_result = p.does_request_violate_policy(&request);
2217
2218        let expected_result = Violates::Directive(Directive { name: String::from("script-src"), value: vec![String::from("'self'")] });
2219
2220        assert!(violation_result == expected_result);
2221    }
2222
2223    #[test]
2224    pub fn prefetch_request_is_allowed_by_directive() {
2225        let request = Request {
2226            url: Url::parse("https://www.notriddle.com/script.js").unwrap(),
2227            origin: Origin::Tuple("https".to_string(), url::Host::Domain("notriddle.com".to_owned()), 443),
2228            redirect_count: 0,
2229            destination: Destination::Script,
2230            initiator: Initiator::Prefetch,
2231            nonce: String::new(),
2232            integrity_metadata: String::new(),
2233            parser_metadata: ParserMetadata::None,
2234        };
2235
2236        let p = Policy::parse("default-src 'none'; child-src 'self'", PolicySource::Header, PolicyDisposition::Enforce);
2237
2238        let violation_result = p.does_request_violate_policy(&request);
2239
2240        assert!(violation_result == Violates::DoesNotViolate);
2241    }
2242
2243    #[test]
2244    pub fn trusted_type_policy_is_valid() {
2245        let p = Policy::parse("trusted-types 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2246        assert!(p.is_valid());
2247        assert_eq!(p.directive_set[0].value, vec!["'none'".to_owned()]);
2248    }
2249
2250    #[test]
2251    pub fn csp_list_is_valid() {
2252        let csp_list = CspList::parse("default-src 'none'; child-src 'self', trusted-types 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2253        assert!(csp_list.is_valid());
2254        assert_eq!(csp_list.0[1].directive_set[0].value, vec!["'none'".to_owned()]);
2255    }
2256
2257    #[test]
2258    pub fn no_trusted_types_specified_allows_all_policies() {
2259        let csp_list = CspList::parse("default-src 'none'; child-src 'self'", PolicySource::Meta, PolicyDisposition::Enforce);
2260        assert!(csp_list.is_valid());
2261        let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec![]);
2262        assert_eq!(check_result, CheckResult::Allowed);
2263        assert!(violations.is_empty());
2264    }
2265
2266    #[test]
2267    pub fn none_does_not_allow_for_any_policy() {
2268        let csp_list = CspList::parse("trusted-types 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2269        assert!(csp_list.is_valid());
2270        let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("some-policy", &vec![]);
2271        assert!(check_result == CheckResult::Blocked);
2272        assert_eq!(violations.len(), 1);
2273    }
2274
2275    #[test]
2276    pub fn extra_none_allows_all_policies() {
2277        let csp_list = CspList::parse("trusted-types some-policy 'none'", PolicySource::Meta, PolicyDisposition::Enforce);
2278        assert!(csp_list.is_valid());
2279        let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("some-policy", &vec![]);
2280        assert!(check_result == CheckResult::Allowed);
2281        assert!(violations.is_empty());
2282    }
2283
2284    #[test]
2285    pub fn explicit_policy_named_is_allowed() {
2286        let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2287        assert!(csp_list.is_valid());
2288        let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec![]);
2289        assert_eq!(check_result, CheckResult::Allowed);
2290        assert!(violations.is_empty());
2291    }
2292
2293    #[test]
2294    pub fn other_policy_name_is_blocked() {
2295        let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2296        assert!(csp_list.is_valid());
2297        let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &vec![]);
2298        assert!(check_result == CheckResult::Blocked);
2299        assert_eq!(violations.len(), 1);
2300    }
2301
2302    #[test]
2303    pub fn invalid_characters_in_policy_name_is_blocked() {
2304        let csp_list = CspList::parse("trusted-types My?Policy", PolicySource::Meta, PolicyDisposition::Enforce);
2305        assert!(csp_list.is_valid());
2306        let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("My?Policy", &vec!["My?Policy"]);
2307        assert!(check_result == CheckResult::Blocked);
2308        assert_eq!(violations.len(), 1);
2309    }
2310
2311    #[test]
2312    pub fn already_created_policy_is_blocked() {
2313        let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2314        assert!(csp_list.is_valid());
2315        let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec!["MyPolicy"]);
2316        assert!(check_result == CheckResult::Blocked);
2317        assert_eq!(violations.len(), 1);
2318    }
2319
2320    #[test]
2321    pub fn already_created_policy_is_allowed_with_allow_duplicates() {
2322        let csp_list = CspList::parse("trusted-types MyPolicy 'allow-duplicates'", PolicySource::Meta, PolicyDisposition::Enforce);
2323        assert!(csp_list.is_valid());
2324        let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec!["MyPolicy"]);
2325        assert!(check_result == CheckResult::Allowed);
2326        assert!(violations.is_empty());
2327    }
2328
2329    #[test]
2330    pub fn only_report_policy_issues_for_disposition_report() {
2331        let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Report);
2332        assert!(csp_list.is_valid());
2333        let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec!["MyPolicy"]);
2334        assert!(check_result == CheckResult::Allowed);
2335        assert_eq!(violations.len(), 1);
2336    }
2337
2338    #[test]
2339    pub fn wildcard_allows_all_policies() {
2340        let csp_list = CspList::parse("trusted-types *", PolicySource::Meta, PolicyDisposition::Report);
2341        assert!(csp_list.is_valid());
2342        let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyPolicy", &vec![]);
2343        assert!(check_result == CheckResult::Allowed);
2344        assert!(violations.is_empty());
2345    }
2346
2347    #[test]
2348    pub fn violation_has_correct_directive() {
2349        let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2350        assert!(csp_list.is_valid());
2351        let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("MyOtherPolicy", &vec![]);
2352        assert!(check_result == CheckResult::Blocked);
2353        assert_eq!(violations.len(), 1);
2354        assert_eq!(violations[0].directive, csp_list.0[0].directive_set[0]);
2355    }
2356
2357    #[test]
2358    pub fn long_policy_name_is_truncated() {
2359        let csp_list = CspList::parse("trusted-types MyPolicy", PolicySource::Meta, PolicyDisposition::Enforce);
2360        assert!(csp_list.is_valid());
2361        let (check_result, violations) = csp_list.is_trusted_type_policy_creation_allowed("SuperLongPolicyNameThatExceeds40Characters", &vec![]);
2362        assert!(check_result == CheckResult::Blocked);
2363        assert_eq!(violations.len(), 1);
2364        assert!(matches!(&violations[0].resource, ViolationResource::TrustedTypePolicy { sample } if sample == "SuperLongPolicyNameThatExceeds40Characte"));
2365    }
2366}