//! The `draft-ietf-moq-transport-18` vector corpus (`tests/vectors/draft-18/`). //! //! Built here from typed values, deterministically, as `draft22_vectors.rs` builds //! -22's: each good vector's bytes are what `joule_moq::draft18` encodes (or bytes //! written out by hand for the forms its encoder never makes), its expected reading //! the value in the JSON view of `joule_moq::draft18::json`; each negative vector the //! refusal it must meet and -18's consequence. The draft's own examples are here: the //! integers of §1.4.1 Table 2 and the first subgroup of §11.6 (the second example of //! §11.6 cannot be reproduced: its Properties Length of 33 and a Length of 21 for a //! 14-byte value describe no one set of bytes, and 33 is what -16's integer would take, //! not -18's). //! //! The negative vectors include each place where -18 refuses what -22 reads: -22's //! LOCATION_FILTER, FILL_PARAMETERS, Range Filters and PUBLISH_STATE_NOTIFY, End of //! Timed-Out Range, a GOAWAY on the control stream without its Request ID, and a //! FETCH whose Fetch Type is unknown. //! //! `JOULE_MOQ_WRITE_VECTORS=1` rewrites the corpus; `vectors/draft-18/moqt18_check.py` //! decodes it independently. use joule_moq::draft18::control::{msg, Fetch, FetchKind, FetchOk, Goaway, NamespaceRequest, Publish, PublishBlocked, PublishDone, Redirect, RequestError, RequestOk, RequestUpdate, Setup, SubscribeOk, TrackRequest}; use joule_moq::draft18::codes::{PublishDoneCode, RequestErrorCode}; use joule_moq::draft18::data::{encode_control_stream, encode_fetch_stream, encode_padding_stream, encode_subgroup_stream, FetchCodec, FetchEntry, RangeEnd, StreamHeader, UniEvent, UniStreamDecoder}; use joule_moq::draft18::kvp::{opt, prop, KeyValuePair, Properties, SetupOptions}; use joule_moq::draft18::params::{GroupOrder, Parameter, Parameters, SubscriptionFilter}; use joule_moq::draft18::{json, ControlMessage}; use joule_moq::draft22::data::{Datagram, DatagramContent, FetchHeader, FetchObject, Limits, ObjectDatagram, ObjectStatus, SubgroupCodec, SubgroupHeader, SubgroupId, SubgroupObject}; use joule_moq::draft22::names::Location; use joule_moq::draft22::token::Token; use joule_moq::draft22::{varint, WireError}; use joule_moq::track::{TrackName, TrackNamespace}; use serde_json::{json, Value}; const CORPUS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/vectors/draft-18/vectors.json"); const PV: &str = "session:PROTOCOL_VIOLATION"; fn hx(b: &[u8]) -> String { json::hex(b) } fn ns(f: &[&str]) -> TrackNamespace { TrackNamespace::from_strs(f) } fn name(s: &str) -> TrackName { TrackName::from_str(s) } fn kv(ty: u64, v: u64) -> KeyValuePair { KeyValuePair::varint(ty, v) } fn kb(ty: u64, b: &[u8]) -> KeyValuePair { KeyValuePair::bytes(ty, b.to_vec()) } fn v(x: u64) -> Vec { varint::to_vec(x) } fn cat(parts: &[&[u8]]) -> Vec { parts.concat() } fn frame(ty: u64, body: &[u8]) -> Vec { let mut out = varint::to_vec(ty); out.extend_from_slice(&(body.len() as u16).to_be_bytes()); out.extend_from_slice(body); out } fn sorted(mut ps: Vec) -> Parameters { ps.sort_by_key(|p| p.type_code()); Parameters(ps) } fn with_immutable(mut outer: Vec, inner: Vec) -> Properties { let mut b = Vec::new(); joule_moq::draft22::kvp::encode_pairs(&inner, &mut b).unwrap(); outer.push(KeyValuePair::bytes(prop::IMMUTABLE_PROPERTIES, b)); outer.sort_by_key(|k| k.ty); Properties(outer) } struct Corpus { vectors: Vec, } impl Corpus { fn push(&mut self, name: &str, kind: &str, bytes: &[u8], canonical: bool, context: Option, expect: Value) { let mut x = json!({"name": name, "kind": kind, "hex": hx(bytes), "canonical": canonical, "expect": expect}); if let Some(c) = context { x["context"] = c; } self.vectors.push(x); } fn control(&mut self, name: &str, m: ControlMessage) { let b = m.encode().expect(name); self.push(name, "control", &b, true, None, json::control(&m)); } fn stream(&mut self, name: &str, bytes: Vec, order: GroupOrder, events: Vec) { let expect = Value::Array(events.iter().map(json::uni_event).collect()); self.push(name, "uni-stream", &bytes, true, Some(json!({"group_order": order as u8})), expect); } fn bad(&mut self, name: &str, kind: &str, bytes: &[u8], context: Option, error: &str, consequence: &str) { self.push(name, kind, bytes, false, context, json!({"error": error, "consequence": consequence})); } } fn build() -> Vec { let mut c = Corpus { vectors: Vec::new() }; // ── §1.4.1, Table 2: the draft's own examples ── for (h, val, shortest) in [ ("25", 37u64, true), ("8025", 37, false), ("bbbd", 15_293, true), ("ed7f3e7d", 226_442_877, true), ("faa1a0e403d8", 2_893_212_287_960, true), ("fc8998abc66bc0", 151_288_809_941_952, true), ("fefa318fa8e3ca11", 70_423_237_261_249_041, true), ("ffffffffffffffffff", u64::MAX, true), ] { let b = unhex(h); c.push(&format!("varint {h} (Table 2)"), "varint", &b, shortest, None, json!({"value": val, "length": b.len()})); } c.bad("varint cut short", "varint", &[0xe0, 0x01], None, "truncated", PV); // ── §10.3 SETUP ── let mut client = SetupOptions::new(); client .push(kb(opt::PATH, b"/moq")) .push(kb(opt::AUTHORIZATION_TOKEN, &Token::UseValue { token_type: 0, value: b"tok".to_vec() }.to_bytes())) .push(kv(opt::MAX_AUTH_TOKEN_CACHE_SIZE, 1024)) .push(kb(opt::AUTHORITY, b"relay.example:443")) .push(kb(opt::MOQT_IMPLEMENTATION, b"example-impl/1.0")); client.0.sort_by_key(|p| p.ty); c.control("SETUP from a client", ControlMessage::Setup(Setup { options: client })); // -22's MAX_FILTER_RANGES (0x06) and MAX_REQUEST_UPDATES (0x08) are unknown options // here, so they may repeat (§10.3). let mut unknown = SetupOptions::new(); unknown.push(kv(0x06, 1)).push(kv(0x06, 2)).push(kv(0x08, 3)).push(kv(0x08, 4)); c.control("SETUP whose unknown options repeat (-22's 0x06 and 0x08)", ControlMessage::Setup(Setup { options: unknown })); // ── §10.4 GOAWAY ── c.control("GOAWAY with a Request ID (on the control stream)", ControlMessage::Goaway(Goaway { new_session_uri: b"moqt://relay.example/next".to_vec(), timeout_ms: 5000, request_id: Some(4) })); c.control("GOAWAY without a Request ID (on a request stream)", ControlMessage::Goaway(Goaway { new_session_uri: vec![], timeout_ms: 0, request_id: None })); // ── §10.7 SUBSCRIBE, each Subscription Filter (§5.1.2) and every parameter it may carry ── let every = sorted(vec![ Parameter::ObjectDeliveryTimeout(250), Parameter::AuthorizationToken(Token::UseValue { token_type: 0, value: b"jwt".to_vec() }), Parameter::RendezvousTimeout(500), Parameter::SubgroupDeliveryTimeout(400), Parameter::Forward(true), Parameter::SubscriberPriority(200), Parameter::SubscriptionFilter(SubscriptionFilter::AbsoluteRange { start: Location::new(5, 1), end_group_delta: 2 }), Parameter::GroupOrder(GroupOrder::Descending), Parameter::NewGroupRequest(0), ]); c.control("SUBSCRIBE with every parameter", ControlMessage::Subscribe(TrackRequest { request_id: 0, namespace: ns(&["moq-test", "interop"]), name: name("test-track"), params: every })); for (label, f) in [ ("Next Group Start", SubscriptionFilter::NextGroupStart), ("Largest Object", SubscriptionFilter::LargestObject), ("AbsoluteStart", SubscriptionFilter::AbsoluteStart { start: Location::new(300, 0) }), ] { c.control( &format!("SUBSCRIBE with filter {label}"), ControlMessage::Subscribe(TrackRequest { request_id: 2, namespace: ns(&["a"]), name: name("b"), params: Parameters::new().with(Parameter::SubscriptionFilter(f)) }), ); } c.control("SUBSCRIBE with an empty namespace", ControlMessage::Subscribe(TrackRequest { request_id: 6, namespace: TrackNamespace(vec![]), name: name("t"), params: Parameters::new() })); // ── §10.8 SUBSCRIBE_OK ── c.control( "SUBSCRIBE_OK with Track Properties", ControlMessage::SubscribeOk(SubscribeOk { track_alias: 7, params: sorted(vec![Parameter::Expires(60_000), Parameter::LargestObject(Location::new(12, 3))]), track_properties: with_immutable(vec![kv(prop::DEFAULT_PUBLISHER_PRIORITY, 64), kv(prop::OBJECT_DELIVERY_TIMEOUT, 500)], vec![kv(0x3a40, 1)]), }), ); // ── §10.10 PUBLISH; §10.5 REQUEST_OK and the PUBLISH_OK of Table 5 ── c.control( "PUBLISH", ControlMessage::Publish(Publish { request_id: 1, namespace: ns(&["live", "a"]), name: name("cam"), track_alias: 10, params: sorted(vec![Parameter::Forward(false), Parameter::LargestObject(Location::new(0, 0)), Parameter::Expires(1000)]), track_properties: Properties(vec![kv(prop::DEFAULT_PUBLISHER_PRIORITY, 96)]), }), ); let pub_ok = sorted(vec![ Parameter::SubscriptionFilter(SubscriptionFilter::NextGroupStart), Parameter::Forward(true), Parameter::SubscriberPriority(1), Parameter::GroupOrder(GroupOrder::Ascending), Parameter::ObjectDeliveryTimeout(100), Parameter::SubgroupDeliveryTimeout(200), Parameter::NewGroupRequest(3), Parameter::Expires(9), ]); c.control("REQUEST_OK answering PUBLISH, with the subscription's parameters", ControlMessage::RequestOk(RequestOk { params: pub_ok.clone(), track_properties: Properties::new() })); c.control("PUBLISH_OK (type 0x1E, Table 5)", ControlMessage::PublishOk(RequestOk { params: pub_ok, track_properties: Properties::new() })); c.control( "REQUEST_OK answering TRACK_STATUS", ControlMessage::RequestOk(RequestOk { params: Parameters::new().with(Parameter::LargestObject(Location::new(9, 0))), track_properties: Properties(vec![kv(prop::DYNAMIC_GROUPS, 1)]) }), ); // ── §10.11 PUBLISH_DONE: Stream Count 2^62-1 when not known exactly ── c.control("PUBLISH_DONE SUBSCRIPTION_ENDED", ControlMessage::PublishDone(PublishDone { status: PublishDoneCode::SUBSCRIPTION_ENDED, stream_count: 3, reason: b"end of filter".to_vec() })); c.control("PUBLISH_DONE with Stream Count unknown (2^62-1)", ControlMessage::PublishDone(PublishDone { status: PublishDoneCode::TRACK_ENDED, stream_count: (1 << 62) - 1, reason: vec![] })); // ── §10.12 FETCH: Standalone, Relative and Absolute Joining ── c.control( "FETCH Standalone, groups 1 to 3 whole (End {4, 0})", ControlMessage::Fetch(Fetch { request_id: 4, kind: FetchKind::Standalone { namespace: ns(&["vod"]), name: name("ep1"), start: Location::new(1, 0), end: Location::new(4, 0) }, params: sorted(vec![Parameter::GroupOrder(GroupOrder::Descending), Parameter::SubscriberPriority(5), Parameter::FillTimeout(2000)]), }), ); c.control("FETCH Relative Joining", ControlMessage::Fetch(Fetch { request_id: 6, kind: FetchKind::RelativeJoining { joining_request_id: 0, joining_start: 2 }, params: Parameters::new() })); c.control("FETCH Absolute Joining", ControlMessage::Fetch(Fetch { request_id: 8, kind: FetchKind::AbsoluteJoining { joining_request_id: 0, joining_start: 100 }, params: Parameters::new() })); // ── §10.13 FETCH_OK: End Location the last Object plus 1 ── c.control( "FETCH_OK, End {3, 5}", ControlMessage::FetchOk(FetchOk { end_of_track: true, end_location: Location::new(3, 5), params: Parameters::new(), track_properties: Properties(vec![kv(prop::DEFAULT_PUBLISHER_GROUP_ORDER, 2)]) }), ); // ── §10.14-10.20 ── c.control("TRACK_STATUS", ControlMessage::TrackStatus(TrackRequest { request_id: 10, namespace: ns(&["a"]), name: name("b"), params: Parameters::new() })); c.control( "PUBLISH_NAMESPACE with a token", ControlMessage::PublishNamespace(NamespaceRequest { request_id: 12, prefix: ns(&["moq-test", "interop"]), params: Parameters::new().with(Parameter::AuthorizationToken(Token::Register { alias: 1, token_type: 7, value: b"t".to_vec() })) }), ); c.control("SUBSCRIBE_NAMESPACE with an empty prefix", ControlMessage::SubscribeNamespace(NamespaceRequest { request_id: 14, prefix: TrackNamespace(vec![]), params: Parameters::new() })); c.control("SUBSCRIBE_TRACKS with FORWARD 0", ControlMessage::SubscribeTracks(NamespaceRequest { request_id: 16, prefix: ns(&["live"]), params: Parameters::new().with(Parameter::Forward(false)) })); c.control("NAMESPACE", ControlMessage::Namespace(ns(&["a", "b"]))); c.control("NAMESPACE_DONE", ControlMessage::NamespaceDone(ns(&["a", "b"]))); c.control("PUBLISH_BLOCKED", ControlMessage::PublishBlocked(PublishBlocked { suffix: ns(&["room1"]), name: name("cam") })); c.control( "REQUEST_UPDATE with a new filter and TRACK_NAMESPACE_PREFIX", ControlMessage::RequestUpdate(RequestUpdate { request_id: 18, params: sorted(vec![Parameter::SubscriptionFilter(SubscriptionFilter::AbsoluteStart { start: Location::new(0, 0) }), Parameter::TrackNamespacePrefix(ns(&["x"]))]), }), ); c.control("REQUEST_ERROR DUPLICATE_SUBSCRIPTION", ControlMessage::RequestError(RequestError { code: RequestErrorCode::DUPLICATE_SUBSCRIPTION, retry_interval: 0, reason: b"dup".to_vec(), redirect: None })); c.control( "REQUEST_ERROR REDIRECT", ControlMessage::RequestError(RequestError { code: RequestErrorCode::REDIRECT, retry_interval: 1001, reason: vec![], redirect: Some(Redirect { connect_uri: b"moqt://other.example".to_vec(), namespace: ns(&["a"]), name: name("b") }), }), ); // ── §3.3 the control stream: SETUP, then GOAWAY with its Request ID ── let setup = Setup { options: SetupOptions(vec![kb(opt::MOQT_IMPLEMENTATION, b"x")]) }; let goaway = Goaway { new_session_uri: vec![], timeout_ms: 100, request_id: Some(2) }; let cs = encode_control_stream(&setup, &[ControlMessage::Goaway(goaway.clone())]).unwrap(); c.stream( "control stream: SETUP then GOAWAY", cs, GroupOrder::Ascending, vec![UniEvent::Header(StreamHeader::Control), UniEvent::Control(ControlMessage::Setup(setup.clone())), UniEvent::Control(ControlMessage::Goaway(goaway))], ); // ── §11.6, the draft's first example: a subgroup on one stream ── let h = SubgroupHeader { track_alias: 2, group_id: 0, subgroup_id: SubgroupId::Explicit(0), publisher_priority: Some(0), properties: false, end_of_group: false, first_object: false }; let objs = vec![SubgroupObject::normal(0, b"abcd".to_vec()), SubgroupObject::normal(1, b"efgh".to_vec())]; let b = encode_subgroup_stream(&h, &objs).unwrap(); assert_eq!(b, cat(&[&[0x14, 2, 0, 0, 0, 0, 4], b"abcd", &[0, 4], b"efgh"]), "§11.6's first example, byte for byte"); let mut ev = vec![UniEvent::Header(StreamHeader::Subgroup(h))]; ev.extend(objs.into_iter().map(UniEvent::SubgroupObject)); c.stream("subgroup: §11.6's first example (type 0x14)", b, GroupOrder::Ascending, ev); // A subgroup carrying this crate's receipt property in Immutable Properties, and // FIRST_OBJECT (bit 0x40, which -16 lacks). let h = SubgroupHeader { track_alias: 3, group_id: 9, subgroup_id: SubgroupId::Zero, publisher_priority: None, properties: true, end_of_group: true, first_object: true }; let o1 = SubgroupObject { object_id: 0, properties: with_immutable(vec![], vec![kb(0x3a41, &[7; 40]), kv(0x3a44, 1)]), status: ObjectStatus::Normal, payload: b"WAI1....".to_vec() }; let o2 = SubgroupObject { object_id: 4, properties: Properties::new(), status: ObjectStatus::EndOfGroup, payload: vec![] }; let b = encode_subgroup_stream(&h, &[o1.clone(), o2.clone()]).unwrap(); c.stream("subgroup with Immutable Properties, FIRST_OBJECT and End of Group", b, GroupOrder::Ascending, vec![UniEvent::Header(StreamHeader::Subgroup(h)), UniEvent::SubgroupObject(o1), UniEvent::SubgroupObject(o2)]); // ── §11.4.4 fetch streams, both End of Range indicators, both orders ── for order in [GroupOrder::Ascending, GroupOrder::Descending] { // An End of Range moves at least one group in the Group Order (the reading of // -22 and -18 that their delta coding gives), so each sits in a group of its own. let (g0, gm, g1) = if order == GroupOrder::Ascending { (1, 2, 4) } else { (4, 3, 1) }; let entries = vec![ FetchEntry::Object(FetchObject { group_id: g0, object_id: 0, subgroup_id: Some(0), publisher_priority: 7, properties: None, payload: b"a".to_vec() }), FetchEntry::Object(FetchObject { group_id: g0, object_id: 2, subgroup_id: Some(1), publisher_priority: 7, properties: Some(Properties::new()), payload: b"b".to_vec() }), FetchEntry::EndOfRange { kind: RangeEnd::NonExistent, location: Location::new(gm, 9) }, FetchEntry::EndOfRange { kind: RangeEnd::Unknown, location: Location::new(g1, 2) }, FetchEntry::Object(FetchObject { group_id: g1, object_id: 3, subgroup_id: None, publisher_priority: 9, properties: None, payload: b"c".to_vec() }), ]; let b = encode_fetch_stream(&FetchHeader { request_id: 4 }, order, &entries).unwrap(); let mut ev = vec![UniEvent::Header(StreamHeader::Fetch(FetchHeader { request_id: 4 }))]; ev.extend(entries.into_iter().map(UniEvent::FetchEntry)); c.stream(&format!("fetch stream, {order:?}"), b, order, ev); } c.stream("padding stream", encode_padding_stream(4), GroupOrder::Ascending, vec![UniEvent::Header(StreamHeader::Padding), UniEvent::Padding(4)]); // ── §11.3.1 datagrams ── let d = ObjectDatagram { track_alias: 1, group_id: 2, object_id: 3, publisher_priority: Some(9), properties: Some(Properties(vec![kv(0x3c, 1)])), end_of_group: true, content: DatagramContent::Payload(b"xyz".to_vec()), }; let b = d.encode().unwrap(); c.push("datagram with properties and End of Group", "datagram", &b, true, None, json::datagram(&Datagram::Object(d))); let d = ObjectDatagram { track_alias: 1, group_id: 2, object_id: 0, publisher_priority: None, properties: None, end_of_group: false, content: DatagramContent::Status(ObjectStatus::EndOfTrack) }; let b = d.encode().unwrap(); c.push("datagram with a status", "datagram", &b, true, None, json::datagram(&Datagram::Object(d))); // ── where -18 refuses what -22 reads ── c.bad("PUBLISH_STATE_NOTIFY (0x22) is no -18 message", "control", &frame(0x22, &[0]), None, "unknown-message-type", PV); c.bad("CLIENT_SETUP (0x20) is reserved", "control", &frame(0x20, &[0]), None, "unknown-message-type", PV); let sub = |params: &[u8]| frame(msg::SUBSCRIBE, &cat(&[&[0, 1, 1, b'a', 1, b'b'], params])); c.bad("SUBSCRIBE: -22's LOCATION_FILTER form (no length) read as a SUBSCRIPTION_FILTER", "control", &sub(&[1, 0x21, 5]), None, "truncated", PV); c.bad("SUBSCRIBE: Filter Type 5 (-22's Next Object)", "control", &sub(&[1, 0x21, 1, 5]), None, "invalid-value", PV); c.bad("SUBSCRIBE: a filter with a byte after it", "control", &sub(&[1, 0x21, 2, 1, 0]), None, "trailing-bytes", PV); c.bad("SUBSCRIBE: FILL_PARAMETERS (0x23) is unknown", "control", &sub(&[1, 0x23, 0]), None, "unknown-parameter", PV); c.bad("SUBSCRIBE: OBJECTID_FILTER (0x26) is unknown", "control", &sub(&[1, 0x26, 0]), None, "unknown-parameter", PV); c.bad("SUBSCRIBE: INCLUDE_PROPERTIES (0x35) is unknown", "control", &sub(&[1, 0x35, 1]), None, "unknown-parameter", PV); c.bad("SUBSCRIBE: FORWARD twice", "control", &sub(&[2, 0x10, 1, 0, 1]), None, "duplicate", PV); c.bad("SUBSCRIBE: an AbsoluteRange end past 2^64-1", "control", &sub(&cat(&[&[1, 0x21, 12, 4], &v(u64::MAX), &[0], &v(1)])), None, "overflow", PV); c.bad( "SUBSCRIBE_TRACKS: GROUP_ORDER (allowed in -22, not -18)", "control", &frame(msg::SUBSCRIBE_TRACKS, &[0, 1, 1, b'a', 1, 0x22, 1]), None, "parameter-not-allowed", PV, ); c.bad("FETCH: Fetch Type 4", "control", &frame(msg::FETCH, &[4, 4, 0]), None, "invalid-value", PV); c.bad("FETCH_OK: End Of Track of 2", "control", &frame(msg::FETCH_OK, &[2, 0, 0, 0]), None, "invalid-value", PV); let asc = Some(json!({"group_order": 1})); let setup_bytes = ControlMessage::Setup(setup).encode().unwrap(); let bare = ControlMessage::Goaway(Goaway { new_session_uri: vec![], timeout_ms: 0, request_id: None }).encode().unwrap(); c.bad("control stream: a GOAWAY without its Request ID", "uni-stream", &cat(&[&setup_bytes, &bare]), asc.clone(), "unexpected-message", PV); let mut fh = Vec::new(); FetchHeader { request_id: 0 }.encode(&mut fh); c.bad("fetch: End of Timed-Out Range (0x20C, -22 only)", "uni-stream", &cat(&[&fh, &v(0x20c), &[1, 1]]), asc.clone(), "invalid-flags", PV); c.bad("subgroup: SUBGROUP_ID_MODE 0b11", "uni-stream", &[0x16, 0, 0, 0, 0], asc.clone(), "invalid-flags", PV); c.bad("stream: type 0x06", "uni-stream", &[0x06, 0], asc.clone(), "unknown-stream-type", PV); c.bad("datagram: STATUS and END_OF_GROUP", "datagram", &[0x22, 1, 2, 3, 0], None, "invalid-flags", PV); c.vectors } fn corpus_doc(vectors: Vec) -> Value { json!({ "about": "draft-ietf-moq-transport-18 wire vectors: varints (the draft's Table 2), every control message of Table 5, unidirectional streams (control, subgroup with the draft's §11.6 example, fetch, padding) and datagrams, and negative vectors for what -18 refuses, each with -18's consequence. The JSON view is joule_moq::draft18::json's (the crate README describes it). A uni-stream vector's context gives the Group Order of the request a fetch stream answers. Generated by tests/draft18_vectors.rs; regenerate with JOULE_MOQ_WRITE_VECTORS=1.", "draft": "draft-ietf-moq-transport-18", "vectors": vectors, }) } fn unhex(s: &str) -> Vec { (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect() } fn order_of(x: &Value) -> GroupOrder { match x["context"]["group_order"].as_u64() { Some(2) => GroupOrder::Descending, _ => GroupOrder::Ascending, } } fn reencode(ev: &[UniEvent], order: GroupOrder) -> Option> { let mut out = Vec::new(); let mut sub: Option = None; let mut fetch: Option = None; for e in ev { match e { UniEvent::Header(StreamHeader::Control) => {} UniEvent::Header(StreamHeader::Padding) => out.extend(varint::to_vec(joule_moq::draft18::data::stream_type::PADDING)), UniEvent::Header(StreamHeader::Subgroup(h)) => { h.encode(&mut out); sub = Some(SubgroupCodec::new(*h)); } UniEvent::Header(StreamHeader::Fetch(h)) => { h.encode(&mut out); fetch = Some(FetchCodec::new(order)); } UniEvent::Control(m) => out.extend(m.encode().ok()?), UniEvent::SubgroupObject(o) => sub.as_mut()?.encode_object(o, &mut out).ok()?, UniEvent::FetchEntry(f) => fetch.as_mut()?.encode(f, &mut out).ok()?, UniEvent::Padding(n) => out.resize(out.len() + n, 0), } } Some(out) } fn decode(x: &Value) -> Result<(Value, Option>), Value> { let b = unhex(x["hex"].as_str().unwrap()); let err = |e: WireError| json::error(&e); match x["kind"].as_str().unwrap() { "varint" => { let (val, n) = varint::get(&b, "varint").map_err(err)?; if n != b.len() { return Err(json!({"error": "trailing-bytes"})); } Ok((json!({"value": val, "length": n}), Some(varint::to_vec(val)))) } "control" => { let m = ControlMessage::decode_exact(&b).map_err(err)?; Ok((json::control(&m), Some(m.encode().unwrap()))) } "uni-stream" => { let order = order_of(x); let ev = joule_moq::draft18::decode_uni_stream(&b, order).map_err(err)?; Ok((Value::Array(ev.iter().map(json::uni_event).collect()), reencode(&ev, order))) } "datagram" => { let d = Datagram::decode(&b).map_err(err)?; let again = match &d { Datagram::Object(o) => o.encode().unwrap(), Datagram::Padding(n) => Datagram::padding(*n), }; Ok((json::datagram(&d), Some(again))) } k => panic!("unknown kind {k}"), } } fn decode_incrementally(b: &[u8], order: GroupOrder, step: usize) -> Result, WireError> { let mut d = UniStreamDecoder::new(order, Limits { max_payload: u64::MAX, max_properties: u64::MAX }); let mut out = Vec::new(); for chunk in b.chunks(step.max(1)) { d.push(chunk); while let Some(e) = d.next()? { out.push(e); } } d.finish()?; Ok(out) } #[test] fn the_draft18_corpus_is_current_and_holds() { let doc = corpus_doc(build()); let text = serde_json::to_string_pretty(&doc).unwrap() + "\n"; if std::env::var("JOULE_MOQ_WRITE_VECTORS").as_deref() == Ok("1") { std::fs::create_dir_all(std::path::Path::new(CORPUS).parent().unwrap()).unwrap(); std::fs::write(CORPUS, &text).unwrap(); } let committed = std::fs::read_to_string(CORPUS).unwrap_or_else(|e| panic!("read {CORPUS}: {e}")); assert!(committed == text, "{CORPUS} is not what tests/draft18_vectors.rs generates; regenerate it with JOULE_MOQ_WRITE_VECTORS=1"); let mut failures = Vec::new(); let (mut good, mut bad) = (0, 0); for x in doc["vectors"].as_array().unwrap() { let name = x["name"].as_str().unwrap(); let expect = &x["expect"]; let b = unhex(x["hex"].as_str().unwrap()); match (expect.get("error"), decode(x)) { (None, Ok((got, again))) => { good += 1; if &got != expect { failures.push(format!("{name}: read as {got}")); } if x["canonical"] == true && again.as_deref() != Some(&b[..]) { failures.push(format!("{name}: re-encodes to other bytes")); } } (None, Err(e)) => failures.push(format!("{name}: refused ({e}); the corpus reads it")), (Some(_), Ok((got, _))) => failures.push(format!("{name}: read as {got}; the corpus refuses it")), (Some(_), Err(e)) => { bad += 1; if &e != expect { failures.push(format!("{name}: refused as {e}, the corpus says {expect}")); } } } if x["kind"] == "uni-stream" { for step in [1, 3, b.len().max(1)] { match (expect.get("error"), decode_incrementally(&b, order_of(x), step)) { (None, Ok(ev)) => { let mut merged: Vec = Vec::new(); for e in ev { match (merged.last_mut(), e) { (Some(UniEvent::Padding(n)), UniEvent::Padding(k)) => *n += k, (_, e) => merged.push(e), } } let got = Value::Array(merged.iter().map(json::uni_event).collect()); if &got != expect { failures.push(format!("{name}: in pieces of {step}, read as {got}")); } } (None, Err(e)) => failures.push(format!("{name}: in pieces of {step}, refused ({e})")), (Some(_), Ok(_)) => failures.push(format!("{name}: in pieces of {step}, accepted")), (Some(_), Err(_)) => {} } } } } assert!(failures.is_empty(), "{} vector(s) disagree:\n{}", failures.len(), failures.join("\n")); assert!(good > 0 && bad > 0); }