open-standards/wai/wai-rs/src/live_compose.rs
2026-10-17 09:24:07 -04:00

920 lines
33 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.

//! Live composition — `wai.live.compose` ([extensions/live-compose.md]).
//!
//! A room's composition is an integer function. A signed **composition descriptor**
//! (`WCD1`), by the room authority or a principal whose grant has the compose power
//! (live-rooms §3.3), fixes the canvas, the background, the layout (a grid, or a grid with
//! a picture-in-picture window), the attribute keys it reads and its limits. The
//! **participant set** at a room-clock instant is derived from the room's lifecycle
//! replay ([`crate::live_room`]): who publishes, each one's video and audio source and its
//! dimensions, the attributes of its grant in effect, and its join order. The layout and
//! the voices of the mix are then a pure function of the two ([`layout::layout`]), and the
//! mix a pure function of decoded PCM ([`layout::mix`]), so every sink composes the same
//! rectangles locally, and a server compositor that publishes a composed track signs a
//! **composition receipt** (`WCR1`) that anyone can check against the same function.
//!
//! What this module implements, by section of the extension:
//!
//! - §3: the descriptor, read, its canvas limits and field rules, its digest and
//! signature, and who may sign it;
//! - §4: the room state at an instant, the participant set, the attributes the descriptor
//! reads, the order, and the set's digest;
//! - §5, §6: the layout and the mixer, in [`layout`], which uses nothing beyond `core`,
//! `Vec` and `String`;
//! - §8: the receipt, read, signed, and verified against the room and the descriptor.
//!
//! The corpus `live-conformance/compose/` is written by [`corpus::generate`] and checked
//! by [`corpus::verify`] here, and by `live-conformance/live_compose_verify.py`, which
//! shares no code with this module.
//!
//! [extensions/live-compose.md]: ../../extensions/live-compose.md
use std::collections::{BTreeMap, BTreeSet};
use ed25519_dalek::{Signer, SigningKey};
use sha2::{Digest, Sha256};
use crate::live_events::{self as events, anchor_track, kind, Event, Payload};
use crate::live_room::{
self as room, layering, media, media_name, power, render_namespace, track_name, track_namespace, verify_room, At, Descriptor, Grant, Held, Owner, RoomOutcome, RoomRun,
RoomState,
};
pub mod corpus;
pub mod layout;
use layout::{Entrant, Layout, Params, Refusal};
/// The record this extension registers.
pub const CAPABILITY: &str = "wai.live.compose";
/// The magic of a composition descriptor (§3.1).
pub const DESCRIPTOR_MAGIC: [u8; 4] = *b"WCD1";
/// The magic of a composition receipt (§8.1).
pub const RECEIPT_MAGIC: [u8; 4] = *b"WCR1";
/// The version of both records in this revision.
pub const RECORD_VERSION: u8 = 1;
/// The domain a descriptor's signature is made over (§3.2).
pub const DESCRIPTOR_DOMAIN: &[u8] = b"wai:live-compose\x01";
/// The domain a receipt's signature is made over (§8.1).
pub const RECEIPT_DOMAIN: &[u8] = b"wai:live-compose-receipt\x01";
/// The domain of a participant set's digest (§4.5).
pub const SET_DOMAIN: &[u8] = b"wai:live-compose-set\x01";
/// A descriptor's `compositor` when no server composes (§3.1, §7).
pub const NO_COMPOSITOR: [u8; 32] = [0; 32];
// ---------------------------------------------------------------------------------------
// §10 Reasons
// ---------------------------------------------------------------------------------------
/// Why a composition, a mix or a receipt is refused (§10).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Reason {
/// A code of live-rooms §11, or of live-events §9 through it.
Room(room::Reason),
CanvasOutOfRange,
ComposeInvalid,
ComposeNotPermitted,
NotYetEffective,
LifecycleIncomplete,
AttrMalformed,
MixInputMalformed,
ReceiptInvalid,
DescriptorMismatch,
CompositorNotPermitted,
OutputNotPublished,
StateMismatch,
InputsMismatch,
}
impl From<room::Reason> for Reason {
fn from(r: room::Reason) -> Self {
Reason::Room(r)
}
}
impl From<events::Reason> for Reason {
fn from(r: events::Reason) -> Self {
Reason::Room(room::Reason::Events(r))
}
}
impl From<Refusal> for Reason {
fn from(r: Refusal) -> Self {
match r {
Refusal::CanvasOutOfRange => Reason::CanvasOutOfRange,
Refusal::ComposeInvalid => Reason::ComposeInvalid,
Refusal::AttrMalformed => Reason::AttrMalformed,
Refusal::MixInputMalformed => Reason::MixInputMalformed,
}
}
}
const TRUNCATED: Reason = Reason::Room(room::Reason::Events(events::Reason::Truncated));
const MALFORMED: Reason = Reason::Room(room::Reason::Events(events::Reason::Malformed));
const UNKNOWN_VERSION: Reason = Reason::Room(room::Reason::Events(events::Reason::UnknownVersion));
const WRONG_ROOM: Reason = Reason::Room(room::Reason::Events(events::Reason::WrongRoom));
impl Reason {
/// The codes this extension adds, in §10's order.
pub const ALL: [Reason; 13] = [
Reason::CanvasOutOfRange,
Reason::ComposeInvalid,
Reason::ComposeNotPermitted,
Reason::NotYetEffective,
Reason::LifecycleIncomplete,
Reason::AttrMalformed,
Reason::MixInputMalformed,
Reason::ReceiptInvalid,
Reason::DescriptorMismatch,
Reason::CompositorNotPermitted,
Reason::OutputNotPublished,
Reason::StateMismatch,
Reason::InputsMismatch,
];
/// The reason's code.
pub fn code(self) -> &'static str {
match self {
Reason::Room(r) => r.code(),
Reason::CanvasOutOfRange => "canvas_out_of_range",
Reason::ComposeInvalid => "compose_invalid",
Reason::ComposeNotPermitted => "compose_not_permitted",
Reason::NotYetEffective => "not_yet_effective",
Reason::LifecycleIncomplete => "lifecycle_incomplete",
Reason::AttrMalformed => "attr_malformed",
Reason::MixInputMalformed => "mix_input_malformed",
Reason::ReceiptInvalid => "receipt_invalid",
Reason::DescriptorMismatch => "descriptor_mismatch",
Reason::CompositorNotPermitted => "compositor_not_permitted",
Reason::OutputNotPublished => "output_not_published",
Reason::StateMismatch => "state_mismatch",
Reason::InputsMismatch => "inputs_mismatch",
}
}
}
impl std::fmt::Display for Reason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.code())
}
}
impl std::error::Error for Reason {}
/// Where a verification stopped (§9).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Place {
/// The room: where live-rooms §9 refused it, or none for the room as a whole.
Room(Option<At>),
Descriptor,
Receipt,
/// The participant set or the layout at this instant.
Set(u64),
}
// ---------------------------------------------------------------------------------------
// Reading
// ---------------------------------------------------------------------------------------
fn sha256(parts: &[&[u8]]) -> [u8; 32] {
let mut h = Sha256::new();
for p in parts {
h.update(p);
}
h.finalize().into()
}
/// A cursor; running out is `truncated`.
struct Rd<'a> {
b: &'a [u8],
p: usize,
}
impl<'a> Rd<'a> {
fn take(&mut self, n: usize) -> Result<&'a [u8], Reason> {
let e = self.p.checked_add(n).filter(|&e| e <= self.b.len()).ok_or(TRUNCATED)?;
let s = &self.b[self.p..e];
self.p = e;
Ok(s)
}
fn arr<const N: usize>(&mut self) -> Result<[u8; N], Reason> {
Ok(self.take(N)?.try_into().expect("N bytes"))
}
fn u8(&mut self) -> Result<u8, Reason> {
Ok(self.take(1)?[0])
}
fn u16(&mut self) -> Result<u16, Reason> {
Ok(u16::from_le_bytes(self.arr()?))
}
fn u32(&mut self) -> Result<u32, Reason> {
Ok(u32::from_le_bytes(self.arr()?))
}
fn u64(&mut self) -> Result<u64, Reason> {
Ok(u64::from_le_bytes(self.arr()?))
}
/// A short field: a u8 length and that many bytes.
fn short(&mut self) -> Result<&'a [u8], Reason> {
let n = self.u8()? as usize;
self.take(n)
}
}
/// live-events §3.3's first two steps, with `magic` for `WEV1`.
fn read_head(b: &[u8], magic: &[u8; 4]) -> Result<(), Reason> {
if b.len() < 4 {
return Err(TRUNCATED);
}
if b[..4] != *magic {
return Err(MALFORMED);
}
if b.len() < 5 {
return Err(TRUNCATED);
}
if b[4] != RECORD_VERSION {
return Err(UNKNOWN_VERSION);
}
Ok(())
}
fn verify_sig(key: &[u8; 32], domain: &[u8], body: &[u8], sig: &[u8; 64]) -> Result<(), Reason> {
let mut m = domain.to_vec();
m.extend_from_slice(body);
crate::sig::verify_strict(key, &m, sig).map_err(|e| match e {
crate::sig::SigError::BadKey => events::Reason::BadKey.into(),
crate::sig::SigError::BadSignature => events::Reason::BadSignature.into(),
})
}
fn put_short(o: &mut Vec<u8>, s: &[u8]) {
assert!(s.len() <= u8::MAX as usize, "a short field is at most 255 bytes");
o.push(s.len() as u8);
o.extend_from_slice(s);
}
fn u16_of(v: u32) -> [u8; 2] {
u16::try_from(v).expect("a field of 16 bits").to_le_bytes()
}
// ---------------------------------------------------------------------------------------
// §3 The composition descriptor
// ---------------------------------------------------------------------------------------
/// A composition descriptor (§3.1).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Compose {
/// The room id (live-rooms §3.2).
pub room: [u8; 32],
/// The key that signs it: the room authority, or a principal with the compose power.
pub signer: [u8; 32],
/// The room-clock instant from which it applies.
pub effective: u64,
/// The principal that may publish the composed tracks; [`NO_COMPOSITOR`] for none.
pub compositor: [u8; 32],
/// sRGB, 8 bits each of red, green and blue.
pub background: [u8; 3],
/// The highest layer composed (§4.2).
pub layer: u8,
/// Everything else, as the layout reads it.
pub params: Params,
pub sig: [u8; 64],
}
impl Compose {
/// The descriptor without `sig` (§3.1).
pub fn body(&self) -> Vec<u8> {
let p = &self.params;
let mut o = Vec::with_capacity(256);
o.extend_from_slice(&DESCRIPTOR_MAGIC);
o.push(RECORD_VERSION);
o.extend_from_slice(&self.room);
o.extend_from_slice(&self.signer);
o.extend_from_slice(&self.effective.to_le_bytes());
o.extend_from_slice(&self.compositor);
o.extend_from_slice(&u16_of(p.width));
o.extend_from_slice(&u16_of(p.height));
o.push(p.fps);
o.extend_from_slice(&self.background);
o.push(self.layer);
o.push(p.layout);
o.extend_from_slice(&u16_of(p.gap));
o.push(p.aspect);
o.push(p.fill);
put_short(&mut o, p.featured_attr.as_bytes());
put_short(&mut o, p.order_attr.as_bytes());
o.push(p.omit_stopped as u8);
put_short(&mut o, p.pip_attr.as_bytes());
o.push(p.corner);
o.extend_from_slice(&u16_of(p.pip_width));
o.extend_from_slice(&u16_of(p.pip_height));
o.extend_from_slice(&u16_of(p.offset));
o.push(p.behaviour);
o.push(p.channels);
o.extend_from_slice(&p.sample_rate.to_le_bytes());
put_short(&mut o, p.gain_attr.as_bytes());
put_short(&mut o, p.pan_attr.as_bytes());
o.push(u8::try_from(p.max_tiles).expect("max_tiles fits a byte"));
o.push(u8::try_from(p.max_voices).expect("max_voices fits a byte"));
o
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut o = self.body();
o.extend_from_slice(&self.sig);
o
}
/// The descriptor's digest (§3.2): SHA-256 over [`Compose::body`].
pub fn digest(&self) -> [u8; 32] {
sha256(&[&self.body()])
}
/// Signed under `key`; `signer` is left as it is.
pub fn sign(mut self, key: &SigningKey) -> Compose {
let mut m = DESCRIPTOR_DOMAIN.to_vec();
m.extend_from_slice(&self.body());
self.sig = key.sign(&m).to_bytes();
self
}
/// Read a descriptor (§3.3): the record, its canvas, its field rules, its signature.
pub fn parse(b: &[u8]) -> Result<Compose, Reason> {
read_head(b, &DESCRIPTOR_MAGIC)?;
let mut r = Rd { b, p: 5 };
let room = r.arr()?;
let signer = r.arr()?;
let effective = r.u64()?;
let compositor = r.arr()?;
let width = r.u16()? as u32;
let height = r.u16()? as u32;
let fps = r.u8()?;
let background = r.arr()?;
let layer = r.u8()?;
let layout = r.u8()?;
let gap = r.u16()? as u32;
let aspect = r.u8()?;
let fill = r.u8()?;
let featured = r.short()?;
let order = r.short()?;
let omit = r.u8()?;
let pip_attr = r.short()?;
let corner = r.u8()?;
let pip_width = r.u16()? as u32;
let pip_height = r.u16()? as u32;
let offset = r.u16()? as u32;
let behaviour = r.u8()?;
let channels = r.u8()?;
let sample_rate = r.u32()?;
let gain = r.short()?;
let pan = r.short()?;
let max_tiles = r.u8()? as u32;
let max_voices = r.u8()? as u32;
let sig = r.arr()?;
if r.p != b.len() {
return Err(MALFORMED);
}
let keys = [featured, order, pip_attr, gain, pan];
let keys_ok = keys.iter().all(|k| k.is_empty() || layout::key_ok(k));
let s = |k: &[u8]| String::from_utf8_lossy(k).into_owned();
let params = Params {
width,
height,
fps,
layout,
gap,
aspect,
fill,
featured_attr: s(featured),
order_attr: s(order),
omit_stopped: omit == 1,
pip_attr: s(pip_attr),
corner,
pip_width,
pip_height,
offset,
behaviour,
channels,
sample_rate,
gain_attr: s(gain),
pan_attr: s(pan),
max_tiles,
max_voices,
};
params.check_canvas()?;
if !keys_ok || omit > 1 {
return Err(Reason::ComposeInvalid);
}
params.check_fields()?;
let c = Compose { room, signer, effective, compositor, background, layer, params, sig };
verify_sig(&c.signer, DESCRIPTOR_DOMAIN, &b[..b.len() - 64], &c.sig)?;
debug_assert_eq!(c.body(), b[..b.len() - 64], "a descriptor that reads re-encodes to its bytes");
Ok(c)
}
}
// ---------------------------------------------------------------------------------------
// §4 The participant set
// ---------------------------------------------------------------------------------------
/// A room as live-rooms §9 is given it.
#[derive(Clone, Copy, Debug)]
pub struct RoomInput<'a> {
pub descriptor: &'a [u8],
pub grants: &'a [Vec<u8>],
pub head: Option<&'a str>,
pub chains: &'a [Vec<Vec<u8>>],
}
/// The room at one instant (§4.1): its state, and each present principal's join order.
#[derive(Clone, Debug)]
pub struct Instant {
pub state: RoomState,
pub joins: BTreeMap<[u8; 32], u32>,
}
/// A room that verified (§9 step 1), with what composition reads of it.
pub struct VerifiedRoom<'a> {
pub input: RoomInput<'a>,
pub run: RoomRun,
pub d: Descriptor,
pub id: [u8; 32],
}
impl<'a> VerifiedRoom<'a> {
/// live-rooms §9 over the room: its refusal; `lifecycle_incomplete` when a room key's
/// chain has a gap (§4.1). A gap in another principal's chain is not a refusal here.
pub fn verify(input: RoomInput<'a>) -> Result<VerifiedRoom<'a>, (Place, Reason)> {
let run = verify_room(input.descriptor, input.grants, input.head, input.chains);
if let RoomOutcome::Refuse { at, reason } = run.outcome {
return Err((Place::Room(Some(at)), reason.into()));
}
let d = run.descriptor.clone().expect("a room that is not refused read its descriptor");
if let RoomOutcome::Gap(gaps) = &run.outcome
&& gaps.iter().any(|g| d.is_room_key(&g.principal))
{
return Err((Place::Room(None), Reason::LifecycleIncomplete));
}
let id = d.room_id();
Ok(VerifiedRoom { input, run, d, id })
}
/// The grant a digest names.
pub fn grant(&self, digest: &[u8; 32]) -> Option<&Grant> {
self.run.grants.iter().find(|h| h.digest == *digest).map(|h| &h.grant)
}
/// The room at `t` (§4.1): live-rooms §9 over each chain's records up to the first
/// whose `t` is above `t`, and the join order of each `join` among them.
pub fn at(&self, t: u64) -> Instant {
let chains: Vec<Vec<Vec<u8>>> = self
.input
.chains
.iter()
.map(|c| c.iter().take_while(|r| Event::parse(r).is_ok_and(|e| e.t <= t)).cloned().collect())
.collect();
let run = verify_room(self.input.descriptor, self.input.grants, self.input.head, &chains);
let mut joins = BTreeMap::new();
let mut seen = BTreeSet::new();
let mut n = 0u32;
for &(c, i, _) in &run.order {
let Ok(ev) = Event::parse(&chains[c][i]) else { continue };
if !seen.insert(ev.digest()) || ev.kind != kind::JOIN || !self.d.is_room_key(&ev.principal) {
continue;
}
if let Ok(Payload::Lifecycle { subject, .. }) = events::parse_payload(ev.kind, &ev.payload, u16::MAX) {
joins.insert(subject, n);
n += 1;
}
}
Instant { state: run.state, joins }
}
/// The grant in effect of `p` at `at`, if `p` is present.
pub fn grant_in_effect(&self, at: &Instant, p: &[u8; 32]) -> Option<&Grant> {
at.state.present.get(p).and_then(|g| self.grant(g))
}
}
/// A participant's video or audio source (§4.2).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Source {
/// Empty for the participant's own track; otherwise the slot it is live in.
pub slot: String,
pub media: u8,
/// The index of its profile in the room's descriptor, and the layers it publishes.
pub profile: u8,
pub layers: u8,
/// The layer composed: the lower of `layers` − 1 and the descriptor's `layer`.
pub layer: u8,
pub layering: u8,
/// That layer's dimensions in the profile; 0 for audio.
pub width: u16,
pub height: u16,
}
impl Source {
/// The tracks a compositor reads it from (§8.2), each as an anchor's `track` digest
/// (live-rooms §6.2): track 0 for `single` and `svc`, track `layer` for `simulcast`,
/// and tracks 0 to `layer` for `staged`.
pub fn tracks(&self, room_id: &[u8; 32], principal: &[u8; 32]) -> Vec<[u8; 32]> {
let owner = if self.slot.is_empty() { Owner::Principal(*principal) } else { Owner::Slot(self.slot.clone()) };
let ns = render_namespace(&track_namespace(room_id, &owner, media_name(self.media).expect("a media kind")));
let layers: Vec<u8> = match self.layering {
layering::SIMULCAST => vec![self.layer],
layering::STAGED => (0..=self.layer).collect(),
_ => vec![0],
};
layers.into_iter().map(|l| anchor_track(&ns, &track_name(l))).collect()
}
fn encode(s: Option<&Source>, o: &mut Vec<u8>) {
match s {
None => o.push(0),
Some(s) => {
o.push(s.media);
put_short(o, s.slot.as_bytes());
o.extend_from_slice(&[s.profile, s.layers, s.layer]);
o.extend_from_slice(&s.width.to_le_bytes());
o.extend_from_slice(&s.height.to_le_bytes());
}
}
}
}
/// One participant of the set (§4.2).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Participant {
pub principal: [u8; 32],
/// The digest of its grant in effect.
pub grant: [u8; 32],
pub join: u32,
/// Its grant's attributes (live-rooms §4.4).
pub attrs: Vec<(String, String)>,
pub video: Option<Source>,
pub audio: Option<Source>,
}
fn source(d: &Descriptor, c: &Compose, held: &Held, slot: &str, m: u8) -> Source {
let p = &d.profiles[held.profile as usize];
let layer = (held.layers - 1).min(c.layer);
let l = &p.layers[layer as usize];
Source { slot: slot.into(), media: m, profile: held.profile, layers: held.layers, layer, layering: p.layering, width: l.width, height: l.height }
}
/// §4.2: the participant set of `c` at `at`, in ascending order of key.
pub fn participants(c: &Compose, vr: &VerifiedRoom, at: &Instant) -> Vec<Participant> {
let st = &at.state;
let d = &vr.d;
let live_in = |p: &[u8; 32], kinds: &[u8]| -> Option<Source> {
d.slots.iter().zip(&st.slots).find_map(|(s, ss)| match ss.live {
Some(h) if h.principal == *p && kinds.contains(&s.media) => Some(source(d, c, &h, &s.name, s.media)),
_ => None,
})
};
let own = |p: &[u8; 32], m: u8| st.own.get(&(*p, m)).map(|h| source(d, c, h, "", m));
let mut out = Vec::new();
for (p, g) in &st.present {
if *p == c.compositor {
continue;
}
let publishes = st.own.keys().any(|(q, _)| q == p) || st.slots.iter().any(|s| s.live.is_some_and(|h| h.principal == *p));
if !publishes {
continue;
}
let video = own(p, media::VIDEO).or_else(|| own(p, media::SCREEN)).or_else(|| live_in(p, &[media::VIDEO, media::SCREEN]));
let audio = own(p, media::AUDIO).or_else(|| live_in(p, &[media::AUDIO]));
let attrs = vr.grant(g).map(|g| g.attrs.clone()).unwrap_or_default();
let join = *at.joins.get(p).expect("a present principal joined");
out.push(Participant { principal: *p, grant: *g, join, attrs, video, audio });
}
out
}
/// §4.5: the digest of the participant set at `t`.
pub fn set_digest(room_id: &[u8; 32], t: u64, ps: &[Participant]) -> [u8; 32] {
let mut o = Vec::new();
o.extend_from_slice(SET_DOMAIN);
o.extend_from_slice(room_id);
o.extend_from_slice(&t.to_le_bytes());
o.extend_from_slice(&(ps.len() as u32).to_le_bytes());
for p in ps {
o.extend_from_slice(&p.principal);
o.extend_from_slice(&p.grant);
o.extend_from_slice(&p.join.to_le_bytes());
Source::encode(p.video.as_ref(), &mut o);
Source::encode(p.audio.as_ref(), &mut o);
}
sha256(&[&o])
}
/// The composition at one instant: the set, its digest, and the layout and voices.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Frame {
pub t: u64,
pub participants: Vec<Participant>,
pub set_digest: [u8; 32],
pub layout: Layout,
}
impl Frame {
fn participant(&self, p: &[u8; 32]) -> &Participant {
self.participants.iter().find(|q| q.principal == *p).expect("a tile's participant is in the set")
}
/// §8.2: the inputs a composed track of `m` reads, in order, as (principal, track):
/// for video, each tile that shows video, in drawing order; for audio, each voice.
pub fn inputs(&self, room_id: &[u8; 32], m: u8) -> Vec<([u8; 32], [u8; 32])> {
let mut out = Vec::new();
let sources: Vec<(&[u8; 32], Option<&Source>)> = if m == media::AUDIO {
self.layout.voices.iter().map(|v| (&v.principal, self.participant(&v.principal).audio.as_ref())).collect()
} else {
self.layout.tiles.iter().filter(|t| t.video.is_some()).map(|t| (&t.principal, self.participant(&t.principal).video.as_ref())).collect()
};
for (p, s) in sources {
for tr in s.expect("a composed source").tracks(room_id, p) {
out.push((*p, tr));
}
}
out
}
}
/// §4, §5 at `t` over the room as it is at `at`.
fn frame_at(c: &Compose, vr: &VerifiedRoom, t: u64, at: &Instant) -> Result<Frame, Reason> {
if t < c.effective {
return Err(Reason::NotYetEffective);
}
let participants = participants(c, vr, at);
let entrants: Vec<Entrant> = participants
.iter()
.map(|p| Entrant {
principal: p.principal,
join: p.join,
video: p.video.as_ref().map(|s| (s.width as u32, s.height as u32)),
audio: p.audio.is_some(),
attrs: &p.attrs,
})
.collect();
let layout = layout::layout(&c.params, &entrants)?;
Ok(Frame { t, set_digest: set_digest(&vr.id, t, &participants), participants, layout })
}
/// The composition of `c` at `t` (§4, §5).
pub fn compose_at(c: &Compose, vr: &VerifiedRoom, t: u64) -> Result<Frame, Reason> {
frame_at(c, vr, t, &vr.at(t))
}
/// §3.3, §3.5: a descriptor read, of this room, signed by its authority or by a principal
/// whose grant in effect at its `effective` has the compose power.
pub fn check_descriptor(b: &[u8], vr: &VerifiedRoom) -> Result<Compose, Reason> {
let c = Compose::parse(b)?;
if c.room != vr.id {
return Err(WRONG_ROOM);
}
if c.signer != vr.d.authority {
let at = vr.at(c.effective);
if !vr.grant_in_effect(&at, &c.signer).is_some_and(|g| g.caps.powers & power::COMPOSE != 0) {
return Err(Reason::ComposeNotPermitted);
}
}
Ok(c)
}
/// The outcome of composing a room at a list of instants.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ComposeOutcome {
Frames(Vec<Frame>),
Refuse { at: Place, reason: Reason },
}
/// §9: the room, the descriptor, then the composition at each instant.
pub fn verify_composition(room: RoomInput, descriptor: &[u8], instants: &[u64]) -> (ComposeOutcome, Option<[u8; 32]>) {
let vr = match VerifiedRoom::verify(room) {
Ok(v) => v,
Err((at, reason)) => return (ComposeOutcome::Refuse { at, reason }, None),
};
let c = match check_descriptor(descriptor, &vr) {
Ok(c) => c,
Err(reason) => return (ComposeOutcome::Refuse { at: Place::Descriptor, reason }, None),
};
let mut frames = Vec::with_capacity(instants.len());
for &t in instants {
match compose_at(&c, &vr, t) {
Ok(f) => frames.push(f),
Err(reason) => return (ComposeOutcome::Refuse { at: Place::Set(t), reason }, Some(c.digest())),
}
}
(ComposeOutcome::Frames(frames), Some(c.digest()))
}
// ---------------------------------------------------------------------------------------
// §8 The composition receipt
// ---------------------------------------------------------------------------------------
/// One MoQ object (§8.1): its track, as an anchor's `track` digest, its Location, and the
/// SHA-256 of its payload.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ObjectRef {
pub track: [u8; 32],
pub group: u64,
pub object: u64,
pub digest: [u8; 32],
}
/// One input object, and the participant it is read from.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InputRef {
pub principal: [u8; 32],
pub object: ObjectRef,
}
/// A composition receipt (§8.1).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Receipt {
pub room: [u8; 32],
pub compositor: [u8; 32],
/// The descriptor's digest.
pub descriptor: [u8; 32],
/// The room-clock instant of the output object.
pub t: u64,
/// The participant set's digest at `t` (§4.5).
pub state: [u8; 32],
/// 1 audio (a mix) or 2 video (a layout).
pub media: u8,
pub output: ObjectRef,
pub inputs: Vec<InputRef>,
pub sig: [u8; 64],
}
fn put_object(o: &mut Vec<u8>, r: &ObjectRef) {
o.extend_from_slice(&r.track);
o.extend_from_slice(&r.group.to_le_bytes());
o.extend_from_slice(&r.object.to_le_bytes());
o.extend_from_slice(&r.digest);
}
fn read_object(r: &mut Rd) -> Result<ObjectRef, Reason> {
Ok(ObjectRef { track: r.arr()?, group: r.u64()?, object: r.u64()?, digest: r.arr()? })
}
impl Receipt {
/// The receipt without `sig` (§8.1).
pub fn body(&self) -> Vec<u8> {
let mut o = Vec::new();
o.extend_from_slice(&RECEIPT_MAGIC);
o.push(RECORD_VERSION);
o.extend_from_slice(&self.room);
o.extend_from_slice(&self.compositor);
o.extend_from_slice(&self.descriptor);
o.extend_from_slice(&self.t.to_le_bytes());
o.extend_from_slice(&self.state);
o.push(self.media);
put_object(&mut o, &self.output);
assert!(self.inputs.len() <= u8::MAX as usize, "at most 255 inputs");
o.push(self.inputs.len() as u8);
for i in &self.inputs {
o.extend_from_slice(&i.principal);
put_object(&mut o, &i.object);
}
o
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut o = self.body();
o.extend_from_slice(&self.sig);
o
}
/// The receipt's digest (§8.1).
pub fn digest(&self) -> [u8; 32] {
sha256(&[&self.body()])
}
/// Signed under `key`; `compositor` is left as it is.
pub fn sign(mut self, key: &SigningKey) -> Receipt {
let mut m = RECEIPT_DOMAIN.to_vec();
m.extend_from_slice(&self.body());
self.sig = key.sign(&m).to_bytes();
self
}
/// Read a receipt (§8.3 step 1): the record, its rules, its signature.
pub fn parse(b: &[u8]) -> Result<Receipt, Reason> {
read_head(b, &RECEIPT_MAGIC)?;
let mut r = Rd { b, p: 5 };
let room = r.arr()?;
let compositor = r.arr()?;
let descriptor = r.arr()?;
let t = r.u64()?;
let state = r.arr()?;
let media = r.u8()?;
let output = read_object(&mut r)?;
let n = r.u8()? as usize;
let mut inputs = Vec::with_capacity(n);
for _ in 0..n {
inputs.push(InputRef { principal: r.arr()?, object: read_object(&mut r)? });
}
let sig = r.arr()?;
if r.p != b.len() {
return Err(MALFORMED);
}
let mut pairs = BTreeSet::new();
if !matches!(media, media::AUDIO | media::VIDEO) || !inputs.iter().all(|i| pairs.insert((i.principal, i.object.track))) {
return Err(Reason::ReceiptInvalid);
}
let rc = Receipt { room, compositor, descriptor, t, state, media, output, inputs, sig };
verify_sig(&rc.compositor, RECEIPT_DOMAIN, &b[..b.len() - 64], &rc.sig)?;
Ok(rc)
}
}
/// The outcome of verifying a receipt (§8.3).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ReceiptOutcome {
Accept { digest: [u8; 32], frame: Frame },
Refuse { at: Place, reason: Reason },
}
/// §8.3: a receipt checked against the room and the descriptor it names.
pub fn verify_receipt(room: RoomInput, descriptor: &[u8], receipt: &[u8]) -> ReceiptOutcome {
let refuse = |at, reason| ReceiptOutcome::Refuse { at, reason };
let vr = match VerifiedRoom::verify(room) {
Ok(v) => v,
Err((at, reason)) => return refuse(at, reason),
};
let c = match check_descriptor(descriptor, &vr) {
Ok(c) => c,
Err(reason) => return refuse(Place::Descriptor, reason),
};
let r = match Receipt::parse(receipt) {
Ok(r) => r,
Err(reason) => return refuse(Place::Receipt, reason),
};
if r.room != vr.id {
return refuse(Place::Receipt, WRONG_ROOM);
}
if r.descriptor != c.digest() {
return refuse(Place::Receipt, Reason::DescriptorMismatch);
}
if r.t < c.effective {
return refuse(Place::Receipt, Reason::NotYetEffective);
}
let at = vr.at(r.t);
let composes = vr.grant_in_effect(&at, &r.compositor).is_some_and(|g| g.caps.powers & power::COMPOSE != 0);
if c.compositor == NO_COMPOSITOR || r.compositor != c.compositor || !composes {
return refuse(Place::Receipt, Reason::CompositorNotPermitted);
}
let ns = render_namespace(&track_namespace(&vr.id, &Owner::Principal(r.compositor), media_name(r.media).expect("a media kind")));
let published = at.state.own.get(&(r.compositor, r.media)).is_some_and(|h| (0..h.layers).any(|l| anchor_track(&ns, &track_name(l)) == r.output.track));
if !published {
return refuse(Place::Receipt, Reason::OutputNotPublished);
}
let frame = match frame_at(&c, &vr, r.t, &at) {
Ok(f) => f,
Err(reason) => return refuse(Place::Set(r.t), reason),
};
if r.state != frame.set_digest {
return refuse(Place::Receipt, Reason::StateMismatch);
}
let stated: Vec<([u8; 32], [u8; 32])> = r.inputs.iter().map(|i| (i.principal, i.object.track)).collect();
if stated != frame.inputs(&vr.id, r.media) {
return refuse(Place::Receipt, Reason::InputsMismatch);
}
ReceiptOutcome::Accept { digest: r.digest(), frame }
}
#[cfg(test)]
mod tests {
use super::*;
/// The corpus regenerates byte for byte, and this module gives every case of it the
/// outcome it states.
#[test]
fn corpus_round_trip() {
let doc = corpus::generate();
let fails = corpus::verify(&doc);
assert!(fails.is_empty(), "{}", fails.join("\n"));
}
/// A descriptor re-encodes to the bytes it was read from, and one bit of its body
/// changed fails its signature.
#[test]
fn descriptor_round_trip() {
let key = SigningKey::from_bytes(&[7; 32]);
let c = Compose {
room: [1; 32],
signer: key.verifying_key().to_bytes(),
effective: 0,
compositor: NO_COMPOSITOR,
background: [0, 0, 0],
layer: 2,
params: corpus::grid_params(),
sig: [0; 64],
}
.sign(&key);
let b = c.to_bytes();
assert_eq!(Compose::parse(&b), Ok(c));
let mut bad = b.clone();
bad[40] ^= 1;
assert_eq!(Compose::parse(&bad), Err(events::Reason::BadSignature.into()));
assert_eq!(Compose::parse(&b[..b.len() - 1]), Err(TRUNCATED));
}
}