moqtap_codec/draft19/data_stream.rs
1//! Draft-19 data stream header encoding and decoding.
2//!
3//! Byte-for-byte identical to draft-18; only Object Property scope and the
4//! object-status payload rule changed semantically, not the wire layout.
5//!
6//! The payload rule is worth spelling out, because it is the one place where a
7//! draft-19 encoder can refuse an object a draft-18 encoder would have
8//! reinterpreted. Draft-18 said every Object with a status other than Normal
9//! has an empty payload; draft-19 Section 11.2.1.1 says instead that an Object
10//! has an empty payload unless its status is registered as permitting one, and
11//! Section 15.9 puts that permission in the Object Status registry. Both
12//! encodings here keep their draft-18 framing — a subgroup object carries a
13//! status exactly when its Object Payload Length is zero, and a datagram
14//! carries one exactly when its type sets the STATUS bit, both stated that way
15//! by the draft — so no frame these decoders can read is able to state a
16//! status and a payload at once. The registry rule therefore bites where a
17//! caller can hold both: `SubgroupObjectReader::write_object` consults the
18//! status's payload permission and refuses an object whose status forbids the
19//! payload handed with it, rather than dropping the status and writing the
20//! bytes as a Normal object. On the way back out, `SubgroupObject` and
21//! `DatagramHeader` answer the same question from the registry, so a reader
22//! never has to recover it from a length.
23//!
24//! Subgroup header type byte: form 0b0XX1XXXX, so bit 4 is set and bit 7 is
25//! clear; the ranges are 0x10..0x1F, 0x30..0x3F, 0x50..0x5F, 0x70..0x7F.
26//! - bit 0 (0x01): PROPERTIES
27//! - bits 1-2 (0x06): SUBGROUP_ID_MODE (0=zero, 1=first_obj, 2=explicit, 3=reserved)
28//! - bit 3 (0x08): END_OF_GROUP
29//! - bit 5 (0x20): DEFAULT_PRIORITY (no priority byte)
30//! - bit 6 (0x40): FIRST_OBJECT
31//!
32//! Datagram type byte: 0b00X0XXXX, so bits 4, 6 and 7 are clear; the ranges
33//! are 0x00..0x0F and 0x20..0x2F.
34//! - bit 0 (0x01): PROPERTIES
35//! - bit 1 (0x02): END_OF_GROUP
36//! - bit 2 (0x04): ZERO_OBJECT_ID (object_id=0, field omitted)
37//! - bit 3 (0x08): DEFAULT_PRIORITY (no priority byte)
38//! - bit 5 (0x20): STATUS (status byte replaces payload)
39//!
40//! Neither range is fully assigned, and draft-19 spells out which values in
41//! them an endpoint must refuse rather than decode, closing the session with a
42//! PROTOCOL_VIOLATION. Section 11.4.2 excludes the subgroup Types whose
43//! SUBGROUP_ID_MODE is the reserved 0b11 — 0x16, 0x17, 0x1E, 0x1F and the same
44//! four offsets in each higher range — because that mode does not say whether
45//! a Subgroup ID field follows the Group ID, so a decoder would have to guess
46//! and a wrong guess shifts every later field by the width of that varint.
47//! Section 11.3.1 excludes the datagram Types setting both STATUS (0x20) and
48//! END_OF_GROUP (0x02) — 0x22, 0x23, 0x26, 0x27, 0x2A, 0x2B, 0x2E and 0x2F —
49//! because an object status message cannot signal end of group.
50//! [`SubgroupHeader::decode`] and [`DatagramHeader::decode`] refuse both
51//! lists, and [`SubgroupHeader::encode_checked`] and
52//! [`DatagramHeader::encode_checked`] refuse to write them.
53//!
54//! Fetch header: stream type 0x05 + request_id. Draft-19 Section 11.4.4
55//! replaced the fixed per-object layout earlier drafts used with a leading
56//! Serialization Flags varint that says which of the object's fields are on
57//! the wire at all; [`FetchObjectHeader`] decodes and encodes one such object
58//! header.
59
60use bytes::{Buf, BufMut};
61
62use super::types::{ObjectStatus, PayloadPermission};
63use crate::error::CodecError;
64use crate::varint::{Moqt18 as Wire, VarInt};
65
66/// Advance `buf` past `len` bytes without copying them.
67fn skip(buf: &mut impl Buf, len: u64) -> Result<(), CodecError> {
68 let len = usize::try_from(len).map_err(|_| CodecError::UnexpectedEnd)?;
69 if buf.remaining() < len {
70 return Err(CodecError::UnexpectedEnd);
71 }
72 buf.advance(len);
73 Ok(())
74}
75
76/// Turn a wire Object Status code into an [`ObjectStatus`], refusing one
77/// draft-19 does not assign.
78///
79/// Draft-19 Section 11.2.1.1 lists the codes an object may carry — the three
80/// rows of the Object Status registry it establishes in Section 15.9 — and
81/// says any other value SHOULD be treated as a protocol error and the session
82/// closed with a PROTOCOL_VIOLATION. Every place this module reads a status
83/// converts it here, so a decoded [`SubgroupObject::object_status`] or
84/// [`DatagramHeader::object_status`] is always a status the draft assigns, and
85/// [`SubgroupObjectMeta::status`] — which stays a raw code because a relay may
86/// carry it to a draft that numbers the set differently — holds one because it
87/// comes from the same conversion.
88fn decoded_status(code: u64) -> Result<ObjectStatus, CodecError> {
89 ObjectStatus::from_u64(code).ok_or(CodecError::InvalidField)
90}
91
92// ── Stream and datagram types ─────────────────────────────────
93
94/// Unidirectional stream type for padding, draft-19 Section 11.5.1: "An
95/// endpoint MAY open a unidirectional stream with a stream type of 0x132B3E28
96/// to send padding data. The stream begins with the stream type, followed by
97/// zero or more bytes that MUST all be set to zero."
98///
99/// Named here because the value is what tells a padding stream from a data
100/// stream, and nothing else in this module would otherwise say so. Under the
101/// draft-19 variable-length integer encoding the value takes five bytes, the
102/// first of which is 0xF0 — an octet with bit 7 set, which the subgroup form
103/// leaves clear. A reader judging the Type by that first byte alone would find
104/// the form broken and call the stream unknown, which Section 3.4 answers by
105/// ending the session. Table 3 assigns the value, so that would be a close over
106/// traffic this draft permits.
107pub const PADDING_STREAM_TYPE: u64 = 0x132B_3E28;
108
109/// Datagram type for padding, draft-19 Section 11.5.2: "An endpoint MAY send a
110/// datagram with a type of 0x132B3E29 to send padding data. The datagram
111/// contains the type followed by zero or more bytes that MUST all be set to
112/// zero."
113///
114/// One more than [`PADDING_STREAM_TYPE`] and encoded the same width, and
115/// assigned the same way, so the same reasoning applies to a datagram reader.
116pub const PADDING_DATAGRAM_TYPE: u64 = 0x132B_3E29;
117
118/// The unidirectional stream Type draft-19 Section 10.3 gives the control
119/// stream.
120const SETUP_STREAM_TYPE: u64 = 0x2F00;
121
122/// Refuse a Type field spelled in more than one byte, before anything narrows
123/// it to a byte.
124///
125/// Returns `Ok(None)` when the next Type is a single byte and the caller should
126/// read it itself, `Ok(Some(err))` when it is wider and `refusal` has named the
127/// failure, and `Err` only when the buffer does not hold the whole field yet.
128///
129/// Every Type the subgroup and datagram forms admit is below 0x80 and so
130/// occupies one byte under the MoQT variable-length integer encoding. A wider
131/// spelling is one of three things, and none of them may be read as a header:
132/// an assigned Type that is not a data stream — SETUP or a padding stream — a
133/// Type no table assigns, or a non-minimal spelling of a Type that is valid.
134/// The last is the dangerous one: narrowing a two-byte 0x8001 to its low octet
135/// turns it into an assigned Type, so a peer could name any Type it liked and
136/// have it parsed as another.
137///
138/// The full varint is decoded before `refusal` sees it, which is what lets the
139/// first case be told from the second. Only the second ends the session.
140fn wide_type_refusal(
141 buf: &mut impl Buf,
142 refusal: fn(u64) -> CodecError,
143) -> Result<Option<CodecError>, CodecError> {
144 if !buf.has_remaining() {
145 return Err(CodecError::UnexpectedEnd);
146 }
147 // Under the MoQT encoding the field's length is the number of leading 1
148 // bits in its first byte plus one, so a first byte below 0x80 is the whole
149 // of it.
150 if buf.chunk()[0] < 0x80 {
151 return Ok(None);
152 }
153 let raw = VarInt::decode_moqt::<Wire>(buf)?.into_inner();
154 Ok(Some(refusal(raw)))
155}
156
157// ── Subgroup ──────────────────────────────────────────────────
158
159const SUBGROUP_PROPERTIES_BIT: u8 = 0x01;
160const SUBGROUP_ID_MODE_MASK: u8 = 0x06;
161const SUBGROUP_END_OF_GROUP_BIT: u8 = 0x08;
162const SUBGROUP_BASE_BIT: u8 = 0x10;
163const SUBGROUP_DEFAULT_PRIORITY_BIT: u8 = 0x20;
164const SUBGROUP_FIRST_OBJECT_BIT: u8 = 0x40;
165/// Bit 7, which the subgroup header's 0b0XX1XXXX form leaves clear.
166const SUBGROUP_FORM_FORBIDDEN_BITS: u8 = 0x80;
167/// The SUBGROUP_ID_MODE value draft-19 reserves, once the mask is applied and
168/// the field shifted down.
169const SUBGROUP_ID_MODE_RESERVED: u8 = 0b11;
170
171/// Refuse a subgroup header Type value draft-19 Section 11.4.2 lists as
172/// invalid.
173///
174/// The section gives two lists and says of both that an endpoint receiving a
175/// stream header with such a Type MUST close the session with a
176/// PROTOCOL_VIOLATION:
177///
178/// - Values outside the form 0b0XX1XXXX — bit 4 clear, or bit 7 set. The
179/// valid ranges it spells out are 0x10..0x1F, 0x30..0x3F, 0x50..0x5F and
180/// 0x70..0x7F.
181/// - Values whose SUBGROUP_ID_MODE (bits 1-2) is 0b11, which is reserved for
182/// future use: 0x16, 0x17, 0x1E, 0x1F, 0x36, 0x37, 0x3E, 0x3F, 0x56,
183/// 0x57, 0x5E, 0x5F, 0x76, 0x77, 0x7E and 0x7F.
184///
185/// The reserved mode is worth separating from a mere unassigned code point,
186/// because it is not decodable rather than merely unknown. The other three
187/// modes each say whether a Subgroup ID field follows the Group ID; 0b11 says
188/// nothing, so a decoder has to guess, and a wrong guess shifts every
189/// subsequent field by the width of that varint. Reading such a header as
190/// though no field were present — which is what this module did before — turns
191/// a header the draft says to reject into objects with plausible, wrong
192/// contents.
193///
194/// Every value in the valid ranges is below 0x80 and so occupies exactly one
195/// byte in the MoQT varint encoding, which is why [`SubgroupHeader::decode`]
196/// can read the Type with a single [`Buf::get_u8`] and check it here. A first
197/// byte with bit 7 set begins a longer varint; this refuses it, which is the
198/// right answer for every value that varint could hold except a non-minimal
199/// spelling of a valid Type.
200fn validate_subgroup_type(raw: u64) -> Result<(), CodecError> {
201 if subgroup_type_is_valid(raw) {
202 Ok(())
203 } else {
204 Err(stream_type_error(raw))
205 }
206}
207
208/// Whether `raw` is a subgroup Type draft-19 admits: inside the form, and not
209/// the reserved SUBGROUP_ID_MODE.
210fn subgroup_type_is_valid(raw: u64) -> bool {
211 raw <= 0xFF && {
212 let t = raw as u8;
213 t & SUBGROUP_FORM_FORBIDDEN_BITS == 0
214 && t & SUBGROUP_BASE_BIT != 0
215 && (t & SUBGROUP_ID_MODE_MASK) >> 1 != SUBGROUP_ID_MODE_RESERVED
216 }
217}
218
219/// Whether `raw` sits inside the subgroup form but names the reserved
220/// SUBGROUP_ID_MODE — the second of Section 11.4.2's two lists.
221fn subgroup_type_is_reserved_mode(raw: u64) -> bool {
222 raw <= 0xFF && {
223 let t = raw as u8;
224 t & SUBGROUP_FORM_FORBIDDEN_BITS == 0
225 && t & SUBGROUP_BASE_BIT != 0
226 && (t & SUBGROUP_ID_MODE_MASK) >> 1 == SUBGROUP_ID_MODE_RESERVED
227 }
228}
229
230/// Which failure a leading unidirectional stream Type that is not the one a
231/// reader wants is.
232///
233/// Draft-19 states two rules about such a Type and answers both with a close,
234/// and telling them apart is the whole job of this function.
235///
236/// Section 3.4 is about the table: "An endpoint that receives an unknown stream
237/// type MUST close the session." A Type Table 3 does not assign is
238/// [`CodecError::UnknownStreamType`].
239///
240/// Section 11.4.2 is about the subgroup form specifically, and the sixteen
241/// Types inside it that name the reserved SUBGROUP_ID_MODE. Those are not
242/// unknown — the form is assigned and the draft lists the values outright — but
243/// they are unreadable, and they are [`CodecError::InvalidTypeValue`].
244///
245/// Table 3 assigns four things, and two of them carry no Objects at all:
246/// FETCH_HEADER, the subgroup form, SETUP and PADDING. A subgroup reader handed
247/// any of them refuses it as [`CodecError::InvalidField`] — the value is one
248/// this draft defines, the disagreement is with the reader that was called, and
249/// the session survives it. See [`PADDING_STREAM_TYPE`] for why that case in
250/// particular is worth the trouble.
251fn stream_type_error(raw: u64) -> CodecError {
252 if raw == FETCH_STREAM_TYPE
253 || raw == SETUP_STREAM_TYPE
254 || raw == PADDING_STREAM_TYPE
255 || subgroup_type_is_valid(raw)
256 {
257 CodecError::InvalidField
258 } else if subgroup_type_is_reserved_mode(raw) {
259 CodecError::InvalidTypeValue {
260 raw,
261 detail: "its SUBGROUP_ID_MODE is 0b11, which this draft reserves",
262 }
263 } else {
264 CodecError::UnknownStreamType(raw)
265 }
266}
267
268#[derive(Debug, Clone)]
269pub struct SubgroupHeader {
270 pub header_type: u8,
271 pub track_alias: VarInt,
272 pub group_id: VarInt,
273 pub subgroup_id: VarInt,
274 pub publisher_priority: Option<u8>,
275}
276
277impl SubgroupHeader {
278 /// Decode a subgroup header, Type field included.
279 ///
280 /// A Type spelled in more than one byte is refused first, by
281 /// `wide_type_refusal`, which decodes it in full so `stream_type_error`
282 /// can tell a padding or SETUP stream — both assigned, both several bytes
283 /// wide — from a Type Table 3 does not assign. Only the last of those ends
284 /// the session.
285 pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
286 if let Some(err) = wide_type_refusal(buf, stream_type_error)? {
287 return Err(err);
288 }
289 let raw = buf.get_u8() as u64;
290 validate_subgroup_type(raw)?;
291 let header_type = raw as u8;
292
293 let track_alias = VarInt::decode_moqt::<Wire>(buf)?;
294 let group_id = VarInt::decode_moqt::<Wire>(buf)?;
295
296 let subgroup_id_mode = (header_type & SUBGROUP_ID_MODE_MASK) >> 1;
297 let subgroup_id = match subgroup_id_mode {
298 0 => VarInt::from_u64_moqt(0),
299 2 => VarInt::decode_moqt::<Wire>(buf)?,
300 // Mode 1 puts no Subgroup ID on the wire either: it is the first
301 // object's ID, which this header cannot see. Store 0 until a
302 // caller resolves it. Mode 3 never reaches here — the type check
303 // above refuses it.
304 _ => VarInt::from_u64_moqt(0),
305 };
306
307 let publisher_priority = if header_type & SUBGROUP_DEFAULT_PRIORITY_BIT == 0 {
308 if buf.remaining() < 1 {
309 return Err(CodecError::UnexpectedEnd);
310 }
311 Some(buf.get_u8())
312 } else {
313 None
314 };
315
316 Ok(SubgroupHeader { header_type, track_alias, group_id, subgroup_id, publisher_priority })
317 }
318
319 /// Serialize the header exactly as its Type byte describes it.
320 ///
321 /// Infallible, and so willing to write a Type draft-19 Section 11.4.2
322 /// tells an endpoint to reject — including a reserved-mode Type this
323 /// module's own [`Self::decode`] refuses to read back. Prefer
324 /// [`Self::encode_checked`], which refuses those Types instead.
325 pub fn encode(&self, buf: &mut impl BufMut) {
326 buf.put_u8(self.header_type);
327 self.track_alias.encode_moqt::<Wire>(buf);
328 self.group_id.encode_moqt::<Wire>(buf);
329
330 let subgroup_id_mode = (self.header_type & SUBGROUP_ID_MODE_MASK) >> 1;
331 if subgroup_id_mode == 2 {
332 self.subgroup_id.encode_moqt::<Wire>(buf);
333 }
334
335 if self.header_type & SUBGROUP_DEFAULT_PRIORITY_BIT == 0 {
336 buf.put_u8(self.publisher_priority.unwrap_or(128));
337 }
338 }
339
340 /// Serialize the header, refusing a Type value draft-19 forbids.
341 ///
342 /// Errors with [`CodecError::InvalidField`] for exactly the Types draft-19
343 /// Section 11.4.2 lists as invalid (the same set [`Self::decode`] refuses),
344 /// before any byte is written, so a refused header leaves `buf` untouched.
345 /// Everything else is written by [`Self::encode`].
346 ///
347 /// The check belongs on the encode side as well as the decode side because
348 /// the two halves would otherwise disagree about which streams exist: a
349 /// reserved-mode header written by [`Self::encode`] cannot be read back by
350 /// [`Self::decode`], and a codec used to rewrite captured traffic would
351 /// emit a stream it could not then parse.
352 pub fn encode_checked(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
353 validate_subgroup_type(self.header_type as u64)?;
354 self.encode(buf);
355 Ok(())
356 }
357
358 pub fn has_properties(&self) -> bool {
359 self.header_type & SUBGROUP_PROPERTIES_BIT != 0
360 }
361
362 /// The subgroup-ID mode: `(header_type & 0x06) >> 1`.
363 ///
364 /// `0` = no subgroup ID on the wire and it is zero; `1` = the subgroup ID
365 /// is the first object's ID; `2` = an explicit ID follows the Group ID;
366 /// `3` = reserved. Exposed because the mask is module-private and
367 /// `dispatch::AnySubgroupHeader::subgroup_id_mode` cannot read it.
368 ///
369 /// `3` never comes back from [`Self::decode`], which refuses the Types
370 /// carrying it. `header_type` is a public field, so a hand-built header
371 /// can still report it; [`Self::encode_checked`] is what refuses to put
372 /// one on the wire.
373 pub fn subgroup_id_mode(&self) -> u8 {
374 (self.header_type & SUBGROUP_ID_MODE_MASK) >> 1
375 }
376
377 pub fn is_end_of_group(&self) -> bool {
378 self.header_type & SUBGROUP_END_OF_GROUP_BIT != 0
379 }
380
381 /// `true` when the FIRST_OBJECT bit (0x40) is set, signaling the first
382 /// object on this stream is the original publisher's first object in the
383 /// subgroup. Added in draft-18.
384 pub fn is_first_object(&self) -> bool {
385 self.header_type & SUBGROUP_FIRST_OBJECT_BIT != 0
386 }
387}
388
389// ── Subgroup objects (stateful) ───────────────────────────────
390
391/// One object within a draft-19 subgroup stream. Object IDs are
392/// delta-encoded; whether a per-object "properties" block (the draft-19
393/// equivalent of extension headers) is present depends on the PROPERTIES
394/// bit on the enclosing [`SubgroupHeader`]. Use [`SubgroupObjectReader`]
395/// to encode/decode.
396#[derive(Debug, Clone)]
397pub struct SubgroupObject {
398 pub object_id: VarInt,
399 /// Raw properties bytes, excluding the byte-length prefix that precedes
400 /// them on the wire. Empty unless the subgroup header sets the
401 /// PROPERTIES bit, or when the block is present but zero-length.
402 /// Opaque: [`SubgroupObjectReader::write_object`] re-emits the prefix
403 /// and these bytes verbatim.
404 pub extension_headers: Vec<u8>,
405 pub payload_length: VarInt,
406 /// The object's status, carried on the wire only when `payload_length` is
407 /// zero. `None` with a zero `payload_length` is written as
408 /// [`ObjectStatus::Normal`].
409 ///
410 /// `None` is not "no status": every Object has one. It means the status is
411 /// the one the encoding elides — [`ObjectStatus::Normal`], the only row of
412 /// the Object Status registry (draft-19 Section 15.9) that permits the
413 /// payload such an object carries. Decoding a payload-bearing object leaves
414 /// this `None` for that reason; [`Self::status`] resolves it either way.
415 ///
416 /// Typed rather than a raw code. The wire field is a varint with room for
417 /// any value, and draft-19 assigns three of them; the decoder refuses the
418 /// rest, and this type is that same refusal on the encode side — 0x1 and
419 /// 0x2 cannot be named here, so [`SubgroupObjectReader::write_object`]
420 /// cannot emit a status this module's own decoder would reject.
421 ///
422 /// A status and a payload can be held here together, which the wire has no
423 /// way to express. That combination is what draft-19's registry rules on:
424 /// [`SubgroupObjectReader::write_object`] accepts it when the status is
425 /// registered as permitting a payload and refuses it otherwise.
426 pub object_status: Option<ObjectStatus>,
427 pub payload: Vec<u8>,
428}
429
430impl SubgroupObject {
431 /// The object's status, with the one draft-19's encoding elides filled in.
432 ///
433 /// A subgroup object states its status only when its Object Payload Length
434 /// is zero. An object that carries bytes therefore has no status field, and
435 /// its status is [`ObjectStatus::Normal`] — the sole row of the Object
436 /// Status registry (draft-19 Section 15.9) permitting a payload, so the
437 /// only status such an object could have had.
438 pub fn status(&self) -> ObjectStatus {
439 self.object_status.unwrap_or(ObjectStatus::Normal)
440 }
441
442 /// Whether the Object Status registry permits this object a non-empty
443 /// payload, per draft-19 Section 15.9.
444 ///
445 /// Answered from the status alone. `payload` and `payload_length` are not
446 /// consulted: on a status that permits a payload they say only whether this
447 /// particular object took the offer, and on one that forbids a payload a
448 /// non-empty payload is the malformation this reports, not evidence about
449 /// the rule.
450 pub fn permits_payload(&self) -> bool {
451 self.status().permits_payload()
452 }
453
454 /// Whether this object's status is allowed to carry the properties it has.
455 ///
456 /// Draft-19 Section 11.2.1.2: "Any Object with status Normal can have
457 /// properties (Section 2.5). If an endpoint receives properties on an
458 /// Object with status that is not Normal, it MUST close the session with a
459 /// PROTOCOL_VIOLATION."
460 ///
461 /// So this is `false` for exactly one shape: a non-empty properties block
462 /// on an object whose status is not [`ObjectStatus::Normal`]. An object
463 /// with no properties is fine at any status, and an object at Normal may
464 /// carry any properties.
465 ///
466 /// Neither [`SubgroupObjectReader::read_object`] nor
467 /// [`SubgroupObjectReader::write_object`] applies this itself, which is a
468 /// deliberate contrast with the payload rule beside it. A status next to a
469 /// payload has no encoding — the two share a position on the wire — so the
470 /// writer refuses it as unrepresentable. Properties next to a status encode
471 /// fine; the frame is well formed and merely non-conforming, and a codec
472 /// that could not read or write it could not reproduce a capture containing
473 /// one. The rule addresses an endpoint receiving such an Object, so the
474 /// endpoint is where it is enforced, and this is what it asks.
475 pub fn properties_permitted(&self) -> bool {
476 self.extension_headers.is_empty() || self.status() == ObjectStatus::Normal
477 }
478}
479
480/// The framing of one draft-19 subgroup object, without its payload.
481///
482/// Produced by [`SubgroupObjectReader::read_object_meta`] for callers that
483/// forward an object's bytes verbatim and never inspect the payload.
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
485pub struct SubgroupObjectMeta {
486 /// Resolved absolute Object ID.
487 pub object_id: u64,
488 /// Byte length of the properties block's contents, excluding its length
489 /// prefix.
490 pub extension_headers_len: u64,
491 /// Declared payload length. Zero when `status` is `Some`.
492 pub payload_length: u64,
493 /// Object status wire code, present only when the payload is empty.
494 pub status: Option<u64>,
495 /// Total bytes this object occupies on the wire, prefix fields included.
496 pub wire_len: u64,
497}
498
499impl SubgroupObjectMeta {
500 /// What the Object Status registry says about this object's payload, or
501 /// `None` if the registry has no row for its status.
502 ///
503 /// Draft-19 Section 15.9, Table 16 gives the registry a "Payload" column,
504 /// and Section 11.2.1.1 makes it the rule: an Object has an empty payload
505 /// unless its status is registered as permitting one. Table 16 fills the
506 /// column in for the three statuses draft-19 assigns — 0x0 Normal is
507 /// "Yes", 0x3 End of Group and 0x4 End of Track are "No" — and requires
508 /// every future registration to fill it in too.
509 ///
510 /// `status` is `None` for an object whose payload length is non-zero,
511 /// because the encoding puts a status field only where the payload does
512 /// not go. Such an object's status is Normal, the one row permitting the
513 /// payload it is carrying, so this answers
514 /// [`PayloadPermission::Permitted`] rather than `None`.
515 ///
516 /// The `None` this does return means something else entirely: a status
517 /// code with no row in the registry, for which the draft supplies no
518 /// answer and this must not invent one. [`SubgroupObjectReader`] never
519 /// produces such a meta — it refuses an unassigned code while decoding —
520 /// but every field here is public, so a caller that assembled a meta by
521 /// hand, or carried a status across from a draft numbering the set
522 /// differently, can hold one.
523 ///
524 /// Answered from the status alone. `payload_length` is not consulted: on a
525 /// status that permits a payload it says only whether this particular
526 /// object took the offer, and on one that forbids a payload a non-zero
527 /// length is the malformation a caller uses this to detect, not evidence
528 /// about the rule.
529 pub fn payload_permission(&self) -> Option<PayloadPermission> {
530 match self.status {
531 None => Some(ObjectStatus::Normal.payload_permission()),
532 Some(code) => ObjectStatus::from_u64(code).map(ObjectStatus::payload_permission),
533 }
534 }
535}
536
537#[derive(Debug, Clone)]
538pub struct SubgroupObjectReader {
539 extensions_present: bool,
540 prev_object_id: Option<u64>,
541}
542
543impl SubgroupObjectReader {
544 pub fn new(header: &SubgroupHeader) -> Self {
545 Self { extensions_present: header.has_properties(), prev_object_id: None }
546 }
547
548 pub fn read_object(&mut self, buf: &mut impl Buf) -> Result<SubgroupObject, CodecError> {
549 let delta = VarInt::decode_moqt::<Wire>(buf)?.into_inner();
550 let object_id_val = match self.prev_object_id {
551 None => delta,
552 Some(prev) => prev
553 .checked_add(1)
554 .and_then(|v| v.checked_add(delta))
555 .ok_or(CodecError::ObjectIdOverflow(prev, delta))?,
556 };
557 self.prev_object_id = Some(object_id_val);
558 let object_id = VarInt::from_u64(object_id_val).map_err(|_| CodecError::InvalidField)?;
559
560 // The properties block is a byte-length-prefixed opaque blob. We
561 // copy the blob verbatim; callers that want structured properties
562 // can parse the returned bytes.
563 let extension_headers = if self.extensions_present {
564 let ext_len = VarInt::decode_moqt::<Wire>(buf)?.into_inner() as usize;
565 crate::types::read_bytes(buf, ext_len)?
566 } else {
567 Vec::new()
568 };
569
570 let payload_length_vi = VarInt::decode_moqt::<Wire>(buf)?;
571 let payload_length_val = payload_length_vi.into_inner() as usize;
572 let (object_status, payload) = if payload_length_val == 0 {
573 let status = VarInt::decode_moqt::<Wire>(buf)?;
574 (Some(decoded_status(status.into_inner())?), Vec::new())
575 } else {
576 let payload = crate::types::read_bytes(buf, payload_length_val)?;
577 (None, payload)
578 };
579
580 Ok(SubgroupObject {
581 object_id,
582 extension_headers,
583 payload_length: payload_length_vi,
584 object_status,
585 payload,
586 })
587 }
588
589 /// Decode the next object's framing without copying its payload.
590 ///
591 /// Consumes exactly the bytes [`Self::read_object`] consumes and leaves
592 /// the same delta state behind, so the two are interchangeable on a
593 /// given stream.
594 pub fn read_object_meta(
595 &mut self,
596 buf: &mut impl Buf,
597 ) -> Result<SubgroupObjectMeta, CodecError> {
598 let start = buf.remaining();
599 let delta = VarInt::decode_moqt::<Wire>(buf)?.into_inner();
600 let object_id_val = match self.prev_object_id {
601 None => delta,
602 Some(prev) => prev
603 .checked_add(1)
604 .and_then(|v| v.checked_add(delta))
605 .ok_or(CodecError::ObjectIdOverflow(prev, delta))?,
606 };
607 self.prev_object_id = Some(object_id_val);
608 let object_id =
609 VarInt::from_u64(object_id_val).map_err(|_| CodecError::InvalidField)?.into_inner();
610
611 let extension_headers_len = if self.extensions_present {
612 let ext_len = VarInt::decode_moqt::<Wire>(buf)?.into_inner();
613 skip(buf, ext_len)?;
614 ext_len
615 } else {
616 0
617 };
618
619 let payload_length = VarInt::decode_moqt::<Wire>(buf)?.into_inner();
620 let status = if payload_length == 0 {
621 let code = VarInt::decode_moqt::<Wire>(buf)?.into_inner();
622 Some(decoded_status(code)?.as_u64())
623 } else {
624 skip(buf, payload_length)?;
625 None
626 };
627
628 Ok(SubgroupObjectMeta {
629 object_id,
630 extension_headers_len,
631 payload_length,
632 status,
633 wire_len: (start - buf.remaining()) as u64,
634 })
635 }
636
637 /// Serialize an object, producing the correct delta encoding.
638 ///
639 /// A zero `payload_length` writes the object's status, with
640 /// [`SubgroupObject::status`] filling in [`ObjectStatus::Normal`] when the
641 /// `object_status` field is `None`. The status is typed, so every value
642 /// that can reach this method is one draft-19 assigns and one
643 /// [`Self::read_object`] accepts; no unassigned code can be written.
644 ///
645 /// Errors with [`CodecError::InvalidField`] when the Object Status registry
646 /// (draft-19 Section 15.9) does not permit the object's status to carry a
647 /// payload and a payload was handed over anyway. Draft-19 Section 11.2.1.1
648 /// makes that object malformed, and the encoding has no way to state it:
649 /// the status field appears only where the payload does not. Writing it
650 /// would mean silently discarding one of the two — emitting the bytes as a
651 /// Normal object and losing an End of Group marker, say — so it is refused
652 /// instead. A status the registry does permit a payload is written as an
653 /// ordinary payload object, its status implicit, since that is how the
654 /// encoding spells it.
655 ///
656 /// Errors with [`CodecError::InvalidField`] when `object.object_id` is
657 /// not strictly greater than the previously written object's ID, since
658 /// no valid delta exists for that case.
659 ///
660 /// Errors with [`CodecError::InvalidField`] when `payload_length` is not
661 /// exactly `payload.len()`. The declared length is written ahead of the
662 /// payload, so a mismatch is a frame [`Self::read_object`] cannot parse
663 /// and one no caller could fix by appending bytes.
664 pub fn write_object(
665 &mut self,
666 object: &SubgroupObject,
667 buf: &mut impl BufMut,
668 ) -> Result<(), CodecError> {
669 // A declared length that disagrees with the payload framed under it
670 // produces bytes no reader can parse and no caller can repair: the
671 // length is already on the wire ahead of the payload. Checked before
672 // anything is written, so a refused object leaves `buf` untouched
673 // rather than half an object the next read would run into.
674 //
675 // A zero declared length is also what puts a status code where the
676 // payload would go, so an object carrying bytes under it is asking for
677 // two framings at once.
678 let declared = object.payload_length.into_inner();
679 if declared != object.payload.len() as u64 {
680 return Err(CodecError::InvalidField);
681 }
682
683 // The registry decides whether these two fields may be filled in at
684 // once, rather than the length deciding on its own which of them gets
685 // written. A status marked "Payload: No" is refused a payload; a status
686 // marked "Yes" keeps it, and gets no status field on the wire because
687 // the encoding elides the status of any object that carries bytes.
688 // Checked alongside the length above, before any byte is emitted.
689 if declared != 0 && !object.permits_payload() {
690 return Err(CodecError::PayloadNotPermitted {
691 status: object.status().as_u64(),
692 len: object.payload.len(),
693 detail: "its status is registered as forbidding one",
694 });
695 }
696
697 // Properties on a non-Normal status are NOT refused here, though
698 // draft-19 Section 11.2.1.2 forbids them. The two rules differ in kind.
699 // A status beside a payload has no encoding at all — the status field
700 // and the payload occupy the same position — so writing one is
701 // impossible rather than merely wrong. Properties beside a status
702 // encode perfectly well; the frame is well formed and non-conforming,
703 // which is a judgement about what a peer may send, not about what these
704 // bytes mean.
705 //
706 // Refusing it here would also make this writer unable to produce a
707 // frame the decoder must be able to read, and the two halves of a codec
708 // that disagree about which frames exist cannot be used to reproduce
709 // captured traffic. [`SubgroupObject::properties_permitted`] reports
710 // the violation instead, and the endpoint acts on it.
711
712 let oid = object.object_id.into_inner();
713 let delta = match self.prev_object_id {
714 None => oid,
715 Some(prev) => oid
716 .checked_sub(prev)
717 .and_then(|v| v.checked_sub(1))
718 .ok_or(CodecError::InvalidField)?,
719 };
720 VarInt::from_u64(delta).map_err(|_| CodecError::InvalidField)?.encode_moqt::<Wire>(buf);
721 if self.extensions_present {
722 VarInt::from_u64(object.extension_headers.len() as u64)
723 .map_err(|_| CodecError::InvalidField)?
724 .encode_moqt::<Wire>(buf);
725 buf.put_slice(&object.extension_headers);
726 }
727 object.payload_length.encode_moqt::<Wire>(buf);
728 if declared == 0 {
729 VarInt::from_u64_moqt(object.status().as_u64()).encode_moqt::<Wire>(buf);
730 } else {
731 buf.put_slice(&object.payload);
732 }
733 self.prev_object_id = Some(oid);
734 Ok(())
735 }
736}
737
738// ── Datagram ──────────────────────────────────────────────────
739
740const DATAGRAM_PROPERTIES_BIT: u8 = 0x01;
741const DATAGRAM_END_OF_GROUP_BIT: u8 = 0x02;
742const DATAGRAM_ZERO_OBJECT_ID_BIT: u8 = 0x04;
743const DATAGRAM_DEFAULT_PRIORITY_BIT: u8 = 0x08;
744const DATAGRAM_STATUS_BIT: u8 = 0x20;
745/// Bits 4, 6 and 7, which the datagram's 0b00X0XXXX form leaves clear.
746const DATAGRAM_FORM_FORBIDDEN_BITS: u8 = 0xD0;
747
748/// Refuse a datagram Type value draft-19 Section 11.3.1 lists as invalid.
749///
750/// The section gives two lists and says of both that an endpoint receiving a
751/// datagram with such a Type MUST close the session with a
752/// PROTOCOL_VIOLATION:
753///
754/// - Values with both the STATUS bit (0x20) and the END_OF_GROUP bit (0x02)
755/// set: 0x22, 0x23, 0x26, 0x27, 0x2A, 0x2B, 0x2E and 0x2F. Its reason is
756/// that "an object status message cannot signal end of group" — the two
757/// bits ask for one datagram to be both a status and an end-of-group
758/// marker for an object it does not carry.
759/// - Values outside the form 0b00X0XXXX, that is anything with bit 4, 6 or
760/// 7 set. The valid ranges are 0x00..0x0F and 0x20..0x2F; bit 4 is what
761/// separates a datagram Type from a subgroup stream header Type.
762///
763/// As with [`validate_subgroup_type`], every valid value is below 0x80 and so
764/// is a one-byte MoQT varint, which is why [`DatagramHeader::decode`] can read
765/// the Type with a single [`Buf::get_u8`].
766fn validate_datagram_type(raw: u64) -> Result<(), CodecError> {
767 if datagram_type_is_valid(raw) {
768 Ok(())
769 } else {
770 Err(datagram_type_error(raw))
771 }
772}
773
774/// Whether `raw` is a datagram Type draft-19 admits.
775fn datagram_type_is_valid(raw: u64) -> bool {
776 raw <= 0xFF && {
777 let t = raw as u8;
778 t & DATAGRAM_FORM_FORBIDDEN_BITS == 0
779 && !(t & DATAGRAM_STATUS_BIT != 0 && t & DATAGRAM_END_OF_GROUP_BIT != 0)
780 }
781}
782
783/// Whether `raw` sits inside the datagram form but sets STATUS and END_OF_GROUP
784/// together — the first of Section 11.3.1's two lists.
785fn datagram_type_is_status_end_of_group(raw: u64) -> bool {
786 raw <= 0xFF && {
787 let t = raw as u8;
788 t & DATAGRAM_FORM_FORBIDDEN_BITS == 0
789 && t & DATAGRAM_STATUS_BIT != 0
790 && t & DATAGRAM_END_OF_GROUP_BIT != 0
791 }
792}
793
794/// Which failure a leading datagram Type that is not one a reader wants is.
795///
796/// The same two-rule split as `stream_type_error`, read against the datagram
797/// table: a Type outside the form is [`CodecError::UnknownDatagramType`], and
798/// one inside it setting STATUS and END_OF_GROUP together is
799/// [`CodecError::InvalidTypeValue`], which is the combination Section 11.3.1
800/// forbids because an object status message cannot signal end of group.
801///
802/// The padding datagram is why the [`CodecError::InvalidField`] arm exists.
803/// [`PADDING_DATAGRAM_TYPE`] is assigned, so a datagram carrying it is not
804/// unknown; it simply carries no Object, and refusing it must not end the
805/// session.
806fn datagram_type_error(raw: u64) -> CodecError {
807 if raw == PADDING_DATAGRAM_TYPE || datagram_type_is_valid(raw) {
808 CodecError::InvalidField
809 } else if datagram_type_is_status_end_of_group(raw) {
810 CodecError::InvalidTypeValue {
811 raw,
812 detail: "it sets both the STATUS bit and the END_OF_GROUP bit",
813 }
814 } else {
815 CodecError::UnknownDatagramType(raw)
816 }
817}
818
819#[derive(Debug, Clone)]
820pub struct DatagramHeader {
821 pub datagram_type: u8,
822 pub track_alias: VarInt,
823 pub group_id: VarInt,
824 pub object_id: VarInt,
825 pub publisher_priority: Option<u8>,
826 /// Raw properties bytes, excluding the byte-length prefix that precedes
827 /// them on the wire. Present only when `datagram_type` sets the PROPERTIES
828 /// bit (0x01), and empty otherwise — the bit is what puts the block on the
829 /// wire, so contents held here with the bit clear are not written.
830 ///
831 /// Opaque: [`Self::encode`] re-emits the prefix and these bytes verbatim,
832 /// and [`Self::decode`] copies them out the same way, so a datagram can be
833 /// decoded and re-encoded without understanding what its properties mean.
834 /// The block sits between the publisher priority and the status field, so
835 /// leaving it out of the struct would put the status where the decoder
836 /// looks for the properties length.
837 pub properties: Vec<u8>,
838 /// The object's status, carried on the wire only when `datagram_type` sets
839 /// the STATUS bit (0x20): such a datagram holds a one-byte status code in
840 /// place of a payload. `None` with the bit set is written as
841 /// [`ObjectStatus::Normal`]; a status with the bit clear is not written at
842 /// all, because the bit is what puts the field on the wire.
843 ///
844 /// `None` is not "no status": a datagram whose type leaves the STATUS bit
845 /// clear carries a payload, and the status of an Object that carries a
846 /// payload is [`ObjectStatus::Normal`], the only row of the Object Status
847 /// registry (draft-19 Section 15.9) permitting one. [`Self::status`]
848 /// resolves the field either way.
849 ///
850 /// Typed rather than a bare byte. The wire field is one octet with 256
851 /// values, and draft-19 Section 11.2.1.1 assigns three of them; the
852 /// decoder refuses the other 253, and this type is that same refusal on
853 /// the encode side — [`Self::encode`] is infallible precisely because a
854 /// status it could not legally write cannot be built.
855 pub object_status: Option<ObjectStatus>,
856}
857
858impl DatagramHeader {
859 /// Decode a datagram header, Type field included.
860 ///
861 /// A Type spelled in more than one byte is refused first, for the reason
862 /// given on [`SubgroupHeader::decode`].
863 pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
864 if let Some(err) = wide_type_refusal(buf, datagram_type_error)? {
865 return Err(err);
866 }
867 let raw = buf.get_u8() as u64;
868 validate_datagram_type(raw)?;
869 let datagram_type = raw as u8;
870
871 let track_alias = VarInt::decode_moqt::<Wire>(buf)?;
872 let group_id = VarInt::decode_moqt::<Wire>(buf)?;
873
874 let object_id = if datagram_type & DATAGRAM_ZERO_OBJECT_ID_BIT != 0 {
875 VarInt::from_usize(0)
876 } else {
877 VarInt::decode_moqt::<Wire>(buf)?
878 };
879
880 let publisher_priority = if datagram_type & DATAGRAM_DEFAULT_PRIORITY_BIT == 0 {
881 if buf.remaining() < 1 {
882 return Err(CodecError::UnexpectedEnd);
883 }
884 Some(buf.get_u8())
885 } else {
886 None
887 };
888
889 let properties = if datagram_type & DATAGRAM_PROPERTIES_BIT != 0 {
890 let props_len = VarInt::decode_moqt::<Wire>(buf)?.into_inner() as usize;
891 crate::types::read_bytes(buf, props_len)?
892 } else {
893 Vec::new()
894 };
895
896 let object_status = if datagram_type & DATAGRAM_STATUS_BIT != 0 {
897 if buf.remaining() < 1 {
898 return Err(CodecError::UnexpectedEnd);
899 }
900 let status = buf.get_u8();
901 Some(decoded_status(status as u64)?)
902 } else {
903 None
904 };
905
906 // Two rules of draft-19 Section 11.3.1 reach the properties block just
907 // read, and neither is applied here — [`Self::properties_permitted`]
908 // and [`Self::properties_block_well_formed`] report them instead, and
909 // [`Self::encode_checked`] refuses to write either shape:
910 //
911 // - "If an endpoint receives a datagram with the PROPERTIES bit set
912 // and an Properties Length of 0, it MUST close the session with a
913 // PROTOCOL_VIOLATION."
914 // - "If an Object Datagram includes both the STATUS bit and
915 // PROPERTIES bit, and the Object Status is not Normal (0x0), the
916 // endpoint MUST close the session with a PROTOCOL_VIOLATION,
917 // because only Normal Objects can have Properties."
918 //
919 // Both describe a datagram that is well framed and non-conforming: the
920 // fields are all where the layout puts them and every one of them
921 // parses, so a decoder can read the datagram back exactly as it
922 // arrived. Refusing here would leave this module unable to reproduce a
923 // capture containing one, and both rules address an endpoint receiving
924 // such a datagram, so the endpoint is where they are enforced — the
925 // same division [`SubgroupObject::properties_permitted`] explains.
926 //
927 // The Type rules above are the contrast, and the contrast is what
928 // decides it: an invalid Type names no layout at all, so reading on
929 // invents the fields behind it rather than reporting them.
930
931 Ok(DatagramHeader {
932 datagram_type,
933 track_alias,
934 group_id,
935 object_id,
936 publisher_priority,
937 properties,
938 object_status,
939 })
940 }
941
942 /// Decode one whole datagram: the header, then the payload that runs to the
943 /// end of `buf`.
944 ///
945 /// `buf` must hold exactly one transport datagram and nothing else, since
946 /// that boundary is the only thing that delimits the payload — draft-19
947 /// Section 11.3.1: "There is no explicit length field for the Object
948 /// Payload; the entirety of the transport datagram following the Object
949 /// header contains the payload."
950 ///
951 /// Which is why the refusal lives here and not in [`Self::decode`]. A
952 /// datagram whose type sets the STATUS bit has no payload at all — the same
953 /// section: "When set to 1, the Object Status field is present and there is
954 /// no Object Payload" — so trailing bytes after its status are not a short
955 /// payload or an odd one, they are bytes the frame does not define. A
956 /// decoder that stops at the header cannot see them, and a caller that
957 /// treats whatever is left as the payload hands the application content the
958 /// publisher never framed as content. That is the case this refuses, and it
959 /// bites hardest on a status datagram carrying the Normal code 0x0, whose
960 /// status alone would report a payload as permitted.
961 ///
962 /// The same refusal covers a status the registry forbids a payload to, per
963 /// Section 11.2.1.1 and the "Payload" column of Section 15.9.
964 ///
965 /// Errors with [`CodecError::PayloadNotPermitted`] when bytes remain and
966 /// the header forbids them, naming which of the two rules refused them.
967 pub fn decode_object(buf: &mut impl Buf) -> Result<(Self, Vec<u8>), CodecError> {
968 let header = Self::decode(buf)?;
969 let payload = crate::types::read_bytes(buf, buf.remaining())?;
970 if !payload.is_empty() && !header.permits_payload() {
971 return Err(CodecError::PayloadNotPermitted {
972 status: header.status().as_u64(),
973 len: payload.len(),
974 detail: if header.has_status() {
975 "its type states a status in place of a payload"
976 } else {
977 "its status is registered as forbidding one"
978 },
979 });
980 }
981 Ok((header, payload))
982 }
983
984 /// Serialize the header, refusing a status the framing cannot carry.
985 ///
986 /// A datagram states a status only when its type byte sets the STATUS bit
987 /// (0x20). With the bit clear there is no status field on the wire, so an
988 /// `object_status` of anything but [`ObjectStatus::Normal`] has nowhere to
989 /// go: [`Self::encode`] drops it, and the datagram parses back as an
990 /// ordinary payload object. An End of Group marker written that way does
991 /// not arrive late or malformed — it does not arrive at all, and the
992 /// receiver sees a normal object in its place.
993 ///
994 /// [`ObjectStatus::Normal`] with the bit clear is not that case and is
995 /// accepted. It is the status the encoding elides for every object that
996 /// carries a payload, so stating it asks for exactly the bytes leaving it
997 /// out asks for, and nothing is lost.
998 ///
999 /// Errors with [`CodecError::InvalidField`] on the lossy combination,
1000 /// before any byte is written, so a refused header leaves `buf` untouched.
1001 /// This is the datagram half of the rule
1002 /// [`SubgroupObjectReader::write_object`] applies on a subgroup stream.
1003 ///
1004 /// Also errors with [`CodecError::InvalidField`] for a Type value draft-19
1005 /// Section 11.3.1 lists as invalid, and for the same reason: a datagram
1006 /// written with one could not be read back by [`Self::decode`], and a
1007 /// codec whose two halves disagree about which datagrams exist cannot be
1008 /// used to rewrite captured traffic.
1009 pub fn encode_checked(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
1010 validate_datagram_type(self.datagram_type as u64)?;
1011 if !self.has_status() && matches!(self.object_status, Some(s) if s != ObjectStatus::Normal)
1012 {
1013 return Err(CodecError::InvalidField);
1014 }
1015 // The two properties rules of Section 11.3.1. [`Self::decode`] reports
1016 // both rather than refusing them, because the datagrams they describe
1017 // are well framed and a codec that could not read one could not
1018 // reproduce a capture containing it. Writing one is the other
1019 // direction and has no such excuse: a conforming peer answers either
1020 // with a PROTOCOL_VIOLATION, so emitting one costs the session and not
1021 // merely the datagram.
1022 if !self.properties_block_well_formed() || !self.properties_permitted() {
1023 return Err(CodecError::InvalidField);
1024 }
1025 self.encode(buf);
1026 Ok(())
1027 }
1028
1029 /// Serialize the header exactly as its type byte describes it.
1030 ///
1031 /// Every field the type byte announces is written, in the order
1032 /// [`Self::decode`] reads them, so the bytes this produces always parse
1033 /// back. The properties block in particular has to be written here: it
1034 /// sits ahead of the status field, and a datagram that skipped it would
1035 /// offer the status byte where the decoder reads the block's length.
1036 ///
1037 /// The type byte is taken as the authority on framing, which is what makes
1038 /// this infallible — and what makes it lossy when the struct disagrees with
1039 /// itself. An `object_status` set while the type byte leaves the STATUS bit
1040 /// clear is discarded here without a word, and a Type value draft-19
1041 /// forbids is written out as readily as one it assigns, including Types
1042 /// this module's own [`Self::decode`] refuses. Prefer
1043 /// [`Self::encode_checked`], which refuses both instead of resolving them.
1044 pub fn encode(&self, buf: &mut impl BufMut) {
1045 buf.put_u8(self.datagram_type);
1046 self.track_alias.encode_moqt::<Wire>(buf);
1047 self.group_id.encode_moqt::<Wire>(buf);
1048
1049 if self.datagram_type & DATAGRAM_ZERO_OBJECT_ID_BIT == 0 {
1050 self.object_id.encode_moqt::<Wire>(buf);
1051 }
1052
1053 if self.datagram_type & DATAGRAM_DEFAULT_PRIORITY_BIT == 0 {
1054 buf.put_u8(self.publisher_priority.unwrap_or(128));
1055 }
1056
1057 if self.datagram_type & DATAGRAM_PROPERTIES_BIT != 0 {
1058 VarInt::from_usize(self.properties.len()).encode_moqt::<Wire>(buf);
1059 buf.put_slice(&self.properties);
1060 }
1061
1062 if self.datagram_type & DATAGRAM_STATUS_BIT != 0 {
1063 buf.put_u8(self.object_status.unwrap_or(ObjectStatus::Normal).as_u8());
1064 }
1065 }
1066
1067 pub fn is_end_of_group(&self) -> bool {
1068 self.datagram_type & DATAGRAM_END_OF_GROUP_BIT != 0
1069 }
1070
1071 pub fn has_status(&self) -> bool {
1072 self.datagram_type & DATAGRAM_STATUS_BIT != 0
1073 }
1074
1075 /// `true` when the type byte sets the PROPERTIES bit (0x01), which is what
1076 /// puts the properties block on the wire.
1077 ///
1078 /// Reports the framing, not the contents. A decoded datagram with this set
1079 /// always has a non-empty [`Self::properties`], because [`Self::decode`]
1080 /// refuses a zero-length block; a header built by hand can hold the two
1081 /// apart, and [`Self::encode_checked`] is what refuses that.
1082 pub fn has_properties(&self) -> bool {
1083 self.datagram_type & DATAGRAM_PROPERTIES_BIT != 0
1084 }
1085
1086 /// The datagram's object status, with the one the encoding elides filled
1087 /// in.
1088 ///
1089 /// A datagram states a status only when its type sets the STATUS bit, and
1090 /// such a datagram has no payload. One without the bit is all payload, and
1091 /// its status is [`ObjectStatus::Normal`] — the sole row of the Object
1092 /// Status registry (draft-19 Section 15.9) permitting a payload, so the
1093 /// only status it could have had.
1094 pub fn status(&self) -> ObjectStatus {
1095 self.object_status.unwrap_or(ObjectStatus::Normal)
1096 }
1097
1098 /// Whether the bytes after this datagram's header are allowed to exist.
1099 ///
1100 /// Two independent rules forbid them, and this reports both:
1101 ///
1102 /// - The framing. Draft-19 Section 11.3.1: "The STATUS bit (0x20)
1103 /// indicates whether the datagram contains an Object Status or Object
1104 /// Payload. When set to 1, the Object Status field is present and there
1105 /// is no Object Payload." A datagram that states a status has no payload
1106 /// field at all, whichever status it states — so a STATUS datagram
1107 /// carrying the Normal code 0x0 has no more room for bytes than one
1108 /// carrying End of Group.
1109 /// - The status. Section 11.2.1.1 and the Object Status registry's
1110 /// "Payload" column, Section 15.9: an Object has an empty payload unless
1111 /// its status is registered as permitting one. This half reaches a
1112 /// datagram whose type byte leaves the STATUS bit clear while the value
1113 /// claims a status that forbids a payload — a disagreement
1114 /// [`Self::encode_checked`] refuses to write, and one a decoded header
1115 /// never shows.
1116 ///
1117 /// The first is the rule a decoded datagram can actually trip, and reading
1118 /// the registry alone misses it: `Some(ObjectStatus::Normal)` under a type
1119 /// byte with the STATUS bit set is exactly the case where the payload the
1120 /// draft says does not exist would otherwise be handed to the application
1121 /// as the object's content, because Normal is the one status the registry
1122 /// marks as permitting a payload.
1123 ///
1124 /// Distinct from [`Self::has_status`], which reports how the datagram is
1125 /// framed rather than whether a payload may follow. A caller holding the
1126 /// bytes after the header wants this one; [`Self::decode_object`] applies
1127 /// it for a caller who would rather the decode simply fail.
1128 pub fn permits_payload(&self) -> bool {
1129 if self.has_status() {
1130 return false;
1131 }
1132 self.status().permits_payload()
1133 }
1134
1135 /// Whether this datagram's status is allowed to carry the properties it
1136 /// has.
1137 ///
1138 /// The same rule the subgroup form obeys. Draft-19 Section 11.3.1 builds
1139 /// the datagram's Properties field out of "the Object Properties structure
1140 /// defined in Section 11.2.1.2", and that section is where the rule sits:
1141 /// "If an endpoint receives properties on an Object with status that is not
1142 /// Normal, it MUST close the session with a PROTOCOL_VIOLATION."
1143 ///
1144 /// See [`SubgroupObject::properties_permitted`] for why the decoder reports
1145 /// this instead of refusing it.
1146 pub fn properties_permitted(&self) -> bool {
1147 self.properties.is_empty() || self.status() == ObjectStatus::Normal
1148 }
1149
1150 /// Whether the properties block is framed the way a datagram may frame it.
1151 ///
1152 /// Draft-19 Section 11.3.1: "If an endpoint receives a datagram with the
1153 /// PROPERTIES bit set and an Properties Length of 0, it MUST close the
1154 /// session with a PROTOCOL_VIOLATION."
1155 ///
1156 /// The bit and a zero length are two ways to spell "no properties", and on
1157 /// a datagram they are not interchangeable: a datagram with none has a type
1158 /// byte that says so, and the block costs bytes the type byte already
1159 /// saved. This rule is the datagram's alone. A subgroup stream says the
1160 /// opposite in Section 11.4.2 — "Objects with no properties set Properties
1161 /// Length to 0" — because there the PROPERTIES bit is fixed for the whole
1162 /// stream, so an object with no properties has nowhere else to say so and a
1163 /// zero-length block is the required spelling rather than a violation.
1164 ///
1165 /// The mirror case is not a wire state but is a state this struct can hold:
1166 /// properties with the bit clear. [`Self::encode`] drops them without a
1167 /// word, so this reports that too, and [`Self::encode_checked`] refuses
1168 /// both.
1169 pub fn properties_block_well_formed(&self) -> bool {
1170 self.has_properties() != self.properties.is_empty()
1171 }
1172}
1173
1174// ── Fetch Header ──────────────────────────────────────────────
1175
1176const FETCH_STREAM_TYPE: u64 = 0x05;
1177
1178#[derive(Debug, Clone)]
1179pub struct FetchHeader {
1180 pub request_id: VarInt,
1181}
1182
1183impl FetchHeader {
1184 pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
1185 let stream_type = VarInt::decode_moqt::<Wire>(buf)?.into_inner();
1186 if stream_type != FETCH_STREAM_TYPE {
1187 return Err(stream_type_error(stream_type));
1188 }
1189 let request_id = VarInt::decode_moqt::<Wire>(buf)?;
1190 Ok(FetchHeader { request_id })
1191 }
1192
1193 pub fn encode(&self, buf: &mut impl BufMut) {
1194 VarInt::from_usize(FETCH_STREAM_TYPE as usize).encode_moqt::<Wire>(buf);
1195 self.request_id.encode_moqt::<Wire>(buf);
1196 }
1197}
1198
1199// ── Fetch objects ─────────────────────────────────────────────
1200
1201/// Serialization Flags bits 0-1, the Subgroup ID encoding
1202/// (draft-19 Section 11.4.4.1, Table 8).
1203const FETCH_SUBGROUP_ID_MODE_MASK: u64 = 0x03;
1204/// Subgroup ID mode 0b11: an explicit Subgroup ID field is on the wire.
1205const FETCH_SUBGROUP_ID_EXPLICIT: u64 = 0b11;
1206/// Table 9 flag: an Object ID Delta field is present.
1207const FETCH_OBJECT_ID_DELTA_BIT: u64 = 0x04;
1208/// Table 9 flag: a Group ID Delta field is present.
1209const FETCH_GROUP_ID_DELTA_BIT: u64 = 0x08;
1210/// Table 9 flag: a Publisher Priority field is present.
1211const FETCH_PRIORITY_BIT: u64 = 0x10;
1212/// Table 9 flag: a Properties field is present.
1213const FETCH_PROPERTIES_BIT: u64 = 0x20;
1214/// Table 9 flag: the Object's Forwarding Preference is Datagram, so it has no
1215/// Subgroup ID and the two low bits are to be ignored.
1216const FETCH_DATAGRAM_BIT: u64 = 0x40;
1217/// The largest Serialization Flags value whose bits are flags. Draft-19
1218/// Section 11.4.4: "When less than 128, the bits represent flags".
1219const FETCH_FLAGS_MAX: u64 = 0x7F;
1220/// Table 7: End of Non-Existent Range.
1221const FETCH_END_OF_NON_EXISTENT_RANGE: u64 = 0x8C;
1222/// Table 7: End of Unknown Range.
1223const FETCH_END_OF_UNKNOWN_RANGE: u64 = 0x10C;
1224
1225/// What an End of Range indicator on a fetch stream asserts about the
1226/// Locations it covers, from draft-19 Section 11.4.4.2.
1227///
1228/// Both kinds say that every Object with a Location between the previous
1229/// serialized Object and this one, inclusive, was not serialized. They differ
1230/// in why: the publisher knows the Objects are not there, or it does not know
1231/// either way. A subscriber can cache the first as a definitive gap and must
1232/// not cache the second, so the two cannot be collapsed.
1233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1234pub enum FetchEndOfRange {
1235 /// Serialization Flags 0x8C. The Objects in the range do not exist.
1236 NonExistent,
1237 /// Serialization Flags 0x10C. The Objects in the range have unknown
1238 /// status.
1239 Unknown,
1240}
1241
1242/// Which optional fields a Serialization Flags value puts on the wire.
1243///
1244/// Derived once by [`FetchObjectHeader::layout`] and then used by both
1245/// [`FetchObjectHeader::decode`] and [`FetchObjectHeader::encode`], so the two
1246/// cannot drift into disagreeing about a shape.
1247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1248struct FetchObjectLayout {
1249 group_id_delta: bool,
1250 subgroup_id: bool,
1251 object_id_delta: bool,
1252 publisher_priority: bool,
1253 properties: bool,
1254}
1255
1256/// One Object on a draft-19 fetch stream, up to but not including its payload.
1257///
1258/// Draft-19 Section 11.4.4 rebuilt the fetch object. Earlier drafts wrote a
1259/// fixed set of fields on every Object; draft-19 writes a Serialization Flags
1260/// varint first, and the flags say which fields follow:
1261///
1262/// ```text
1263/// {
1264/// Serialization Flags (vi64),
1265/// [Group ID Delta (vi64),]
1266/// [Subgroup ID (vi64),]
1267/// [Object ID Delta (vi64),]
1268/// [Publisher Priority (8),]
1269/// [Properties (..),]
1270/// Object Payload Length (vi64),
1271/// [Object Payload (..),]
1272/// }
1273/// ```
1274///
1275/// Every field is optional except the flags and the payload length, and an
1276/// absent field means *the same as the previous Object's*, not "zero" — the
1277/// whole point of the layout is that a run of Objects in one group at one
1278/// priority costs one byte of framing each. This type therefore holds what is
1279/// on the wire and nothing more: the deltas, not the Group and Object IDs they
1280/// resolve to. Resolving them needs the previous Object on the same stream and
1281/// the FETCH's Group Order, neither of which a single Object header knows,
1282/// and Section 11.4.4 spells out the arithmetic a caller must apply:
1283///
1284/// - The first Object MUST carry both deltas, and they are the absolute
1285/// Group ID and Object ID.
1286/// - Later on, a Group ID Delta moves the group by `delta + 1` — forwards
1287/// under Ascending Group Order and backwards under Descending — and
1288/// restarts the Object ID from the Object ID Delta. With no Group ID
1289/// Delta, the group is unchanged and the Object ID Delta is added to the
1290/// previous Object's ID; with no Object ID Delta either, the Object ID is
1291/// the previous one plus one.
1292///
1293/// There is no Object Status field. Draft-19 Section 11.2.1.1 states that
1294/// Object Status is present only on Objects delivered via a subscription and
1295/// absent from Objects delivered via a FETCH, which is why this type has no
1296/// counterpart to [`SubgroupObject::object_status`] and why a zero
1297/// `payload_length` here is simply an Object with no payload.
1298///
1299/// Two Serialization Flags values name an End of Range indicator rather than
1300/// an Object; [`Self::end_of_range`] reports which, and Section 11.4.4.2 gives
1301/// the rules such a frame follows.
1302#[derive(Debug, Clone, PartialEq, Eq)]
1303pub struct FetchObjectHeader {
1304 /// The raw Serialization Flags value, kept whole rather than split into
1305 /// booleans because it is also the field that names an End of Range
1306 /// indicator, and because re-encoding must reproduce the value the
1307 /// publisher chose.
1308 pub serialization_flags: VarInt,
1309 /// Group ID Delta, present when the flags set 0x08. Its meaning depends on
1310 /// the Object's position in the stream and on the Group Order; see the
1311 /// type's own documentation.
1312 pub group_id_delta: Option<VarInt>,
1313 /// An explicit Subgroup ID, present only when the two low flag bits are
1314 /// 0b11 and the Datagram bit is clear. The other three modes derive the
1315 /// Subgroup ID from the previous Object and put nothing on the wire.
1316 pub subgroup_id: Option<VarInt>,
1317 /// Object ID Delta, present when the flags set 0x04. Absent means the
1318 /// previous Object's ID plus one.
1319 pub object_id_delta: Option<VarInt>,
1320 /// Publisher Priority, present when the flags set 0x10. Absent means the
1321 /// previous Object's priority.
1322 pub publisher_priority: Option<u8>,
1323 /// Raw properties bytes, excluding the byte-length prefix that precedes
1324 /// them on the wire, and `None` when the flags leave 0x20 clear.
1325 ///
1326 /// `Some(vec![])` and `None` are different frames: the first writes a zero
1327 /// length prefix, the second writes nothing at all. Opaque, like the
1328 /// property blocks on the subgroup and datagram forms — draft-19
1329 /// Section 11.4.4 defines the field as the Object Properties structure of
1330 /// Section 11.2.1.2, and these bytes are re-emitted verbatim.
1331 pub properties: Option<Vec<u8>>,
1332 /// Object Payload Length. Always on the wire; the payload itself follows
1333 /// this header and is not held here.
1334 pub payload_length: VarInt,
1335}
1336
1337impl FetchObjectHeader {
1338 /// The Serialization Flags as a plain integer.
1339 pub fn flags(&self) -> u64 {
1340 self.serialization_flags.into_inner()
1341 }
1342
1343 /// Which End of Range indicator this is, or `None` for an ordinary Object.
1344 ///
1345 /// Draft-19 Section 11.4.4, Table 7 gives the two indicators their own
1346 /// Serialization Flags values rather than a flag bit, so this is an
1347 /// equality test on the whole field and not a mask.
1348 ///
1349 /// An indicator uses the same two positions on the wire an ordinary Object
1350 /// uses for its deltas, but Section 11.4.4.2 reads them as the absolute
1351 /// Group ID and Object ID of the end of the range. They are still reached
1352 /// through [`Self::group_id_delta`] and [`Self::object_id_delta`], since
1353 /// those are the fields the wire has; what changes is what they mean, and
1354 /// this is how a caller knows which reading applies.
1355 pub fn end_of_range(&self) -> Option<FetchEndOfRange> {
1356 match self.flags() {
1357 FETCH_END_OF_NON_EXISTENT_RANGE => Some(FetchEndOfRange::NonExistent),
1358 FETCH_END_OF_UNKNOWN_RANGE => Some(FetchEndOfRange::Unknown),
1359 _ => None,
1360 }
1361 }
1362
1363 /// The two-bit Subgroup ID mode, `flags & 0x03`.
1364 ///
1365 /// `0b00` = the Subgroup ID is zero; `0b01` = the previous Object's
1366 /// Subgroup ID; `0b10` = the previous Object's Subgroup ID plus one;
1367 /// `0b11` = an explicit field is present. Draft-19 Section 11.4.4.1
1368 /// assigns all four, unlike the subgroup stream header's reserved
1369 /// `0b11`.
1370 ///
1371 /// Meaningless when [`Self::is_datagram`] is true: such an Object has no
1372 /// Subgroup ID and the section says the subscriber MUST ignore these bits.
1373 pub fn subgroup_id_mode(&self) -> u8 {
1374 (self.flags() & FETCH_SUBGROUP_ID_MODE_MASK) as u8
1375 }
1376
1377 /// `true` when the flags set 0x40, marking an Object whose Forwarding
1378 /// Preference is Datagram. Such an Object has no Subgroup ID at all, so
1379 /// the Subgroup ID mode bits carry no meaning and no Subgroup ID field is
1380 /// on the wire whatever they say.
1381 pub fn is_datagram(&self) -> bool {
1382 self.flags() & FETCH_DATAGRAM_BIT != 0
1383 }
1384
1385 /// Which optional fields `flags` puts on the wire, or
1386 /// [`CodecError::InvalidField`] if draft-19 does not define that
1387 /// Serialization Flags value.
1388 ///
1389 /// Section 11.4.4 defines the field in two pieces: values below 128 are a
1390 /// set of flags, and Table 7 adds exactly two values above that, 0x8C and
1391 /// 0x10C. "Any other value is a PROTOCOL_VIOLATION", which is what the
1392 /// error covers — every value with bit 7 set that is not one of the two.
1393 ///
1394 /// The two indicators get their layout from Section 11.4.4.2 rather than
1395 /// from their bits: "the Group ID and Object ID fields are present.
1396 /// Subgroup ID, Priority and Properties are not present." Their low bits
1397 /// happen to spell exactly that (both are `0x0C` in the low seven bits:
1398 /// Group ID Delta and Object ID Delta set, Subgroup ID mode 0b00, no
1399 /// priority, no properties), but that is a property of the two values the
1400 /// draft chose and not a rule, so the layout is taken from the section
1401 /// that states it.
1402 fn layout(flags: u64) -> Result<FetchObjectLayout, CodecError> {
1403 if flags == FETCH_END_OF_NON_EXISTENT_RANGE || flags == FETCH_END_OF_UNKNOWN_RANGE {
1404 return Ok(FetchObjectLayout {
1405 group_id_delta: true,
1406 subgroup_id: false,
1407 object_id_delta: true,
1408 publisher_priority: false,
1409 properties: false,
1410 });
1411 }
1412 if flags > FETCH_FLAGS_MAX {
1413 return Err(CodecError::InvalidField);
1414 }
1415 Ok(FetchObjectLayout {
1416 group_id_delta: flags & FETCH_GROUP_ID_DELTA_BIT != 0,
1417 // An Object with the Datagram bit set has no Subgroup ID to write,
1418 // whatever the mode bits hold, so the field is absent.
1419 subgroup_id: flags & FETCH_DATAGRAM_BIT == 0
1420 && flags & FETCH_SUBGROUP_ID_MODE_MASK == FETCH_SUBGROUP_ID_EXPLICIT,
1421 object_id_delta: flags & FETCH_OBJECT_ID_DELTA_BIT != 0,
1422 publisher_priority: flags & FETCH_PRIORITY_BIT != 0,
1423 properties: flags & FETCH_PROPERTIES_BIT != 0,
1424 })
1425 }
1426
1427 /// Decode one fetch object header, leaving the payload in `buf`.
1428 ///
1429 /// Errors with [`CodecError::InvalidField`] for a Serialization Flags
1430 /// value draft-19 Section 11.4.4 does not define, and with
1431 /// [`CodecError::UnexpectedEnd`] or a varint error when the buffer runs
1432 /// out mid-field.
1433 ///
1434 /// The flags are validated before any field is read, because they are what
1435 /// says where the fields are: decoding an undefined value would mean
1436 /// picking a layout the draft never described and then consuming a
1437 /// plausible number of bytes under it.
1438 pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
1439 let serialization_flags = VarInt::decode_moqt::<Wire>(buf)?;
1440 let layout = Self::layout(serialization_flags.into_inner())?;
1441
1442 let group_id_delta =
1443 layout.group_id_delta.then(|| VarInt::decode_moqt::<Wire>(buf)).transpose()?;
1444 let subgroup_id =
1445 layout.subgroup_id.then(|| VarInt::decode_moqt::<Wire>(buf)).transpose()?;
1446 let object_id_delta =
1447 layout.object_id_delta.then(|| VarInt::decode_moqt::<Wire>(buf)).transpose()?;
1448
1449 let publisher_priority = if layout.publisher_priority {
1450 if buf.remaining() < 1 {
1451 return Err(CodecError::UnexpectedEnd);
1452 }
1453 Some(buf.get_u8())
1454 } else {
1455 None
1456 };
1457
1458 let properties = if layout.properties {
1459 let len = VarInt::decode_moqt::<Wire>(buf)?.into_inner() as usize;
1460 Some(crate::types::read_bytes(buf, len)?)
1461 } else {
1462 None
1463 };
1464
1465 let payload_length = VarInt::decode_moqt::<Wire>(buf)?;
1466
1467 Ok(FetchObjectHeader {
1468 serialization_flags,
1469 group_id_delta,
1470 subgroup_id,
1471 object_id_delta,
1472 publisher_priority,
1473 properties,
1474 payload_length,
1475 })
1476 }
1477
1478 /// Serialize the header, refusing one whose fields disagree with its own
1479 /// Serialization Flags.
1480 ///
1481 /// Errors with [`CodecError::InvalidField`] when the flags are a value
1482 /// draft-19 does not define, and when any optional field is present while
1483 /// its flag is clear or absent while its flag is set. Checked before any
1484 /// byte is written, so a refused header leaves `buf` untouched.
1485 ///
1486 /// Fallible for the same reason [`DatagramHeader::encode_checked`] is: the
1487 /// flags decide the framing, so writing them as the authority and dropping
1488 /// whatever they do not cover is silent data loss. A Group ID Delta held
1489 /// with the 0x08 bit clear is not written, the reader takes the Object as
1490 /// belonging to the previous Object's group, and nothing about the
1491 /// resulting stream looks wrong. The mirror case is worse: a flag set with
1492 /// no value behind it would have to invent one, and an invented Object ID
1493 /// Delta of zero is a real Object ID.
1494 ///
1495 /// Nothing here checks the deltas against the previous Object — that no
1496 /// Object other than the first may reference a prior Object that does not
1497 /// exist, for one. A single header has no way to see that, and this type
1498 /// deliberately does not carry stream state.
1499 pub fn encode(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
1500 let layout = Self::layout(self.flags())?;
1501 if layout.group_id_delta != self.group_id_delta.is_some()
1502 || layout.subgroup_id != self.subgroup_id.is_some()
1503 || layout.object_id_delta != self.object_id_delta.is_some()
1504 || layout.publisher_priority != self.publisher_priority.is_some()
1505 || layout.properties != self.properties.is_some()
1506 {
1507 return Err(CodecError::InvalidField);
1508 }
1509
1510 self.serialization_flags.encode_moqt::<Wire>(buf);
1511 // Wire order, from Figure 27: Group ID Delta, then Subgroup ID, then
1512 // Object ID Delta. The `layout` check above has already established
1513 // that exactly the fields the flags call for are present, so whichever
1514 // of the three are `Some` are the ones that belong here.
1515 for field in
1516 [self.group_id_delta, self.subgroup_id, self.object_id_delta].into_iter().flatten()
1517 {
1518 field.encode_moqt::<Wire>(buf);
1519 }
1520 if let Some(priority) = self.publisher_priority {
1521 buf.put_u8(priority);
1522 }
1523 if let Some(properties) = &self.properties {
1524 VarInt::from_usize(properties.len()).encode_moqt::<Wire>(buf);
1525 buf.put_slice(properties);
1526 }
1527 self.payload_length.encode_moqt::<Wire>(buf);
1528 Ok(())
1529 }
1530}
1531
1532/// The order a FETCH response's Groups arrive in, which decides how a Group ID
1533/// Delta is applied.
1534///
1535/// Draft-19 Section 11.4.4.1: "If the Group Order is Ascending, the Group ID is
1536/// the prior Object's Group ID plus the Group ID Delta + 1. If the Group Order
1537/// is Descending, the Group ID is the prior Object's Group ID minus the (Group
1538/// ID Delta + 1)."
1539///
1540/// The order is not on the data stream — it is settled by the control exchange
1541/// that opened the FETCH, whose GROUP_ORDER parameter (Section 10.2.8) spells
1542/// Ascending 0x1 and Descending 0x2 — so [`FetchObjectReader`] has to be told
1543/// which one it is reading. Getting it wrong does not fail to parse: it decodes
1544/// every Object under a Group ID that walks the wrong way.
1545#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1546pub enum GroupOrder {
1547 /// Group IDs increase: a delta adds to the prior Group ID.
1548 Ascending,
1549 /// Group IDs decrease: a delta subtracts from the prior Group ID.
1550 Descending,
1551}
1552
1553/// One frame from a FETCH stream with its delta-encoded fields resolved.
1554///
1555/// The header is kept alongside the resolved values so that a caller can
1556/// forward the frame's bytes unchanged while acting on what they mean.
1557#[derive(Debug, Clone, PartialEq, Eq)]
1558pub struct FetchObject {
1559 /// The frame as it appeared on the wire.
1560 pub header: FetchObjectHeader,
1561 /// Resolved absolute Group ID. On an End of Range marker, the Group ID of
1562 /// the Location the marker names.
1563 pub group_id: u64,
1564 /// Resolved Subgroup ID. `None` for an End of Range marker, which has
1565 /// none, and for an Object whose forwarding preference is Datagram.
1566 pub subgroup_id: Option<u64>,
1567 /// Resolved absolute Object ID. On an End of Range marker, the Object ID of
1568 /// the Location the marker names.
1569 pub object_id: u64,
1570 /// The Publisher Priority in force for this frame, whether this frame wrote
1571 /// it or an earlier one did, and `None` while no frame has written one.
1572 ///
1573 /// An End of Range marker carries no Priority field of its own, so what it
1574 /// reports is the one still in force from the last Object before it —
1575 /// Section 11.4.4.2: "Prior Priority: The Priority from the last actual
1576 /// Object before the End of Range indicator."
1577 ///
1578 /// The draft-19 fallback for a subscription that never stated a priority is
1579 /// left to the caller rather than substituted here, so that "no frame has
1580 /// said" stays distinguishable from "a frame said 128".
1581 pub publisher_priority: Option<u8>,
1582}
1583
1584/// Resolves the delta-encoded fields of the frames on one FETCH stream.
1585///
1586/// Draft-19 Section 11.4.4.1 defines nearly every field of a fetch frame
1587/// against "the prior Object", so no frame after the first can be understood on
1588/// its own. This holds what the frames so far established, in the two parts the
1589/// draft keeps separate: Section 11.4.4.2 says that after an End of Range
1590/// marker the prior Group ID and Object ID are the marker's, while the prior
1591/// Subgroup ID and Priority are still "from the last actual Object before the
1592/// End of Range indicator".
1593///
1594/// Every rule the section answers with a PROTOCOL_VIOLATION is refused here
1595/// with [`CodecError::InvalidField`]: a first Object that references fields no
1596/// prior Object established, a Subgroup ID or Priority inherited when there is
1597/// none to inherit, and an arithmetic result outside the 64-bit range.
1598#[derive(Debug, Clone)]
1599pub struct FetchObjectReader {
1600 group_order: GroupOrder,
1601 /// Group ID and Object ID of the last frame, marker or Object.
1602 prior_location: Option<(u64, u64)>,
1603 /// Subgroup ID of the last actual Object that had one.
1604 prior_subgroup_id: Option<u64>,
1605 /// Publisher Priority of the last actual Object.
1606 prior_publisher_priority: Option<u8>,
1607}
1608
1609impl FetchObjectReader {
1610 /// A reader for a stream whose Groups arrive in `group_order`.
1611 pub fn new(group_order: GroupOrder) -> Self {
1612 Self {
1613 group_order,
1614 prior_location: None,
1615 prior_subgroup_id: None,
1616 prior_publisher_priority: None,
1617 }
1618 }
1619
1620 /// Decode the next frame's header and resolve its fields.
1621 ///
1622 /// Consumes the header only. The Object Payload is
1623 /// `header.payload_length` bytes and stays in `buf`, so a caller that
1624 /// forwards payloads never copies them and one that ignores them can skip.
1625 ///
1626 /// Errors with [`CodecError::InvalidField`] on every rule
1627 /// Section 11.4.4.1 states:
1628 ///
1629 /// - "The first Object MUST include a Group ID Delta and Object ID Delta,
1630 /// and these values are the absolute Group ID and Object ID. If the first
1631 /// Object in the FETCH response uses a flag that references fields in the
1632 /// prior Object, the Subscriber MUST close the session with a
1633 /// PROTOCOL_VIOLATION." Each such flag is refused where it is read, so
1634 /// the reason survives: a missing delta, an inherited Priority and an
1635 /// inherited Subgroup ID are three different frames, all of them
1636 /// referencing an Object that does not exist.
1637 /// - "If the computed Group ID would be less than 0 or greater than
1638 /// 2^64-1, the Subscriber MUST close the Session with error
1639 /// 'PROTOCOL_VIOLATION'" — the descending and ascending ends of the same
1640 /// rule.
1641 /// - "If the computed Object ID would be greater than 2^64-1, the
1642 /// Subscriber MUST close the Session with error 'PROTOCOL_VIOLATION'."
1643 pub fn read_object_header(&mut self, buf: &mut impl Buf) -> Result<FetchObject, CodecError> {
1644 let header = FetchObjectHeader::decode(buf)?;
1645
1646 // An End of Range marker states a Location outright and inherits
1647 // nothing, so it is resolved before any of the prior-Object rules.
1648 // Section 11.4.4.2 reads its two delta fields as the absolute Group ID
1649 // and Object ID of the end of the range, so nothing is added to them.
1650 if header.end_of_range().is_some() {
1651 let group_id = header.group_id_delta.ok_or(CodecError::InvalidField)?.into_inner();
1652 let object_id = header.object_id_delta.ok_or(CodecError::InvalidField)?.into_inner();
1653 self.prior_location = Some((group_id, object_id));
1654 let publisher_priority = self.prior_publisher_priority;
1655 return Ok(FetchObject {
1656 header,
1657 group_id,
1658 subgroup_id: None,
1659 object_id,
1660 publisher_priority,
1661 });
1662 }
1663
1664 let group_id = match (self.prior_location, header.group_id_delta) {
1665 // The first object's delta is its absolute Group ID.
1666 (None, Some(delta)) => delta.into_inner(),
1667 (None, None) => return Err(CodecError::InvalidField),
1668 (Some((prior_group, _)), None) => prior_group,
1669 (Some((prior_group, _)), Some(delta)) => {
1670 let delta = delta.into_inner();
1671 match self.group_order {
1672 GroupOrder::Ascending => prior_group
1673 .checked_add(delta)
1674 .and_then(|v| v.checked_add(1))
1675 .ok_or(CodecError::InvalidField)?,
1676 GroupOrder::Descending => prior_group
1677 .checked_sub(delta)
1678 .and_then(|v| v.checked_sub(1))
1679 .ok_or(CodecError::InvalidField)?,
1680 }
1681 }
1682 };
1683
1684 let object_id =
1685 match (self.prior_location, header.group_id_delta.is_some(), header.object_id_delta) {
1686 // A Group ID Delta restarts the Object ID from its own delta,
1687 // which is why a new group does not continue the previous
1688 // group's numbering.
1689 (_, true, Some(delta)) => delta.into_inner(),
1690 (Some((_, prior_object)), false, Some(delta)) => {
1691 prior_object.checked_add(delta.into_inner()).ok_or(CodecError::InvalidField)?
1692 }
1693 (Some((_, prior_object)), _, None) => {
1694 prior_object.checked_add(1).ok_or(CodecError::InvalidField)?
1695 }
1696 (None, _, _) => return Err(CodecError::InvalidField),
1697 };
1698
1699 let subgroup_id = if header.is_datagram() {
1700 None
1701 } else {
1702 Some(match header.subgroup_id_mode() {
1703 0x00 => 0,
1704 0x01 => self.prior_subgroup_id.ok_or(CodecError::InvalidField)?,
1705 0x02 => self
1706 .prior_subgroup_id
1707 .ok_or(CodecError::InvalidField)?
1708 .checked_add(1)
1709 .ok_or(CodecError::InvalidField)?,
1710 // Mode 0b11, the only value left: the field is on the wire.
1711 _ => header.subgroup_id.ok_or(CodecError::InvalidField)?.into_inner(),
1712 })
1713 };
1714
1715 let publisher_priority = match header.publisher_priority {
1716 Some(p) => p,
1717 None => self.prior_publisher_priority.ok_or(CodecError::InvalidField)?,
1718 };
1719
1720 self.prior_location = Some((group_id, object_id));
1721 // A Datagram-forwarded object has no Subgroup ID to leave behind, so it
1722 // does not clear the running one: the object after it inherits from the
1723 // last object that had one.
1724 if let Some(subgroup_id) = subgroup_id {
1725 self.prior_subgroup_id = Some(subgroup_id);
1726 }
1727 self.prior_publisher_priority = Some(publisher_priority);
1728
1729 Ok(FetchObject {
1730 header,
1731 group_id,
1732 subgroup_id,
1733 object_id,
1734 publisher_priority: Some(publisher_priority),
1735 })
1736 }
1737}
1738
1739/// Re-encodes resolved fetch frames onto one FETCH stream.
1740///
1741/// The exact inverse of [`FetchObjectReader`], and it exists for one caller:
1742/// something that has read a stream and is writing a different stream from the
1743/// same frames. Removing a frame changes what the frames after it are encoded
1744/// *against*, and draft-19 Section 11.4.4.1 defines nearly every field against
1745/// "the prior Object", so the survivor that follows a removed run cannot keep
1746/// its original bytes. What has to change is not one field: an Object that
1747/// carried no Group ID Delta because it shared its predecessor's group needs
1748/// one once that predecessor is gone, so a field appears and a flag bit with
1749/// it.
1750///
1751/// # Why this is not a general encoder
1752///
1753/// Every frame it writes came off a stream, so the caller already holds the
1754/// frame's own [`FetchObjectHeader`] alongside the resolved values. That header
1755/// is used as the preference: wherever the original shape still encodes the
1756/// same meaning against the new predecessor, it is kept, so a stream with
1757/// nothing removed from it is reproduced byte for byte. Only where the original
1758/// shape would now decode to something else is a different one chosen. An
1759/// encoder built from the resolved values alone could not do that — it would
1760/// have to invent a canonical form and would rewrite every frame on a stream
1761/// that needed no rewriting at all.
1762///
1763/// # What it refuses
1764///
1765/// [`CodecError::InvalidField`] where no encoding exists rather than picking
1766/// one: a Group ID that moves against the FETCH's Group Order, an Object ID
1767/// that does not advance, an Object with neither a Subgroup ID nor the Datagram
1768/// bit, and the arithmetic overflows. Each of these is a frame this writer was
1769/// handed that no draft-19 stream could carry, and inventing a value for it
1770/// would put a different Object on the wire than the one it was given.
1771#[derive(Debug, Clone)]
1772pub struct FetchObjectWriter {
1773 group_order: GroupOrder,
1774 /// Group ID and Object ID of the last frame written, marker or Object.
1775 prior_location: Option<(u64, u64)>,
1776 /// Subgroup ID of the last actual Object written that had one.
1777 prior_subgroup_id: Option<u64>,
1778 /// Publisher Priority of the last actual Object written.
1779 prior_publisher_priority: Option<u8>,
1780}
1781
1782impl FetchObjectWriter {
1783 /// A writer for a stream whose Groups are being written in `group_order`.
1784 ///
1785 /// The order has to match the one the FETCH was opened with, for the same
1786 /// reason [`FetchObjectReader::new`] takes it: it decides whether a Group
1787 /// ID Delta adds or subtracts, and it is not on the data stream.
1788 pub fn new(group_order: GroupOrder) -> Self {
1789 Self {
1790 group_order,
1791 prior_location: None,
1792 prior_subgroup_id: None,
1793 prior_publisher_priority: None,
1794 }
1795 }
1796
1797 /// The header that encodes `frame` against everything written so far.
1798 ///
1799 /// Does not advance the writer — [`Self::write_object_header`] is the call
1800 /// that does both. Separated so that a caller can measure the bytes a
1801 /// re-encode would take before committing to it.
1802 ///
1803 /// # Errors
1804 ///
1805 /// [`CodecError::InvalidField`] for a frame that cannot be encoded against
1806 /// the current predecessor; see the type's own documentation for the list.
1807 pub fn header_for(&self, frame: &FetchObject) -> Result<FetchObjectHeader, CodecError> {
1808 let original = &frame.header;
1809
1810 // An End of Range marker states its Location outright and inherits
1811 // nothing, so its two fields are the same whatever precedes it and the
1812 // frame is reproduced as it arrived.
1813 if original.end_of_range().is_some() {
1814 return Ok(FetchObjectHeader {
1815 serialization_flags: original.serialization_flags,
1816 group_id_delta: Some(VarInt::from_u64(frame.group_id)?),
1817 subgroup_id: None,
1818 object_id_delta: Some(VarInt::from_u64(frame.object_id)?),
1819 publisher_priority: None,
1820 properties: None,
1821 payload_length: original.payload_length,
1822 });
1823 }
1824
1825 let (group_id_delta, object_id_delta) = self.identity_fields(frame, original)?;
1826 let (subgroup_mode, subgroup_id) = self.subgroup_field(frame, original)?;
1827 let publisher_priority = self.priority_field(frame, original)?;
1828
1829 let mut flags = subgroup_mode;
1830 if original.is_datagram() {
1831 flags |= FETCH_DATAGRAM_BIT;
1832 }
1833 if group_id_delta.is_some() {
1834 flags |= FETCH_GROUP_ID_DELTA_BIT;
1835 }
1836 if object_id_delta.is_some() {
1837 flags |= FETCH_OBJECT_ID_DELTA_BIT;
1838 }
1839 if publisher_priority.is_some() {
1840 flags |= FETCH_PRIORITY_BIT;
1841 }
1842 if original.properties.is_some() {
1843 flags |= FETCH_PROPERTIES_BIT;
1844 }
1845
1846 Ok(FetchObjectHeader {
1847 serialization_flags: VarInt::from_u64(flags)?,
1848 group_id_delta,
1849 subgroup_id,
1850 object_id_delta,
1851 publisher_priority,
1852 properties: original.properties.clone(),
1853 payload_length: original.payload_length,
1854 })
1855 }
1856
1857 /// The Group ID Delta and Object ID Delta fields, as this predecessor needs
1858 /// them.
1859 ///
1860 /// Presence is forced by the frame rather than chosen: a group that differs
1861 /// from the predecessor's has to be stated, and one that matches has to be
1862 /// left off, since a delta of zero means the next group along and not this
1863 /// one. Only the Object ID Delta has a choice to make, and it is made in
1864 /// favour of the shape the frame arrived in.
1865 fn identity_fields(
1866 &self,
1867 frame: &FetchObject,
1868 original: &FetchObjectHeader,
1869 ) -> Result<(Option<VarInt>, Option<VarInt>), CodecError> {
1870 let Some((prior_group, prior_object)) = self.prior_location else {
1871 // Section 11.4.4.1: "The first Object MUST include a Group ID Delta
1872 // and Object ID Delta, and these values are the absolute Group ID
1873 // and Object ID."
1874 return Ok((
1875 Some(VarInt::from_u64(frame.group_id)?),
1876 Some(VarInt::from_u64(frame.object_id)?),
1877 ));
1878 };
1879
1880 if frame.group_id != prior_group {
1881 // A Group ID Delta moves the group by delta + 1, forwards under
1882 // Ascending and backwards under Descending, and restarts the Object
1883 // ID from its own delta. The restart is why the Object ID Delta is
1884 // always written here: without it the Object ID would be read as
1885 // the predecessor's plus one, which is the previous group's
1886 // numbering carried into the new one.
1887 let step = match self.group_order {
1888 GroupOrder::Ascending => frame.group_id.checked_sub(prior_group),
1889 GroupOrder::Descending => prior_group.checked_sub(frame.group_id),
1890 };
1891 let delta = step.and_then(|s| s.checked_sub(1)).ok_or(CodecError::InvalidField)?;
1892 return Ok((Some(VarInt::from_u64(delta)?), Some(VarInt::from_u64(frame.object_id)?)));
1893 }
1894
1895 // Same group. The Object ID is the predecessor's plus the delta, or
1896 // plus one when no delta is written, so an Object that does not advance
1897 // has no encoding at all.
1898 let advance = frame.object_id.checked_sub(prior_object).ok_or(CodecError::InvalidField)?;
1899 if advance == 0 {
1900 return Err(CodecError::InvalidField);
1901 }
1902 if advance == 1 && original.object_id_delta.is_none() {
1903 return Ok((None, None));
1904 }
1905 Ok((None, Some(VarInt::from_u64(advance)?)))
1906 }
1907
1908 /// The Subgroup ID mode bits and the explicit field, if one is needed.
1909 ///
1910 /// The frame's own mode is tried first, so a run of Objects that inherited
1911 /// their Subgroup ID keeps inheriting it and its bytes do not move. Only
1912 /// when the predecessor changed under it does a different mode get chosen,
1913 /// and then the cheapest one that says the right number.
1914 fn subgroup_field(
1915 &self,
1916 frame: &FetchObject,
1917 original: &FetchObjectHeader,
1918 ) -> Result<(u64, Option<VarInt>), CodecError> {
1919 // Section 11.4.4.1 has the subscriber ignore these bits on a
1920 // Datagram-forwarded Object, and no field is on the wire whatever they
1921 // say, so the frame's own bits are carried across untouched.
1922 if original.is_datagram() {
1923 return Ok((original.flags() & FETCH_SUBGROUP_ID_MODE_MASK, None));
1924 }
1925
1926 let subgroup_id = frame.subgroup_id.ok_or(CodecError::InvalidField)?;
1927 let inherits = self.prior_subgroup_id == Some(subgroup_id);
1928 let successor =
1929 self.prior_subgroup_id.is_some_and(|p| p.checked_add(1) == Some(subgroup_id));
1930
1931 // The frame's own mode, kept when it still names this number.
1932 let kept = match original.flags() & FETCH_SUBGROUP_ID_MODE_MASK {
1933 0x00 if subgroup_id == 0 => Some((0x00, None)),
1934 0x01 if inherits => Some((0x01, None)),
1935 0x02 if successor => Some((0x02, None)),
1936 FETCH_SUBGROUP_ID_EXPLICIT => Some((FETCH_SUBGROUP_ID_EXPLICIT, Some(subgroup_id))),
1937 _ => None,
1938 };
1939 let (mode, explicit) = match kept {
1940 Some(pair) => pair,
1941 None if subgroup_id == 0 => (0x00, None),
1942 None if inherits => (0x01, None),
1943 None if successor => (0x02, None),
1944 None => (FETCH_SUBGROUP_ID_EXPLICIT, Some(subgroup_id)),
1945 };
1946 Ok((mode, explicit.map(VarInt::from_u64).transpose()?))
1947 }
1948
1949 /// The Publisher Priority field, or `None` when the predecessor already
1950 /// carries it.
1951 ///
1952 /// Written whenever the frame wrote one, so a publisher that stated a
1953 /// priority on every Object keeps its bytes, and written anyway when the
1954 /// predecessor's differs or when there is no predecessor to inherit from.
1955 fn priority_field(
1956 &self,
1957 frame: &FetchObject,
1958 original: &FetchObjectHeader,
1959 ) -> Result<Option<u8>, CodecError> {
1960 let priority = frame.publisher_priority.ok_or(CodecError::InvalidField)?;
1961 if original.publisher_priority.is_some() || self.prior_publisher_priority != Some(priority)
1962 {
1963 return Ok(Some(priority));
1964 }
1965 Ok(None)
1966 }
1967
1968 /// Encode `frame` against everything written so far and advance.
1969 ///
1970 /// Writes the header only. The payload is `frame.header.payload_length`
1971 /// bytes and is the caller's to copy, unchanged — nothing about it depends
1972 /// on what preceded the Object.
1973 ///
1974 /// # Errors
1975 ///
1976 /// [`CodecError::InvalidField`] for a frame with no encoding against the
1977 /// current predecessor. The writer is left untouched when this happens, so
1978 /// a caller that gives up on one frame and carries on with the next is
1979 /// writing against the same predecessor it thought it was.
1980 pub fn write_object_header(
1981 &mut self,
1982 frame: &FetchObject,
1983 out: &mut impl BufMut,
1984 ) -> Result<FetchObjectHeader, CodecError> {
1985 let header = self.header_for(frame)?;
1986 header.encode(out)?;
1987 self.advance(frame);
1988 Ok(header)
1989 }
1990
1991 /// Record `frame` as the predecessor of whatever is written next.
1992 ///
1993 /// Split from the write so that a caller re-emitting bytes it already holds
1994 /// can advance without producing a header twice — which is what happens
1995 /// whenever the framing a frame arrived in still encodes the same meaning
1996 /// against the frame before it, and is why this is public.
1997 pub fn advance(&mut self, frame: &FetchObject) {
1998 self.prior_location = Some((frame.group_id, frame.object_id));
1999 // Mirrors the reader: a Datagram-forwarded Object leaves no Subgroup ID
2000 // behind, so the running one survives it.
2001 if let Some(subgroup_id) = frame.subgroup_id {
2002 self.prior_subgroup_id = Some(subgroup_id);
2003 }
2004 if frame.header.end_of_range().is_none() {
2005 if let Some(priority) = frame.publisher_priority {
2006 self.prior_publisher_priority = Some(priority);
2007 }
2008 }
2009 }
2010}
2011
2012#[cfg(test)]
2013mod tests {
2014 use super::*;
2015
2016 /// Canonically encoded subgroup stream vectors from
2017 /// `test-vectors/transport/draft19/codec/data-streams/subgroup.json`.
2018 /// `subgroup-explicit-subgroup-id` is omitted: it encodes group_id 100 as a
2019 /// two-byte varint, which does not survive a minimal-width re-encode.
2020 const VECTORS: &[&str] = &[
2021 // subgroup-single-object
2022 "100100800004deadbeef",
2023 // subgroup-two-objects
2024 "100100800004deadbeef0002cafe",
2025 // subgroup-no-priority
2026 "3001000004deadbeef",
2027 // subgroup-with-extensions
2028 "11010080000004deadbeef",
2029 // subgroup-end-of-group
2030 "180105800004deadbeef",
2031 // subgroup-id-mode-01
2032 "120100800504deadbeef",
2033 // subgroup-with-object-properties
2034 "1101008000043c02020104deadbeef",
2035 // subgroup-object-status-end-of-group
2036 "100100800004deadbeef000003",
2037 // subgroup-object-status-end-of-track
2038 "10010080000004",
2039 // subgroup-properties-two-objects-empty
2040 "11010080000004deadbeef000002cafe",
2041 // subgroup-properties-two-objects-nonempty
2042 "1101008000023c0204deadbeef00023c0302cafe",
2043 // subgroup-properties-status-object
2044 "1101008000023c010003",
2045 // subgroup-first-object-bit
2046 "500100800004deadbeef",
2047 // subgroup-first-object-and-end-of-group
2048 "580102800002cafe",
2049 ];
2050
2051 fn vi(v: u64) -> VarInt {
2052 VarInt::from_u64_moqt(v)
2053 }
2054
2055 fn hex(s: &str) -> Vec<u8> {
2056 (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
2057 }
2058
2059 /// Decode a whole subgroup stream: the header, then every object up to
2060 /// the end of the buffer.
2061 fn decode_all(bytes: &[u8]) -> (SubgroupHeader, Vec<SubgroupObject>) {
2062 let mut cursor = bytes;
2063 let header = SubgroupHeader::decode(&mut cursor)
2064 .unwrap_or_else(|e| panic!("header decode failed: {e:?}"));
2065 let mut reader = SubgroupObjectReader::new(&header);
2066 let mut objects = Vec::new();
2067 while cursor.has_remaining() {
2068 objects.push(
2069 reader
2070 .read_object(&mut cursor)
2071 .unwrap_or_else(|e| panic!("object {} decode failed: {e:?}", objects.len())),
2072 );
2073 }
2074 (header, objects)
2075 }
2076
2077 fn encode_all(header: &SubgroupHeader, objects: &[SubgroupObject]) -> Vec<u8> {
2078 let mut buf = Vec::new();
2079 header.encode(&mut buf);
2080 let mut writer = SubgroupObjectReader::new(header);
2081 for o in objects {
2082 writer.write_object(o, &mut buf).unwrap_or_else(|e| panic!("write failed: {e:?}"));
2083 }
2084 buf
2085 }
2086
2087 fn object(id: u64, extensions: Vec<u8>, payload: Vec<u8>) -> SubgroupObject {
2088 SubgroupObject {
2089 object_id: vi(id),
2090 extension_headers: extensions,
2091 payload_length: vi(payload.len() as u64),
2092 object_status: None,
2093 payload,
2094 }
2095 }
2096
2097 // ── Object ID deltas ────────────────────────────────────
2098
2099 #[test]
2100 fn two_objects_with_properties_have_distinct_ids() {
2101 // Vector `subgroup-properties-two-objects-empty`: two objects, each
2102 // carrying an empty properties block and a delta of 0. The delta is
2103 // biased by one whether or not the properties bit is set, so the IDs
2104 // are 0 and 1 — not 0 and 0.
2105 let bytes = hex("11010080000004deadbeef000002cafe");
2106 let (header, objects) = decode_all(&bytes);
2107 assert!(header.has_properties());
2108 assert_eq!(objects.len(), 2);
2109 assert_eq!(objects[0].object_id.into_inner(), 0);
2110 assert_eq!(objects[1].object_id.into_inner(), 1);
2111 assert_eq!(objects[0].payload, hex("deadbeef"));
2112 assert_eq!(objects[1].payload, hex("cafe"));
2113 assert!(objects.iter().all(|o| o.extension_headers.is_empty()));
2114 }
2115
2116 #[test]
2117 fn deltas_resolve_sparse_ids() {
2118 let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
2119 let objects: Vec<_> =
2120 [3u64, 4, 40].iter().map(|&id| object(id, vec![], vec![0xAA, id as u8])).collect();
2121 let (_, decoded) = decode_all(&encode_all(&header, &objects));
2122 let ids: Vec<u64> = decoded.iter().map(|o| o.object_id.into_inner()).collect();
2123 assert_eq!(ids, vec![3, 4, 40]);
2124 }
2125
2126 #[test]
2127 fn write_rejects_non_increasing_ids() {
2128 let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
2129 let mut writer = SubgroupObjectReader::new(&header);
2130 let mut buf = Vec::new();
2131 writer.write_object(&object(7, vec![], vec![0x01]), &mut buf).unwrap();
2132 for id in [7u64, 6, 0] {
2133 let err = writer.write_object(&object(id, vec![], vec![0x01]), &mut buf).unwrap_err();
2134 assert!(matches!(err, CodecError::InvalidField), "id {id} gave {err:?}");
2135 }
2136 }
2137
2138 #[test]
2139 fn eliding_an_object_renumbers_its_successor() {
2140 let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
2141 let all: Vec<_> = (0..5u64).map(|id| object(id, vec![], vec![id as u8])).collect();
2142 for elided in 0..5u64 {
2143 let kept: Vec<_> =
2144 all.iter().filter(|o| o.object_id.into_inner() != elided).cloned().collect();
2145 let (_, decoded) = decode_all(&encode_all(&header, &kept));
2146 let ids: Vec<u64> = decoded.iter().map(|o| o.object_id.into_inner()).collect();
2147 let expected: Vec<u64> = (0..5u64).filter(|&i| i != elided).collect();
2148 assert_eq!(ids, expected, "eliding object {elided}");
2149 }
2150 }
2151
2152 // ── Properties blocks ──────────────────────────────
2153
2154 #[test]
2155 fn properties_blob_excludes_its_length_prefix() {
2156 // Vector `subgroup-properties-two-objects-nonempty`: each
2157 // object carries a two-byte block, so the blob is those two bytes
2158 // with the `02` length prefix stripped.
2159 let bytes = hex("1101008000023c0204deadbeef00023c0302cafe");
2160 let (_, objects) = decode_all(&bytes);
2161 assert_eq!(objects.len(), 2);
2162 assert_eq!(objects[0].object_id.into_inner(), 0);
2163 assert_eq!(objects[1].object_id.into_inner(), 1);
2164 assert_eq!(objects[0].extension_headers, hex("3c02"));
2165 assert_eq!(objects[1].extension_headers, hex("3c03"));
2166 assert_eq!(objects[0].payload, hex("deadbeef"));
2167 assert_eq!(objects[1].payload, hex("cafe"));
2168 }
2169
2170 #[test]
2171 fn status_object_carries_its_properties_block() {
2172 let (_, objects) = decode_all(&hex("1101008000023c010003"));
2173 assert_eq!(objects.len(), 1);
2174 assert_eq!(objects[0].extension_headers, hex("3c01"));
2175 assert_eq!(objects[0].payload_length.into_inner(), 0);
2176 assert_eq!(objects[0].object_status.map(ObjectStatus::as_u64), Some(3));
2177 assert!(objects[0].payload.is_empty());
2178 }
2179
2180 // ── Re-encoding ─────────────────────────────────────────
2181
2182 #[test]
2183 fn vectors_re_encode_byte_identically() {
2184 for vector in VECTORS {
2185 let bytes = hex(vector);
2186 let (header, objects) = decode_all(&bytes);
2187 assert_eq!(encode_all(&header, &objects), bytes, "[{vector}] re-encode");
2188 }
2189 }
2190
2191 // ── Payload-free framing ────────────────────────────────
2192
2193 #[test]
2194 fn meta_matches_read_object() {
2195 for vector in VECTORS {
2196 let bytes = hex(vector);
2197 let mut cursor = &bytes[..];
2198 let header = SubgroupHeader::decode(&mut cursor).unwrap();
2199 let mut full_reader = SubgroupObjectReader::new(&header);
2200 let mut meta_reader = SubgroupObjectReader::new(&header);
2201 let mut full_cursor = cursor;
2202 let mut meta_cursor = cursor;
2203 while meta_cursor.has_remaining() {
2204 let before = meta_cursor.remaining();
2205 let object = full_reader.read_object(&mut full_cursor).unwrap();
2206 let meta = meta_reader.read_object_meta(&mut meta_cursor).unwrap();
2207 assert_eq!(meta.object_id, object.object_id.into_inner(), "[{vector}]");
2208 assert_eq!(
2209 meta.extension_headers_len,
2210 object.extension_headers.len() as u64,
2211 "[{vector}]"
2212 );
2213 assert_eq!(meta.payload_length, object.payload_length.into_inner(), "[{vector}]");
2214 assert_eq!(
2215 meta.status,
2216 object.object_status.map(ObjectStatus::as_u64),
2217 "[{vector}]"
2218 );
2219 assert_eq!(meta.wire_len, (before - meta_cursor.remaining()) as u64, "[{vector}]");
2220 assert_eq!(full_cursor.remaining(), meta_cursor.remaining(), "[{vector}]");
2221 }
2222 }
2223 }
2224
2225 #[test]
2226 fn short_buffers_report_unexpected_end() {
2227 let bytes = hex("1101008000023c0204deadbeef00023c0302cafe");
2228 let mut cursor = &bytes[..];
2229 let header = SubgroupHeader::decode(&mut cursor).unwrap();
2230 let objects_start = bytes.len() - cursor.len();
2231 for cut in objects_start..bytes.len() {
2232 let mut reader = SubgroupObjectReader::new(&header);
2233 let mut meta_reader = SubgroupObjectReader::new(&header);
2234 let mut cursor = &bytes[objects_start..cut];
2235 let mut meta_cursor = cursor;
2236 while cursor.has_remaining() {
2237 if let Err(err) = reader.read_object(&mut cursor) {
2238 assert!(
2239 matches!(err, CodecError::UnexpectedEnd | CodecError::VarInt(_)),
2240 "cut {cut} gave {err:?}"
2241 );
2242 break;
2243 }
2244 }
2245 while meta_cursor.has_remaining() {
2246 if let Err(err) = meta_reader.read_object_meta(&mut meta_cursor) {
2247 assert!(
2248 matches!(err, CodecError::UnexpectedEnd | CodecError::VarInt(_)),
2249 "cut {cut} gave {err:?}"
2250 );
2251 break;
2252 }
2253 }
2254 }
2255 }
2256
2257 // ── Object status ───────────────────────────────────────
2258
2259 /// A one-object subgroup stream whose object carries `status` in place of
2260 /// a payload: header type 0x10 (no properties, subgroup-ID mode 0, no
2261 /// FIRST_OBJECT bit), track alias 1, group 0, publisher priority 128; then
2262 /// an Object ID delta of 0, a payload length of 0, and the status code.
2263 fn subgroup_status_stream(status: u64) -> Vec<u8> {
2264 vec![0x10, 0x01, 0x00, 0x80, 0x00, 0x00, status as u8]
2265 }
2266
2267 /// A status datagram carrying `status`: type 0x20 (STATUS bit set,
2268 /// explicit Object ID, explicit priority), track alias 1, group 0, object
2269 /// 0, priority 128, then the status byte.
2270 fn status_datagram(status: u64) -> Vec<u8> {
2271 vec![0x20, 0x01, 0x00, 0x00, 0x80, status as u8]
2272 }
2273
2274 /// The object [`subgroup_status_stream`] describes, as a value.
2275 fn status_object(status: Option<ObjectStatus>) -> SubgroupObject {
2276 SubgroupObject {
2277 object_id: vi(0),
2278 extension_headers: Vec::new(),
2279 payload_length: vi(0),
2280 object_status: status,
2281 payload: Vec::new(),
2282 }
2283 }
2284
2285 /// An object carrying both `payload` and, in the caller's hands, `status`.
2286 /// The wire has no room for both, which is what the registry rules on.
2287 fn payload_object(status: Option<ObjectStatus>, payload: Vec<u8>) -> SubgroupObject {
2288 SubgroupObject {
2289 object_id: vi(0),
2290 extension_headers: Vec::new(),
2291 payload_length: vi(payload.len() as u64),
2292 object_status: status,
2293 payload,
2294 }
2295 }
2296
2297 /// The datagram [`status_datagram`] describes, as a value.
2298 fn status_datagram_header(status: Option<ObjectStatus>) -> DatagramHeader {
2299 DatagramHeader {
2300 datagram_type: 0x20,
2301 track_alias: vi(1),
2302 group_id: vi(0),
2303 object_id: vi(0),
2304 publisher_priority: Some(128),
2305 properties: Vec::new(),
2306 object_status: status,
2307 }
2308 }
2309
2310 /// Every status draft-19 assigns can be written and read back as the same
2311 /// status, on both a subgroup stream and a status datagram.
2312 ///
2313 /// The set is read from `ObjectStatus::ALL` — the three rows of the Object
2314 /// Status registry — rather than restated here, so this moves with the
2315 /// draft if a code is ever reassigned. It is the gate on typing the two
2316 /// `object_status` fields: a typed field that silently narrowed or
2317 /// renumbered the set would fail here even though it still compiled.
2318 ///
2319 /// Writing `ObjectStatus::Normal` when a zero-length object's status is
2320 /// `None` is checked too — without it the encoder emits an object whose
2321 /// declared payload length promises a status field that never arrives.
2322 ///
2323 /// Made `write_object` encode a constant `ObjectStatus::Normal` instead of
2324 /// the object's own status, ran it, and got:
2325 ///
2326 /// ```text
2327 /// assertion `left == right` failed: subgroup object status
2328 /// left: Some(Normal)
2329 /// right: Some(EndOfGroup)
2330 /// ```
2331 ///
2332 /// The same change to `DatagramHeader::encode` gives:
2333 ///
2334 /// ```text
2335 /// assertion `left == right` failed: datagram object status
2336 /// left: Some(Normal)
2337 /// right: Some(EndOfGroup)
2338 /// ```
2339 #[test]
2340 fn every_assigned_status_survives_a_round_trip() {
2341 let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
2342 for &status in ObjectStatus::ALL {
2343 let mut buf = Vec::new();
2344 SubgroupObjectReader::new(&header)
2345 .write_object(&status_object(Some(status)), &mut buf)
2346 .unwrap_or_else(|e| panic!("write_object refused {status:?}: {e:?}"));
2347
2348 let mut cursor = &buf[..];
2349 let object =
2350 SubgroupObjectReader::new(&header).read_object(&mut cursor).unwrap_or_else(|e| {
2351 panic!("read_object refused the bytes written for {status:?}: {e:?}")
2352 });
2353 assert_eq!(object.object_status, Some(status), "subgroup object status");
2354 assert!(!cursor.has_remaining(), "{status:?}: bytes left over after read_object");
2355
2356 let meta =
2357 SubgroupObjectReader::new(&header).read_object_meta(&mut &buf[..]).unwrap_or_else(
2358 |e| panic!("read_object_meta refused the bytes written for {status:?}: {e:?}"),
2359 );
2360 assert_eq!(meta.status, Some(status.as_u64()), "subgroup meta status");
2361
2362 let mut datagram = Vec::new();
2363 status_datagram_header(Some(status)).encode(&mut datagram);
2364 let decoded = DatagramHeader::decode(&mut &datagram[..]).unwrap_or_else(|e| {
2365 panic!("datagram decode refused the bytes written for {status:?}: {e:?}")
2366 });
2367 assert_eq!(decoded.object_status, Some(status), "datagram object status");
2368 }
2369
2370 let mut buf = Vec::new();
2371 SubgroupObjectReader::new(&header).write_object(&status_object(None), &mut buf).unwrap();
2372 let object = SubgroupObjectReader::new(&header)
2373 .read_object(&mut &buf[..])
2374 .expect("a zero-length object with no status must still decode");
2375 assert_eq!(object.object_status, Some(ObjectStatus::Normal));
2376
2377 let mut datagram = Vec::new();
2378 status_datagram_header(None).encode(&mut datagram);
2379 let decoded = DatagramHeader::decode(&mut &datagram[..])
2380 .expect("a status datagram with no status must still decode");
2381 assert_eq!(decoded.object_status, Some(ObjectStatus::Normal));
2382 }
2383
2384 /// The encoder writes exactly the frames the decoder accepts.
2385 ///
2386 /// Sweeps every status code `0x00..=0x3f` — one wire byte under both the
2387 /// varint on a subgroup stream and the bare byte on a datagram, and wide
2388 /// enough to contain the gap at `0x2` and the `0x1` draft-16 dropped. For
2389 /// a code the draft assigns, the hand-built frame must decode *and* the
2390 /// encoder handed that status must reproduce those exact bytes. For a code
2391 /// it does not assign, the same frame must be refused at all three decode
2392 /// sites — and no `ObjectStatus` exists to hand the encoder, so the frame
2393 /// has no way to be produced in the first place.
2394 ///
2395 /// This is a gate on the status code points only. Which of those statuses
2396 /// may carry a payload is the separate question
2397 /// [`the_registry_decides_which_statuses_may_carry_a_payload`] gates.
2398 ///
2399 /// # What this catches, observed by making each change and running it
2400 ///
2401 /// Encoding a constant `ObjectStatus::Normal` in `write_object` instead of
2402 /// the object's own status:
2403 ///
2404 /// ```text
2405 /// assertion `left == right` failed: the encoder must produce the frame the decoder accepted for status 0x3
2406 /// left: [16, 1, 0, 128, 0, 0, 0]
2407 /// right: [16, 1, 0, 128, 0, 0, 3]
2408 /// ```
2409 ///
2410 /// The same change in `DatagramHeader::encode`:
2411 ///
2412 /// ```text
2413 /// assertion `left == right` failed: the encoder must produce the datagram the decoder accepted for status 0x3
2414 /// left: [32, 1, 0, 0, 128, 0]
2415 /// right: [32, 1, 0, 0, 128, 3]
2416 /// ```
2417 ///
2418 /// The decoder drifting away from `ALL` — adding `0x2` to
2419 /// `ObjectStatus::from_u64`, so a code the draft does not assign starts
2420 /// decoding:
2421 ///
2422 /// ```text
2423 /// subgroup read_object accepted status 0x2, which the draft does not assign
2424 /// ```
2425 ///
2426 /// # The encode-side refusal is a type, not an assertion
2427 ///
2428 /// Once `object_status` is typed there is no runtime path that offers the
2429 /// encoder the `0x2` that this module's decoder is documented as refusing,
2430 /// so no test here can watch one be refused. Reverting
2431 /// `DatagramHeader::object_status` to `Option<u8>` with an `unwrap_or(0)`
2432 /// encoder does not make this test fail — it makes it stop compiling,
2433 /// which is the guarantee:
2434 ///
2435 /// ```text
2436 /// error[E0308]: mismatched types
2437 /// = note: expected enum `Option<u8>`
2438 /// found enum `Option<draft19::types::ObjectStatus>`
2439 /// ```
2440 #[test]
2441 fn the_encoder_writes_exactly_the_frames_the_decoder_accepts() {
2442 for code in 0x00u64..=0x3f {
2443 let assigned = ObjectStatus::ALL.iter().copied().find(|s| s.as_u64() == code);
2444
2445 let stream = subgroup_status_stream(code);
2446 let mut cursor: &[u8] = &stream;
2447 let header = SubgroupHeader::decode(&mut cursor).unwrap();
2448 let objects = cursor;
2449 let read = SubgroupObjectReader::new(&header).read_object(&mut { objects });
2450 let meta = SubgroupObjectReader::new(&header).read_object_meta(&mut { objects });
2451
2452 let datagram = status_datagram(code);
2453 let decoded = DatagramHeader::decode(&mut &datagram[..]);
2454
2455 match assigned {
2456 Some(status) => {
2457 let object = read.unwrap_or_else(|e| {
2458 panic!(
2459 "read_object refused status {code:#x}, which the draft assigns: {e:?}"
2460 )
2461 });
2462 assert_eq!(object.object_status, Some(status));
2463 assert_eq!(meta.unwrap().status, Some(code));
2464 assert_eq!(decoded.unwrap().object_status, Some(status));
2465
2466 let mut written = Vec::new();
2467 header.encode(&mut written);
2468 SubgroupObjectReader::new(&header)
2469 .write_object(&status_object(Some(status)), &mut written)
2470 .unwrap();
2471 assert_eq!(
2472 written, stream,
2473 "the encoder must produce the frame the decoder accepted for status {code:#x}"
2474 );
2475
2476 let mut written = Vec::new();
2477 status_datagram_header(Some(status)).encode(&mut written);
2478 assert_eq!(
2479 written, datagram,
2480 "the encoder must produce the datagram the decoder accepted for status {code:#x}"
2481 );
2482 }
2483 None => {
2484 for (site, result) in [
2485 ("subgroup read_object", read.map(|_| ())),
2486 ("subgroup read_object_meta", meta.map(|_| ())),
2487 ("status datagram", decoded.map(|_| ())),
2488 ] {
2489 match result {
2490 Ok(()) => panic!(
2491 "{site} accepted status {code:#x}, which the draft does not assign"
2492 ),
2493 Err(error) => assert!(
2494 matches!(error, CodecError::InvalidField),
2495 "{site} refused status {code:#x} with {error:?}, not InvalidField"
2496 ),
2497 }
2498 }
2499 }
2500 }
2501 }
2502 }
2503
2504 // ── The registry's payload column ───────────────────────
2505
2506 /// Draft-19 Section 15.9, Table 16, "Payload" column: Normal "Yes", End of
2507 /// Group "No", End of Track "No".
2508 ///
2509 /// Restated here rather than read from [`ObjectStatus::payload_permission`]
2510 /// so the gate holds its own copy of the registry. A codec that changed a
2511 /// row would still agree with itself; only a second copy notices.
2512 const PAYLOAD_COLUMN: &[(ObjectStatus, bool)] = &[
2513 (ObjectStatus::Normal, true),
2514 (ObjectStatus::EndOfGroup, false),
2515 (ObjectStatus::EndOfTrack, false),
2516 ];
2517
2518 /// Which statuses may carry a payload is decided by the registry, not by a
2519 /// payload length.
2520 ///
2521 /// Draft-18 said an Object with any status other than Normal has an empty
2522 /// payload, so the rule could be read off the status number — and this
2523 /// encoder read it off the length instead, which came to the same thing: a
2524 /// zero length was what put a status on the wire, and a status handed in
2525 /// alongside a payload was dropped on the floor. Draft-19 Section 11.2.1.1
2526 /// replaces the blanket rule with "an Object MUST have an empty payload
2527 /// unless its Object Status value is registered as permitting a payload",
2528 /// the permission being a column of the Object Status registry in
2529 /// Section 15.9. The three rows assigned today give the same answers
2530 /// draft-18's rule gave; what this gate observes is that the answers now
2531 /// come from the rows.
2532 ///
2533 /// Each row is driven both ways. A zero-length object with that status must
2534 /// encode and read back — Normal included, since it permits a payload
2535 /// without requiring one and so has to stay expressible with none. An
2536 /// object handed a payload under that status must be accepted exactly when
2537 /// the row permits one, and otherwise refused with nothing written.
2538 ///
2539 /// # What this catches, observed by making each change and running it
2540 ///
2541 /// Dropping the registry check from `write_object`, leaving the length to
2542 /// decide as it did before — an End of Group object with a payload is then
2543 /// written as a plain payload object and its status is gone:
2544 ///
2545 /// ```text
2546 /// write_object must refuse a payload under EndOfGroup, which the registry forbids one; got Ok(())
2547 /// ```
2548 ///
2549 /// Moving End of Group into the permitting column, as a status registered
2550 /// later with "Payload: Yes" would be:
2551 ///
2552 /// ```text
2553 /// assertion `left == right` failed: decoded EndOfGroup reports the wrong payload permission
2554 /// left: true
2555 /// right: false
2556 /// ```
2557 ///
2558 /// Letting the permission pick the framing as well as govern it — writing
2559 /// the status field for the statuses that forbid a payload instead of for
2560 /// the objects whose payload length is zero, which is the wrong reading of
2561 /// the registry and the one that costs the zero-length Normal object its
2562 /// encoding, since the draft frames the status on payload length alone:
2563 ///
2564 /// ```text
2565 /// read_object refused a zero-length Normal: VarInt(UnexpectedEnd)
2566 /// ```
2567 #[test]
2568 fn the_registry_decides_which_statuses_may_carry_a_payload() {
2569 let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
2570 assert_eq!(
2571 PAYLOAD_COLUMN.len(),
2572 ObjectStatus::ALL.len(),
2573 "every assigned status needs a row in the payload column"
2574 );
2575
2576 for &(status, permitted) in PAYLOAD_COLUMN {
2577 assert!(ObjectStatus::ALL.contains(&status), "{status:?} is not an assigned status");
2578
2579 // A zero-length object is legal under every row, and is the only
2580 // framing that states a status on a subgroup stream.
2581 let mut empty = Vec::new();
2582 SubgroupObjectReader::new(&header)
2583 .write_object(&status_object(Some(status)), &mut empty)
2584 .unwrap_or_else(|e| panic!("write_object refused a zero-length {status:?}: {e:?}"));
2585 let object = SubgroupObjectReader::new(&header)
2586 .read_object(&mut &empty[..])
2587 .unwrap_or_else(|e| panic!("read_object refused a zero-length {status:?}: {e:?}"));
2588 assert_eq!(object.status(), status, "zero-length {status:?} lost its status");
2589 assert!(object.payload.is_empty(), "zero-length {status:?} gained a payload");
2590 assert_eq!(
2591 object.permits_payload(),
2592 permitted,
2593 "decoded {status:?} reports the wrong payload permission"
2594 );
2595
2596 // A status datagram is the one carrier where the registry is not
2597 // the last word. Section 11.3.1 puts the status field in the
2598 // payload's place — "When set to 1, the Object Status field is
2599 // present and there is no Object Payload" — so no status makes
2600 // trailing bytes part of such a datagram, Normal included, and
2601 // `permitted` is not the expected answer here.
2602 let datagram = DatagramHeader::decode(&mut &status_datagram(status.as_u64())[..])
2603 .unwrap_or_else(|e| panic!("datagram decode refused {status:?}: {e:?}"));
2604 assert!(
2605 !datagram.permits_payload(),
2606 "a status datagram has no Object Payload field, so {status:?} permits no bytes"
2607 );
2608 assert_eq!(
2609 datagram.status().permits_payload(),
2610 permitted,
2611 "decoded {status:?} datagram reports the wrong registry row"
2612 );
2613 let mut whole = status_datagram(status.as_u64());
2614 whole.extend_from_slice(&hex("deadbeef"));
2615 let trailing = DatagramHeader::decode_object(&mut &whole[..]);
2616 assert!(
2617 matches!(trailing, Err(CodecError::PayloadNotPermitted { .. })),
2618 "trailing bytes on a {status:?} status datagram must be refused; got {trailing:?}"
2619 );
2620
2621 // The same status, handed a payload the wire cannot frame beside it.
2622 let mut written = Vec::new();
2623 let result = SubgroupObjectReader::new(&header)
2624 .write_object(&payload_object(Some(status), hex("deadbeef")), &mut written);
2625
2626 if permitted {
2627 result.unwrap_or_else(|e| {
2628 panic!(
2629 "write_object refused a payload under {status:?}, \
2630 which the registry permits: {e:?}"
2631 )
2632 });
2633 let object = SubgroupObjectReader::new(&header)
2634 .read_object(&mut &written[..])
2635 .unwrap_or_else(|e| {
2636 panic!("read_object refused its own output for {status:?}: {e:?}")
2637 });
2638 assert_eq!(object.payload, hex("deadbeef"), "{status:?} lost its payload");
2639 assert_eq!(object.status(), status, "{status:?} came back as another status");
2640 } else {
2641 assert!(
2642 matches!(result, Err(CodecError::PayloadNotPermitted { .. })),
2643 "write_object must refuse a payload under {status:?}, \
2644 which the registry forbids one; got {result:?}"
2645 );
2646 assert!(written.is_empty(), "a refused {status:?} object still wrote {written:?}");
2647 }
2648 }
2649
2650 // A datagram without the STATUS bit is all payload, and the status its
2651 // framing leaves out is the one row that permits a payload.
2652 let plain = DatagramHeader::decode(&mut &[0x00u8, 0x01, 0x00, 0x00, 0x80][..])
2653 .expect("a datagram with no status field must decode");
2654 assert_eq!(plain.status(), ObjectStatus::Normal);
2655 assert!(plain.permits_payload(), "a payload-carrying datagram must be permitted one");
2656 }
2657}