#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum PermissionStatus {
Granted,
Denied,
NotDetermined,
Restricted,
}
impl std::fmt::Display for PermissionStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PermissionStatus::Granted => write!(f, "granted"),
PermissionStatus::Denied => write!(f, "denied"),
PermissionStatus::NotDetermined => write!(f, "not_determined"),
PermissionStatus::Restricted => write!(f, "restricted"),
}
}
}
pub fn check_permission() -> PermissionStatus {
check_permission_detailed().status
}
pub fn check_permission_detailed() -> PermissionInfo {
#[cfg(target_os = "windows")]
{
check_permission_windows()
}
#[cfg(target_os = "macos")]
{
check_permission_macos()
}
#[cfg(target_os = "linux")]
{
check_permission_linux()
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
{
PermissionInfo {
status: PermissionStatus::NotDetermined,
message: "Platform not supported".to_string(),
can_request: false,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PermissionInfo {
pub status: PermissionStatus,
pub message: String,
pub can_request: bool,
}
#[cfg(target_os = "windows")]
fn check_permission_windows() -> PermissionInfo {
use nokhwa::query;
match query(nokhwa::utils::ApiBackend::Auto) {
Ok(devices) if !devices.is_empty() => PermissionInfo {
status: PermissionStatus::Granted,
message: "Camera access granted via Windows Privacy settings".to_string(),
can_request: false,
},
Ok(_) => PermissionInfo {
status: PermissionStatus::NotDetermined,
message: "No cameras found - permission may not be granted".to_string(),
can_request: true,
},
Err(e) => PermissionInfo {
status: PermissionStatus::Denied,
message: format!("Camera access denied: {e}"),
can_request: true,
},
}
}
#[cfg(target_os = "macos")]
fn check_permission_macos() -> PermissionInfo {
use objc::runtime::{Class, Object};
use objc::{msg_send, sel, sel_impl};
use std::ffi::CString;
unsafe {
let Some(av_capture_device_class) = Class::get("AVCaptureDevice") else {
return PermissionInfo {
status: PermissionStatus::NotDetermined,
message: "AVFoundation not available".to_string(),
can_request: false,
};
};
let Some(ns_string_class) = Class::get("NSString") else {
return PermissionInfo {
status: PermissionStatus::NotDetermined,
message: "Foundation not available".to_string(),
can_request: false,
};
};
let Ok(av_media_type_video) = CString::new(crate::constants::AV_MEDIA_TYPE_VIDEO) else {
return PermissionInfo {
status: PermissionStatus::NotDetermined,
message: "Invalid media type string".to_string(),
can_request: false,
};
};
let media_type: *mut Object =
msg_send![ns_string_class, stringWithUTF8String: av_media_type_video.as_ptr()];
let auth_status: i64 =
msg_send![av_capture_device_class, authorizationStatusForMediaType: media_type];
match auth_status {
3 => PermissionInfo {
status: PermissionStatus::Granted,
message: "Camera access authorized".to_string(),
can_request: false,
},
2 => PermissionInfo {
status: PermissionStatus::Denied,
message: "Camera access denied - enable in System Preferences > Security & Privacy > Camera".to_string(),
can_request: false,
},
1 => PermissionInfo {
status: PermissionStatus::Restricted,
message: "Camera access restricted by system policy".to_string(),
can_request: false,
},
_ => PermissionInfo {
status: PermissionStatus::NotDetermined,
message: "Camera permission not yet requested".to_string(),
can_request: true,
},
}
}
}
#[cfg(target_os = "linux")]
fn check_permission_linux() -> PermissionInfo {
use crate::constants::LINUX_VIDEO_DEVICE_PREFIX;
use std::fs;
use std::path::Path;
let video_devices: Vec<_> = (0..10)
.map(|i| format!("{LINUX_VIDEO_DEVICE_PREFIX}{i}"))
.filter(|path| Path::new(path).exists())
.collect();
if video_devices.is_empty() {
return PermissionInfo {
status: PermissionStatus::NotDetermined,
message: format!("No video devices found at {LINUX_VIDEO_DEVICE_PREFIX}*"),
can_request: false,
};
}
let first_device = &video_devices[0];
match fs::metadata(first_device) {
Ok(_metadata) => {
if check_linux_group_membership() {
PermissionInfo {
status: PermissionStatus::Granted,
message: format!(
"Camera access granted (user in video group, {first_device} found)"
),
can_request: false,
}
} else {
PermissionInfo {
status: PermissionStatus::Denied,
message: format!("Camera device {first_device} exists but user not in video group - run: sudo usermod -a -G video $USER"),
can_request: true,
}
}
}
Err(e) => PermissionInfo {
status: PermissionStatus::Denied,
message: format!("Cannot access {first_device}: {e}"),
can_request: true,
},
}
}
#[cfg(target_os = "linux")]
fn check_linux_group_membership() -> bool {
use std::process::Command;
let output = Command::new("groups").output().ok();
if let Some(output) = output {
if let Ok(groups) = String::from_utf8(output.stdout) {
return groups.contains("video") || groups.contains("plugdev");
}
}
false
}
#[cfg(test)]
mod tests {
use super::{check_permission, check_permission_detailed, PermissionInfo, PermissionStatus};
#[test]
fn test_permission_status_display_values() {
assert_eq!(PermissionStatus::Granted.to_string(), "granted");
assert_eq!(PermissionStatus::Denied.to_string(), "denied");
assert_eq!(
PermissionStatus::NotDetermined.to_string(),
"not_determined"
);
assert_eq!(PermissionStatus::Restricted.to_string(), "restricted");
}
#[test]
fn test_check_permission_returns_valid_status() {
let status = check_permission();
match status {
PermissionStatus::Granted
| PermissionStatus::Denied
| PermissionStatus::NotDetermined
| PermissionStatus::Restricted => {}
}
}
#[test]
fn test_check_permission_detailed_shape() {
let info = check_permission_detailed();
assert!(!info.message.is_empty());
match info.status {
PermissionStatus::Granted
| PermissionStatus::Denied
| PermissionStatus::NotDetermined
| PermissionStatus::Restricted => {}
}
}
#[test]
fn test_permission_info_serde_roundtrip() {
let info = PermissionInfo {
status: PermissionStatus::Denied,
message: "camera blocked".to_string(),
can_request: true,
};
let json = serde_json::to_string(&info).expect("PermissionInfo should serialize");
let decoded: PermissionInfo =
serde_json::from_str(&json).expect("PermissionInfo should deserialize");
assert_eq!(decoded.status, PermissionStatus::Denied);
assert_eq!(decoded.message, "camera blocked");
assert!(decoded.can_request);
}
}