open-standards/wai/wai-rs/src/browser_sink.rs
Transaction Science bfa44f4c1d Merge #463 (wai/energy-honesty @13964c94, replayed): EN-C, the energy order of dispatch, a self-metered relay, HwShunt closed to new profiles
#463's head carries #452's original history, and with it #438's first
research note (docs/research/wai-cross-fabric.md), which names fleet hosts and
addresses; #465 replaces that history with one squash so the note never enters
main's. So #463 is not merged by its head: its own commits are replayed with
cherry-pick -x onto the train's #465 merge (767d2aa7a), the second parent here:

- #452's platform-decode commits, which the review's split gave to EN-C and
  #465 left out: 6ad92e2e8, 8695bc75f, fed8694f4;
- EN-C's fourteen: df5ea962f 11b255682 ff069acc2 244a715b3 2a8d45aa0 a203eae23
  9b6e4bada 1ffceb508 6c03ba72c ce38948da 1ca593124 507f4cd9e c95337739
  13964c944.

#463's merges of main and of #452 are not replayed; their conflict
resolutions (SPEC §7.1 step 2, the CHANGELOG) are reached by the later commits.
Checked: the replay's tree equals a three-way merge of #463's head onto the
replay's platform-decode commits with #452's head merged into main 1a465f23 as
the base, file for file, except the two files that merge conflicts on
(int_kernels_vectors.rs: #465's wgsl line and EN-C's platform-decode line, both
kept), and its CHANGELOG is #465's with #463's energy-honesty entry exactly.
No address of the fleet appears in the replayed commits.

Merging the replay here: CHANGELOG only, #463's entry on top. The registry
export is up to date after the merge.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 08:32:50 -04:00

897 lines
36 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Browser sink ([extensions/browser-sink.md]): how a sink that runs in a web page routes an
//! object to a decoder, evaluates constraint sets, reports energy beside the user agent's
//! `powerEfficient`, and binds every frame it decodes to the receipted object it came from.
//!
//! What this module implements, by section of the extension:
//!
//! - §3: the capability report, read whole or refused ([`route_report`]);
//! - §4: the user agent's decoders ([`UA_ROWS`]), the WebCodecs Codec Registry's rows and the
//! gate they make ([`CODEC_REGISTRY`], [`registry_row`]), the steps that route an object, and
//! the routing table;
//! - §5: constraint sets and their evaluation over capability-constraints §3.1's register
//! ([`evaluate`], [`REGISTER`]);
//! - §4.7: the choice among alternatives, in SPEC §4's energy order ([`choose_report`]);
//! - §6: the energy entry beside `powerEfficient`;
//! - §7: the content binding, its canonical form and its reading ([`Binding`]), and the tier a
//! frame holds ([`frame_tier`]);
//! - §9.2: the side table a page carries bindings in, as a model ([`SideTable`]) the page's own
//! JavaScript is held to by the corpus;
//! - §10: the state of the frame on screen ([`frame_state`]).
//!
//! Everything here is a pure function of its inputs, with nothing beyond `serde_json` and
//! BLAKE3, so it builds for wasm32 and the page runs the same code (`wai/wai-live-web`).
//!
//! The corpus `live-conformance/browser/` is written by [`corpus::generate`] and checked by
//! [`corpus::verify`], and by `live-conformance/browser_sink_verify.py`, which shares no code
//! with this module.
//!
//! [extensions/browser-sink.md]: ../../extensions/browser-sink.md
use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use crate::codecs::{self, registry, ReplicateClass};
// The corpus generator holds the int_motion payload it wraps; a page never needs it.
#[cfg(not(target_arch = "wasm32"))]
pub mod corpus;
#[cfg(test)]
mod tests;
/// §3: the report's version.
pub(crate) const REPORT_VERSION: u64 = 1;
/// §7.1: the algorithm this revision defines, jwp-receipts §2.1's content hash.
pub const ALG_JWP_OBJECT: &str = "jwp-object-1";
/// jwp-receipts §3 step 3: the domain of a group receipt's link hash.
pub const DOMAIN_RECEIPT_LINK: &[u8] = b"moq-jwp:receipt-link\x01";
/// The largest magnitude §5 reads as an integer, and §6's bound on a count.
pub(crate) const MAX_SAFE: i64 = (1 << 53) - 1;
/// §9.2 step 1: the entries a page holds pending per decoder by default.
pub(crate) const PENDING_BOUND: usize = 64;
// ─────────────────────────────── tiers ───────────────────────────────
/// §2: a tier, ordered `none` < `entropy-consistency` < `decode-equivalence`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Tier {
None,
EntropyConsistency,
DecodeEquivalence,
}
impl Tier {
pub const fn token(self) -> &'static str {
match self {
Tier::None => "none",
Tier::EntropyConsistency => "entropy-consistency",
Tier::DecodeEquivalence => "decode-equivalence",
}
}
/// A token read as determinism-tiers §3 reads a pin's: one outside the vocabulary is `none`.
pub fn read(s: &str) -> Tier {
match s {
"decode-equivalence" => Tier::DecodeEquivalence,
"entropy-consistency" => Tier::EntropyConsistency,
_ => Tier::None,
}
}
}
/// §2: the registered tier of a capability, from its replicate class (SPEC §7).
pub fn registered_tier(capability: &str) -> Tier {
match codecs::capability_replicate_class(capability) {
Some(ReplicateClass::StandardDefined | ReplicateClass::NeuralIntegerExact) => Tier::DecodeEquivalence,
Some(ReplicateClass::NeuralEntropyExact) => Tier::EntropyConsistency,
_ => Tier::None,
}
}
/// SPEC §4's *dispatchable*, the predicate §4.3 step 1 applies.
pub fn dispatchable(capability: &str) -> bool {
registry::is_dispatchable(capability)
}
// ─────────────────────────────── reasons ───────────────────────────────
/// §11: the codes this extension adds.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Reason {
ReportMalformed,
NotDispatchable,
TierExceedsClass,
ConstraintsUnevaluable,
ConstraintsUnmet,
NoUaMapping,
CodecMismatch,
CodecUnregistered,
UaNotAsked,
UaUnsupported,
UaInexact,
UaUnprobed,
UaProbeMismatch,
NoHost,
HostInexact,
NoWasm,
NoDecoder,
NoAlternative,
ConstraintMalformed,
ParameterUnregistered,
EnergyClassInvalid,
UnmeteredAsZero,
EnergyCountInvalid,
EnergyEvidenceMissing,
BindingMalformed,
BindingAlgUnknown,
BindingUnverifiedReference,
BindingAmbiguous,
}
/// Every code, in §11's order. Private: a public array constant is one the C header
/// generator would try to export, and none of this module belongs in `include/wai.h`.
const ALL_REASONS: [Reason; 28] = [
Reason::ReportMalformed,
Reason::NotDispatchable,
Reason::TierExceedsClass,
Reason::ConstraintsUnevaluable,
Reason::ConstraintsUnmet,
Reason::NoUaMapping,
Reason::CodecMismatch,
Reason::CodecUnregistered,
Reason::UaNotAsked,
Reason::UaUnsupported,
Reason::UaInexact,
Reason::UaUnprobed,
Reason::UaProbeMismatch,
Reason::NoHost,
Reason::HostInexact,
Reason::NoWasm,
Reason::NoDecoder,
Reason::NoAlternative,
Reason::ConstraintMalformed,
Reason::ParameterUnregistered,
Reason::EnergyClassInvalid,
Reason::UnmeteredAsZero,
Reason::EnergyCountInvalid,
Reason::EnergyEvidenceMissing,
Reason::BindingMalformed,
Reason::BindingAlgUnknown,
Reason::BindingUnverifiedReference,
Reason::BindingAmbiguous,
];
impl Reason {
/// Every code, in §11's order.
pub fn all() -> &'static [Reason] {
&ALL_REASONS
}
pub const fn code(self) -> &'static str {
match self {
Reason::ReportMalformed => "report_malformed",
Reason::NotDispatchable => "not_dispatchable",
Reason::TierExceedsClass => "tier_exceeds_class",
Reason::ConstraintsUnevaluable => "constraints_unevaluable",
Reason::ConstraintsUnmet => "constraints_unmet",
Reason::NoUaMapping => "no_ua_mapping",
Reason::CodecMismatch => "codec_mismatch",
Reason::CodecUnregistered => "codec_unregistered",
Reason::UaNotAsked => "ua_not_asked",
Reason::UaUnsupported => "ua_unsupported",
Reason::UaInexact => "ua_inexact",
Reason::UaUnprobed => "ua_unprobed",
Reason::UaProbeMismatch => "ua_probe_mismatch",
Reason::NoHost => "no_host",
Reason::HostInexact => "host_inexact",
Reason::NoWasm => "no_wasm",
Reason::NoDecoder => "no_decoder",
Reason::NoAlternative => "no_alternative",
Reason::ConstraintMalformed => "constraint_malformed",
Reason::ParameterUnregistered => "parameter_unregistered",
Reason::EnergyClassInvalid => "energy_class_invalid",
Reason::UnmeteredAsZero => "unmetered_as_zero",
Reason::EnergyCountInvalid => "energy_count_invalid",
Reason::EnergyEvidenceMissing => "energy_evidence_missing",
Reason::BindingMalformed => "binding_malformed",
Reason::BindingAlgUnknown => "binding_alg_unknown",
Reason::BindingUnverifiedReference => "binding_unverified_reference",
Reason::BindingAmbiguous => "binding_ambiguous",
}
}
}
impl std::fmt::Display for Reason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.code())
}
}
// ─────────────────────────── §4.1, §4.2: the tables ───────────────────────────
/// §4.1: one row of the user agent's decoders.
#[derive(Clone, Copy, Debug)]
pub struct UaRow {
pub capabilities: &'static [&'static str],
pub decoder: &'static str,
/// The codec string's forms: one ending `.` is that prefix and at least one more
/// character; any other is the whole string.
pub forms: &'static [&'static str],
pub tier: Tier,
}
/// §4.1's table.
pub const UA_ROWS: [UaRow; 8] = [
UaRow { capabilities: &["wai.video.vp9"], decoder: "VideoDecoder", forms: &["vp09."], tier: Tier::DecodeEquivalence },
UaRow { capabilities: &["wai.video.avc"], decoder: "VideoDecoder", forms: &["avc1.", "avc3."], tier: Tier::DecodeEquivalence },
UaRow { capabilities: &["wai.video.hevc"], decoder: "VideoDecoder", forms: &["hev1.", "hvc1."], tier: Tier::DecodeEquivalence },
UaRow { capabilities: &["wai.video.av1", "wai.video.av1.lossless"], decoder: "VideoDecoder", forms: &["av01."], tier: Tier::None },
UaRow { capabilities: &["wai.video.vp8"], decoder: "VideoDecoder", forms: &["vp8"], tier: Tier::None },
UaRow { capabilities: &["wai.audio.opus", "wai.audio.opus.packet"], decoder: "AudioDecoder", forms: &["opus"], tier: Tier::None },
UaRow { capabilities: &["wai.audio.flac"], decoder: "AudioDecoder", forms: &["flac"], tier: Tier::None },
UaRow { capabilities: &["wai.audio.aac"], decoder: "AudioDecoder", forms: &["mp4a."], tier: Tier::None },
];
/// §4.2: the WebCodecs Codec Registry's rows (W3C Registry Draft, 24 September 2026).
pub const CODEC_REGISTRY: [(&str, &str); 15] = [
("video", "av01.*"),
("video", "avc1.*"),
("video", "avc3.*"),
("video", "hev1.*"),
("video", "hvc1.*"),
("video", "vp8"),
("video", "vp09.*"),
("audio", "flac"),
("audio", "mp3"),
("audio", "mp4a.*"),
("audio", "opus"),
("audio", "vorbis"),
("audio", "ulaw"),
("audio", "alaw"),
("audio", "pcm-*"),
];
/// §4.2: whether `codec` matches `row`.
pub fn row_matches(row: &str, codec: &str) -> bool {
match row.strip_suffix('*') {
Some(prefix) => codec.len() > prefix.len() && codec.starts_with(prefix),
None => codec == row,
}
}
/// §4.2: the registry row a codec string matches, if any.
pub fn registry_row(codec: &str) -> Option<&'static str> {
CODEC_REGISTRY.iter().map(|&(_, r)| r).find(|r| row_matches(r, codec))
}
/// §4.1: the row of a capability.
pub fn ua_row(capability: &str) -> Option<&'static UaRow> {
UA_ROWS.iter().find(|r| r.capabilities.contains(&capability))
}
fn form_matches(form: &str, codec: &str) -> bool {
if form.ends_with('.') {
codec.len() > form.len() && codec.starts_with(form)
} else {
codec == form
}
}
// ─────────────────────────────── §5: constraints ───────────────────────────────
/// capability-constraints §3.1: the register this revision holds.
pub const REGISTER: [&str; 10] = [
"wai:codec",
"wai:frame_width",
"wai:frame_height",
"wai:frame_rate_milli",
"wai:bit_depth",
"wai:chroma",
"wai:sample_rate",
"wai:channel_count",
"wai:bitrate",
"wai:transfer",
];
/// §5: what evaluating a capability's sets gives.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Constraints {
Unconstrained,
Satisfied,
Unsatisfied,
/// Step 2 or 3, with its code.
Unevaluable(Reason),
}
impl Constraints {
pub fn outcome(self) -> &'static str {
match self {
Constraints::Unconstrained => "unconstrained",
Constraints::Satisfied => "satisfied",
Constraints::Unsatisfied => "unsatisfied",
Constraints::Unevaluable(_) => "unevaluable",
}
}
pub fn reason(self) -> Option<Reason> {
match self {
Constraints::Unevaluable(r) => Some(r),
Constraints::Unsatisfied => Some(Reason::ConstraintsUnmet),
_ => None,
}
}
}
/// §5: an integer as this text reads one: a JSON number with no fraction and no exponent,
/// within ±(2^53 − 1). serde_json reads a number with a fraction or an exponent as a float.
fn integer(v: &Value) -> Option<i64> {
let i = v.as_number()?.as_i64()?;
(-MAX_SAFE..=MAX_SAFE).contains(&i).then_some(i)
}
fn is_value(v: &Value) -> bool {
v.is_string() || integer(v).is_some()
}
fn constraint_well_formed(c: &Value) -> bool {
let Some(o) = c.as_object() else { return false };
if o.is_empty() || o.keys().any(|k| !matches!(k.as_str(), "enum" | "minimum" | "maximum")) {
return false;
}
if let Some(e) = o.get("enum") {
match e.as_array() {
Some(a) if !a.is_empty() && a.iter().all(is_value) => {}
_ => return false,
}
}
let lo = match o.get("minimum") {
Some(v) => match integer(v) {
Some(i) => Some(i),
None => return false,
},
None => None,
};
let hi = match o.get("maximum") {
Some(v) => match integer(v) {
Some(i) => Some(i),
None => return false,
},
None => None,
};
!matches!((lo, hi), (Some(a), Some(b)) if a > b)
}
fn meets(c: &Map<String, Value>, value: Option<&Value>) -> bool {
let Some(v) = value.filter(|v| is_value(v)) else { return false };
if let Some(e) = c.get("enum").and_then(Value::as_array) {
let hit = e.iter().any(|x| match (x.as_str(), v.as_str()) {
(Some(a), Some(b)) => a == b,
(None, None) => integer(x) == integer(v),
_ => false,
});
if !hit {
return false;
}
}
if let Some(lo) = c.get("minimum").and_then(integer)
&& integer(v).is_none_or(|i| i < lo)
{
return false;
}
if let Some(hi) = c.get("maximum").and_then(integer)
&& integer(v).is_none_or(|i| i > hi)
{
return false;
}
true
}
/// §5: `evaluate(sets, params)`.
pub fn evaluate(sets: Option<&Value>, params: &Value) -> Constraints {
let Some(sets) = sets else { return Constraints::Unconstrained };
let Some(arr) = sets.as_array().filter(|a| !a.is_empty()) else {
return Constraints::Unevaluable(Reason::ConstraintMalformed);
};
for set in arr {
let Some(o) = set.as_object() else { return Constraints::Unevaluable(Reason::ConstraintMalformed) };
if !o.values().all(constraint_well_formed) {
return Constraints::Unevaluable(Reason::ConstraintMalformed);
}
}
for set in arr {
if set.as_object().is_some_and(|o| o.keys().any(|k| !REGISTER.contains(&k.as_str()))) {
return Constraints::Unevaluable(Reason::ParameterUnregistered);
}
}
let declared = params.as_object();
let satisfied = arr.iter().any(|set| {
set.as_object()
.is_some_and(|o| o.iter().all(|(k, c)| c.as_object().is_some_and(|c| meets(c, declared.and_then(|d| d.get(k))))))
});
if satisfied {
Constraints::Satisfied
} else {
Constraints::Unsatisfied
}
}
// ─────────────────────────────── §3: the report ───────────────────────────────
fn is_bool_or_null(v: Option<&Value>) -> bool {
matches!(v, Some(Value::Bool(_)) | Some(Value::Null))
}
/// §3: whether a report reads in its shape.
fn report_reads(r: &Value) -> bool {
let Some(o) = r.as_object() else { return false };
if o.get("browser_sink").and_then(Value::as_u64) != Some(REPORT_VERSION) || o.get("browser_sink").is_some_and(|v| !v.is_u64()) {
return false;
}
let Some(ua) = o.get("ua").and_then(Value::as_object) else { return false };
for a in ua.values() {
let Some(a) = a.as_object() else { return false };
if !a.get("supported").is_some_and(Value::is_boolean)
|| !is_bool_or_null(a.get("smooth"))
|| !is_bool_or_null(a.get("power_efficient"))
|| !matches!(a.get("probe").and_then(Value::as_str), Some("match" | "mismatch" | "not_run"))
{
return false;
}
}
if !o.get("host").and_then(Value::as_object).is_some_and(|h| h.values().all(Value::is_string)) {
return false;
}
if !o.get("wasm").and_then(Value::as_array).is_some_and(|w| w.iter().all(Value::is_string)) {
return false;
}
if !o.get("constraints").is_some_and(Value::is_object) {
return false;
}
if !o.get("energy").and_then(Value::as_object).is_some_and(|e| e.values().all(Value::is_object)) {
return false;
}
let Some(objects) = o.get("objects").and_then(Value::as_array) else { return false };
objects.iter().all(|x| {
x.as_object().is_some_and(|x| {
x.get("name").is_some_and(Value::is_string)
&& x.get("capability").is_some_and(Value::is_string)
&& x.get("codec").is_some_and(|c| c.is_string() || c.is_null())
&& x.get("need").is_some_and(Value::is_string)
&& x.get("params").is_some_and(Value::is_object)
&& x.get("choice").is_none_or(|c| c.as_str().is_some_and(|c| !c.is_empty()))
})
})
}
// ─────────────────────────────── §6: energy ───────────────────────────────
/// §6: a measurement read, or the code that refuses it: `energy_order::read_measurement`,
/// the one reader the order uses too.
fn read_measurement(m: &Value) -> Result<Value, Reason> {
use crate::energy_order::MeasurementRefusal as R;
crate::energy_order::read_measurement(m).map_err(|e| match e {
R::ClassInvalid => Reason::EnergyClassInvalid,
R::UnmeteredAsZero => Reason::UnmeteredAsZero,
R::CountInvalid => Reason::EnergyCountInvalid,
R::EvidenceMissing => Reason::EnergyEvidenceMissing,
})
}
fn energy_entry(report: &Map<String, Value>, capability: &str, codec: Option<&str>, route: &str) -> Value {
let power_efficient = if route == "ua" {
codec.and_then(|c| report["ua"].get(c)).and_then(|a| a.get("power_efficient")).cloned().unwrap_or(Value::Null)
} else {
Value::Null
};
let (figure, reason) = match (route != "none").then(|| report["energy"].get(format!("{capability} {route}"))).flatten() {
None => (Value::Null, Value::Null),
Some(m) => match read_measurement(m) {
Ok(f) => (f, Value::Null),
Err(r) => (Value::Null, json!(r.code())),
},
};
json!({ "power_efficient": power_efficient, "figure": figure, "reason": reason })
}
// ─────────────────────────────── §4.3: routing ───────────────────────────────
const NOT_REACHED: &str = "not_reached";
const TAKEN: &str = "taken";
/// §4.3 for one object of a report already read.
fn route_object(report: &Map<String, Value>, o: &Map<String, Value>) -> Value {
let name = o["name"].clone();
let capability = o["capability"].as_str().unwrap_or_default();
let codec = o["codec"].as_str();
let need = Tier::read(o["need"].as_str().unwrap_or_default());
let registered = registered_tier(capability);
let legs_none = || json!({ "ua": NOT_REACHED, "host": NOT_REACHED, "wasm": NOT_REACHED });
let decided = |reason: Reason| {
json!({
"name": name, "route": "none", "reason": reason.code(), "legs": legs_none(), "asked": false,
"tier": Tier::None.token(), "energy": energy_entry(report, capability, codec, "none"),
})
};
// Steps 1-3.
if !dispatchable(capability) {
return decided(Reason::NotDispatchable);
}
if need > registered {
return decided(Reason::TierExceedsClass);
}
match evaluate(report["constraints"].get(capability), &o["params"]) {
Constraints::Unevaluable(_) => return decided(Reason::ConstraintsUnevaluable),
Constraints::Unsatisfied => return decided(Reason::ConstraintsUnmet),
_ => {}
}
// Step 4: the UA leg.
let row = ua_row(capability);
let mut asked = false;
let ua: Result<Tier, Reason> = (|| {
let codec = codec.ok_or(Reason::NoUaMapping)?;
registry_row(codec).ok_or(Reason::CodecUnregistered)?;
let row = row.ok_or(Reason::NoUaMapping)?;
if !row.forms.iter().any(|f| form_matches(f, codec)) {
return Err(Reason::CodecMismatch);
}
asked = true;
let answer = report["ua"].get(codec).ok_or(Reason::UaNotAsked)?;
if answer["supported"] != Value::Bool(true) {
return Err(Reason::UaUnsupported);
}
if need > row.tier {
return Err(Reason::UaInexact);
}
let probe = answer["probe"].as_str().unwrap_or_default();
if need == Tier::DecodeEquivalence {
match probe {
"match" => {}
"mismatch" => return Err(Reason::UaProbeMismatch),
_ => return Err(Reason::UaUnprobed),
}
}
let tier = if probe == "match" { row.tier } else { Tier::None };
Ok(tier.min(registered))
})();
// Step 5: the host leg.
let host: Result<Tier, Reason> = match report["host"].get(capability).and_then(Value::as_str) {
None => Err(Reason::NoHost),
Some(t) if Tier::read(t) < need => Err(Reason::HostInexact),
Some(t) => Ok(Tier::read(t).min(registered)),
};
// Step 6: the wasm leg.
let in_wasm = report["wasm"].as_array().is_some_and(|w| w.iter().any(|c| c.as_str() == Some(capability)));
let wasm: Result<Tier, Reason> = if in_wasm { Ok(registered) } else { Err(Reason::NoWasm) };
// Step 7.
let leg = |r: &Result<Tier, Reason>| match r {
Ok(_) => TAKEN,
Err(e) => e.code(),
};
let (route, tier, legs) = match (&ua, &host, &wasm) {
(Ok(t), _, _) => ("ua", *t, json!({ "ua": TAKEN, "host": NOT_REACHED, "wasm": NOT_REACHED })),
(Err(_), Ok(t), _) => ("host", *t, json!({ "ua": leg(&ua), "host": TAKEN, "wasm": NOT_REACHED })),
(Err(_), Err(_), Ok(t)) => ("wasm", *t, json!({ "ua": leg(&ua), "host": leg(&host), "wasm": TAKEN })),
_ => ("none", Tier::None, json!({ "ua": leg(&ua), "host": leg(&host), "wasm": leg(&wasm) })),
};
json!({
"name": name,
"route": route,
"reason": if route == "none" { json!(Reason::NoDecoder.code()) } else { Value::Null },
"legs": legs,
"asked": asked,
"tier": tier.token(),
"energy": energy_entry(report, capability, codec, route),
})
}
/// §3 and §4: a report's routing table, one entry an object, in order; or the report refused
/// whole (`report_malformed`).
pub fn route_report(report: &Value) -> Result<Vec<Value>, Reason> {
if !report_reads(report) {
return Err(Reason::ReportMalformed);
}
let r = report.as_object().expect("read");
Ok(r["objects"].as_array().expect("read").iter().map(|o| route_object(r, o.as_object().expect("read"))).collect())
}
// ─────────────────────────────── §4.7: alternatives ───────────────────────────────
/// §4.7: the choice table of a report: for each `choice`, in order of first appearance,
/// its alternatives in SPEC §4's energy order ([`crate::energy_order`]), the rank of each,
/// and the one the page decodes; or the report refused whole (`report_malformed`). A report
/// whose objects carry no `choice` has an empty table.
///
/// The order sits inside the routing: it orders the alternatives by their routes, and
/// changes none. An alternative routed `ua` ranks by the user agent's answer for its codec
/// string (`supported`, `smooth`, `power_efficient`; a `null` is not a `true`); one routed
/// `host` or `wasm` has no answer that is about the decoder that runs it, and ranks
/// `classical`; a learned one ranks `classical` when the report holds a measurement of its
/// decode under its route's key that qualifies (§6; `energy_order::qualifies`: a hardware
/// class, coverage `Whole`, its value not read), and `learned-without-budget` otherwise;
/// one routed `none` comes last, with no rank. Report order within a rank. The chosen
/// alternative is the first in that order whose route is not `none`.
pub fn choose_report(report: &Value) -> Result<Vec<Value>, Reason> {
use crate::energy_order::{DispatchPolicy, PlatformAnswer, Rank};
let table = route_report(report)?;
let objects = report["objects"].as_array().expect("read");
let mut groups: Vec<(String, Vec<usize>)> = Vec::new();
for (i, o) in objects.iter().enumerate() {
let Some(c) = o.get("choice").and_then(Value::as_str) else { continue };
match groups.iter_mut().find(|(g, _)| g == c) {
Some((_, members)) => members.push(i),
None => groups.push((c.to_string(), vec![i])),
}
}
// The rank of one alternative: `None` for a route of `none`.
let rank = |i: usize| -> Option<Rank> {
let route = table[i]["route"].as_str().unwrap_or("none");
if route == "none" {
return None;
}
let capability = objects[i]["capability"].as_str().unwrap_or_default();
let answer = (route == "ua").then(|| {
let a = objects[i]["codec"].as_str().and_then(|c| report["ua"].get(c)).cloned().unwrap_or(Value::Null);
PlatformAnswer { supported: a["supported"] == Value::Bool(true), smooth: a["smooth"].as_bool(), power_efficient: a["power_efficient"].as_bool() }
});
let measured = report["energy"].get(format!("{capability} {route}")).cloned();
let platform = move |_: &str| answer;
let figure = move |_: &str| measured.clone();
Some(DispatchPolicy::energy().with_platform(&platform).with_measured(&figure).rank(capability))
};
Ok(groups
.into_iter()
.map(|(choice, members)| {
let mut ordered: Vec<(u8, usize, Option<Rank>)> = members
.iter()
.map(|&i| {
let r = rank(i);
(r.map_or(u8::MAX, |r| r as u8), i, r)
})
.collect();
ordered.sort_by_key(|&(k, i, _)| (k, i));
let chosen = ordered.iter().find(|(_, _, r)| r.is_some()).map(|&(_, i, _)| i);
json!({
"choice": choice,
"order": ordered.iter().map(|&(_, i, _)| objects[i]["name"].clone()).collect::<Vec<_>>(),
"ranks": ordered.iter().map(|(_, _, r)| r.map_or(Value::Null, |r| json!(r.as_str()))).collect::<Vec<_>>(),
"chosen": chosen.map(|i| objects[i]["name"].clone()).unwrap_or(Value::Null),
"reason": if chosen.is_some() { Value::Null } else { json!(Reason::NoAlternative.code()) },
})
})
.collect())
}
// ─────────────────────────────── §7: the binding ───────────────────────────────
/// jwp-receipts §3 step 3: a group receipt's link hash, over its canonical bytes.
pub fn link_hash(canonical: &[u8]) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(DOMAIN_RECEIPT_LINK);
h.update(canonical);
*h.finalize().as_bytes()
}
/// §7.1: a content binding.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Binding {
pub digest: [u8; 32],
pub receipt: Option<[u8; 32]>,
pub tier: Tier,
}
fn hex32(s: &str) -> Option<[u8; 32]> {
if s.len() != 64 || !s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) {
return None;
}
let mut out = [0u8; 32];
for (i, o) in out.iter_mut().enumerate() {
*o = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).ok()?;
}
Some(out)
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
impl Binding {
/// §7.2: the binding of a frame decoded from `object`, the receipted object's bytes as
/// received, verified (or not) under the group receipt whose link hash is `receipt`.
pub fn of_object(object: &[u8], receipt: Option<[u8; 32]>, tier: Tier) -> Binding {
Binding { digest: crate::jwp_hash::content_hash(object), receipt, tier }
}
/// §7.1: the value, its members in order.
pub fn to_json(&self) -> Value {
let mut m = Map::new();
m.insert("alg".into(), json!(ALG_JWP_OBJECT));
m.insert("digest".into(), json!(hex(&self.digest)));
if let Some(r) = self.receipt {
m.insert("receipt".into(), json!(hex(&r)));
}
m.insert("tier".into(), json!(self.tier.token()));
Value::Object(m)
}
/// §7.1: the canonical form.
pub fn canonical(&self) -> String {
serde_json::to_string(&self.to_json()).expect("JSON")
}
/// §7.1: a value read, or the code that refuses it.
pub fn read(v: &Value) -> Result<Binding, Reason> {
let o = v.as_object().ok_or(Reason::BindingMalformed)?;
let s = |k: &str| o.get(k).and_then(Value::as_str);
let (Some(alg), Some(digest), Some(tier)) = (s("alg"), s("digest"), s("tier")) else {
return Err(Reason::BindingMalformed);
};
let digest = hex32(digest).ok_or(Reason::BindingMalformed)?;
let receipt = match o.get("receipt") {
None => None,
Some(r) => Some(r.as_str().and_then(hex32).ok_or(Reason::BindingMalformed)?),
};
if alg != ALG_JWP_OBJECT {
return Err(Reason::BindingAlgUnknown);
}
Ok(Binding { digest, receipt, tier: Tier::read(tier) })
}
}
/// §7.2: the tier a decoded frame's samples hold: the route's tier, lowered to `none` for a
/// UA decoder's frame whose `format` is null, and for a frame the page altered.
pub fn frame_tier(route: &str, route_tier: Tier, format_null: bool, altered: bool) -> Tier {
if altered || (route == "ua" && format_null) {
Tier::None
} else {
route_tier
}
}
// ─────────────────────────────── §10: the frame on screen ───────────────────────────────
/// §10: the state of the frame a tile last drew. `binding` is the value the page holds, or
/// none; the three verdicts are §10's tokens. A token outside them is an input error, not a
/// state.
pub fn frame_state(binding: Option<&Value>, object: &str, chain: &str, provenance: &str) -> Result<Value, String> {
if !matches!(object, "verified" | "refused" | "unverified") {
return Err(format!("object verdict {object:?}"));
}
if !matches!(chain, "linked" | "anchor" | "history" | "held" | "refused") {
return Err(format!("chain verdict {chain:?}"));
}
if !matches!(provenance, "accepted" | "refused" | "missing" | "not_applicable") {
return Err(format!("provenance verdict {provenance:?}"));
}
let read = binding.map(Binding::read);
let (state, reason, b) = match read {
None => ("unbound", Value::Null, None),
Some(Err(r)) => ("unbound", json!(r.code()), None),
Some(Ok(b)) => {
let s = if object == "refused" || chain == "refused" || provenance == "refused" {
"refused"
} else if object == "unverified" || chain == "held" || provenance == "missing" {
"pending"
} else {
"verified"
};
(s, Value::Null, Some(b))
}
};
Ok(json!({
"state": state,
"reason": reason,
"tier": b.as_ref().map(|b| json!(b.tier.token())).unwrap_or(Value::Null),
"digest": b.as_ref().map(|b| json!(hex(&b.digest))).unwrap_or(Value::Null),
"receipt": b.as_ref().and_then(|b| b.receipt).map(|r| json!(hex(&r))).unwrap_or(Value::Null),
}))
}
// ─────────────────────────────── §9.2: the side table ───────────────────────────────
#[derive(Clone, Debug)]
enum Pending {
Entry(Option<Binding>),
Struck,
}
/// §9.2 as a model: decoders and frames named by strings, the operations a page performs in
/// order. The page's JavaScript keeps the same table over live objects; the corpus holds both
/// to the same sequences.
#[derive(Clone, Debug)]
pub struct SideTable {
bound: usize,
/// Per decoder, its pending entries by timestamp, in the order they were entered.
pending: BTreeMap<String, Vec<(i64, Pending)>>,
frames: BTreeMap<String, Option<Binding>>,
refusals: Vec<(usize, Reason)>,
struck: u64,
}
impl SideTable {
pub fn new(bound: usize) -> SideTable {
SideTable { bound, pending: BTreeMap::new(), frames: BTreeMap::new(), refusals: Vec::new(), struck: 0 }
}
/// One operation of a sequence (the corpus's form). `at` is its index, for a refusal.
pub fn apply(&mut self, at: usize, op: &Value) -> Result<(), String> {
let s = |k: &str| op.get(k).and_then(Value::as_str).map(str::to_owned).ok_or_else(|| format!("op {at}: no {k}"));
let ts = || op.get("timestamp").and_then(Value::as_i64).ok_or_else(|| format!("op {at}: no timestamp"));
match op.get("op").and_then(Value::as_str) {
Some("chunk") => {
let decoder = s("decoder")?;
let b = match op.get("binding") {
None | Some(Value::Null) => None,
Some(v) => Some(Binding::read(v).map_err(|r| format!("op {at}: binding {r}"))?),
};
let t = ts()?;
let q = self.pending.entry(decoder).or_default();
if let Some(e) = q.iter_mut().find(|(x, _)| *x == t) {
if matches!(e.1, Pending::Entry(_)) {
self.refusals.push((at, Reason::BindingAmbiguous));
}
e.1 = Pending::Struck;
} else {
q.push((t, Pending::Entry(b)));
if q.len() > self.bound {
q.remove(0);
self.struck += 1;
}
}
}
Some("output") => {
let (decoder, frame, t) = (s("decoder")?, s("frame")?, ts()?);
let q = self.pending.entry(decoder).or_default();
let b = match q.iter().position(|(x, _)| *x == t) {
Some(i) => match q.remove(i).1 {
Pending::Entry(b) => b,
Pending::Struck => None,
},
None => None,
};
self.frames.insert(frame, b);
}
Some("clone") => {
let (from, frame) = (s("from")?, s("frame")?);
let b = self.frames.get(&from).ok_or_else(|| format!("op {at}: clone of a frame not open"))?.clone();
self.frames.insert(frame, b);
}
Some("transfer") => {
let (from, frame) = (s("from")?, s("frame")?);
let b = self.frames.remove(&from).ok_or_else(|| format!("op {at}: transfer of a frame not open"))?;
let with = op.get("with_binding").and_then(Value::as_bool).unwrap_or(false);
self.frames.insert(frame, if with { b } else { None });
}
Some("close") => {
self.frames.remove(&s("frame")?);
}
Some("reset" | "flush") => {
let n = self.pending.remove(&s("decoder")?).map_or(0, |q| q.len());
self.struck += n as u64;
}
other => return Err(format!("op {at}: {other:?}")),
}
Ok(())
}
/// The open frames and the binding each holds, in canonical form or null; the refusals,
/// and the count of entries struck by a bound, a reset or a flush.
pub fn result(&self) -> Value {
let frames: Map<String, Value> =
self.frames.iter().map(|(k, b)| (k.clone(), b.as_ref().map(|b| json!(b.canonical())).unwrap_or(Value::Null))).collect();
json!({
"frames": frames,
"refusals": self.refusals.iter().map(|(at, r)| json!({ "at": at, "reason": r.code() })).collect::<Vec<_>>(),
"struck": self.struck,
})
}
}
/// Run one sequence through a fresh table.
pub fn run_table(bound: usize, ops: &[Value]) -> Result<Value, String> {
let mut t = SideTable::new(bound);
for (i, op) in ops.iter().enumerate() {
t.apply(i, op)?;
}
Ok(t.result())
}