use serde::{Deserialize, Serialize};
use std::fmt;
use std::collections::HashSet;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum MiniAppPermission {
Microphone,
Camera,
Geolocation,
ClipboardRead,
ClipboardWrite,
Fullscreen,
Autoplay,
DisplayCapture,
Midi,
PictureInPicture,
ScreenWakeLock,
SpeakerSelection,
Accelerometer,
Gyroscope,
Magnetometer,
AmbientLightSensor,
Bluetooth,
}
impl MiniAppPermission {
pub fn all() -> Vec<MiniAppPermission> {
vec![
MiniAppPermission::Microphone,
MiniAppPermission::Camera,
MiniAppPermission::Geolocation,
MiniAppPermission::ClipboardRead,
MiniAppPermission::ClipboardWrite,
MiniAppPermission::Fullscreen,
MiniAppPermission::Autoplay,
MiniAppPermission::DisplayCapture,
MiniAppPermission::Midi,
MiniAppPermission::PictureInPicture,
MiniAppPermission::ScreenWakeLock,
MiniAppPermission::SpeakerSelection,
MiniAppPermission::Accelerometer,
MiniAppPermission::Gyroscope,
MiniAppPermission::Magnetometer,
MiniAppPermission::AmbientLightSensor,
MiniAppPermission::Bluetooth,
]
}
pub fn policy_name(&self) -> &'static str {
match self {
MiniAppPermission::Microphone => "microphone",
MiniAppPermission::Camera => "camera",
MiniAppPermission::Geolocation => "geolocation",
MiniAppPermission::ClipboardRead => "clipboard-read",
MiniAppPermission::ClipboardWrite => "clipboard-write",
MiniAppPermission::Fullscreen => "fullscreen",
MiniAppPermission::Autoplay => "autoplay",
MiniAppPermission::DisplayCapture => "display-capture",
MiniAppPermission::Midi => "midi",
MiniAppPermission::PictureInPicture => "picture-in-picture",
MiniAppPermission::ScreenWakeLock => "screen-wake-lock",
MiniAppPermission::SpeakerSelection => "speaker-selection",
MiniAppPermission::Accelerometer => "accelerometer",
MiniAppPermission::Gyroscope => "gyroscope",
MiniAppPermission::Magnetometer => "magnetometer",
MiniAppPermission::AmbientLightSensor => "ambient-light-sensor",
MiniAppPermission::Bluetooth => "bluetooth",
}
}
pub fn label(&self) -> &'static str {
match self {
MiniAppPermission::Microphone => "Microphone",
MiniAppPermission::Camera => "Camera",
MiniAppPermission::Geolocation => "Location",
MiniAppPermission::ClipboardRead => "Read Clipboard",
MiniAppPermission::ClipboardWrite => "Write Clipboard",
MiniAppPermission::Fullscreen => "Fullscreen",
MiniAppPermission::Autoplay => "Autoplay Media",
MiniAppPermission::DisplayCapture => "Screen Capture",
MiniAppPermission::Midi => "MIDI Devices",
MiniAppPermission::PictureInPicture => "Picture-in-Picture",
MiniAppPermission::ScreenWakeLock => "Keep Screen On",
MiniAppPermission::SpeakerSelection => "Speaker Selection",
MiniAppPermission::Accelerometer => "Accelerometer",
MiniAppPermission::Gyroscope => "Gyroscope",
MiniAppPermission::Magnetometer => "Magnetometer",
MiniAppPermission::AmbientLightSensor => "Light Sensor",
MiniAppPermission::Bluetooth => "Bluetooth",
}
}
pub fn description(&self) -> &'static str {
match self {
MiniAppPermission::Microphone => "Access your microphone for voice chat or recording",
MiniAppPermission::Camera => "Access your camera for video calls or photos",
MiniAppPermission::Geolocation => "Access your device location",
MiniAppPermission::ClipboardRead => "Read text from your clipboard",
MiniAppPermission::ClipboardWrite => "Copy text to your clipboard",
MiniAppPermission::Fullscreen => "Enter fullscreen mode",
MiniAppPermission::Autoplay => "Automatically play audio and video",
MiniAppPermission::DisplayCapture => "Capture your screen or window",
MiniAppPermission::Midi => "Connect to MIDI instruments and controllers",
MiniAppPermission::PictureInPicture => "Play video in a floating window",
MiniAppPermission::ScreenWakeLock => "Prevent your screen from sleeping",
MiniAppPermission::SpeakerSelection => "Choose which speaker to use for audio",
MiniAppPermission::Accelerometer => "Detect device acceleration and movement",
MiniAppPermission::Gyroscope => "Detect device rotation and orientation",
MiniAppPermission::Magnetometer => "Access compass and magnetic field data",
MiniAppPermission::AmbientLightSensor => "Detect ambient light levels",
MiniAppPermission::Bluetooth => "Connect to Bluetooth devices",
}
}
pub fn icon(&self) -> &'static str {
match self {
MiniAppPermission::Microphone => "microphone",
MiniAppPermission::Camera => "camera",
MiniAppPermission::Geolocation => "location",
MiniAppPermission::ClipboardRead => "clipboard",
MiniAppPermission::ClipboardWrite => "clipboard",
MiniAppPermission::Fullscreen => "maximize",
MiniAppPermission::Autoplay => "play",
MiniAppPermission::DisplayCapture => "monitor",
MiniAppPermission::Midi => "music",
MiniAppPermission::PictureInPicture => "picture-in-picture",
MiniAppPermission::ScreenWakeLock => "sun",
MiniAppPermission::SpeakerSelection => "speaker",
MiniAppPermission::Accelerometer => "activity",
MiniAppPermission::Gyroscope => "rotate-3d",
MiniAppPermission::Magnetometer => "compass",
MiniAppPermission::AmbientLightSensor => "lightbulb",
MiniAppPermission::Bluetooth => "bluetooth",
}
}
}
impl fmt::Display for MiniAppPermission {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.policy_name())
}
}
impl FromStr for MiniAppPermission {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().trim() {
"microphone" => Ok(MiniAppPermission::Microphone),
"camera" => Ok(MiniAppPermission::Camera),
"geolocation" | "location" => Ok(MiniAppPermission::Geolocation),
"clipboard-read" | "clipboardread" => Ok(MiniAppPermission::ClipboardRead),
"clipboard-write" | "clipboardwrite" => Ok(MiniAppPermission::ClipboardWrite),
"fullscreen" => Ok(MiniAppPermission::Fullscreen),
"autoplay" => Ok(MiniAppPermission::Autoplay),
"display-capture" | "displaycapture" => Ok(MiniAppPermission::DisplayCapture),
"midi" => Ok(MiniAppPermission::Midi),
"picture-in-picture" | "pictureinpicture" | "pip" => Ok(MiniAppPermission::PictureInPicture),
"screen-wake-lock" | "screenwakelock" => Ok(MiniAppPermission::ScreenWakeLock),
"speaker-selection" | "speakerselection" => Ok(MiniAppPermission::SpeakerSelection),
"accelerometer" => Ok(MiniAppPermission::Accelerometer),
"gyroscope" => Ok(MiniAppPermission::Gyroscope),
"magnetometer" => Ok(MiniAppPermission::Magnetometer),
"ambient-light-sensor" | "ambientlightsensor" => Ok(MiniAppPermission::AmbientLightSensor),
"bluetooth" => Ok(MiniAppPermission::Bluetooth),
other => Err(format!("Unknown permission: {}", other)),
}
}
}
pub fn parse_permissions(permissions_str: &str) -> HashSet<MiniAppPermission> {
permissions_str
.split(',')
.filter_map(|s| s.trim().parse().ok())
.collect()
}
#[cfg(test)]
pub fn serialize_permissions(permissions: &HashSet<MiniAppPermission>) -> String {
let mut perms: Vec<_> = permissions.iter().map(|p| p.to_string()).collect();
perms.sort();
perms.join(",")
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionInfo {
pub id: String,
pub label: String,
pub description: String,
pub icon: String,
}
impl From<MiniAppPermission> for PermissionInfo {
fn from(perm: MiniAppPermission) -> Self {
PermissionInfo {
id: perm.to_string(),
label: perm.label().to_string(),
description: perm.description().to_string(),
icon: perm.icon().to_string(),
}
}
}
pub fn get_all_permission_info() -> Vec<PermissionInfo> {
MiniAppPermission::all().into_iter().map(|p| p.into()).collect()
}
const PERMISSIONS_POLICY_DENY_ALL: &str = concat!(
"accelerometer=(), ",
"ambient-light-sensor=(), ",
"attribution-reporting=(), ",
"autoplay=(self), ",
"battery=(), ",
"bluetooth=(), ",
"camera=(), ",
"ch-ua=(), ",
"ch-ua-arch=(), ",
"ch-ua-bitness=(), ",
"ch-ua-full-version=(), ",
"ch-ua-full-version-list=(), ",
"ch-ua-high-entropy-values=(), ",
"ch-ua-mobile=(), ",
"ch-ua-model=(), ",
"ch-ua-platform=(), ",
"ch-ua-platform-version=(), ",
"ch-ua-wow64=(), ",
"compute-pressure=(), ",
"cross-origin-isolated=(), ",
"direct-sockets=(), ",
"display-capture=(), ",
"encrypted-media=(), ",
"execution-while-not-rendered=(), ",
"execution-while-out-of-viewport=(), ",
"fullscreen=(), ",
"geolocation=(), ",
"gyroscope=(), ",
"hid=(), ",
"identity-credentials-get=(), ",
"idle-detection=(), ",
"keyboard-map=(), ",
"magnetometer=(), ",
"mediasession=(), ",
"microphone=(), ",
"midi=(), ",
"navigation-override=(), ",
"otp-credentials=(), ",
"payment=(), ",
"picture-in-picture=(), ",
"publickey-credentials-create=(), ",
"publickey-credentials-get=(), ",
"screen-wake-lock=(), ",
"serial=(), ",
"sync-xhr=(), ",
"storage-access=(), ",
"usb=(), ",
"web-share=(), ",
"window-management=(), ",
"xr-spatial-tracking=(), ",
"autofill=(), ",
"clipboard-read=(), ",
"clipboard-write=(), ",
"deferred-fetch=(), ",
"gamepad=(self), ",
"language-detector=(), ",
"language-model=(), ",
"manual-text=(), ",
"rewriter=(), ",
"speaker-selection=(), ",
"summarizer=(), ",
"translator=(), ",
"writer=(), ",
"all-screens-capture=(), ",
"browsing-topics=(), ",
"captured-surface-control=(), ",
"conversion-measurement=(), ",
"digital-credentials-get=(), ",
"digital-credentials-create=(), ",
"focus-without-user-activation=(), ",
"join-ad-interest-group=(), ",
"local-fonts=(), ",
"monetization=(), ",
"run-ad-auction=(), ",
"smart-card=(), ",
"sync-script=(), ",
"trust-token-redemption=(), ",
"unload=(), ",
"vertical-scroll=(), ",
"document-domain=(), ",
"window-placement=()",
);
const GRANTABLE_PERMISSIONS: &[(&str, &str)] = &[
("microphone", "microphone"),
("camera", "camera"),
("geolocation", "geolocation"),
("clipboard-read", "clipboard-read"),
("clipboard-write", "clipboard-write"),
("fullscreen", "fullscreen"),
("autoplay", "autoplay"),
("display-capture", "display-capture"),
("midi", "midi"),
("picture-in-picture", "picture-in-picture"),
("screen-wake-lock", "screen-wake-lock"),
("speaker-selection", "speaker-selection"),
("accelerometer", "accelerometer"),
("gyroscope", "gyroscope"),
("magnetometer", "magnetometer"),
("ambient-light-sensor", "ambient-light-sensor"),
("bluetooth", "bluetooth"),
];
pub fn build_permissions_policy(granted_permissions: &str) -> String {
let granted: std::collections::HashSet<&str> = granted_permissions
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
if granted.is_empty() {
return PERMISSIONS_POLICY_DENY_ALL.to_string();
}
let mut policy = PERMISSIONS_POLICY_DENY_ALL.to_string();
for (perm_name, directive) in GRANTABLE_PERMISSIONS {
if granted.contains(*perm_name) {
let deny_pattern = format!("{}=()", directive);
let allow_pattern = format!("{}=(self)", directive);
policy = policy.replace(&deny_pattern, &allow_pattern);
}
}
policy
}
pub fn build_isolated_permissions_policy(granted_permissions: &str) -> String {
let policy = build_permissions_policy(granted_permissions);
debug_assert!(policy.contains("cross-origin-isolated=()"));
policy.replacen("cross-origin-isolated=()", "cross-origin-isolated=(self)", 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn isolated_policy_allows_only_cross_origin_isolation_for_self() {
let base = build_permissions_policy("camera");
let isolated = build_isolated_permissions_policy("camera");
assert!(base.contains("cross-origin-isolated=()"));
assert!(isolated.contains("cross-origin-isolated=(self)"));
assert!(!isolated.contains("cross-origin-isolated=()"));
assert_eq!(base.replace("cross-origin-isolated=()", "cross-origin-isolated=(self)"), isolated);
assert!(isolated.contains("camera=(self)") && isolated.contains("microphone=()"));
assert!(build_isolated_permissions_policy("").contains("cross-origin-isolated=(self)"));
}
#[test]
fn test_parse_permissions() {
let perms = parse_permissions("microphone, camera, fullscreen");
assert!(perms.contains(&MiniAppPermission::Microphone));
assert!(perms.contains(&MiniAppPermission::Camera));
assert!(perms.contains(&MiniAppPermission::Fullscreen));
assert_eq!(perms.len(), 3);
}
#[test]
fn test_serialize_permissions() {
let mut perms = HashSet::new();
perms.insert(MiniAppPermission::Camera);
perms.insert(MiniAppPermission::Microphone);
let serialized = serialize_permissions(&perms);
assert!(serialized == "camera,microphone" || serialized == "microphone,camera");
}
#[test]
fn test_parse_unknown_permission() {
let perms = parse_permissions("microphone, unknown, camera");
assert_eq!(perms.len(), 2); }
}