GoqCatalogDocument (the immutable catalog.json envelope both peers must agree on byte-for-byte) was defined three times - host producer, Portal subscriber, and the probe's subscriber module - with independently phrased validation and no test tying the copies together. A serde or validation change to one copy compiled cleanly and failed only at runtime when the peer rejected the catalog. The document now lives in sigil-protocol (which already owned its only dependency, MoqCatalogExtensionV1) with a round-trip golden test; all three call sites consume the shared type. sigil-protocol gains the workspace hang dependency the envelope requires.
- commit
- c4ae230c5481b60e496d4cfcb39c65f71e538bb0
- parent
- 5f090adffd773ab596726969758c545128078cf4
- author
- Dami <dami.etoile@gmail.com>
| M | Cargo.lock | 1 | + |
| M | crates/ | 39 | +++----------------------- |
| M | crates/ | 34 | +++-------------------- |
| M | crates/ | 1 | + |
| M | crates/ | 6 | ++-- |
| M | crates/ | 44 | ++++++++++++++++++++++++++++++ |
| M | src-tauri/ | 39 | +++----------------------- |
@@ -5660,6 +5660,7 @@ version = "0.1.0"
56605660 dependencies = [56615661 "base64 0.22.1",56625662 "ed25519-dalek",5663+ "hang",56635664 "serde",56645665 "serde_json",56655666 "thiserror 2.0.19",@@ -2,36 +2,7 @@ use std::time::Duration;
22 33 use anyhow::{Context, Result};44 use moq_net::{BroadcastConsumer, Error as MoqError, Track, TrackConsumer};5-use serde::{Deserialize, Serialize};6-use sigil_protocol::{MAX_MOQ_CATALOG_BYTES, MoqCatalogExtensionV1};7-8-9-10-struct GoqCatalogDocument {11- 12- media: hang::Catalog,13- goq: MoqCatalogExtensionV1,14-}15-16-impl GoqCatalogDocument {17- 18- fn video_h264() -> Self {19- Self {20- media: hang::Catalog::default(),21- goq: MoqCatalogExtensionV1::video_h264(),22- }23- }24-25- fn validate(&self) -> Result<()> {26- anyhow::ensure!(27- self.media == hang::Catalog::default(),28- "Goq catalog must not advertise a standard Hang rendition for enveloped media"29- );30- self.goq31- .validate()32- .context("validating Goq catalog extension")33- }34-}5+use sigil_protocol::{GoqCatalogDocument, MAX_MOQ_CATALOG_BYTES};356 367 378 pub(crate) enum MoqCatalogMode {@@ -118,7 +89,9 @@ pub(crate) async fn subscribe_goq_video_track(
11889 .context("reading Goq catalog frame")?;11990 let document: GoqCatalogDocument =12091 serde_json::from_slice(&snapshot).context("decoding Goq catalog snapshot")?;121- document.validate()?;92+ document93+ .validate()94+ .context("validating Goq catalog document")?;12295 let track = broadcast12396 .subscribe_track(&Track::new(document.goq.video.track.name))12497 .context("subscribing to catalog-selected Goq video track")?;@@ -232,9 +205,7 @@ mod tests {
232205 subscribe_goq_video_track(&invalid_broadcast.consume(), Duration::from_millis(100))233206 .await;234207 assert!(result.err().is_some_and(|error| {235- error236- .to_string()237- .contains("validating Goq catalog extension")208+ format!("{error:#}").contains("validating Goq catalog document")238209 }));239210 240211 let mut stalled_broadcast = Broadcast::new().produce();@@ -1,34 +1,6 @@
11 use anyhow::{Context, Result, ensure};22 use moq_net::BroadcastProducer;3-use serde::{Deserialize, Serialize};4-use sigil_protocol::{MAX_MOQ_CATALOG_BYTES, MoqCatalogExtensionV1};5-6-7-8-struct GoqCatalogDocument {9- 10- media: hang::Catalog,11- goq: MoqCatalogExtensionV1,12-}13-14-impl GoqCatalogDocument {15- fn video_h264() -> Self {16- Self {17- media: hang::Catalog::default(),18- goq: MoqCatalogExtensionV1::video_h264(),19- }20- }21-22- fn validate(&self) -> Result<()> {23- ensure!(24- self.media == hang::Catalog::default(),25- "Goq catalog must not advertise a standard Hang rendition for enveloped media"26- );27- self.goq28- .validate()29- .context("validating Goq catalog extension")30- }31-}3+use sigil_protocol::{GoqCatalogDocument, MAX_MOQ_CATALOG_BYTES};324 335 pub(crate) struct GoqCatalogProducer {346 producer: moq_json::Producer<GoqCatalogDocument>,@@ -48,7 +20,9 @@ pub(crate) fn publish_goq_catalog(broadcast: &mut BroadcastProducer) -> Result<G
4820 .context("creating catalog.json track")?;4921 let mut producer = moq_json::Producer::new(track, moq_json::Config::default());5022 let document = GoqCatalogDocument::video_h264();51- document.validate()?;23+ document24+ .validate()25+ .context("validating Goq catalog document")?;5226 let snapshot = serde_json::to_vec(&document).context("serializing Goq catalog snapshot")?;5327 ensure!(5428 snapshot.len() <= MAX_MOQ_CATALOG_BYTES,@@ -51,9 +51,9 @@ pub use media_v3::{
5151 write_media_control_request_v3, write_media_object_v3,5252 };5353 pub use moq_catalog::{54- MAX_MOQ_CATALOG_BYTES, MOQ_CATALOG_EXTENSION_VERSION_V1, MOQ_GOP_GROUP_FORMAT_V1,55- MOQ_MEDIA_OBJECT_FORMAT_V1, MOQ_VIDEO_TRACK_PRIORITY, MoqCatalogExtensionV1,56- MoqTrackDescriptorV1, MoqVideoCatalogV1,54+ GoqCatalogDocument, MAX_MOQ_CATALOG_BYTES, MOQ_CATALOG_EXTENSION_VERSION_V1,55+ MOQ_GOP_GROUP_FORMAT_V1, MOQ_MEDIA_OBJECT_FORMAT_V1, MOQ_VIDEO_TRACK_PRIORITY,56+ MoqCatalogExtensionV1, MoqTrackDescriptorV1, MoqVideoCatalogV1,5757 };5858 5959 /// Protocol version encoded in v1 messages.@@ -89,6 +89,37 @@ impl MoqCatalogExtensionV1 {
8989 }9090 }9191 92+/// The immutable catalog.json document both peers must agree on byte-for-byte:93+/// a default (empty) Hang catalog envelope carrying the Goq extension. This is94+/// the single definition — the host producer, Portal subscriber, and probe all95+/// consume it from here so the wire shape cannot drift between peers.96+97+98+pub struct GoqCatalogDocument {99+ 100+ pub media: hang::Catalog,101+ pub goq: MoqCatalogExtensionV1,102+}103+104+impl GoqCatalogDocument {105+ pub fn video_h264() -> Self {106+ Self {107+ media: hang::Catalog::default(),108+ goq: MoqCatalogExtensionV1::video_h264(),109+ }110+ }111+112+ pub fn validate(&self) -> Result<()> {113+ if self.media != hang::Catalog::default() {114+ return Err(ProtocolError::InvalidMessage {115+ message_type: "Goq MoQ catalog document",116+ reason: "catalog must not advertise a standard Hang rendition for enveloped media",117+ });118+ }119+ self.goq.validate()120+ }121+}122+92123 93124 mod tests {94125 use super::*;@@ -103,6 +134,19 @@ mod tests {
103134 );104135 }105136 137+ 138+ fn goq_catalog_document_has_a_stable_golden_envelope() {139+ let document = GoqCatalogDocument::video_h264();140+ document.validate().unwrap();141+ let json = serde_json::to_value(&document).unwrap();142+ assert_eq!(143+ json["goq"],144+ serde_json::to_value(MoqCatalogExtensionV1::video_h264()).unwrap()145+ );146+ let round_trip: GoqCatalogDocument = serde_json::from_value(json).unwrap();147+ assert_eq!(round_trip, document);148+ }149+106150 107151 fn goq_catalog_validation_rejects_every_mismatched_contract_field() {108152 let mut cases = Vec::new();@@ -1,38 +1,7 @@
11 use std::time::Duration;22 33 use moq_net::{BroadcastConsumer, Error as MoqError, Track, TrackConsumer};4-use serde::{Deserialize, Serialize};5-use sigil_protocol::{MAX_MOQ_CATALOG_BYTES, MoqCatalogExtensionV1};6-7-8-9-struct GoqCatalogDocument {10- 11- media: hang::Catalog,12- goq: MoqCatalogExtensionV1,13-}14-15-impl GoqCatalogDocument {16- 17- fn video_h264() -> Self {18- Self {19- media: hang::Catalog::default(),20- goq: MoqCatalogExtensionV1::video_h264(),21- }22- }23-24- fn validate(&self) -> Result<(), String> {25- if self.media != hang::Catalog::default() {26- return Err(27- "Goq catalog falsely advertises a standard Hang rendition for enveloped media"28- .to_string(),29- );30- }31- self.goq32- .validate()33- .map_err(|error| format!("Invalid Goq catalog extension: {error}"))34- }35-}4+use sigil_protocol::{GoqCatalogDocument, MAX_MOQ_CATALOG_BYTES};365 376 387 pub(crate) enum MoqCatalogMode {@@ -126,7 +95,9 @@ pub(crate) async fn subscribe_goq_video_track(
12695 .map_err(|error| format!("Failed to read Goq catalog frame: {error}"))?;12796 let document: GoqCatalogDocument = serde_json::from_slice(&snapshot)12897 .map_err(|error| format!("Failed to decode Goq catalog snapshot: {error}"))?;129- document.validate()?;98+ document99+ .validate()100+ .map_err(|error| format!("Invalid Goq catalog: {error}"))?;130101 let track = broadcast131102 .subscribe_track(&Track::new(document.goq.video.track.name))132103 .map_err(|error| {@@ -244,7 +215,7 @@ mod tests {
244215 assert!(245216 result246217 .err()247- .is_some_and(|error| error.contains("Invalid Goq catalog extension"))218+ .is_some_and(|error| error.contains("Invalid Goq catalog"))248219 );249220 250221 let mut stalled_broadcast = Broadcast::new().produce();