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}