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