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}