Skip to main content

moqtap_codec/draft18/
data_stream.rs

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