Skip to main content

csp_parse/
ast.rs

1//! Structured representation of a serialized CSP (or CSP list).
2//!
3//! Phase 02 scope: the generic, directive-independent top-level split
4//! only. A [`Directive`]'s value is kept as a raw string -- interpreting
5//! it further (source lists, sandbox tokens, etc.) is later phases' job
6//! (see `plan/03-source-list-grammar.md`, `plan/04-directive-registry.md`).
7
8/// A parsed list of CSP policies, as found in a `Content-Security-Policy`
9/// HTTP header (a comma-separated list of `serialized-policy`, CSP3 §2.2).
10#[derive(Debug, Clone, PartialEq, Eq, Default)]
11#[non_exhaustive]
12pub struct PolicyList {
13    /// The policies, in the order they appeared in the input (comma-separated).
14    pub policies: Vec<Policy>,
15}
16
17/// A single serialized CSP policy: an ordered list of directives
18/// (CSP3 §2.2, `serialized-policy`).
19#[derive(Debug, Clone, PartialEq, Eq, Default)]
20#[non_exhaustive]
21pub struct Policy {
22    /// The directives, in the order they appeared in the input (semicolon-separated).
23    pub directives: Vec<Directive>,
24}
25
26/// A single directive within a policy: a name and an optional raw value
27/// (CSP3 §2.3, `serialized-directive`).
28///
29/// `raw_value` is exactly the substring that followed the directive name,
30/// with only the separating whitespace run stripped -- it is not yet
31/// parsed against any directive-specific grammar.
32#[derive(Debug, Clone, PartialEq, Eq)]
33#[non_exhaustive]
34pub struct Directive {
35    /// The directive name, exactly as it appeared in the input (original
36    /// casing preserved -- CSP3 directive-name matching is ASCII-case-
37    /// insensitive, see [`Directive::name_is_valid`] and
38    /// [`crate::registry_lookup`]).
39    pub name: String,
40    /// The raw value that followed the directive name, if any.
41    pub raw_value: Option<String>,
42}
43
44impl Directive {
45    /// Whether [`Directive::name`] matches the ABNF `directive-name`
46    /// production (`1*( ALPHA / DIGIT / "-" )`, CSP3 §2.3).
47    ///
48    /// A `false` result does not mean this directive was dropped --
49    /// `csp-parse` parses leniently (see `plan/DECISIONS.md`,
50    /// 2026-08-22) and still reports it; callers that need strict
51    /// conformance checking should check this explicitly.
52    pub fn name_is_valid(&self) -> bool {
53        !self.name.is_empty()
54            && self
55                .name
56                .bytes()
57                .all(|b| b.is_ascii_alphanumeric() || b == b'-')
58    }
59
60    /// Whether [`Directive::raw_value`] (if present) matches the ABNF
61    /// `directive-value` production (CSP3 §2.3): any run of ASCII
62    /// whitespace or a byte in `%x21-2B / %x2D-3A / %x3C-7E` (printable
63    /// ASCII excluding `,` and `;`).
64    pub fn value_is_valid(&self) -> bool {
65        match &self.raw_value {
66            None => true,
67            Some(value) => value.bytes().all(|b| {
68                b.is_ascii_whitespace() || matches!(b, 0x21..=0x2B | 0x2D..=0x3A | 0x3C..=0x7E)
69            }),
70        }
71    }
72}