car-ffi-common 0.25.0

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
//! JSON wrapper around `car-permissions` for CLI/bindings/server.

use car_permissions::{Domain, PermissionStatus};
use serde_json::{json, Value};

fn err_json(e: car_permissions::PermissionError) -> String {
    use car_permissions::PermissionError::*;
    let code = match e {
        Unavailable(_) => "unavailable",
        Backend(_) => "backend",
    };
    serde_json::json!({"code": code, "message": e.to_string()}).to_string()
}

fn parse_domain(name: &str) -> Result<Domain, String> {
    Ok(match name {
        "browser" => Domain::Browser,
        "contacts" => Domain::Contacts,
        "calendar" => Domain::Calendar,
        "mail" => Domain::Mail,
        "accessibility" => Domain::Accessibility,
        "screen_capture" => Domain::ScreenCapture,
        "automation" => Domain::Automation,
        "camera" => Domain::Camera,
        "microphone" => Domain::Microphone,
        "health_read" => Domain::HealthRead,
        other => {
            return Err(serde_json::json!({
                "code": "invalid_domain",
                "message": format!("unknown permission domain: {}", other),
            })
            .to_string())
        }
    })
}

fn status_label(s: PermissionStatus) -> &'static str {
    match s {
        PermissionStatus::Granted => "granted",
        PermissionStatus::Denied => "denied",
        PermissionStatus::NotDetermined => "not_determined",
        PermissionStatus::Restricted => "restricted",
        PermissionStatus::NotApplicable => "not_applicable",
        PermissionStatus::RestartRequired => "restart_required",
        PermissionStatus::SignatureChanged => "signature_changed",
        // `PermissionStatus` is `#[non_exhaustive]`; report unknown future
        // variants conservatively rather than panicking.
        _ => "unknown",
    }
}

fn make_ctx(target_bundle_id: Option<&str>) -> car_permissions::Context {
    let mut c = car_permissions::Context::default();
    if let Some(t) = target_bundle_id {
        c.target_bundle_id = Some(t.to_string());
    }
    c
}

pub fn status(domain: &str, target_bundle_id: Option<&str>) -> Result<Value, String> {
    let d = parse_domain(domain)?;
    let s = car_permissions::status_with(d, &make_ctx(target_bundle_id)).map_err(err_json)?;
    Ok(json!({
        "domain": domain,
        "status": status_label(s),
        "target_bundle_id": target_bundle_id,
    }))
}

pub fn request(domain: &str, target_bundle_id: Option<&str>) -> Result<Value, String> {
    let d = parse_domain(domain)?;
    let s = car_permissions::request_with(d, &make_ctx(target_bundle_id)).map_err(err_json)?;
    Ok(json!({
        "domain": domain,
        "status": status_label(s),
        "target_bundle_id": target_bundle_id,
    }))
}

pub fn explain(domain: &str, target_bundle_id: Option<&str>) -> Result<Value, String> {
    let d = parse_domain(domain)?;
    let e = car_permissions::explain_with(d, &make_ctx(target_bundle_id)).map_err(err_json)?;
    Ok(json!({
        "domain": domain,
        "status": status_label(e.status),
        "message": e.message,
        "fix": e.fix,
        "target_bundle_id": target_bundle_id,
    }))
}

pub fn domains() -> Value {
    let all: Vec<&str> = car_permissions::domains()
        .into_iter()
        .map(|d| d.as_str())
        .collect();
    json!({"domains": all})
}