Skip to main content

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    ///
968    /// # The two Properties rules are enforced here too
969    ///
970    /// Section 11.3.1 states them of a receiving endpoint, and this is the
971    /// endpoint's read:
972    ///
973    /// * "If an endpoint receives a datagram with the PROPERTIES bit set and an
974    ///   Properties Length of 0, it MUST close the session with a
975    ///   PROTOCOL_VIOLATION." The bit and a zero length are two ways to spell
976    ///   *no properties* and a datagram may use only the first, because a
977    ///   datagram with none has a Type that says so and the block costs bytes
978    ///   the Type already saved. A subgroup stream says the opposite in
979    ///   Section 11.4.2 — there the PROPERTIES bit is fixed for the whole stream,
980    ///   so an object with no properties has nowhere else to say so and a
981    ///   zero-length block is the required spelling.
982    /// * "If an Object Datagram includes both the STATUS bit and PROPERTIES
983    ///   bit, and the Object Status is not Normal (0x0), the endpoint MUST close
984    ///   the session with a PROTOCOL_VIOLATION, because only Normal Objects can
985    ///   have Properties."
986    ///
987    /// Both errors are [`CodecError::InvalidField`].
988    ///
989    /// [`Self::decode`] does **not** apply them, and the split is deliberate.
990    /// Both describe a datagram that is well framed and non-conforming: every
991    /// field is where the layout puts it and every one of them parses, so the
992    /// header reads back exactly as it arrived and a tool reproducing a capture
993    /// can re-emit it. What it may not do is hand such a datagram to an
994    /// application as an ordinary Object, which is what this entry point would
995    /// be doing. [`Self::properties_block_well_formed`] and
996    /// [`Self::properties_permitted`] report the two for a caller that wants
997    /// the header without the judgement, and [`Self::encode_checked`] refuses
998    /// to write either shape.
999    pub fn decode_object(buf: &mut impl Buf) -> Result<(Self, Vec<u8>), CodecError> {
1000        let header = Self::decode(buf)?;
1001        if !header.properties_block_well_formed() || !header.properties_permitted() {
1002            return Err(CodecError::InvalidField);
1003        }
1004        let payload = crate::types::read_bytes(buf, buf.remaining())?;
1005        if !payload.is_empty() && !header.permits_payload() {
1006            return Err(CodecError::PayloadNotPermitted {
1007                status: header.status().as_u64(),
1008                len: payload.len(),
1009                detail: if header.has_status() {
1010                    "its type states a status in place of a payload"
1011                } else {
1012                    "its status is registered as forbidding one"
1013                },
1014            });
1015        }
1016        Ok((header, payload))
1017    }
1018
1019    /// Serialize the header, refusing a status the framing cannot carry.
1020    ///
1021    /// A datagram states a status only when its type byte sets the STATUS bit
1022    /// (0x20). With the bit clear there is no status field on the wire, so an
1023    /// `object_status` of anything but [`ObjectStatus::Normal`] has nowhere to
1024    /// go: [`Self::encode`] drops it, and the datagram parses back as an
1025    /// ordinary payload object. An End of Group marker written that way does
1026    /// not arrive late or malformed — it does not arrive at all, and the
1027    /// receiver sees a normal object in its place.
1028    ///
1029    /// [`ObjectStatus::Normal`] with the bit clear is not that case and is
1030    /// accepted. It is the status the encoding elides for every object that
1031    /// carries a payload, so stating it asks for exactly the bytes leaving it
1032    /// out asks for, and nothing is lost.
1033    ///
1034    /// Errors with [`CodecError::InvalidField`] on the lossy combination,
1035    /// before any byte is written, so a refused header leaves `buf` untouched.
1036    /// This is the datagram half of the rule
1037    /// [`SubgroupObjectReader::write_object`] applies on a subgroup stream.
1038    ///
1039    /// Also errors with [`CodecError::InvalidField`] for a Type value draft-19
1040    /// Section 11.3.1 lists as invalid, and for the same reason: a datagram
1041    /// written with one could not be read back by [`Self::decode`], and a
1042    /// codec whose two halves disagree about which datagrams exist cannot be
1043    /// used to rewrite captured traffic.
1044    pub fn encode_checked(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
1045        validate_datagram_type(self.datagram_type as u64)?;
1046        if !self.has_status() && matches!(self.object_status, Some(s) if s != ObjectStatus::Normal)
1047        {
1048            return Err(CodecError::InvalidField);
1049        }
1050        // The two properties rules of Section 11.3.1. [`Self::decode`] reports
1051        // both rather than refusing them, because the datagrams they describe
1052        // are well framed and a codec that could not read one could not
1053        // reproduce a capture containing it. Writing one is the other
1054        // direction and has no such excuse: a conforming peer answers either
1055        // with a PROTOCOL_VIOLATION, so emitting one costs the session and not
1056        // merely the datagram.
1057        if !self.properties_block_well_formed() || !self.properties_permitted() {
1058            return Err(CodecError::InvalidField);
1059        }
1060        self.encode(buf);
1061        Ok(())
1062    }
1063
1064    /// Serialize the header exactly as its type byte describes it.
1065    ///
1066    /// Every field the type byte announces is written, in the order
1067    /// [`Self::decode`] reads them, so the bytes this produces always parse
1068    /// back. The properties block in particular has to be written here: it
1069    /// sits ahead of the status field, and a datagram that skipped it would
1070    /// offer the status byte where the decoder reads the block's length.
1071    ///
1072    /// The type byte is taken as the authority on framing, which is what makes
1073    /// this infallible — and what makes it lossy when the struct disagrees with
1074    /// itself. An `object_status` set while the type byte leaves the STATUS bit
1075    /// clear is discarded here without a word, and a Type value draft-19
1076    /// forbids is written out as readily as one it assigns, including Types
1077    /// this module's own [`Self::decode`] refuses. Prefer
1078    /// [`Self::encode_checked`], which refuses both instead of resolving them.
1079    pub fn encode(&self, buf: &mut impl BufMut) {
1080        buf.put_u8(self.datagram_type);
1081        self.track_alias.encode_moqt::<Wire>(buf);
1082        self.group_id.encode_moqt::<Wire>(buf);
1083
1084        if self.datagram_type & DATAGRAM_ZERO_OBJECT_ID_BIT == 0 {
1085            self.object_id.encode_moqt::<Wire>(buf);
1086        }
1087
1088        if self.datagram_type & DATAGRAM_DEFAULT_PRIORITY_BIT == 0 {
1089            buf.put_u8(self.publisher_priority.unwrap_or(128));
1090        }
1091
1092        if self.datagram_type & DATAGRAM_PROPERTIES_BIT != 0 {
1093            VarInt::from_usize(self.properties.len()).encode_moqt::<Wire>(buf);
1094            buf.put_slice(&self.properties);
1095        }
1096
1097        if self.datagram_type & DATAGRAM_STATUS_BIT != 0 {
1098            buf.put_u8(self.object_status.unwrap_or(ObjectStatus::Normal).as_u8());
1099        }
1100    }
1101
1102    pub fn is_end_of_group(&self) -> bool {
1103        self.datagram_type & DATAGRAM_END_OF_GROUP_BIT != 0
1104    }
1105
1106    pub fn has_status(&self) -> bool {
1107        self.datagram_type & DATAGRAM_STATUS_BIT != 0
1108    }
1109
1110    /// `true` when the type byte sets the PROPERTIES bit (0x01), which is what
1111    /// puts the properties block on the wire.
1112    ///
1113    /// Reports the framing, not the contents. A decoded datagram with this set
1114    /// always has a non-empty [`Self::properties`], because [`Self::decode`]
1115    /// refuses a zero-length block; a header built by hand can hold the two
1116    /// apart, and [`Self::encode_checked`] is what refuses that.
1117    pub fn has_properties(&self) -> bool {
1118        self.datagram_type & DATAGRAM_PROPERTIES_BIT != 0
1119    }
1120
1121    /// The datagram's object status, with the one the encoding elides filled
1122    /// in.
1123    ///
1124    /// A datagram states a status only when its type sets the STATUS bit, and
1125    /// such a datagram has no payload. One without the bit is all payload, and
1126    /// its status is [`ObjectStatus::Normal`] — the sole row of the Object
1127    /// Status registry (draft-19 Section 15.9) permitting a payload, so the
1128    /// only status it could have had.
1129    pub fn status(&self) -> ObjectStatus {
1130        self.object_status.unwrap_or(ObjectStatus::Normal)
1131    }
1132
1133    /// Whether the bytes after this datagram's header are allowed to exist.
1134    ///
1135    /// Two independent rules forbid them, and this reports both:
1136    ///
1137    /// - The framing. Draft-19 Section 11.3.1: "The STATUS bit (0x20)
1138    ///   indicates whether the datagram contains an Object Status or Object
1139    ///   Payload. When set to 1, the Object Status field is present and there
1140    ///   is no Object Payload." A datagram that states a status has no payload
1141    ///   field at all, whichever status it states — so a STATUS datagram
1142    ///   carrying the Normal code 0x0 has no more room for bytes than one
1143    ///   carrying End of Group.
1144    /// - The status. Section 11.2.1.1 and the Object Status registry's
1145    ///   "Payload" column, Section 15.9: an Object has an empty payload unless
1146    ///   its status is registered as permitting one. This half reaches a
1147    ///   datagram whose type byte leaves the STATUS bit clear while the value
1148    ///   claims a status that forbids a payload — a disagreement
1149    ///   [`Self::encode_checked`] refuses to write, and one a decoded header
1150    ///   never shows.
1151    ///
1152    /// The first is the rule a decoded datagram can actually trip, and reading
1153    /// the registry alone misses it: `Some(ObjectStatus::Normal)` under a type
1154    /// byte with the STATUS bit set is exactly the case where the payload the
1155    /// draft says does not exist would otherwise be handed to the application
1156    /// as the object's content, because Normal is the one status the registry
1157    /// marks as permitting a payload.
1158    ///
1159    /// Distinct from [`Self::has_status`], which reports how the datagram is
1160    /// framed rather than whether a payload may follow. A caller holding the
1161    /// bytes after the header wants this one; [`Self::decode_object`] applies
1162    /// it for a caller who would rather the decode simply fail.
1163    pub fn permits_payload(&self) -> bool {
1164        if self.has_status() {
1165            return false;
1166        }
1167        self.status().permits_payload()
1168    }
1169
1170    /// Whether this datagram's status is allowed to carry the properties it
1171    /// has.
1172    ///
1173    /// The same rule the subgroup form obeys. Draft-19 Section 11.3.1 builds
1174    /// the datagram's Properties field out of "the Object Properties structure
1175    /// defined in Section 11.2.1.2", and that section is where the rule sits:
1176    /// "If an endpoint receives properties on an Object with status that is not
1177    /// Normal, it MUST close the session with a PROTOCOL_VIOLATION."
1178    ///
1179    /// See [`SubgroupObject::properties_permitted`] for why the decoder reports
1180    /// this instead of refusing it.
1181    pub fn properties_permitted(&self) -> bool {
1182        self.properties.is_empty() || self.status() == ObjectStatus::Normal
1183    }
1184
1185    /// Whether the properties block is framed the way a datagram may frame it.
1186    ///
1187    /// Draft-19 Section 11.3.1: "If an endpoint receives a datagram with the
1188    /// PROPERTIES bit set and an Properties Length of 0, it MUST close the
1189    /// session with a PROTOCOL_VIOLATION."
1190    ///
1191    /// The bit and a zero length are two ways to spell "no properties", and on
1192    /// a datagram they are not interchangeable: a datagram with none has a type
1193    /// byte that says so, and the block costs bytes the type byte already
1194    /// saved. This rule is the datagram's alone. A subgroup stream says the
1195    /// opposite in Section 11.4.2 — "Objects with no properties set Properties
1196    /// Length to 0" — because there the PROPERTIES bit is fixed for the whole
1197    /// stream, so an object with no properties has nowhere else to say so and a
1198    /// zero-length block is the required spelling rather than a violation.
1199    ///
1200    /// The mirror case is not a wire state but is a state this struct can hold:
1201    /// properties with the bit clear. [`Self::encode`] drops them without a
1202    /// word, so this reports that too, and [`Self::encode_checked`] refuses
1203    /// both.
1204    pub fn properties_block_well_formed(&self) -> bool {
1205        self.has_properties() != self.properties.is_empty()
1206    }
1207}
1208
1209// ── Fetch Header ──────────────────────────────────────────────
1210
1211const FETCH_STREAM_TYPE: u64 = 0x05;
1212
1213#[derive(Debug, Clone)]
1214pub struct FetchHeader {
1215    pub request_id: VarInt,
1216}
1217
1218impl FetchHeader {
1219    pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
1220        let stream_type = VarInt::decode_moqt::<Wire>(buf)?.into_inner();
1221        if stream_type != FETCH_STREAM_TYPE {
1222            return Err(stream_type_error(stream_type));
1223        }
1224        let request_id = VarInt::decode_moqt::<Wire>(buf)?;
1225        Ok(FetchHeader { request_id })
1226    }
1227
1228    pub fn encode(&self, buf: &mut impl BufMut) {
1229        VarInt::from_usize(FETCH_STREAM_TYPE as usize).encode_moqt::<Wire>(buf);
1230        self.request_id.encode_moqt::<Wire>(buf);
1231    }
1232}
1233
1234// ── Fetch objects ─────────────────────────────────────────────
1235
1236/// Serialization Flags bits 0-1, the Subgroup ID encoding
1237/// (draft-19 Section 11.4.4.1, Table 8).
1238const FETCH_SUBGROUP_ID_MODE_MASK: u64 = 0x03;
1239/// Subgroup ID mode 0b11: an explicit Subgroup ID field is on the wire.
1240const FETCH_SUBGROUP_ID_EXPLICIT: u64 = 0b11;
1241/// Table 9 flag: an Object ID Delta field is present.
1242const FETCH_OBJECT_ID_DELTA_BIT: u64 = 0x04;
1243/// Table 9 flag: a Group ID Delta field is present.
1244const FETCH_GROUP_ID_DELTA_BIT: u64 = 0x08;
1245/// Table 9 flag: a Publisher Priority field is present.
1246const FETCH_PRIORITY_BIT: u64 = 0x10;
1247/// Table 9 flag: a Properties field is present.
1248const FETCH_PROPERTIES_BIT: u64 = 0x20;
1249/// Table 9 flag: the Object's Forwarding Preference is Datagram, so it has no
1250/// Subgroup ID and the two low bits are to be ignored.
1251const FETCH_DATAGRAM_BIT: u64 = 0x40;
1252/// The largest Serialization Flags value whose bits are flags. Draft-19
1253/// Section 11.4.4: "When less than 128, the bits represent flags".
1254const FETCH_FLAGS_MAX: u64 = 0x7F;
1255/// Table 7: End of Non-Existent Range.
1256const FETCH_END_OF_NON_EXISTENT_RANGE: u64 = 0x8C;
1257/// Table 7: End of Unknown Range.
1258const FETCH_END_OF_UNKNOWN_RANGE: u64 = 0x10C;
1259
1260/// What an End of Range indicator on a fetch stream asserts about the
1261/// Locations it covers, from draft-19 Section 11.4.4.2.
1262///
1263/// Both kinds say that every Object with a Location between the previous
1264/// serialized Object and this one, inclusive, was not serialized. They differ
1265/// in why: the publisher knows the Objects are not there, or it does not know
1266/// either way. A subscriber can cache the first as a definitive gap and must
1267/// not cache the second, so the two cannot be collapsed.
1268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1269pub enum FetchEndOfRange {
1270    /// Serialization Flags 0x8C. The Objects in the range do not exist.
1271    NonExistent,
1272    /// Serialization Flags 0x10C. The Objects in the range have unknown
1273    /// status.
1274    Unknown,
1275}
1276
1277/// Which optional fields a Serialization Flags value puts on the wire.
1278///
1279/// Derived once by [`FetchObjectHeader::layout`] and then used by both
1280/// [`FetchObjectHeader::decode`] and [`FetchObjectHeader::encode`], so the two
1281/// cannot drift into disagreeing about a shape.
1282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1283struct FetchObjectLayout {
1284    group_id_delta: bool,
1285    subgroup_id: bool,
1286    object_id_delta: bool,
1287    publisher_priority: bool,
1288    properties: bool,
1289}
1290
1291/// One Object on a draft-19 fetch stream, up to but not including its payload.
1292///
1293/// Draft-19 Section 11.4.4 rebuilt the fetch object. Earlier drafts wrote a
1294/// fixed set of fields on every Object; draft-19 writes a Serialization Flags
1295/// varint first, and the flags say which fields follow:
1296///
1297/// ```text
1298/// {
1299///   Serialization Flags (vi64),
1300///   [Group ID Delta (vi64),]
1301///   [Subgroup ID (vi64),]
1302///   [Object ID Delta (vi64),]
1303///   [Publisher Priority (8),]
1304///   [Properties (..),]
1305///   Object Payload Length (vi64),
1306///   [Object Payload (..),]
1307/// }
1308/// ```
1309///
1310/// Every field is optional except the flags and the payload length, and an
1311/// absent field means *the same as the previous Object's*, not "zero" — the
1312/// whole point of the layout is that a run of Objects in one group at one
1313/// priority costs one byte of framing each. This type therefore holds what is
1314/// on the wire and nothing more: the deltas, not the Group and Object IDs they
1315/// resolve to. Resolving them needs the previous Object on the same stream and
1316/// the FETCH's Group Order, neither of which a single Object header knows,
1317/// and Section 11.4.4 spells out the arithmetic a caller must apply:
1318///
1319///   - The first Object MUST carry both deltas, and they are the absolute
1320///     Group ID and Object ID.
1321///   - Later on, a Group ID Delta moves the group by `delta + 1` — forwards
1322///     under Ascending Group Order and backwards under Descending — and
1323///     restarts the Object ID from the Object ID Delta. With no Group ID
1324///     Delta, the group is unchanged and the Object ID Delta is added to the
1325///     previous Object's ID; with no Object ID Delta either, the Object ID is
1326///     the previous one plus one.
1327///
1328/// There is no Object Status field. Draft-19 Section 11.2.1.1 states that
1329/// Object Status is present only on Objects delivered via a subscription and
1330/// absent from Objects delivered via a FETCH, which is why this type has no
1331/// counterpart to [`SubgroupObject::object_status`] and why a zero
1332/// `payload_length` here is simply an Object with no payload.
1333///
1334/// Two Serialization Flags values name an End of Range indicator rather than
1335/// an Object; [`Self::end_of_range`] reports which, and Section 11.4.4.2 gives
1336/// the rules such a frame follows.
1337#[derive(Debug, Clone, PartialEq, Eq)]
1338pub struct FetchObjectHeader {
1339    /// The raw Serialization Flags value, kept whole rather than split into
1340    /// booleans because it is also the field that names an End of Range
1341    /// indicator, and because re-encoding must reproduce the value the
1342    /// publisher chose.
1343    pub serialization_flags: VarInt,
1344    /// Group ID Delta, present when the flags set 0x08. Its meaning depends on
1345    /// the Object's position in the stream and on the Group Order; see the
1346    /// type's own documentation.
1347    pub group_id_delta: Option<VarInt>,
1348    /// An explicit Subgroup ID, present only when the two low flag bits are
1349    /// 0b11 and the Datagram bit is clear. The other three modes derive the
1350    /// Subgroup ID from the previous Object and put nothing on the wire.
1351    pub subgroup_id: Option<VarInt>,
1352    /// Object ID Delta, present when the flags set 0x04. Absent means the
1353    /// previous Object's ID plus one.
1354    pub object_id_delta: Option<VarInt>,
1355    /// Publisher Priority, present when the flags set 0x10. Absent means the
1356    /// previous Object's priority.
1357    pub publisher_priority: Option<u8>,
1358    /// Raw properties bytes, excluding the byte-length prefix that precedes
1359    /// them on the wire, and `None` when the flags leave 0x20 clear.
1360    ///
1361    /// `Some(vec![])` and `None` are different frames: the first writes a zero
1362    /// length prefix, the second writes nothing at all. Opaque, like the
1363    /// property blocks on the subgroup and datagram forms — draft-19
1364    /// Section 11.4.4 defines the field as the Object Properties structure of
1365    /// Section 11.2.1.2, and these bytes are re-emitted verbatim.
1366    pub properties: Option<Vec<u8>>,
1367    /// Object Payload Length. Always on the wire; the payload itself follows
1368    /// this header and is not held here.
1369    pub payload_length: VarInt,
1370}
1371
1372impl FetchObjectHeader {
1373    /// The Serialization Flags as a plain integer.
1374    pub fn flags(&self) -> u64 {
1375        self.serialization_flags.into_inner()
1376    }
1377
1378    /// Which End of Range indicator this is, or `None` for an ordinary Object.
1379    ///
1380    /// Draft-19 Section 11.4.4, Table 7 gives the two indicators their own
1381    /// Serialization Flags values rather than a flag bit, so this is an
1382    /// equality test on the whole field and not a mask.
1383    ///
1384    /// An indicator uses the same two positions on the wire an ordinary Object
1385    /// uses for its deltas, but Section 11.4.4.2 reads them as the absolute
1386    /// Group ID and Object ID of the end of the range. They are still reached
1387    /// through [`Self::group_id_delta`] and [`Self::object_id_delta`], since
1388    /// those are the fields the wire has; what changes is what they mean, and
1389    /// this is how a caller knows which reading applies.
1390    pub fn end_of_range(&self) -> Option<FetchEndOfRange> {
1391        match self.flags() {
1392            FETCH_END_OF_NON_EXISTENT_RANGE => Some(FetchEndOfRange::NonExistent),
1393            FETCH_END_OF_UNKNOWN_RANGE => Some(FetchEndOfRange::Unknown),
1394            _ => None,
1395        }
1396    }
1397
1398    /// The two-bit Subgroup ID mode, `flags & 0x03`.
1399    ///
1400    /// `0b00` = the Subgroup ID is zero; `0b01` = the previous Object's
1401    /// Subgroup ID; `0b10` = the previous Object's Subgroup ID plus one;
1402    /// `0b11` = an explicit field is present. Draft-19 Section 11.4.4.1
1403    /// assigns all four, unlike the subgroup stream header's reserved
1404    /// `0b11`.
1405    ///
1406    /// Meaningless when [`Self::is_datagram`] is true: such an Object has no
1407    /// Subgroup ID and the section says the subscriber MUST ignore these bits.
1408    pub fn subgroup_id_mode(&self) -> u8 {
1409        (self.flags() & FETCH_SUBGROUP_ID_MODE_MASK) as u8
1410    }
1411
1412    /// `true` when the flags set 0x40, marking an Object whose Forwarding
1413    /// Preference is Datagram. Such an Object has no Subgroup ID at all, so
1414    /// the Subgroup ID mode bits carry no meaning and no Subgroup ID field is
1415    /// on the wire whatever they say.
1416    pub fn is_datagram(&self) -> bool {
1417        self.flags() & FETCH_DATAGRAM_BIT != 0
1418    }
1419
1420    /// Which optional fields `flags` puts on the wire, or
1421    /// [`CodecError::InvalidField`] if draft-19 does not define that
1422    /// Serialization Flags value.
1423    ///
1424    /// Section 11.4.4 defines the field in two pieces: values below 128 are a
1425    /// set of flags, and Table 7 adds exactly two values above that, 0x8C and
1426    /// 0x10C. "Any other value is a PROTOCOL_VIOLATION", which is what the
1427    /// error covers — every value with bit 7 set that is not one of the two.
1428    ///
1429    /// The two indicators get their layout from Section 11.4.4.2 rather than
1430    /// from their bits: "the Group ID and Object ID fields are present.
1431    /// Subgroup ID, Priority and Properties are not present." Their low bits
1432    /// happen to spell exactly that (both are `0x0C` in the low seven bits:
1433    /// Group ID Delta and Object ID Delta set, Subgroup ID mode 0b00, no
1434    /// priority, no properties), but that is a property of the two values the
1435    /// draft chose and not a rule, so the layout is taken from the section
1436    /// that states it.
1437    fn layout(flags: u64) -> Result<FetchObjectLayout, CodecError> {
1438        if flags == FETCH_END_OF_NON_EXISTENT_RANGE || flags == FETCH_END_OF_UNKNOWN_RANGE {
1439            return Ok(FetchObjectLayout {
1440                group_id_delta: true,
1441                subgroup_id: false,
1442                object_id_delta: true,
1443                publisher_priority: false,
1444                properties: false,
1445            });
1446        }
1447        if flags > FETCH_FLAGS_MAX {
1448            return Err(CodecError::InvalidField);
1449        }
1450        Ok(FetchObjectLayout {
1451            group_id_delta: flags & FETCH_GROUP_ID_DELTA_BIT != 0,
1452            // An Object with the Datagram bit set has no Subgroup ID to write,
1453            // whatever the mode bits hold, so the field is absent.
1454            subgroup_id: flags & FETCH_DATAGRAM_BIT == 0
1455                && flags & FETCH_SUBGROUP_ID_MODE_MASK == FETCH_SUBGROUP_ID_EXPLICIT,
1456            object_id_delta: flags & FETCH_OBJECT_ID_DELTA_BIT != 0,
1457            publisher_priority: flags & FETCH_PRIORITY_BIT != 0,
1458            properties: flags & FETCH_PROPERTIES_BIT != 0,
1459        })
1460    }
1461
1462    /// Decode one fetch object header, leaving the payload in `buf`.
1463    ///
1464    /// Errors with [`CodecError::InvalidField`] for a Serialization Flags
1465    /// value draft-19 Section 11.4.4 does not define, and with
1466    /// [`CodecError::UnexpectedEnd`] or a varint error when the buffer runs
1467    /// out mid-field.
1468    ///
1469    /// The flags are validated before any field is read, because they are what
1470    /// says where the fields are: decoding an undefined value would mean
1471    /// picking a layout the draft never described and then consuming a
1472    /// plausible number of bytes under it.
1473    pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
1474        let serialization_flags = VarInt::decode_moqt::<Wire>(buf)?;
1475        let layout = Self::layout(serialization_flags.into_inner())?;
1476
1477        let group_id_delta =
1478            layout.group_id_delta.then(|| VarInt::decode_moqt::<Wire>(buf)).transpose()?;
1479        let subgroup_id =
1480            layout.subgroup_id.then(|| VarInt::decode_moqt::<Wire>(buf)).transpose()?;
1481        let object_id_delta =
1482            layout.object_id_delta.then(|| VarInt::decode_moqt::<Wire>(buf)).transpose()?;
1483
1484        let publisher_priority = if layout.publisher_priority {
1485            if buf.remaining() < 1 {
1486                return Err(CodecError::UnexpectedEnd);
1487            }
1488            Some(buf.get_u8())
1489        } else {
1490            None
1491        };
1492
1493        let properties = if layout.properties {
1494            let len = VarInt::decode_moqt::<Wire>(buf)?.into_inner() as usize;
1495            Some(crate::types::read_bytes(buf, len)?)
1496        } else {
1497            None
1498        };
1499
1500        let payload_length = VarInt::decode_moqt::<Wire>(buf)?;
1501
1502        Ok(FetchObjectHeader {
1503            serialization_flags,
1504            group_id_delta,
1505            subgroup_id,
1506            object_id_delta,
1507            publisher_priority,
1508            properties,
1509            payload_length,
1510        })
1511    }
1512
1513    /// Serialize the header, refusing one whose fields disagree with its own
1514    /// Serialization Flags.
1515    ///
1516    /// Errors with [`CodecError::InvalidField`] when the flags are a value
1517    /// draft-19 does not define, and when any optional field is present while
1518    /// its flag is clear or absent while its flag is set. Checked before any
1519    /// byte is written, so a refused header leaves `buf` untouched.
1520    ///
1521    /// Fallible for the same reason [`DatagramHeader::encode_checked`] is: the
1522    /// flags decide the framing, so writing them as the authority and dropping
1523    /// whatever they do not cover is silent data loss. A Group ID Delta held
1524    /// with the 0x08 bit clear is not written, the reader takes the Object as
1525    /// belonging to the previous Object's group, and nothing about the
1526    /// resulting stream looks wrong. The mirror case is worse: a flag set with
1527    /// no value behind it would have to invent one, and an invented Object ID
1528    /// Delta of zero is a real Object ID.
1529    ///
1530    /// Nothing here checks the deltas against the previous Object — that no
1531    /// Object other than the first may reference a prior Object that does not
1532    /// exist, for one. A single header has no way to see that, and this type
1533    /// deliberately does not carry stream state.
1534    pub fn encode(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
1535        let layout = Self::layout(self.flags())?;
1536        if layout.group_id_delta != self.group_id_delta.is_some()
1537            || layout.subgroup_id != self.subgroup_id.is_some()
1538            || layout.object_id_delta != self.object_id_delta.is_some()
1539            || layout.publisher_priority != self.publisher_priority.is_some()
1540            || layout.properties != self.properties.is_some()
1541        {
1542            return Err(CodecError::InvalidField);
1543        }
1544
1545        self.serialization_flags.encode_moqt::<Wire>(buf);
1546        // Wire order, from Figure 27: Group ID Delta, then Subgroup ID, then
1547        // Object ID Delta. The `layout` check above has already established
1548        // that exactly the fields the flags call for are present, so whichever
1549        // of the three are `Some` are the ones that belong here.
1550        for field in
1551            [self.group_id_delta, self.subgroup_id, self.object_id_delta].into_iter().flatten()
1552        {
1553            field.encode_moqt::<Wire>(buf);
1554        }
1555        if let Some(priority) = self.publisher_priority {
1556            buf.put_u8(priority);
1557        }
1558        if let Some(properties) = &self.properties {
1559            VarInt::from_usize(properties.len()).encode_moqt::<Wire>(buf);
1560            buf.put_slice(properties);
1561        }
1562        self.payload_length.encode_moqt::<Wire>(buf);
1563        Ok(())
1564    }
1565}
1566
1567/// The order a FETCH response's Groups arrive in, which decides how a Group ID
1568/// Delta is applied.
1569///
1570/// Draft-19 Section 11.4.4.1: "If the Group Order is Ascending, the Group ID is
1571/// the prior Object's Group ID plus the Group ID Delta + 1. If the Group Order
1572/// is Descending, the Group ID is the prior Object's Group ID minus the (Group
1573/// ID Delta + 1)."
1574///
1575/// The order is not on the data stream — it is settled by the control exchange
1576/// that opened the FETCH, whose GROUP_ORDER parameter (Section 10.2.8) spells
1577/// Ascending 0x1 and Descending 0x2 — so [`FetchObjectReader`] has to be told
1578/// which one it is reading. Getting it wrong does not fail to parse: it decodes
1579/// every Object under a Group ID that walks the wrong way.
1580#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1581pub enum GroupOrder {
1582    /// Group IDs increase: a delta adds to the prior Group ID.
1583    Ascending,
1584    /// Group IDs decrease: a delta subtracts from the prior Group ID.
1585    Descending,
1586}
1587
1588/// One frame from a FETCH stream with its delta-encoded fields resolved.
1589///
1590/// The header is kept alongside the resolved values so that a caller can
1591/// forward the frame's bytes unchanged while acting on what they mean.
1592#[derive(Debug, Clone, PartialEq, Eq)]
1593pub struct FetchObject {
1594    /// The frame as it appeared on the wire.
1595    pub header: FetchObjectHeader,
1596    /// Resolved absolute Group ID. On an End of Range marker, the Group ID of
1597    /// the Location the marker names.
1598    pub group_id: u64,
1599    /// Resolved Subgroup ID. `None` for an End of Range marker, which has
1600    /// none, and for an Object whose forwarding preference is Datagram.
1601    pub subgroup_id: Option<u64>,
1602    /// Resolved absolute Object ID. On an End of Range marker, the Object ID of
1603    /// the Location the marker names.
1604    pub object_id: u64,
1605    /// The Publisher Priority in force for this frame, whether this frame wrote
1606    /// it or an earlier one did, and `None` while no frame has written one.
1607    ///
1608    /// An End of Range marker carries no Priority field of its own, so what it
1609    /// reports is the one still in force from the last Object before it —
1610    /// Section 11.4.4.2: "Prior Priority: The Priority from the last actual
1611    /// Object before the End of Range indicator."
1612    ///
1613    /// The draft-19 fallback for a subscription that never stated a priority is
1614    /// left to the caller rather than substituted here, so that "no frame has
1615    /// said" stays distinguishable from "a frame said 128".
1616    pub publisher_priority: Option<u8>,
1617}
1618
1619/// Resolves the delta-encoded fields of the frames on one FETCH stream.
1620///
1621/// Draft-19 Section 11.4.4.1 defines nearly every field of a fetch frame
1622/// against "the prior Object", so no frame after the first can be understood on
1623/// its own. This holds what the frames so far established, in the two parts the
1624/// draft keeps separate: Section 11.4.4.2 says that after an End of Range
1625/// marker the prior Group ID and Object ID are the marker's, while the prior
1626/// Subgroup ID and Priority are still "from the last actual Object before the
1627/// End of Range indicator".
1628///
1629/// Every rule the section answers with a PROTOCOL_VIOLATION is refused here
1630/// with [`CodecError::InvalidField`]: a first Object that references fields no
1631/// prior Object established, a Subgroup ID or Priority inherited when there is
1632/// none to inherit, and an arithmetic result outside the 64-bit range.
1633#[derive(Debug, Clone)]
1634pub struct FetchObjectReader {
1635    group_order: GroupOrder,
1636    /// Group ID and Object ID of the last frame, marker or Object.
1637    prior_location: Option<(u64, u64)>,
1638    /// Subgroup ID of the last actual Object that had one.
1639    prior_subgroup_id: Option<u64>,
1640    /// Publisher Priority of the last actual Object.
1641    prior_publisher_priority: Option<u8>,
1642}
1643
1644impl FetchObjectReader {
1645    /// A reader for a stream whose Groups arrive in `group_order`.
1646    pub fn new(group_order: GroupOrder) -> Self {
1647        Self {
1648            group_order,
1649            prior_location: None,
1650            prior_subgroup_id: None,
1651            prior_publisher_priority: None,
1652        }
1653    }
1654
1655    /// Decode the next frame's header and resolve its fields.
1656    ///
1657    /// Consumes the header only. The Object Payload is
1658    /// `header.payload_length` bytes and stays in `buf`, so a caller that
1659    /// forwards payloads never copies them and one that ignores them can skip.
1660    ///
1661    /// Errors with [`CodecError::InvalidField`] on every rule
1662    /// Section 11.4.4.1 states:
1663    ///
1664    /// - "The first Object MUST include a Group ID Delta and Object ID Delta,
1665    ///   and these values are the absolute Group ID and Object ID. If the first
1666    ///   Object in the FETCH response uses a flag that references fields in the
1667    ///   prior Object, the Subscriber MUST close the session with a
1668    ///   PROTOCOL_VIOLATION." Each such flag is refused where it is read, so
1669    ///   the reason survives: a missing delta, an inherited Priority and an
1670    ///   inherited Subgroup ID are three different frames, all of them
1671    ///   referencing an Object that does not exist.
1672    /// - "If the computed Group ID would be less than 0 or greater than
1673    ///   2^64-1, the Subscriber MUST close the Session with error
1674    ///   'PROTOCOL_VIOLATION'" — the descending and ascending ends of the same
1675    ///   rule.
1676    /// - "If the computed Object ID would be greater than 2^64-1, the
1677    ///   Subscriber MUST close the Session with error 'PROTOCOL_VIOLATION'."
1678    pub fn read_object_header(&mut self, buf: &mut impl Buf) -> Result<FetchObject, CodecError> {
1679        let header = FetchObjectHeader::decode(buf)?;
1680
1681        // An End of Range marker states a Location outright and inherits
1682        // nothing, so it is resolved before any of the prior-Object rules.
1683        // Section 11.4.4.2 reads its two delta fields as the absolute Group ID
1684        // and Object ID of the end of the range, so nothing is added to them.
1685        if header.end_of_range().is_some() {
1686            let group_id = header.group_id_delta.ok_or(CodecError::InvalidField)?.into_inner();
1687            let object_id = header.object_id_delta.ok_or(CodecError::InvalidField)?.into_inner();
1688            self.prior_location = Some((group_id, object_id));
1689            let publisher_priority = self.prior_publisher_priority;
1690            return Ok(FetchObject {
1691                header,
1692                group_id,
1693                subgroup_id: None,
1694                object_id,
1695                publisher_priority,
1696            });
1697        }
1698
1699        let group_id = match (self.prior_location, header.group_id_delta) {
1700            // The first object's delta is its absolute Group ID.
1701            (None, Some(delta)) => delta.into_inner(),
1702            (None, None) => return Err(CodecError::InvalidField),
1703            (Some((prior_group, _)), None) => prior_group,
1704            (Some((prior_group, _)), Some(delta)) => {
1705                let delta = delta.into_inner();
1706                match self.group_order {
1707                    GroupOrder::Ascending => prior_group
1708                        .checked_add(delta)
1709                        .and_then(|v| v.checked_add(1))
1710                        .ok_or(CodecError::InvalidField)?,
1711                    GroupOrder::Descending => prior_group
1712                        .checked_sub(delta)
1713                        .and_then(|v| v.checked_sub(1))
1714                        .ok_or(CodecError::InvalidField)?,
1715                }
1716            }
1717        };
1718
1719        let object_id =
1720            match (self.prior_location, header.group_id_delta.is_some(), header.object_id_delta) {
1721                // A Group ID Delta restarts the Object ID from its own delta,
1722                // which is why a new group does not continue the previous
1723                // group's numbering.
1724                (_, true, Some(delta)) => delta.into_inner(),
1725                (Some((_, prior_object)), false, Some(delta)) => {
1726                    prior_object.checked_add(delta.into_inner()).ok_or(CodecError::InvalidField)?
1727                }
1728                (Some((_, prior_object)), _, None) => {
1729                    prior_object.checked_add(1).ok_or(CodecError::InvalidField)?
1730                }
1731                (None, _, _) => return Err(CodecError::InvalidField),
1732            };
1733
1734        let subgroup_id = if header.is_datagram() {
1735            None
1736        } else {
1737            Some(match header.subgroup_id_mode() {
1738                0x00 => 0,
1739                0x01 => self.prior_subgroup_id.ok_or(CodecError::InvalidField)?,
1740                0x02 => self
1741                    .prior_subgroup_id
1742                    .ok_or(CodecError::InvalidField)?
1743                    .checked_add(1)
1744                    .ok_or(CodecError::InvalidField)?,
1745                // Mode 0b11, the only value left: the field is on the wire.
1746                _ => header.subgroup_id.ok_or(CodecError::InvalidField)?.into_inner(),
1747            })
1748        };
1749
1750        let publisher_priority = match header.publisher_priority {
1751            Some(p) => p,
1752            None => self.prior_publisher_priority.ok_or(CodecError::InvalidField)?,
1753        };
1754
1755        self.prior_location = Some((group_id, object_id));
1756        // A Datagram-forwarded object has no Subgroup ID to leave behind, so it
1757        // does not clear the running one: the object after it inherits from the
1758        // last object that had one.
1759        if let Some(subgroup_id) = subgroup_id {
1760            self.prior_subgroup_id = Some(subgroup_id);
1761        }
1762        self.prior_publisher_priority = Some(publisher_priority);
1763
1764        Ok(FetchObject {
1765            header,
1766            group_id,
1767            subgroup_id,
1768            object_id,
1769            publisher_priority: Some(publisher_priority),
1770        })
1771    }
1772}
1773
1774/// Re-encodes resolved fetch frames onto one FETCH stream.
1775///
1776/// The exact inverse of [`FetchObjectReader`], and it exists for one caller:
1777/// something that has read a stream and is writing a different stream from the
1778/// same frames. Removing a frame changes what the frames after it are encoded
1779/// *against*, and draft-19 Section 11.4.4.1 defines nearly every field against
1780/// "the prior Object", so the survivor that follows a removed run cannot keep
1781/// its original bytes. What has to change is not one field: an Object that
1782/// carried no Group ID Delta because it shared its predecessor's group needs
1783/// one once that predecessor is gone, so a field appears and a flag bit with
1784/// it.
1785///
1786/// # Why this is not a general encoder
1787///
1788/// Every frame it writes came off a stream, so the caller already holds the
1789/// frame's own [`FetchObjectHeader`] alongside the resolved values. That header
1790/// is used as the preference: wherever the original shape still encodes the
1791/// same meaning against the new predecessor, it is kept, so a stream with
1792/// nothing removed from it is reproduced byte for byte. Only where the original
1793/// shape would now decode to something else is a different one chosen. An
1794/// encoder built from the resolved values alone could not do that — it would
1795/// have to invent a canonical form and would rewrite every frame on a stream
1796/// that needed no rewriting at all.
1797///
1798/// # What it refuses
1799///
1800/// [`CodecError::InvalidField`] where no encoding exists rather than picking
1801/// one: a Group ID that moves against the FETCH's Group Order, an Object ID
1802/// that does not advance, an Object with neither a Subgroup ID nor the Datagram
1803/// bit, and the arithmetic overflows. Each of these is a frame this writer was
1804/// handed that no draft-19 stream could carry, and inventing a value for it
1805/// would put a different Object on the wire than the one it was given.
1806#[derive(Debug, Clone)]
1807pub struct FetchObjectWriter {
1808    group_order: GroupOrder,
1809    /// Group ID and Object ID of the last frame written, marker or Object.
1810    prior_location: Option<(u64, u64)>,
1811    /// Subgroup ID of the last actual Object written that had one.
1812    prior_subgroup_id: Option<u64>,
1813    /// Publisher Priority of the last actual Object written.
1814    prior_publisher_priority: Option<u8>,
1815}
1816
1817impl FetchObjectWriter {
1818    /// A writer for a stream whose Groups are being written in `group_order`.
1819    ///
1820    /// The order has to match the one the FETCH was opened with, for the same
1821    /// reason [`FetchObjectReader::new`] takes it: it decides whether a Group
1822    /// ID Delta adds or subtracts, and it is not on the data stream.
1823    pub fn new(group_order: GroupOrder) -> Self {
1824        Self {
1825            group_order,
1826            prior_location: None,
1827            prior_subgroup_id: None,
1828            prior_publisher_priority: None,
1829        }
1830    }
1831
1832    /// The header that encodes `frame` against everything written so far.
1833    ///
1834    /// Does not advance the writer — [`Self::write_object_header`] is the call
1835    /// that does both. Separated so that a caller can measure the bytes a
1836    /// re-encode would take before committing to it.
1837    ///
1838    /// # Errors
1839    ///
1840    /// [`CodecError::InvalidField`] for a frame that cannot be encoded against
1841    /// the current predecessor; see the type's own documentation for the list.
1842    pub fn header_for(&self, frame: &FetchObject) -> Result<FetchObjectHeader, CodecError> {
1843        let original = &frame.header;
1844
1845        // An End of Range marker states its Location outright and inherits
1846        // nothing, so its two fields are the same whatever precedes it and the
1847        // frame is reproduced as it arrived.
1848        if original.end_of_range().is_some() {
1849            return Ok(FetchObjectHeader {
1850                serialization_flags: original.serialization_flags,
1851                group_id_delta: Some(VarInt::from_u64(frame.group_id)?),
1852                subgroup_id: None,
1853                object_id_delta: Some(VarInt::from_u64(frame.object_id)?),
1854                publisher_priority: None,
1855                properties: None,
1856                payload_length: original.payload_length,
1857            });
1858        }
1859
1860        let (group_id_delta, object_id_delta) = self.identity_fields(frame, original)?;
1861        let (subgroup_mode, subgroup_id) = self.subgroup_field(frame, original)?;
1862        let publisher_priority = self.priority_field(frame, original)?;
1863
1864        let mut flags = subgroup_mode;
1865        if original.is_datagram() {
1866            flags |= FETCH_DATAGRAM_BIT;
1867        }
1868        if group_id_delta.is_some() {
1869            flags |= FETCH_GROUP_ID_DELTA_BIT;
1870        }
1871        if object_id_delta.is_some() {
1872            flags |= FETCH_OBJECT_ID_DELTA_BIT;
1873        }
1874        if publisher_priority.is_some() {
1875            flags |= FETCH_PRIORITY_BIT;
1876        }
1877        if original.properties.is_some() {
1878            flags |= FETCH_PROPERTIES_BIT;
1879        }
1880
1881        Ok(FetchObjectHeader {
1882            serialization_flags: VarInt::from_u64(flags)?,
1883            group_id_delta,
1884            subgroup_id,
1885            object_id_delta,
1886            publisher_priority,
1887            properties: original.properties.clone(),
1888            payload_length: original.payload_length,
1889        })
1890    }
1891
1892    /// The Group ID Delta and Object ID Delta fields, as this predecessor needs
1893    /// them.
1894    ///
1895    /// Presence is forced by the frame rather than chosen: a group that differs
1896    /// from the predecessor's has to be stated, and one that matches has to be
1897    /// left off, since a delta of zero means the next group along and not this
1898    /// one. Only the Object ID Delta has a choice to make, and it is made in
1899    /// favour of the shape the frame arrived in.
1900    fn identity_fields(
1901        &self,
1902        frame: &FetchObject,
1903        original: &FetchObjectHeader,
1904    ) -> Result<(Option<VarInt>, Option<VarInt>), CodecError> {
1905        let Some((prior_group, prior_object)) = self.prior_location else {
1906            // Section 11.4.4.1: "The first Object MUST include a Group ID Delta
1907            // and Object ID Delta, and these values are the absolute Group ID
1908            // and Object ID."
1909            return Ok((
1910                Some(VarInt::from_u64(frame.group_id)?),
1911                Some(VarInt::from_u64(frame.object_id)?),
1912            ));
1913        };
1914
1915        if frame.group_id != prior_group {
1916            // A Group ID Delta moves the group by delta + 1, forwards under
1917            // Ascending and backwards under Descending, and when an Object ID
1918            // Delta accompanies it the Object ID is that delta outright rather
1919            // than an advance on the predecessor.
1920            let step = match self.group_order {
1921                GroupOrder::Ascending => frame.group_id.checked_sub(prior_group),
1922                GroupOrder::Descending => prior_group.checked_sub(frame.group_id),
1923            };
1924            let delta = step.and_then(|s| s.checked_sub(1)).ok_or(CodecError::InvalidField)?;
1925
1926            // Omitting the Object ID Delta across a group boundary is legal and
1927            // is a byte shorter. Section 11.4.4.1: "If Object ID Delta is not
1928            // present, the Object ID is the prior Object's ID plus one,
1929            // REGARDLESS OF WHICH GROUP IT BELONGS TO." So an Object that
1930            // continues the numbering into a new group encodes without one --
1931            // the Object ID does not restart at the group boundary unless a
1932            // delta says so.
1933            //
1934            // Gated on the frame's own framing, like the same-group case below,
1935            // so re-emitting a stream reproduces the publisher's bytes instead
1936            // of silently rewriting the shorter form into the longer one. It
1937            // also keeps markers correct without a special case: a marker
1938            // always arrives with an Object ID Delta and never takes this
1939            // branch.
1940            if original.object_id_delta.is_none()
1941                && frame.object_id == prior_object.wrapping_add(1)
1942                && prior_object != u64::MAX
1943            {
1944                return Ok((Some(VarInt::from_u64(delta)?), None));
1945            }
1946
1947            return Ok((Some(VarInt::from_u64(delta)?), Some(VarInt::from_u64(frame.object_id)?)));
1948        }
1949
1950        // Same group. The Object ID is the predecessor's plus the delta, or
1951        // plus one when no delta is written, so an Object that does not advance
1952        // has no encoding at all.
1953        let advance = frame.object_id.checked_sub(prior_object).ok_or(CodecError::InvalidField)?;
1954        if advance == 0 {
1955            return Err(CodecError::InvalidField);
1956        }
1957        if advance == 1 && original.object_id_delta.is_none() {
1958            return Ok((None, None));
1959        }
1960        Ok((None, Some(VarInt::from_u64(advance)?)))
1961    }
1962
1963    /// The Subgroup ID mode bits and the explicit field, if one is needed.
1964    ///
1965    /// The frame's own mode is tried first, so a run of Objects that inherited
1966    /// their Subgroup ID keeps inheriting it and its bytes do not move. Only
1967    /// when the predecessor changed under it does a different mode get chosen,
1968    /// and then the cheapest one that says the right number.
1969    fn subgroup_field(
1970        &self,
1971        frame: &FetchObject,
1972        original: &FetchObjectHeader,
1973    ) -> Result<(u64, Option<VarInt>), CodecError> {
1974        // Section 11.4.4.1 has the subscriber ignore these bits on a
1975        // Datagram-forwarded Object, and no field is on the wire whatever they
1976        // say, so the frame's own bits are carried across untouched.
1977        if original.is_datagram() {
1978            return Ok((original.flags() & FETCH_SUBGROUP_ID_MODE_MASK, None));
1979        }
1980
1981        let subgroup_id = frame.subgroup_id.ok_or(CodecError::InvalidField)?;
1982        let inherits = self.prior_subgroup_id == Some(subgroup_id);
1983        let successor =
1984            self.prior_subgroup_id.is_some_and(|p| p.checked_add(1) == Some(subgroup_id));
1985
1986        // The frame's own mode, kept when it still names this number.
1987        let kept = match original.flags() & FETCH_SUBGROUP_ID_MODE_MASK {
1988            0x00 if subgroup_id == 0 => Some((0x00, None)),
1989            0x01 if inherits => Some((0x01, None)),
1990            0x02 if successor => Some((0x02, None)),
1991            FETCH_SUBGROUP_ID_EXPLICIT => Some((FETCH_SUBGROUP_ID_EXPLICIT, Some(subgroup_id))),
1992            _ => None,
1993        };
1994        let (mode, explicit) = match kept {
1995            Some(pair) => pair,
1996            None if subgroup_id == 0 => (0x00, None),
1997            None if inherits => (0x01, None),
1998            None if successor => (0x02, None),
1999            None => (FETCH_SUBGROUP_ID_EXPLICIT, Some(subgroup_id)),
2000        };
2001        Ok((mode, explicit.map(VarInt::from_u64).transpose()?))
2002    }
2003
2004    /// The Publisher Priority field, or `None` when the predecessor already
2005    /// carries it.
2006    ///
2007    /// Written whenever the frame wrote one, so a publisher that stated a
2008    /// priority on every Object keeps its bytes, and written anyway when the
2009    /// predecessor's differs or when there is no predecessor to inherit from.
2010    fn priority_field(
2011        &self,
2012        frame: &FetchObject,
2013        original: &FetchObjectHeader,
2014    ) -> Result<Option<u8>, CodecError> {
2015        let priority = frame.publisher_priority.ok_or(CodecError::InvalidField)?;
2016        if original.publisher_priority.is_some() || self.prior_publisher_priority != Some(priority)
2017        {
2018            return Ok(Some(priority));
2019        }
2020        Ok(None)
2021    }
2022
2023    /// Encode `frame` against everything written so far and advance.
2024    ///
2025    /// Writes the header only. The payload is `frame.header.payload_length`
2026    /// bytes and is the caller's to copy, unchanged — nothing about it depends
2027    /// on what preceded the Object.
2028    ///
2029    /// # Errors
2030    ///
2031    /// [`CodecError::InvalidField`] for a frame with no encoding against the
2032    /// current predecessor. The writer is left untouched when this happens, so
2033    /// a caller that gives up on one frame and carries on with the next is
2034    /// writing against the same predecessor it thought it was.
2035    pub fn write_object_header(
2036        &mut self,
2037        frame: &FetchObject,
2038        out: &mut impl BufMut,
2039    ) -> Result<FetchObjectHeader, CodecError> {
2040        let header = self.header_for(frame)?;
2041        header.encode(out)?;
2042        self.advance(frame);
2043        Ok(header)
2044    }
2045
2046    /// Record `frame` as the predecessor of whatever is written next.
2047    ///
2048    /// Split from the write so that a caller re-emitting bytes it already holds
2049    /// can advance without producing a header twice — which is what happens
2050    /// whenever the framing a frame arrived in still encodes the same meaning
2051    /// against the frame before it, and is why this is public.
2052    pub fn advance(&mut self, frame: &FetchObject) {
2053        self.prior_location = Some((frame.group_id, frame.object_id));
2054        // Mirrors the reader: a Datagram-forwarded Object leaves no Subgroup ID
2055        // behind, so the running one survives it.
2056        if let Some(subgroup_id) = frame.subgroup_id {
2057            self.prior_subgroup_id = Some(subgroup_id);
2058        }
2059        if frame.header.end_of_range().is_none() {
2060            if let Some(priority) = frame.publisher_priority {
2061                self.prior_publisher_priority = Some(priority);
2062            }
2063        }
2064    }
2065}
2066
2067#[cfg(test)]
2068mod tests {
2069    use super::*;
2070
2071    /// Canonically encoded subgroup stream vectors from
2072    /// `test-vectors/transport/draft19/codec/data-streams/subgroup.json`.
2073    /// `subgroup-explicit-subgroup-id` is omitted: it encodes group_id 100 as a
2074    /// two-byte varint, which does not survive a minimal-width re-encode.
2075    const VECTORS: &[&str] = &[
2076        // subgroup-single-object
2077        "100100800004deadbeef",
2078        // subgroup-two-objects
2079        "100100800004deadbeef0002cafe",
2080        // subgroup-no-priority
2081        "3001000004deadbeef",
2082        // subgroup-with-extensions
2083        "11010080000004deadbeef",
2084        // subgroup-end-of-group
2085        "180105800004deadbeef",
2086        // subgroup-id-mode-01
2087        "120100800504deadbeef",
2088        // subgroup-with-object-properties
2089        "1101008000043c02020104deadbeef",
2090        // subgroup-object-status-end-of-group
2091        "100100800004deadbeef000003",
2092        // subgroup-object-status-end-of-track
2093        "10010080000004",
2094        // subgroup-properties-two-objects-empty
2095        "11010080000004deadbeef000002cafe",
2096        // subgroup-properties-two-objects-nonempty
2097        "1101008000023c0204deadbeef00023c0302cafe",
2098        // subgroup-properties-status-object
2099        "1101008000023c010003",
2100        // subgroup-first-object-bit
2101        "500100800004deadbeef",
2102        // subgroup-first-object-and-end-of-group
2103        "580102800002cafe",
2104    ];
2105
2106    fn vi(v: u64) -> VarInt {
2107        VarInt::from_u64_moqt(v)
2108    }
2109
2110    fn hex(s: &str) -> Vec<u8> {
2111        (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
2112    }
2113
2114    /// Decode a whole subgroup stream: the header, then every object up to
2115    /// the end of the buffer.
2116    fn decode_all(bytes: &[u8]) -> (SubgroupHeader, Vec<SubgroupObject>) {
2117        let mut cursor = bytes;
2118        let header = SubgroupHeader::decode(&mut cursor)
2119            .unwrap_or_else(|e| panic!("header decode failed: {e:?}"));
2120        let mut reader = SubgroupObjectReader::new(&header);
2121        let mut objects = Vec::new();
2122        while cursor.has_remaining() {
2123            objects.push(
2124                reader
2125                    .read_object(&mut cursor)
2126                    .unwrap_or_else(|e| panic!("object {} decode failed: {e:?}", objects.len())),
2127            );
2128        }
2129        (header, objects)
2130    }
2131
2132    fn encode_all(header: &SubgroupHeader, objects: &[SubgroupObject]) -> Vec<u8> {
2133        let mut buf = Vec::new();
2134        header.encode(&mut buf);
2135        let mut writer = SubgroupObjectReader::new(header);
2136        for o in objects {
2137            writer.write_object(o, &mut buf).unwrap_or_else(|e| panic!("write failed: {e:?}"));
2138        }
2139        buf
2140    }
2141
2142    fn object(id: u64, extensions: Vec<u8>, payload: Vec<u8>) -> SubgroupObject {
2143        SubgroupObject {
2144            object_id: vi(id),
2145            extension_headers: extensions,
2146            payload_length: vi(payload.len() as u64),
2147            object_status: None,
2148            payload,
2149        }
2150    }
2151
2152    // ── Object ID deltas ────────────────────────────────────
2153
2154    #[test]
2155    fn two_objects_with_properties_have_distinct_ids() {
2156        // Vector `subgroup-properties-two-objects-empty`: two objects, each
2157        // carrying an empty properties block and a delta of 0. The delta is
2158        // biased by one whether or not the properties bit is set, so the IDs
2159        // are 0 and 1 — not 0 and 0.
2160        let bytes = hex("11010080000004deadbeef000002cafe");
2161        let (header, objects) = decode_all(&bytes);
2162        assert!(header.has_properties());
2163        assert_eq!(objects.len(), 2);
2164        assert_eq!(objects[0].object_id.into_inner(), 0);
2165        assert_eq!(objects[1].object_id.into_inner(), 1);
2166        assert_eq!(objects[0].payload, hex("deadbeef"));
2167        assert_eq!(objects[1].payload, hex("cafe"));
2168        assert!(objects.iter().all(|o| o.extension_headers.is_empty()));
2169    }
2170
2171    #[test]
2172    fn deltas_resolve_sparse_ids() {
2173        let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
2174        let objects: Vec<_> =
2175            [3u64, 4, 40].iter().map(|&id| object(id, vec![], vec![0xAA, id as u8])).collect();
2176        let (_, decoded) = decode_all(&encode_all(&header, &objects));
2177        let ids: Vec<u64> = decoded.iter().map(|o| o.object_id.into_inner()).collect();
2178        assert_eq!(ids, vec![3, 4, 40]);
2179    }
2180
2181    #[test]
2182    fn write_rejects_non_increasing_ids() {
2183        let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
2184        let mut writer = SubgroupObjectReader::new(&header);
2185        let mut buf = Vec::new();
2186        writer.write_object(&object(7, vec![], vec![0x01]), &mut buf).unwrap();
2187        for id in [7u64, 6, 0] {
2188            let err = writer.write_object(&object(id, vec![], vec![0x01]), &mut buf).unwrap_err();
2189            assert!(matches!(err, CodecError::InvalidField), "id {id} gave {err:?}");
2190        }
2191    }
2192
2193    #[test]
2194    fn eliding_an_object_renumbers_its_successor() {
2195        let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
2196        let all: Vec<_> = (0..5u64).map(|id| object(id, vec![], vec![id as u8])).collect();
2197        for elided in 0..5u64 {
2198            let kept: Vec<_> =
2199                all.iter().filter(|o| o.object_id.into_inner() != elided).cloned().collect();
2200            let (_, decoded) = decode_all(&encode_all(&header, &kept));
2201            let ids: Vec<u64> = decoded.iter().map(|o| o.object_id.into_inner()).collect();
2202            let expected: Vec<u64> = (0..5u64).filter(|&i| i != elided).collect();
2203            assert_eq!(ids, expected, "eliding object {elided}");
2204        }
2205    }
2206
2207    // ── Properties blocks ──────────────────────────────
2208
2209    #[test]
2210    fn properties_blob_excludes_its_length_prefix() {
2211        // Vector `subgroup-properties-two-objects-nonempty`: each
2212        // object carries a two-byte block, so the blob is those two bytes
2213        // with the `02` length prefix stripped.
2214        let bytes = hex("1101008000023c0204deadbeef00023c0302cafe");
2215        let (_, objects) = decode_all(&bytes);
2216        assert_eq!(objects.len(), 2);
2217        assert_eq!(objects[0].object_id.into_inner(), 0);
2218        assert_eq!(objects[1].object_id.into_inner(), 1);
2219        assert_eq!(objects[0].extension_headers, hex("3c02"));
2220        assert_eq!(objects[1].extension_headers, hex("3c03"));
2221        assert_eq!(objects[0].payload, hex("deadbeef"));
2222        assert_eq!(objects[1].payload, hex("cafe"));
2223    }
2224
2225    #[test]
2226    fn status_object_carries_its_properties_block() {
2227        let (_, objects) = decode_all(&hex("1101008000023c010003"));
2228        assert_eq!(objects.len(), 1);
2229        assert_eq!(objects[0].extension_headers, hex("3c01"));
2230        assert_eq!(objects[0].payload_length.into_inner(), 0);
2231        assert_eq!(objects[0].object_status.map(ObjectStatus::as_u64), Some(3));
2232        assert!(objects[0].payload.is_empty());
2233    }
2234
2235    // ── Re-encoding ─────────────────────────────────────────
2236
2237    #[test]
2238    fn vectors_re_encode_byte_identically() {
2239        for vector in VECTORS {
2240            let bytes = hex(vector);
2241            let (header, objects) = decode_all(&bytes);
2242            assert_eq!(encode_all(&header, &objects), bytes, "[{vector}] re-encode");
2243        }
2244    }
2245
2246    // ── Payload-free framing ────────────────────────────────
2247
2248    #[test]
2249    fn meta_matches_read_object() {
2250        for vector in VECTORS {
2251            let bytes = hex(vector);
2252            let mut cursor = &bytes[..];
2253            let header = SubgroupHeader::decode(&mut cursor).unwrap();
2254            let mut full_reader = SubgroupObjectReader::new(&header);
2255            let mut meta_reader = SubgroupObjectReader::new(&header);
2256            let mut full_cursor = cursor;
2257            let mut meta_cursor = cursor;
2258            while meta_cursor.has_remaining() {
2259                let before = meta_cursor.remaining();
2260                let object = full_reader.read_object(&mut full_cursor).unwrap();
2261                let meta = meta_reader.read_object_meta(&mut meta_cursor).unwrap();
2262                assert_eq!(meta.object_id, object.object_id.into_inner(), "[{vector}]");
2263                assert_eq!(
2264                    meta.extension_headers_len,
2265                    object.extension_headers.len() as u64,
2266                    "[{vector}]"
2267                );
2268                assert_eq!(meta.payload_length, object.payload_length.into_inner(), "[{vector}]");
2269                assert_eq!(
2270                    meta.status,
2271                    object.object_status.map(ObjectStatus::as_u64),
2272                    "[{vector}]"
2273                );
2274                assert_eq!(meta.wire_len, (before - meta_cursor.remaining()) as u64, "[{vector}]");
2275                assert_eq!(full_cursor.remaining(), meta_cursor.remaining(), "[{vector}]");
2276            }
2277        }
2278    }
2279
2280    #[test]
2281    fn short_buffers_report_unexpected_end() {
2282        let bytes = hex("1101008000023c0204deadbeef00023c0302cafe");
2283        let mut cursor = &bytes[..];
2284        let header = SubgroupHeader::decode(&mut cursor).unwrap();
2285        let objects_start = bytes.len() - cursor.len();
2286        for cut in objects_start..bytes.len() {
2287            let mut reader = SubgroupObjectReader::new(&header);
2288            let mut meta_reader = SubgroupObjectReader::new(&header);
2289            let mut cursor = &bytes[objects_start..cut];
2290            let mut meta_cursor = cursor;
2291            while cursor.has_remaining() {
2292                if let Err(err) = reader.read_object(&mut cursor) {
2293                    assert!(
2294                        matches!(err, CodecError::UnexpectedEnd | CodecError::VarInt(_)),
2295                        "cut {cut} gave {err:?}"
2296                    );
2297                    break;
2298                }
2299            }
2300            while meta_cursor.has_remaining() {
2301                if let Err(err) = meta_reader.read_object_meta(&mut meta_cursor) {
2302                    assert!(
2303                        matches!(err, CodecError::UnexpectedEnd | CodecError::VarInt(_)),
2304                        "cut {cut} gave {err:?}"
2305                    );
2306                    break;
2307                }
2308            }
2309        }
2310    }
2311
2312    // ── Object status ───────────────────────────────────────
2313
2314    /// A one-object subgroup stream whose object carries `status` in place of
2315    /// a payload: header type 0x10 (no properties, subgroup-ID mode 0, no
2316    /// FIRST_OBJECT bit), track alias 1, group 0, publisher priority 128; then
2317    /// an Object ID delta of 0, a payload length of 0, and the status code.
2318    fn subgroup_status_stream(status: u64) -> Vec<u8> {
2319        vec![0x10, 0x01, 0x00, 0x80, 0x00, 0x00, status as u8]
2320    }
2321
2322    /// A status datagram carrying `status`: type 0x20 (STATUS bit set,
2323    /// explicit Object ID, explicit priority), track alias 1, group 0, object
2324    /// 0, priority 128, then the status byte.
2325    fn status_datagram(status: u64) -> Vec<u8> {
2326        vec![0x20, 0x01, 0x00, 0x00, 0x80, status as u8]
2327    }
2328
2329    /// The object [`subgroup_status_stream`] describes, as a value.
2330    fn status_object(status: Option<ObjectStatus>) -> SubgroupObject {
2331        SubgroupObject {
2332            object_id: vi(0),
2333            extension_headers: Vec::new(),
2334            payload_length: vi(0),
2335            object_status: status,
2336            payload: Vec::new(),
2337        }
2338    }
2339
2340    /// An object carrying both `payload` and, in the caller's hands, `status`.
2341    /// The wire has no room for both, which is what the registry rules on.
2342    fn payload_object(status: Option<ObjectStatus>, payload: Vec<u8>) -> SubgroupObject {
2343        SubgroupObject {
2344            object_id: vi(0),
2345            extension_headers: Vec::new(),
2346            payload_length: vi(payload.len() as u64),
2347            object_status: status,
2348            payload,
2349        }
2350    }
2351
2352    /// The datagram [`status_datagram`] describes, as a value.
2353    fn status_datagram_header(status: Option<ObjectStatus>) -> DatagramHeader {
2354        DatagramHeader {
2355            datagram_type: 0x20,
2356            track_alias: vi(1),
2357            group_id: vi(0),
2358            object_id: vi(0),
2359            publisher_priority: Some(128),
2360            properties: Vec::new(),
2361            object_status: status,
2362        }
2363    }
2364
2365    /// Every status draft-19 assigns can be written and read back as the same
2366    /// status, on both a subgroup stream and a status datagram.
2367    ///
2368    /// The set is read from `ObjectStatus::ALL` — the three rows of the Object
2369    /// Status registry — rather than restated here, so this moves with the
2370    /// draft if a code is ever reassigned. It is the gate on typing the two
2371    /// `object_status` fields: a typed field that silently narrowed or
2372    /// renumbered the set would fail here even though it still compiled.
2373    ///
2374    /// Writing `ObjectStatus::Normal` when a zero-length object's status is
2375    /// `None` is checked too — without it the encoder emits an object whose
2376    /// declared payload length promises a status field that never arrives.
2377    ///
2378    /// Made `write_object` encode a constant `ObjectStatus::Normal` instead of
2379    /// the object's own status, ran it, and got:
2380    ///
2381    /// ```text
2382    /// assertion `left == right` failed: subgroup object status
2383    ///   left: Some(Normal)
2384    ///  right: Some(EndOfGroup)
2385    /// ```
2386    ///
2387    /// The same change to `DatagramHeader::encode` gives:
2388    ///
2389    /// ```text
2390    /// assertion `left == right` failed: datagram object status
2391    ///   left: Some(Normal)
2392    ///  right: Some(EndOfGroup)
2393    /// ```
2394    #[test]
2395    fn every_assigned_status_survives_a_round_trip() {
2396        let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
2397        for &status in ObjectStatus::ALL {
2398            let mut buf = Vec::new();
2399            SubgroupObjectReader::new(&header)
2400                .write_object(&status_object(Some(status)), &mut buf)
2401                .unwrap_or_else(|e| panic!("write_object refused {status:?}: {e:?}"));
2402
2403            let mut cursor = &buf[..];
2404            let object =
2405                SubgroupObjectReader::new(&header).read_object(&mut cursor).unwrap_or_else(|e| {
2406                    panic!("read_object refused the bytes written for {status:?}: {e:?}")
2407                });
2408            assert_eq!(object.object_status, Some(status), "subgroup object status");
2409            assert!(!cursor.has_remaining(), "{status:?}: bytes left over after read_object");
2410
2411            let meta =
2412                SubgroupObjectReader::new(&header).read_object_meta(&mut &buf[..]).unwrap_or_else(
2413                    |e| panic!("read_object_meta refused the bytes written for {status:?}: {e:?}"),
2414                );
2415            assert_eq!(meta.status, Some(status.as_u64()), "subgroup meta status");
2416
2417            let mut datagram = Vec::new();
2418            status_datagram_header(Some(status)).encode(&mut datagram);
2419            let decoded = DatagramHeader::decode(&mut &datagram[..]).unwrap_or_else(|e| {
2420                panic!("datagram decode refused the bytes written for {status:?}: {e:?}")
2421            });
2422            assert_eq!(decoded.object_status, Some(status), "datagram object status");
2423        }
2424
2425        let mut buf = Vec::new();
2426        SubgroupObjectReader::new(&header).write_object(&status_object(None), &mut buf).unwrap();
2427        let object = SubgroupObjectReader::new(&header)
2428            .read_object(&mut &buf[..])
2429            .expect("a zero-length object with no status must still decode");
2430        assert_eq!(object.object_status, Some(ObjectStatus::Normal));
2431
2432        let mut datagram = Vec::new();
2433        status_datagram_header(None).encode(&mut datagram);
2434        let decoded = DatagramHeader::decode(&mut &datagram[..])
2435            .expect("a status datagram with no status must still decode");
2436        assert_eq!(decoded.object_status, Some(ObjectStatus::Normal));
2437    }
2438
2439    /// The encoder writes exactly the frames the decoder accepts.
2440    ///
2441    /// Sweeps every status code `0x00..=0x3f` — one wire byte under both the
2442    /// varint on a subgroup stream and the bare byte on a datagram, and wide
2443    /// enough to contain the gap at `0x2` and the `0x1` draft-16 dropped. For
2444    /// a code the draft assigns, the hand-built frame must decode *and* the
2445    /// encoder handed that status must reproduce those exact bytes. For a code
2446    /// it does not assign, the same frame must be refused at all three decode
2447    /// sites — and no `ObjectStatus` exists to hand the encoder, so the frame
2448    /// has no way to be produced in the first place.
2449    ///
2450    /// This is a gate on the status code points only. Which of those statuses
2451    /// may carry a payload is the separate question
2452    /// [`the_registry_decides_which_statuses_may_carry_a_payload`] gates.
2453    ///
2454    /// # What this catches, observed by making each change and running it
2455    ///
2456    /// Encoding a constant `ObjectStatus::Normal` in `write_object` instead of
2457    /// the object's own status:
2458    ///
2459    /// ```text
2460    /// assertion `left == right` failed: the encoder must produce the frame the decoder accepted for status 0x3
2461    ///   left: [16, 1, 0, 128, 0, 0, 0]
2462    ///  right: [16, 1, 0, 128, 0, 0, 3]
2463    /// ```
2464    ///
2465    /// The same change in `DatagramHeader::encode`:
2466    ///
2467    /// ```text
2468    /// assertion `left == right` failed: the encoder must produce the datagram the decoder accepted for status 0x3
2469    ///   left: [32, 1, 0, 0, 128, 0]
2470    ///  right: [32, 1, 0, 0, 128, 3]
2471    /// ```
2472    ///
2473    /// The decoder drifting away from `ALL` — adding `0x2` to
2474    /// `ObjectStatus::from_u64`, so a code the draft does not assign starts
2475    /// decoding:
2476    ///
2477    /// ```text
2478    /// subgroup read_object accepted status 0x2, which the draft does not assign
2479    /// ```
2480    ///
2481    /// # The encode-side refusal is a type, not an assertion
2482    ///
2483    /// Once `object_status` is typed there is no runtime path that offers the
2484    /// encoder the `0x2` that this module's decoder is documented as refusing,
2485    /// so no test here can watch one be refused. Reverting
2486    /// `DatagramHeader::object_status` to `Option<u8>` with an `unwrap_or(0)`
2487    /// encoder does not make this test fail — it makes it stop compiling,
2488    /// which is the guarantee:
2489    ///
2490    /// ```text
2491    /// error[E0308]: mismatched types
2492    ///     = note: expected enum `Option<u8>`
2493    ///                found enum `Option<draft19::types::ObjectStatus>`
2494    /// ```
2495    #[test]
2496    fn the_encoder_writes_exactly_the_frames_the_decoder_accepts() {
2497        for code in 0x00u64..=0x3f {
2498            let assigned = ObjectStatus::ALL.iter().copied().find(|s| s.as_u64() == code);
2499
2500            let stream = subgroup_status_stream(code);
2501            let mut cursor: &[u8] = &stream;
2502            let header = SubgroupHeader::decode(&mut cursor).unwrap();
2503            let objects = cursor;
2504            let read = SubgroupObjectReader::new(&header).read_object(&mut { objects });
2505            let meta = SubgroupObjectReader::new(&header).read_object_meta(&mut { objects });
2506
2507            let datagram = status_datagram(code);
2508            let decoded = DatagramHeader::decode(&mut &datagram[..]);
2509
2510            match assigned {
2511                Some(status) => {
2512                    let object = read.unwrap_or_else(|e| {
2513                        panic!(
2514                            "read_object refused status {code:#x}, which the draft assigns: {e:?}"
2515                        )
2516                    });
2517                    assert_eq!(object.object_status, Some(status));
2518                    assert_eq!(meta.unwrap().status, Some(code));
2519                    assert_eq!(decoded.unwrap().object_status, Some(status));
2520
2521                    let mut written = Vec::new();
2522                    header.encode(&mut written);
2523                    SubgroupObjectReader::new(&header)
2524                        .write_object(&status_object(Some(status)), &mut written)
2525                        .unwrap();
2526                    assert_eq!(
2527                        written, stream,
2528                        "the encoder must produce the frame the decoder accepted for status {code:#x}"
2529                    );
2530
2531                    let mut written = Vec::new();
2532                    status_datagram_header(Some(status)).encode(&mut written);
2533                    assert_eq!(
2534                        written, datagram,
2535                        "the encoder must produce the datagram the decoder accepted for status {code:#x}"
2536                    );
2537                }
2538                None => {
2539                    for (site, result) in [
2540                        ("subgroup read_object", read.map(|_| ())),
2541                        ("subgroup read_object_meta", meta.map(|_| ())),
2542                        ("status datagram", decoded.map(|_| ())),
2543                    ] {
2544                        match result {
2545                            Ok(()) => panic!(
2546                                "{site} accepted status {code:#x}, which the draft does not assign"
2547                            ),
2548                            Err(error) => assert!(
2549                                matches!(error, CodecError::InvalidField),
2550                                "{site} refused status {code:#x} with {error:?}, not InvalidField"
2551                            ),
2552                        }
2553                    }
2554                }
2555            }
2556        }
2557    }
2558
2559    // ── The registry's payload column ───────────────────────
2560
2561    /// Draft-19 Section 15.9, Table 16, "Payload" column: Normal "Yes", End of
2562    /// Group "No", End of Track "No".
2563    ///
2564    /// Restated here rather than read from [`ObjectStatus::payload_permission`]
2565    /// so the gate holds its own copy of the registry. A codec that changed a
2566    /// row would still agree with itself; only a second copy notices.
2567    const PAYLOAD_COLUMN: &[(ObjectStatus, bool)] = &[
2568        (ObjectStatus::Normal, true),
2569        (ObjectStatus::EndOfGroup, false),
2570        (ObjectStatus::EndOfTrack, false),
2571    ];
2572
2573    /// Which statuses may carry a payload is decided by the registry, not by a
2574    /// payload length.
2575    ///
2576    /// Draft-18 said an Object with any status other than Normal has an empty
2577    /// payload, so the rule could be read off the status number — and this
2578    /// encoder read it off the length instead, which came to the same thing: a
2579    /// zero length was what put a status on the wire, and a status handed in
2580    /// alongside a payload was dropped on the floor. Draft-19 Section 11.2.1.1
2581    /// replaces the blanket rule with "an Object MUST have an empty payload
2582    /// unless its Object Status value is registered as permitting a payload",
2583    /// the permission being a column of the Object Status registry in
2584    /// Section 15.9. The three rows assigned today give the same answers
2585    /// draft-18's rule gave; what this gate observes is that the answers now
2586    /// come from the rows.
2587    ///
2588    /// Each row is driven both ways. A zero-length object with that status must
2589    /// encode and read back — Normal included, since it permits a payload
2590    /// without requiring one and so has to stay expressible with none. An
2591    /// object handed a payload under that status must be accepted exactly when
2592    /// the row permits one, and otherwise refused with nothing written.
2593    ///
2594    /// # What this catches, observed by making each change and running it
2595    ///
2596    /// Dropping the registry check from `write_object`, leaving the length to
2597    /// decide as it did before — an End of Group object with a payload is then
2598    /// written as a plain payload object and its status is gone:
2599    ///
2600    /// ```text
2601    /// write_object must refuse a payload under EndOfGroup, which the registry forbids one; got Ok(())
2602    /// ```
2603    ///
2604    /// Moving End of Group into the permitting column, as a status registered
2605    /// later with "Payload: Yes" would be:
2606    ///
2607    /// ```text
2608    /// assertion `left == right` failed: decoded EndOfGroup reports the wrong payload permission
2609    ///   left: true
2610    ///  right: false
2611    /// ```
2612    ///
2613    /// Letting the permission pick the framing as well as govern it — writing
2614    /// the status field for the statuses that forbid a payload instead of for
2615    /// the objects whose payload length is zero, which is the wrong reading of
2616    /// the registry and the one that costs the zero-length Normal object its
2617    /// encoding, since the draft frames the status on payload length alone:
2618    ///
2619    /// ```text
2620    /// read_object refused a zero-length Normal: VarInt(UnexpectedEnd)
2621    /// ```
2622    #[test]
2623    fn the_registry_decides_which_statuses_may_carry_a_payload() {
2624        let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
2625        assert_eq!(
2626            PAYLOAD_COLUMN.len(),
2627            ObjectStatus::ALL.len(),
2628            "every assigned status needs a row in the payload column"
2629        );
2630
2631        for &(status, permitted) in PAYLOAD_COLUMN {
2632            assert!(ObjectStatus::ALL.contains(&status), "{status:?} is not an assigned status");
2633
2634            // A zero-length object is legal under every row, and is the only
2635            // framing that states a status on a subgroup stream.
2636            let mut empty = Vec::new();
2637            SubgroupObjectReader::new(&header)
2638                .write_object(&status_object(Some(status)), &mut empty)
2639                .unwrap_or_else(|e| panic!("write_object refused a zero-length {status:?}: {e:?}"));
2640            let object = SubgroupObjectReader::new(&header)
2641                .read_object(&mut &empty[..])
2642                .unwrap_or_else(|e| panic!("read_object refused a zero-length {status:?}: {e:?}"));
2643            assert_eq!(object.status(), status, "zero-length {status:?} lost its status");
2644            assert!(object.payload.is_empty(), "zero-length {status:?} gained a payload");
2645            assert_eq!(
2646                object.permits_payload(),
2647                permitted,
2648                "decoded {status:?} reports the wrong payload permission"
2649            );
2650
2651            // A status datagram is the one carrier where the registry is not
2652            // the last word. Section 11.3.1 puts the status field in the
2653            // payload's place — "When set to 1, the Object Status field is
2654            // present and there is no Object Payload" — so no status makes
2655            // trailing bytes part of such a datagram, Normal included, and
2656            // `permitted` is not the expected answer here.
2657            let datagram = DatagramHeader::decode(&mut &status_datagram(status.as_u64())[..])
2658                .unwrap_or_else(|e| panic!("datagram decode refused {status:?}: {e:?}"));
2659            assert!(
2660                !datagram.permits_payload(),
2661                "a status datagram has no Object Payload field, so {status:?} permits no bytes"
2662            );
2663            assert_eq!(
2664                datagram.status().permits_payload(),
2665                permitted,
2666                "decoded {status:?} datagram reports the wrong registry row"
2667            );
2668            let mut whole = status_datagram(status.as_u64());
2669            whole.extend_from_slice(&hex("deadbeef"));
2670            let trailing = DatagramHeader::decode_object(&mut &whole[..]);
2671            assert!(
2672                matches!(trailing, Err(CodecError::PayloadNotPermitted { .. })),
2673                "trailing bytes on a {status:?} status datagram must be refused; got {trailing:?}"
2674            );
2675
2676            // The same status, handed a payload the wire cannot frame beside it.
2677            let mut written = Vec::new();
2678            let result = SubgroupObjectReader::new(&header)
2679                .write_object(&payload_object(Some(status), hex("deadbeef")), &mut written);
2680
2681            if permitted {
2682                result.unwrap_or_else(|e| {
2683                    panic!(
2684                        "write_object refused a payload under {status:?}, \
2685                         which the registry permits: {e:?}"
2686                    )
2687                });
2688                let object = SubgroupObjectReader::new(&header)
2689                    .read_object(&mut &written[..])
2690                    .unwrap_or_else(|e| {
2691                        panic!("read_object refused its own output for {status:?}: {e:?}")
2692                    });
2693                assert_eq!(object.payload, hex("deadbeef"), "{status:?} lost its payload");
2694                assert_eq!(object.status(), status, "{status:?} came back as another status");
2695            } else {
2696                assert!(
2697                    matches!(result, Err(CodecError::PayloadNotPermitted { .. })),
2698                    "write_object must refuse a payload under {status:?}, \
2699                     which the registry forbids one; got {result:?}"
2700                );
2701                assert!(written.is_empty(), "a refused {status:?} object still wrote {written:?}");
2702            }
2703        }
2704
2705        // A datagram without the STATUS bit is all payload, and the status its
2706        // framing leaves out is the one row that permits a payload.
2707        let plain = DatagramHeader::decode(&mut &[0x00u8, 0x01, 0x00, 0x00, 0x80][..])
2708            .expect("a datagram with no status field must decode");
2709        assert_eq!(plain.status(), ObjectStatus::Normal);
2710        assert!(plain.permits_payload(), "a payload-carrying datagram must be permitted one");
2711    }
2712}