mod directive;
pub use self::directive::{
AllowlistSource, PermissionsPolicyDirective, PermissionsPolicyDirectiveName,
};
use std::fmt;
use rama_http_types::{HeaderName, HeaderValue};
use rama_utils::macros::generate_set_and_with;
use crate::{Error, HeaderDecode, HeaderEncode, TypedHeader};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PermissionsPolicy {
directives: Vec<PermissionsPolicyDirective>,
}
impl PermissionsPolicy {
#[must_use]
pub const fn empty() -> Self {
Self {
directives: Vec::new(),
}
}
pub fn directives(&self) -> impl Iterator<Item = &PermissionsPolicyDirective> + '_ {
self.directives.iter()
}
generate_set_and_with! {
pub fn directive(mut self, directive: PermissionsPolicyDirective) -> Self {
if let Some(slot) = self
.directives
.iter_mut()
.find(|d| d.name == directive.name)
{
slot.allow_list = directive.allow_list;
} else {
self.directives.push(directive);
}
self
}
}
generate_set_and_with! {
pub fn deny_camera(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::Camera,
));
self
}
}
generate_set_and_with! {
pub fn deny_microphone(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::Microphone,
));
self
}
}
generate_set_and_with! {
pub fn deny_geolocation(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::Geolocation,
));
self
}
}
generate_set_and_with! {
pub fn deny_payment(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::Payment,
));
self
}
}
generate_set_and_with! {
pub fn deny_usb(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::Usb,
));
self
}
}
generate_set_and_with! {
pub fn deny_interest_cohort(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::InterestCohort,
));
self
}
}
generate_set_and_with! {
pub fn deny_browsing_topics(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::BrowsingTopics,
));
self
}
}
generate_set_and_with! {
pub fn deny_attribution_reporting(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::AttributionReporting,
));
self
}
}
generate_set_and_with! {
pub fn deny_accelerometer(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::Accelerometer,
));
self
}
}
generate_set_and_with! {
pub fn deny_ambient_light_sensor(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::AmbientLightSensor,
));
self
}
}
generate_set_and_with! {
pub fn deny_autoplay(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::Autoplay,
));
self
}
}
generate_set_and_with! {
pub fn deny_battery(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::Battery,
));
self
}
}
generate_set_and_with! {
pub fn deny_bluetooth(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::Bluetooth,
));
self
}
}
generate_set_and_with! {
pub fn deny_display_capture(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::DisplayCapture,
));
self
}
}
generate_set_and_with! {
pub fn deny_encrypted_media(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::EncryptedMedia,
));
self
}
}
generate_set_and_with! {
pub fn deny_fullscreen(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::Fullscreen,
));
self
}
}
generate_set_and_with! {
pub fn deny_gyroscope(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::Gyroscope,
));
self
}
}
generate_set_and_with! {
pub fn deny_idle_detection(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::IdleDetection,
));
self
}
}
generate_set_and_with! {
pub fn deny_magnetometer(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::Magnetometer,
));
self
}
}
generate_set_and_with! {
pub fn deny_midi(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::Midi,
));
self
}
}
generate_set_and_with! {
pub fn deny_picture_in_picture(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::PictureInPicture,
));
self
}
}
generate_set_and_with! {
pub fn deny_publickey_credentials_get(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::PublickeyCredentialsGet,
));
self
}
}
generate_set_and_with! {
pub fn deny_screen_wake_lock(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::ScreenWakeLock,
));
self
}
}
generate_set_and_with! {
pub fn deny_sync_xhr(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::SyncXhr,
));
self
}
}
generate_set_and_with! {
pub fn deny_web_share(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::WebShare,
));
self
}
}
generate_set_and_with! {
pub fn deny_xr_spatial_tracking(mut self) -> Self {
self.set_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::XrSpatialTracking,
));
self
}
}
}
impl fmt::Display for PermissionsPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, d) in self.directives.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
fmt::Display::fmt(d, f)?;
}
Ok(())
}
}
impl TypedHeader for PermissionsPolicy {
fn name() -> &'static HeaderName {
&::rama_http_types::header::PERMISSIONS_POLICY
}
}
impl HeaderDecode for PermissionsPolicy {
fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
let mut out = Self::empty();
let mut any = false;
for value in values {
any = true;
let s = value.to_str().map_err(|_err| Error::invalid())?;
for raw in split_top_level_commas(s) {
let trimmed = raw.trim();
if trimmed.is_empty() {
continue;
}
let Some(directive) = parse_directive(trimmed) else {
continue;
};
out.set_directive(directive);
}
}
if !any {
return Err(Error::invalid());
}
Ok(out)
}
}
impl HeaderEncode for PermissionsPolicy {
fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
let rendered = self.to_string();
match HeaderValue::try_from(rendered) {
Ok(v) => values.extend(::std::iter::once(v)),
Err(_) => {
values.extend(::std::iter::once(HeaderValue::from_static("")));
}
}
}
}
fn split_top_level_commas(s: &str) -> impl Iterator<Item = &str> {
let bytes = s.as_bytes();
let mut start = 0usize;
let mut depth = 0i32;
let mut out: Vec<&str> = Vec::new();
for (i, b) in bytes.iter().enumerate() {
match b {
b'(' => depth += 1,
b')' => depth = depth.saturating_sub(1),
b',' if depth == 0 => {
out.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
if start <= s.len() {
out.push(&s[start..]);
}
out.into_iter()
}
fn parse_directive(s: &str) -> Option<PermissionsPolicyDirective> {
let eq = s.find('=')?;
let name_raw = s[..eq].trim();
let value_raw = s[eq + 1..].trim();
if name_raw.is_empty() {
return None;
}
let inner = value_raw
.strip_prefix('(')
.and_then(|t| t.strip_suffix(')'))?;
let name = PermissionsPolicyDirectiveName::from(name_raw);
let allow_list = inner
.split_whitespace()
.filter_map(AllowlistSource::from_token)
.collect();
Some(PermissionsPolicyDirective { name, allow_list })
}
#[cfg(test)]
mod tests {
use super::super::{test_decode, test_encode};
use super::*;
#[test]
fn empty_renders_to_empty_string() {
let pp = PermissionsPolicy::empty();
assert_eq!(pp.to_string(), "");
}
#[test]
fn keyword_shortcuts_render_deny_all_chain() {
let pp = PermissionsPolicy::empty()
.with_deny_camera()
.with_deny_microphone()
.with_deny_geolocation();
assert_eq!(pp.to_string(), "camera=(), microphone=(), geolocation=()");
}
#[test]
fn keyword_shortcuts_share_path_with_generic_with() {
let via_shortcut = PermissionsPolicy::empty().with_deny_camera();
let via_generic = PermissionsPolicy::empty().with_directive(
PermissionsPolicyDirective::deny(PermissionsPolicyDirectiveName::Camera),
);
assert_eq!(via_shortcut, via_generic);
}
#[test]
fn set_mutates_in_place() {
let mut pp = PermissionsPolicy::empty();
pp.set_deny_camera();
pp.set_directive(PermissionsPolicyDirective::allow(
PermissionsPolicyDirectiveName::Microphone,
AllowlistSource::SelfOrigin,
));
assert_eq!(pp.to_string(), "camera=(), microphone=(self)");
}
#[test]
fn allow_list_self_and_origin_render() {
let pp = PermissionsPolicy::empty().with_directive(PermissionsPolicyDirective::allow_from(
PermissionsPolicyDirectiveName::Camera,
[
AllowlistSource::SelfOrigin,
AllowlistSource::origin("https://example.com"),
],
));
assert_eq!(pp.to_string(), r#"camera=(self "https://example.com")"#);
}
#[test]
fn wildcard_and_src_render() {
let pp_wild = PermissionsPolicy::empty().with_directive(PermissionsPolicyDirective::allow(
PermissionsPolicyDirectiveName::Camera,
AllowlistSource::Wildcard,
));
assert_eq!(pp_wild.to_string(), "camera=(*)");
let pp_src = PermissionsPolicy::empty().with_directive(PermissionsPolicyDirective::allow(
PermissionsPolicyDirectiveName::Camera,
AllowlistSource::Src,
));
assert_eq!(pp_src.to_string(), "camera=(src)");
}
#[test]
fn unknown_feature_via_other_round_trips() {
let pp = PermissionsPolicy::empty().with_directive(PermissionsPolicyDirective::deny(
PermissionsPolicyDirectiveName::from("x-vendor-experimental"),
));
assert_eq!(pp.to_string(), "x-vendor-experimental=()");
let parsed = test_decode::<PermissionsPolicy>(&[pp.to_string().as_str()]).expect("decode");
assert_eq!(parsed, pp);
}
#[test]
fn decode_parses_canonical_deny_all_chain() {
let parsed = test_decode::<PermissionsPolicy>(&[
"camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()",
])
.expect("decode");
let names: Vec<&str> = parsed.directives().map(|d| d.name.as_str()).collect();
assert_eq!(
names,
vec![
"camera",
"microphone",
"geolocation",
"payment",
"usb",
"interest-cohort",
]
);
for d in parsed.directives() {
assert!(
d.allow_list.is_empty(),
"{} should be deny-all",
d.name.as_str()
);
}
}
#[test]
fn decode_preserves_declared_order() {
let parsed = test_decode::<PermissionsPolicy>(&["usb=(), camera=()"]).expect("decode");
let names: Vec<&str> = parsed.directives().map(|d| d.name.as_str()).collect();
assert_eq!(names, vec!["usb", "camera"]);
}
#[test]
fn decode_collapses_repeated_feature_last_wins() {
let parsed =
test_decode::<PermissionsPolicy>(&["camera=(), camera=(self)"]).expect("decode");
let directives: Vec<_> = parsed.directives().collect();
assert_eq!(directives.len(), 1);
assert_eq!(directives[0].name, PermissionsPolicyDirectiveName::Camera);
assert_eq!(
directives[0].allow_list.as_slice(),
&[AllowlistSource::SelfOrigin]
);
}
#[test]
fn decode_handles_multiple_header_values() {
let parsed = test_decode::<PermissionsPolicy>(&["camera=()", "microphone=()"])
.expect("decode multi-value");
let names: Vec<&str> = parsed.directives().map(|d| d.name.as_str()).collect();
assert_eq!(names, vec!["camera", "microphone"]);
}
#[test]
fn decode_tolerates_whitespace() {
let parsed =
test_decode::<PermissionsPolicy>(&[" camera = ( self ) , microphone = ( ) "])
.expect("decode whitespace-heavy");
let directives: Vec<_> = parsed.directives().collect();
assert_eq!(directives.len(), 2);
assert_eq!(
directives[0].allow_list.as_slice(),
&[AllowlistSource::SelfOrigin]
);
assert!(directives[1].allow_list.is_empty());
}
#[test]
fn decode_case_insensitive_on_known_features() {
let parsed = test_decode::<PermissionsPolicy>(&["Camera=()"]).expect("decode");
let directives: Vec<_> = parsed.directives().collect();
assert_eq!(directives.len(), 1);
assert_eq!(directives[0].name, PermissionsPolicyDirectiveName::Camera);
}
#[test]
fn decode_mixed_sources() {
let parsed = test_decode::<PermissionsPolicy>(&[r#"camera=(self "https://a.example" *)"#])
.expect("decode");
let directives: Vec<_> = parsed.directives().collect();
assert_eq!(directives.len(), 1);
assert_eq!(
directives[0].allow_list.as_slice(),
&[
AllowlistSource::SelfOrigin,
AllowlistSource::origin("https://a.example"),
AllowlistSource::Wildcard,
]
);
}
#[test]
fn decode_empty_returns_error() {
assert_eq!(test_decode::<PermissionsPolicy>(&[] as &[&str]), None);
}
#[test]
fn newer_feature_names_round_trip_as_typed_variants() {
for (token, expected) in [
(
"browsing-topics",
PermissionsPolicyDirectiveName::BrowsingTopics,
),
(
"attribution-reporting",
PermissionsPolicyDirectiveName::AttributionReporting,
),
(
"clipboard-read",
PermissionsPolicyDirectiveName::ClipboardRead,
),
(
"clipboard-write",
PermissionsPolicyDirectiveName::ClipboardWrite,
),
(
"compute-pressure",
PermissionsPolicyDirectiveName::ComputePressure,
),
("gamepad", PermissionsPolicyDirectiveName::Gamepad),
("hid", PermissionsPolicyDirectiveName::Hid),
("serial", PermissionsPolicyDirectiveName::Serial),
(
"storage-access",
PermissionsPolicyDirectiveName::StorageAccess,
),
(
"publickey-credentials-create",
PermissionsPolicyDirectiveName::PublickeyCredentialsCreate,
),
(
"window-management",
PermissionsPolicyDirectiveName::WindowManagement,
),
("local-fonts", PermissionsPolicyDirectiveName::LocalFonts),
("unload", PermissionsPolicyDirectiveName::Unload),
] {
let raw = format!("{token}=()");
let parsed = test_decode::<PermissionsPolicy>(&[raw.as_str()])
.unwrap_or_else(|| panic!("decode {token}"));
let directive = parsed.directives().next().expect("one directive");
assert_eq!(directive.name, expected, "token `{token}` parsed wrong");
assert_eq!(parsed.to_string(), raw, "round-trip changed `{token}`");
}
}
#[test]
fn topics_and_attribution_shortcuts_render_canonical_tokens() {
let pp = PermissionsPolicy::empty()
.with_deny_interest_cohort()
.with_deny_browsing_topics()
.with_deny_attribution_reporting();
assert_eq!(
pp.to_string(),
"interest-cohort=(), browsing-topics=(), attribution-reporting=()",
);
}
#[test]
fn encode_round_trips_through_header_map() {
let pp = PermissionsPolicy::empty()
.with_deny_camera()
.with_deny_microphone()
.with_directive(PermissionsPolicyDirective::allow_from(
PermissionsPolicyDirectiveName::Geolocation,
[
AllowlistSource::SelfOrigin,
AllowlistSource::origin("https://example.com"),
],
));
let map = test_encode(pp.clone());
let raw = map
.get(PermissionsPolicy::name())
.expect("set")
.to_str()
.unwrap()
.to_owned();
assert_eq!(raw, pp.to_string());
let parsed = test_decode::<PermissionsPolicy>(&[raw.as_str()]).expect("decode");
assert_eq!(parsed, pp);
}
}