use sim_kernel::{CapabilityName, Cx, Error, EventKind, EventLedger, Expr, Ref, Result, Symbol};
use sim_lib_scene::{GlanceCard, GlanceMetric};
use sim_value::{access, build};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DeviceCapability {
Pose,
Camera,
Health,
Location,
Mic,
VendorReport,
}
impl DeviceCapability {
pub const ALL: [Self; 6] = [
Self::Pose,
Self::Camera,
Self::Health,
Self::Location,
Self::Mic,
Self::VendorReport,
];
pub fn as_str(self) -> &'static str {
match self {
Self::Pose => "device/pose",
Self::Camera => "device/camera",
Self::Health => "device/health",
Self::Location => "device/location",
Self::Mic => "device/mic",
Self::VendorReport => "device/vendor-report",
}
}
pub fn capability_name(self) -> CapabilityName {
CapabilityName::new(self.as_str())
}
pub fn grant_symbol(self) -> Symbol {
Symbol::qualified("device", self.local_name())
}
pub fn from_name(name: &str) -> Option<Self> {
Self::ALL
.into_iter()
.find(|capability| capability.as_str() == name)
}
fn local_name(self) -> &'static str {
self.as_str()
.strip_prefix("device/")
.expect("device capability names keep the device/ prefix")
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EdgeId(Symbol);
impl EdgeId {
pub fn new(symbol: Symbol) -> Self {
Self(symbol)
}
pub fn named(name: impl Into<String>) -> Self {
Self(Symbol::qualified("device/session", name.into()))
}
pub fn as_symbol(&self) -> &Symbol {
&self.0
}
pub fn to_expr(&self) -> Expr {
Expr::Symbol(self.0.clone())
}
pub fn from_expr(expr: &Expr) -> Result<Self> {
match expr {
Expr::Symbol(symbol) => Ok(Self(symbol.clone())),
_ => Err(Error::TypeMismatch {
expected: "device edge id symbol",
found: "non-symbol",
}),
}
}
}
impl From<Symbol> for EdgeId {
fn from(symbol: Symbol) -> Self {
Self::new(symbol)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConsentReceipt {
pub grants: Vec<Symbol>,
pub retain_ms: u64,
pub redact: Vec<Symbol>,
pub session: EdgeId,
pub seq: u64,
}
impl ConsentReceipt {
pub fn new(
grants: Vec<Symbol>,
retain_ms: u64,
redact: Vec<Symbol>,
session: EdgeId,
seq: u64,
) -> Self {
Self {
grants: dedup_symbols(grants),
retain_ms,
redact: dedup_symbols(redact),
session,
seq,
}
}
pub fn to_expr(&self) -> Expr {
build::map(vec![
("kind", build::qsym("device", "consent-receipt")),
("grants", symbol_list(&self.grants)),
("retain-ms", build::uint(self.retain_ms)),
("redact", symbol_list(&self.redact)),
("session", self.session.to_expr()),
("seq", build::uint(self.seq)),
])
}
pub fn from_expr(expr: &Expr) -> Result<Self> {
ensure_kind(expr)?;
let grants = symbol_vec(access::required(expr, "grants", "device consent receipt")?)?;
let retain_ms = uint_field(expr, "retain-ms")?;
let redact = symbol_vec(access::required(expr, "redact", "device consent receipt")?)?;
let session =
EdgeId::from_expr(access::required(expr, "session", "device consent receipt")?)?;
let seq = uint_field(expr, "seq")?;
Ok(Self::new(grants, retain_ms, redact, session, seq))
}
pub fn to_badge_scene(&self) -> Expr {
sim_lib_scene::badge("ok", &format!("consent {}", self.seq))
}
pub fn to_glance_scene(&self) -> Expr {
GlanceCard::new(
"Device consent",
Some(GlanceMetric::new(
"session",
self.session.as_symbol().as_qualified_str(),
)),
None,
"info",
1,
)
.to_scene()
}
}
pub fn record_consent_receipt(
ledger: &mut EventLedger,
run: Ref,
grants: Vec<Symbol>,
retain_ms: u64,
redact: Vec<Symbol>,
session: EdgeId,
) -> Result<ConsentReceipt> {
let next_seq = ledger.len_for_run(&run) as u64;
let event = ledger.push(
run,
EventKind::Card {
subject: Ref::Symbol(session.as_symbol().clone()),
card: Ref::Symbol(Symbol::qualified(
"device/consent",
format!("receipt-{next_seq}"),
)),
},
)?;
Ok(ConsentReceipt::new(
grants, retain_ms, redact, session, event.seq,
))
}
pub fn require_with_consent(
cx: &Cx,
name: &str,
receipt: &ConsentReceipt,
session: &EdgeId,
) -> Result<()> {
cx.require(&CapabilityName::new(name.to_owned()))?;
if &receipt.session != session {
return Err(Error::HostError(format!(
"{name}: consent not for this session"
)));
}
if !receipt
.grants
.iter()
.any(|grant| grant_matches(grant, name))
{
return Err(Error::HostError(format!("{name} requires visible consent")));
}
Ok(())
}
fn ensure_kind(expr: &Expr) -> Result<()> {
match access::field_sym(expr, "kind") {
Some(kind)
if kind.namespace.as_deref() == Some("device")
&& kind.name.as_ref() == "consent-receipt" =>
{
Ok(())
}
_ => Err(Error::HostError(
"expected device/consent-receipt".to_owned(),
)),
}
}
fn uint_field(expr: &Expr, name: &str) -> Result<u64> {
let value = access::required(expr, name, "device consent receipt")?;
match value {
Expr::Number(number) if number.domain.namespace.is_none() => number
.canonical
.parse()
.map_err(|_| Error::Eval(format!("device consent receipt field {name} is not u64"))),
_ => Err(Error::Eval(format!(
"device consent receipt field {name} is not u64"
))),
}
}
fn symbol_vec(expr: &Expr) -> Result<Vec<Symbol>> {
match expr {
Expr::List(items) => items
.iter()
.map(|item| match item {
Expr::Symbol(symbol) => Ok(symbol.clone()),
_ => Err(Error::TypeMismatch {
expected: "symbol list",
found: "non-symbol",
}),
})
.collect(),
_ => Err(Error::TypeMismatch {
expected: "symbol list",
found: "non-list",
}),
}
}
fn symbol_list(symbols: &[Symbol]) -> Expr {
build::list(symbols.iter().cloned().map(Expr::Symbol).collect())
}
fn grant_matches(grant: &Symbol, name: &str) -> bool {
grant.as_qualified_str() == name
}
fn dedup_symbols(mut symbols: Vec<Symbol>) -> Vec<Symbol> {
symbols.sort();
symbols.dedup();
symbols
}