Skip to main content

rama_http_headers/common/permissions_policy/
directive.rs

1use std::fmt;
2
3use rama_utils::collections::smallvec::{SmallVec, smallvec};
4use rama_utils::macros::enums::enum_builder;
5
6/// A single entry in a [`PermissionsPolicy`](super::PermissionsPolicy):
7/// pairs a feature name with the origins allowed to use it.
8///
9/// Construct via [`Self::deny`], [`Self::allow`], or [`Self::allow_from`]
10/// — those are the three shapes the spec actually defines.
11///
12/// Note: the [`PermissionsPolicy`](super::PermissionsPolicy) struct
13/// itself has per-feature `with_deny_*` / `set_deny_*` shortcuts for
14/// the deny-all case (the overwhelmingly common one); reach for these
15/// constructors when you need an actual allow-list.
16#[derive(Clone, Debug, PartialEq, Eq, Hash)]
17pub struct PermissionsPolicyDirective {
18    pub name: PermissionsPolicyDirectiveName,
19    /// Empty allow-list serialises to `name=()` — the deny-all form.
20    /// The W3C spec has no `None` allow-list state, so the deny-all
21    /// directive is just an empty list.
22    pub allow_list: SmallVec<[AllowlistSource; 4]>,
23}
24
25impl PermissionsPolicyDirective {
26    /// Deny-all directive: `name=()`.
27    #[must_use]
28    pub fn deny(name: impl Into<PermissionsPolicyDirectiveName>) -> Self {
29        Self {
30            name: name.into(),
31            allow_list: SmallVec::new(),
32        }
33    }
34
35    /// Single-source allow-list: `name=(source)`.
36    #[must_use]
37    pub fn allow(name: impl Into<PermissionsPolicyDirectiveName>, source: AllowlistSource) -> Self {
38        Self {
39            name: name.into(),
40            allow_list: smallvec![source],
41        }
42    }
43
44    /// Multi-source allow-list: `name=(src1 src2 …)`.
45    #[must_use]
46    pub fn allow_from(
47        name: impl Into<PermissionsPolicyDirectiveName>,
48        sources: impl IntoIterator<Item = AllowlistSource>,
49    ) -> Self {
50        Self {
51            name: name.into(),
52            allow_list: sources.into_iter().collect(),
53        }
54    }
55}
56
57impl fmt::Display for PermissionsPolicyDirective {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        write!(f, "{}=(", self.name.as_str())?;
60        for (i, src) in self.allow_list.iter().enumerate() {
61            if i > 0 {
62                f.write_str(" ")?;
63            }
64            fmt::Display::fmt(src, f)?;
65        }
66        f.write_str(")")
67    }
68}
69
70enum_builder! {
71    /// W3C-registered feature names plus an escape hatch for the long
72    /// tail (proposed / draft / vendor-prefixed features).
73    ///
74    /// Comparisons are case-insensitive on the wire — the
75    /// [`From<&str>`](Self) impl folds unrecognised tokens into the
76    /// [`Unknown`](Self::Unknown) variant. The registry grows over
77    /// time so callers should expect to land there for less-common
78    /// features.
79    @String
80    pub enum PermissionsPolicyDirectiveName {
81        // ---- W3C registry / MDN, in spec order roughly ----
82        Accelerometer => "accelerometer",
83        AmbientLightSensor => "ambient-light-sensor",
84        AttributionReporting => "attribution-reporting",
85        Autoplay => "autoplay",
86        Battery => "battery",
87        Bluetooth => "bluetooth",
88        BrowsingTopics => "browsing-topics",
89        Camera => "camera",
90        ClipboardRead => "clipboard-read",
91        ClipboardWrite => "clipboard-write",
92        ComputePressure => "compute-pressure",
93        CrossOriginIsolated => "cross-origin-isolated",
94        DisplayCapture => "display-capture",
95        EncryptedMedia => "encrypted-media",
96        Fullscreen => "fullscreen",
97        Gamepad => "gamepad",
98        Geolocation => "geolocation",
99        Gyroscope => "gyroscope",
100        Hid => "hid",
101        IdentityCredentialsGet => "identity-credentials-get",
102        IdleDetection => "idle-detection",
103        /// Deprecated FLoC opt-out. Kept for parsing legacy policies;
104        /// the modern equivalent is `BrowsingTopics`.
105        InterestCohort => "interest-cohort",
106        LocalFonts => "local-fonts",
107        Magnetometer => "magnetometer",
108        Microphone => "microphone",
109        Midi => "midi",
110        OtpCredentials => "otp-credentials",
111        Payment => "payment",
112        PictureInPicture => "picture-in-picture",
113        PublickeyCredentialsCreate => "publickey-credentials-create",
114        PublickeyCredentialsGet => "publickey-credentials-get",
115        ScreenWakeLock => "screen-wake-lock",
116        Serial => "serial",
117        SpeakerSelection => "speaker-selection",
118        StorageAccess => "storage-access",
119        SyncXhr => "sync-xhr",
120        Unload => "unload",
121        Usb => "usb",
122        WebShare => "web-share",
123        WindowManagement => "window-management",
124        XrSpatialTracking => "xr-spatial-tracking",
125    }
126}
127
128enum_builder! {
129    /// Allowlist token shapes per the W3C Permissions Policy spec.
130    ///
131    /// The deny-all directive is represented by an empty
132    /// [`PermissionsPolicyDirective::allow_list`] (no `None` variant
133    /// here) so that the typed state and the wire form
134    /// (`feature=()`) line up one-to-one.
135    ///
136    /// Specific origins (e.g. `"https://example.com"`) land in the
137    /// auto-generated [`Unknown`](Self::Unknown) variant — use
138    /// [`Self::origin`] to construct one with the required RFC 8941
139    /// sf-string double-quoting baked in.
140    @String
141    pub enum AllowlistSource {
142        /// `self` — same-origin only (no surrounding quotes on the
143        /// wire, unlike CSP).
144        SelfOrigin => "self",
145        /// `*` — any origin.
146        Wildcard => "*",
147        /// `src` — legacy `<iframe allow=…>` token, lets the iframe
148        /// inherit from its `src` attribute. Rare outside iframe
149        /// context.
150        Src => "src",
151    }
152}
153
154impl AllowlistSource {
155    /// Construct an origin allow-list source from a bare origin string
156    /// — the surrounding RFC 8941 sf-string double-quotes are added
157    /// for you. Pass `https://example.com`, not `"https://example.com"`.
158    #[must_use]
159    pub fn origin(origin: impl AsRef<str>) -> Self {
160        Self::Unknown(format!("\"{}\"", origin.as_ref()).into())
161    }
162
163    /// Parse a single allowlist token. Returns `None` on a
164    /// structurally invalid token — caller decides whether to drop
165    /// the directive or just the token.
166    pub(crate) fn from_token(s: &str) -> Option<Self> {
167        if let Some(inner) = s.strip_prefix('"').and_then(|t| t.strip_suffix('"')) {
168            // Quoted sf-string origin. Keep the quotes on the stored
169            // value so Display (macro-generated for `Unknown`) emits
170            // the canonical wire form unchanged.
171            if inner.is_empty() {
172                None
173            } else {
174                Some(Self::Unknown(format!("\"{inner}\"").into()))
175            }
176        } else {
177            // Bare keyword: macro-generated `From<&str>` matches
178            // `self`/`*`/`src` case-insensitively and falls through
179            // to `Unknown(String)` otherwise. We reject the latter —
180            // only quoted strings are valid non-keyword tokens per
181            // the spec.
182            match Self::from(s) {
183                Self::Unknown(_) => None,
184                parsed => Some(parsed),
185            }
186        }
187    }
188}