sim_lib_stream_host/
watch.rs1use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, Symbol};
4use sim_value::access::field;
5
6pub const CAP_WATCH_HEALTH: &str = "watch/health";
8
9pub const CAP_WATCH_LOCATION: &str = "watch/location";
11
12pub const CAP_WATCH_MIC: &str = "watch/mic";
14
15pub const CAP_WATCH_VENDOR_REPORT: &str = "watch/vendor-report";
17
18const WORN_SENSOR_NAMESPACE: &str = "stream/worn-sensor";
19const WORN_SAMPLE_NAMESPACE: &str = "stream/device-sample";
20const WORN_SAMPLE_KIND: &str = "worn-event";
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum WatchCapability {
25 Health,
27 Location,
29 Mic,
31 VendorReport,
33}
34
35impl WatchCapability {
36 pub const ALL: [Self; 4] = [Self::Health, Self::Location, Self::Mic, Self::VendorReport];
38
39 pub fn as_str(self) -> &'static str {
41 match self {
42 Self::Health => CAP_WATCH_HEALTH,
43 Self::Location => CAP_WATCH_LOCATION,
44 Self::Mic => CAP_WATCH_MIC,
45 Self::VendorReport => CAP_WATCH_VENDOR_REPORT,
46 }
47 }
48
49 pub fn local_name(self) -> &'static str {
51 match self {
52 Self::Health => "health",
53 Self::Location => "location",
54 Self::Mic => "mic",
55 Self::VendorReport => "vendor-report",
56 }
57 }
58
59 pub fn capability_name(self) -> CapabilityName {
61 CapabilityName::new(self.as_str())
62 }
63
64 pub fn grant_symbol(self) -> Symbol {
66 Symbol::qualified("watch", self.local_name())
67 }
68
69 pub fn from_name(name: &str) -> Option<Self> {
71 Self::ALL
72 .into_iter()
73 .find(|capability| capability.as_str() == name)
74 }
75}
76
77pub fn watch_health_grant() -> Symbol {
79 WatchCapability::Health.grant_symbol()
80}
81
82pub fn watch_location_grant() -> Symbol {
84 WatchCapability::Location.grant_symbol()
85}
86
87pub fn watch_mic_grant() -> Symbol {
89 WatchCapability::Mic.grant_symbol()
90}
91
92pub fn watch_vendor_report_grant() -> Symbol {
94 WatchCapability::VendorReport.grant_symbol()
95}
96
97pub fn watch_capability_for_worn_event(event: &Expr) -> Result<WatchCapability> {
99 ensure_worn_event_sample(event)?;
100 let sensor = required_symbol_field(event, "sensor")?;
101 if sensor.namespace.as_deref() != Some(WORN_SENSOR_NAMESPACE) {
102 return Err(Error::HostError(format!(
103 "watch worn sensor must be in {WORN_SENSOR_NAMESPACE}, found {sensor}"
104 )));
105 }
106 Ok(match sensor.name.as_ref() {
107 "gps" | "route" => WatchCapability::Location,
108 "mic-audio" => WatchCapability::Mic,
109 _ => WatchCapability::Health,
110 })
111}
112
113pub fn require_watch_consent(
115 cx: &Cx,
116 capability: WatchCapability,
117 grants: &[Symbol],
118 receipt_session: &Symbol,
119 session: &Symbol,
120) -> Result<()> {
121 cx.require(&capability.capability_name())?;
122 if receipt_session != session {
123 return Err(Error::HostError(format!(
124 "{}: consent not for this session",
125 capability.as_str()
126 )));
127 }
128 if !grants
129 .iter()
130 .any(|grant| grant.as_qualified_str() == capability.as_str())
131 {
132 return Err(Error::HostError(format!(
133 "{} requires visible consent",
134 capability.as_str()
135 )));
136 }
137 Ok(())
138}
139
140pub fn require_watch_worn_ingest(
142 cx: &Cx,
143 event: &Expr,
144 grants: &[Symbol],
145 receipt_session: &Symbol,
146 session: &Symbol,
147) -> Result<WatchCapability> {
148 let capability = watch_capability_for_worn_event(event)?;
149 require_watch_consent(cx, capability, grants, receipt_session, session)?;
150 Ok(capability)
151}
152
153fn ensure_worn_event_sample(event: &Expr) -> Result<()> {
154 let sample = required_symbol_field(event, "sample")?;
155 if sample.namespace.as_deref() == Some(WORN_SAMPLE_NAMESPACE)
156 && sample.name.as_ref() == WORN_SAMPLE_KIND
157 {
158 Ok(())
159 } else {
160 Err(Error::HostError(
161 "expected stream/device-sample worn-event".to_owned(),
162 ))
163 }
164}
165
166fn required_symbol_field(expr: &Expr, name: &str) -> Result<Symbol> {
167 match field(expr, name) {
168 Some(Expr::Symbol(symbol)) => Ok(symbol.clone()),
169 Some(_) => Err(Error::TypeMismatch {
170 expected: "symbol field",
171 found: "non-symbol",
172 }),
173 None => Err(Error::Eval(format!("missing field {name}"))),
174 }
175}