1use std::collections::BTreeMap;
34use std::sync::{Arc, Mutex, MutexGuard};
35
36use blue_lang_waku::{Capability, Provided};
37use serde_json::{json, Value as Json};
38
39use crate::Pid;
40
41pub const FORMAT: &str = "blue-receipt";
43pub const VERSION: u64 = 1;
45
46fn locked<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
47 m.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
48}
49
50#[derive(Clone, Debug, Default, PartialEq, Eq)]
52pub struct Frame {
53 pub granted: Vec<Capability>,
55 pub provided: Vec<Provided>,
58 pub withheld: Vec<Capability>,
60}
61
62fn labels(caps: &[Capability]) -> Json {
63 Json::Array(caps.iter().map(|c| json!(c.label())).collect())
64}
65
66fn capability(label: &str) -> Option<Capability> {
67 Capability::host_effects()
68 .into_iter()
69 .find(|c| c.label() == label)
70}
71
72fn capabilities(j: Option<&Json>, what: &str) -> Result<Vec<Capability>, String> {
73 j.and_then(Json::as_array)
74 .ok_or_else(|| format!("a hold without `{what}`"))?
75 .iter()
76 .map(|l| {
77 l.as_str()
78 .and_then(capability)
79 .ok_or_else(|| format!("`{l}` in `{what}` is not a host capability"))
80 })
81 .collect()
82}
83
84impl Frame {
85 #[must_use]
87 pub fn block(&self, host_io: u64) -> Json {
88 json!({
89 "granted": labels(&self.granted),
90 "withheld": labels(&self.withheld),
91 "host_io": host_io,
92 })
93 }
94
95 #[must_use]
98 pub fn provided_json(&self) -> Json {
99 Json::Array(
100 self.provided
101 .iter()
102 .map(|p| serde_json::to_value(p).unwrap_or(Json::Null))
103 .collect(),
104 )
105 }
106
107 #[must_use]
109 pub fn to_json(&self) -> Json {
110 json!({
111 "granted": labels(&self.granted),
112 "provided": self.provided_json(),
113 "withheld": labels(&self.withheld),
114 })
115 }
116
117 pub fn from_json(j: &Json) -> Result<Self, String> {
124 let provided = j
125 .get("provided")
126 .and_then(Json::as_array)
127 .ok_or("a hold without `provided`")?
128 .iter()
129 .map(|p| serde_json::from_value(p.clone()).map_err(|e| format!("a provider: {e}")))
130 .collect::<Result<_, _>>()?;
131 Ok(Self {
132 granted: capabilities(j.get("granted"), "granted")?,
133 provided,
134 withheld: capabilities(j.get("withheld"), "withheld")?,
135 })
136 }
137
138 #[must_use]
140 pub fn withheld_names(&self) -> Vec<(&'static str, Capability)> {
141 self.withheld
142 .iter()
143 .flat_map(|c| c.names().into_iter().map(move |n| (n, *c)))
144 .collect()
145 }
146}
147
148#[derive(Debug, Default)]
149struct Ledger {
150 current: Pid,
151 total: u64,
152 by_pid: BTreeMap<Pid, u64>,
153}
154
155#[derive(Clone, Debug, Default)]
157pub struct HostIo(Arc<Mutex<Ledger>>);
158
159impl HostIo {
160 #[must_use]
161 pub fn new() -> Self {
162 Self::default()
163 }
164
165 pub fn tally(&self) {
167 let mut l = locked(&self.0);
168 l.total += 1;
169 let pid = l.current;
170 *l.by_pid.entry(pid).or_default() += 1;
171 }
172
173 #[must_use]
175 pub fn total(&self) -> u64 {
176 locked(&self.0).total
177 }
178
179 #[must_use]
181 pub fn of(&self, pid: Pid) -> u64 {
182 locked(&self.0).by_pid.get(&pid).copied().unwrap_or(0)
183 }
184
185 pub fn enter(&self, pid: Pid) {
187 locked(&self.0).current = pid;
188 }
189
190 pub fn take(&self, pid: Pid) -> u64 {
192 locked(&self.0).by_pid.remove(&pid).unwrap_or(0)
193 }
194}
195
196type Deferred = Box<dyn FnOnce() -> BTreeMap<String, String> + Send>;
197
198#[derive(Default)]
199enum Table {
200 #[default]
201 Unknown,
202 Deferred(Deferred),
203 Known(BTreeMap<String, String>),
204}
205
206#[derive(Clone, Default)]
208pub struct Identities(Arc<Mutex<Table>>);
209
210impl std::fmt::Debug for Identities {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 let state = match &*locked(&self.0) {
213 Table::Unknown => "unknown",
214 Table::Deferred(_) => "deferred",
215 Table::Known(_) => "known",
216 };
217 write!(f, "Identities({state})")
218 }
219}
220
221impl Identities {
222 pub fn defer(&self, hash: impl FnOnce() -> BTreeMap<String, String> + Send + 'static) {
224 *locked(&self.0) = Table::Deferred(Box::new(hash));
225 }
226
227 #[must_use]
229 pub fn of(&self, key: &str) -> Option<String> {
230 let mut state = locked(&self.0);
231 if let Table::Deferred(_) = &*state {
232 if let Table::Deferred(hash) = std::mem::take(&mut *state) {
233 *state = Table::Known(hash());
234 }
235 }
236 match &*state {
237 Table::Known(table) => table.get(key).cloned(),
238 Table::Unknown | Table::Deferred(_) => None,
239 }
240 }
241}
242
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
245pub enum Subject {
246 Actor { pid: Pid, incarnation: usize },
248 Run,
250}
251
252#[derive(Clone, Debug, PartialEq, Eq)]
254pub struct Receipt {
255 pub subject: Subject,
256 pub definition: Option<String>,
258 pub def_hash: Option<String>,
260 pub frame: Frame,
261 pub host_io: u64,
263 pub exit_kind: String,
264 pub exit_text: String,
265}
266
267impl Receipt {
268 #[must_use]
270 pub fn to_json(&self) -> Json {
271 let (subject, pid, incarnation) = match self.subject {
272 Subject::Actor { pid, incarnation } => ("actor", pid.0, incarnation),
273 Subject::Run => ("run", crate::ROOT.0, 0),
274 };
275 json!({
276 "format": FORMAT,
277 "version": VERSION,
278 "subject": subject,
279 "pid": pid,
280 "incarnation": incarnation,
281 "definition": self.definition,
282 "def_hash": self.def_hash,
283 "exit": { "kind": self.exit_kind, "text": self.exit_text },
284 "frame": self.frame.block(self.host_io),
285 "provided": self.frame.provided_json(),
286 })
287 }
288
289 #[must_use]
291 pub fn bytes(&self) -> Vec<u8> {
292 canonical(&self.to_json()).into_bytes()
293 }
294}
295
296pub type ReceiptSink = Arc<dyn Fn(&Receipt) + Send + Sync>;
298
299#[must_use]
301pub fn canonical(j: &Json) -> String {
302 let mut out = String::new();
303 write_canonical(j, &mut out);
304 out
305}
306
307fn write_canonical(j: &Json, out: &mut String) {
308 match j {
309 Json::Object(m) => {
310 let mut keys: Vec<&String> = m.keys().collect();
311 keys.sort();
312 out.push('{');
313 for (i, k) in keys.into_iter().enumerate() {
314 if i > 0 {
315 out.push(',');
316 }
317 out.push_str(&Json::String(k.clone()).to_string());
318 out.push(':');
319 write_canonical(&m[k], out);
320 }
321 out.push('}');
322 }
323 Json::Array(items) => {
324 out.push('[');
325 for (i, v) in items.iter().enumerate() {
326 if i > 0 {
327 out.push(',');
328 }
329 write_canonical(v, out);
330 }
331 out.push(']');
332 }
333 other => out.push_str(&other.to_string()),
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 fn frame() -> Frame {
342 Frame {
343 granted: vec![Capability::FileSystem],
344 provided: vec![
345 blue_lang_waku::Provision::DeterministicStepped {
346 at_ms: 5,
347 step_ms: 1,
348 }
349 .into(),
350 Provided {
351 provision: blue_lang_waku::Provision::NullSink,
352 scope: Some(std::collections::BTreeSet::from(["kazu".to_string()])),
353 },
354 ],
355 withheld: vec![
356 Capability::Process,
357 Capability::Environment,
358 Capability::Network,
359 Capability::Entropy,
360 ],
361 }
362 }
363
364 #[test]
365 fn the_frame_block_is_granted_withheld_and_host_io_and_nothing_else() {
366 let block = frame().block(3);
367 let keys: Vec<&String> = block.as_object().expect("object").keys().collect();
368 let mut sorted = keys.clone();
369 sorted.sort();
370 assert_eq!(sorted, ["granted", "host_io", "withheld"]);
371 assert_eq!(
372 canonical(&block),
373 r#"{"granted":["filesystem"],"host_io":3,"withheld":["process","environment","network","entropy"]}"#
374 );
375 }
376
377 #[test]
378 fn a_hold_survives_the_trace_format() {
379 let f = frame();
380 assert_eq!(Frame::from_json(&f.to_json()).expect("parse"), f);
381 let mut bad = f.to_json();
382 bad["withheld"] = json!(["disk"]);
383 assert!(Frame::from_json(&bad).unwrap_err().contains("disk"));
384 assert!(Frame::from_json(&json!({})).is_err());
385 }
386
387 #[test]
388 fn withheld_names_are_every_name_of_every_withheld_capability() {
389 let names = frame().withheld_names();
390 assert!(names.contains(&("getenv", Capability::Environment)));
391 assert!(names.iter().all(|(_, c)| *c != Capability::FileSystem));
392 assert_eq!(
393 names.len(),
394 frame()
395 .withheld
396 .iter()
397 .map(|c| c.names().len())
398 .sum::<usize>()
399 );
400 assert!(Frame::default().withheld_names().is_empty());
401 }
402
403 #[test]
404 fn the_ledger_tallies_to_the_process_running_and_takes_per_incarnation() {
405 let io = HostIo::new();
406 io.tally();
407 io.enter(Pid(3));
408 io.tally();
409 io.tally();
410 io.enter(crate::ROOT);
411 io.tally();
412 assert_eq!(io.total(), 4);
413 assert_eq!(io.of(crate::ROOT), 2);
414 assert_eq!(io.of(Pid(3)), 2);
415 assert_eq!(io.take(Pid(3)), 2);
416 assert_eq!(io.of(Pid(3)), 0);
417 assert_eq!(io.total(), 4, "the run total keeps every call");
418 assert_eq!(HostIo::new().total(), 0);
419 }
420
421 #[test]
422 fn identities_are_hashed_once_and_only_when_asked() {
423 let ids = Identities::default();
424 assert_eq!(ids.of("%root/f"), None);
425 let calls = Arc::new(Mutex::new(0));
426 let c = Arc::clone(&calls);
427 ids.defer(move || {
428 *locked(&c) += 1;
429 BTreeMap::from([("%root/f".to_string(), "b3:00".to_string())])
430 });
431 assert_eq!(*locked(&calls), 0);
432 assert_eq!(ids.of("%root/f").as_deref(), Some("b3:00"));
433 assert_eq!(ids.clone().of("%root/g"), None);
434 assert_eq!(*locked(&calls), 1);
435 }
436
437 #[test]
438 fn canonical_text_sorts_keys_at_every_depth_and_has_no_whitespace() {
439 let j = json!({"b": [ {"z": 1, "a": "x y"} ], "a": null});
440 assert_eq!(canonical(&j), r#"{"a":null,"b":[{"a":"x y","z":1}]}"#);
441 let r = Receipt {
442 subject: Subject::Actor {
443 pid: Pid(2),
444 incarnation: 1,
445 },
446 definition: Some("%root/w".into()),
447 def_hash: Some("b3:ab".into()),
448 frame: frame(),
449 host_io: 7,
450 exit_kind: "normal".into(),
451 exit_text: "normal".into(),
452 };
453 let text = String::from_utf8(r.bytes()).expect("utf8");
454 assert!(!text.contains(' ') && !text.contains('\n'), "{text}");
455 let back: Json = serde_json::from_str(&text).expect("json");
456 assert_eq!(canonical(&back), text);
457 assert_eq!(back["frame"]["host_io"], 7);
458 assert_eq!(back["subject"], "actor");
459 assert_eq!(back["provided"][0]["provider"], "deterministic-stepped");
460 assert!(
461 back["provided"][0].get("for").is_none(),
462 "a whole-program provider"
463 );
464 assert_eq!(
465 back["provided"][1],
466 json!({"provider": "null-sink", "for": ["kazu"]})
467 );
468 }
469}