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