moqtap_codec/draft15/data_stream.rs
1//! Draft-15 data stream header encoding and decoding.
2//!
3//! Draft-15 data streams differ significantly from draft-14:
4//! - Subgroup stream types encode flags in the type byte. Section 10.4.2
5//! Table 6 assigns twenty-four: `0x10`-`0x15`, `0x18`-`0x1D`, `0x30`-`0x35`
6//! and `0x38`-`0x3D`.
7//! - Priority is absent when `type & 0x20`, and the object then inherits the
8//! priority the subscription established
9//! - `type & 0x06` decides how the Subgroup ID is carried: it is zero, it is
10//! the first object's ID, or it is a field on the wire. Table 6 states that
11//! as two columns — Subgroup ID Field Present, and Subgroup ID Value — not
12//! as a mode field, and the fourth combination is simply not a row in it
13//! - `type & 0x08` marks a stream whose last object ends its group
14//! - Extensions flag (`type & 0x01`) affects per-object parsing
15//! - Fetch objects use serialization_flags for delta encoding
16//! - Object IDs in subgroups use delta encoding (first=absolute, subsequent=delta+1)
17
18use super::types::ObjectStatus;
19use crate::error::CodecError;
20use crate::varint::VarInt;
21use bytes::{Buf, BufMut};
22
23/// Advance `buf` past `len` bytes without copying them.
24fn skip(buf: &mut impl Buf, len: u64) -> Result<(), CodecError> {
25 let len = usize::try_from(len).map_err(|_| CodecError::UnexpectedEnd)?;
26 if buf.remaining() < len {
27 return Err(CodecError::UnexpectedEnd);
28 }
29 buf.advance(len);
30 Ok(())
31}
32
33/// Turn a wire Object Status code into the status draft-15 gives it, refusing
34/// any code the draft does not assign.
35///
36/// Draft-15 Section 10.2.1.1 lists the codes an object may carry and says any
37/// other value SHOULD be treated as a protocol error and the session
38/// terminated with a PROTOCOL_VIOLATION. Every place this module reads a
39/// status runs the wire code through here.
40///
41/// [`SubgroupObject::object_status`] and [`DatagramHeader::object_status`]
42/// then store the [`ObjectStatus`] this returns rather than the raw code, so
43/// the refusal is not something a future decode site can forget: those fields
44/// cannot hold an unassigned value at all, in either direction, and the encode
45/// paths need no check of their own.
46/// [`SubgroupObjectMeta::status`] deliberately keeps the raw code — it is a
47/// decode-only view that never feeds an encoder — but it is filtered through
48/// here too, so the two readers agree byte for byte on what parses.
49fn decoded_status(code: u64) -> Result<ObjectStatus, CodecError> {
50 ObjectStatus::from_u64(code).ok_or(CodecError::InvalidField)
51}
52
53/// Refuse an object that would be written with extension headers beside a
54/// status other than Normal.
55///
56/// Draft-15 Section 10.2.1.2: "Any Object with status Normal can have extension
57/// headers. If an endpoint receives extension headers on Objects with status
58/// that is not Normal, it MUST close the session with a PROTOCOL_VIOLATION."
59///
60/// Wider than the rule drafts 11 through 14 carry. There, at
61/// draft-14 Section 10.2.1.2, the sentence read "Any Object may have extension
62/// headers except those with Object Status 'Object Does Not Exist'", which left
63/// extensions beside End of Group and End of Track legal; draft-15 is where the
64/// single exception became the general case. Using the earlier wording here
65/// would let an End of Group object carry metadata draft-15 says to close the
66/// session over.
67///
68/// **Called from [`DatagramHeader::encode_checked`] and nowhere else, and
69/// deliberately so.** A frame carrying extensions beside a non-Normal status is
70/// well formed — every length is honest and every field parses — so a decoder
71/// can hand it back intact, and a tool that reproduces a capture has to.
72/// Refusing it on decode would make a captured violation unreadable, which
73/// loses the one artifact anybody debugging it needs.
74///
75/// The same argument bars it from a carrier's only writer.
76/// [`SubgroupObjectReader::write_object`] and
77/// [`FetchObjectReader::write_object_header`] are the sole way to write their
78/// objects, so a refusal there would leave a captured violation impossible to
79/// re-emit; the corpus ships exactly such a subgroup frame. A datagram is the
80/// one carrier with two encoders, so `encode_checked` can refuse while
81/// [`DatagramHeader::encode`] still reproduces bytes verbatim. That is the
82/// whole of the rule: opt-in strictness where an unchecked path exists, and a
83/// predicate — [`SubgroupObject::extensions_permitted`],
84/// [`SubgroupObjectMeta::extensions_permitted`],
85/// [`DatagramHeader::extensions_permitted`],
86/// [`FetchObjectHeader::extensions_permitted`] — everywhere else.
87///
88/// That is why drafts 11 through 14 look different and should stay different:
89/// they state only the narrow Object Does Not Exist form, they apply it on both
90/// sides, and no vector exercises it. Drafts 15 through 19 state the broad form
91/// and enforce it on encode alone. The split is intentional, not an
92/// inconsistency to harmonise away.
93///
94/// `status` is the code the object resolves to, not the field as it appears on
95/// the wire. An object whose framing omits the status field has status Normal —
96/// on a subgroup or fetch stream because its payload length is non-zero, on a
97/// datagram because its type byte leaves the status bit clear — and such an
98/// object may carry extensions. Passing `None` says exactly that.
99fn check_extensions_against_status_on_encode(
100 status: Option<u64>,
101 extension_headers_len: u64,
102) -> Result<(), CodecError> {
103 if extension_headers_len != 0
104 && matches!(status, Some(code) if code != ObjectStatus::Normal.as_u64())
105 {
106 return Err(CodecError::InvalidField);
107 }
108 Ok(())
109}
110
111// ── Payload permission ─────────────────────────────────────
112
113/// Whether an object carrying a given status may hold a non-empty payload.
114///
115/// Draft-15 Section 10.2.1.1 states the rule in one sentence — "Any object with
116/// a status code other than zero MUST have an empty payload" — and Section
117/// 10.2.1 says the same from the other side, listing the Object Payload as
118/// "Only present when 'Object Status' is Normal (0x0)". On this draft the
119/// answer therefore falls out of the code being zero or not, and every status
120/// but [`ObjectStatus::Normal`] forbids a payload.
121///
122/// It is worth a type all the same, because that arithmetic is not something a
123/// consumer can safely perform on a raw wire code. A code draft-15 does not
124/// assign is not *non-zero, and therefore forbidden*: it is a code with no
125/// meaning at all, and the draft's answer to it is to terminate the session,
126/// not to infer a payload rule. Handing back a `PayloadPermission` keeps the
127/// two apart, and lets a caller ask the question without restating the rule —
128/// or, worse, restating it slightly differently.
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum PayloadPermission {
131 /// The status permits a payload but does not require one: a zero-length
132 /// object with such a status is well formed, and draft-15's encodings have
133 /// a way to spell it.
134 Permitted,
135 /// An object with such a status has an empty payload, and one carrying
136 /// bytes is malformed.
137 Forbidden,
138}
139
140impl PayloadPermission {
141 /// The permission draft-15 gives objects carrying `status`.
142 ///
143 /// Written as a match over every assigned status rather than as a test for
144 /// zero, so that a status added to [`ObjectStatus`] later cannot quietly
145 /// inherit *not Normal, therefore forbidden* — it stops the crate compiling
146 /// until its own answer is written down. Drafts after 15 moved this rule
147 /// into a column of the Object Status registry, where a future status may
148 /// well permit a payload; the shape here does not have to change when a
149 /// caller crosses that boundary.
150 pub fn for_status(status: ObjectStatus) -> Self {
151 match status {
152 ObjectStatus::Normal => PayloadPermission::Permitted,
153 ObjectStatus::ObjectDoesNotExist => PayloadPermission::Forbidden,
154 ObjectStatus::EndOfGroup => PayloadPermission::Forbidden,
155 ObjectStatus::EndOfTrack => PayloadPermission::Forbidden,
156 }
157 }
158
159 /// `true` for [`PayloadPermission::Permitted`].
160 ///
161 /// The permission answers on its own, with no payload length in hand,
162 /// which is the point of asking the status rather than the framing.
163 pub fn permits(self) -> bool {
164 matches!(self, PayloadPermission::Permitted)
165 }
166}
167
168// ── Subgroup streams ───────────────────────────────────────
169
170/// Subgroup stream header for draft-15.
171///
172/// Draft-15 Section 10.4.2 Table 6 assigns twenty-four stream types, built from
173/// a base of `0x10` or `0x30` and three independent fields:
174/// - `& 0x01`: extensions present on objects
175/// - `& 0x06`: how the Subgroup ID is carried — `0x00` it is zero, `0x02` it
176/// is the Object ID of the first object on the stream and is not
177/// transmitted, `0x04` it is a field on the wire. `0x06` is not a row
178/// - `& 0x08`: the last object on the stream ends its group
179/// - `& 0x20`: no publisher priority; the object inherits the one the
180/// subscription established
181///
182/// **These two bits are read together, not as two independent flags**, and the
183/// table is what settles it: it gives them as a pair of columns, Subgroup ID
184/// Field Present and Subgroup ID Value, and no row carries both a present
185/// field and a value taken from the first object. Reading `0x02` as an
186/// end-of-group marker — which is what this did — answers the wrong question
187/// in both directions: a real end-of-group stream (`0x18`-`0x1D`,
188/// `0x38`-`0x3D`) reports `false`, and a first-object stream (`0x12`, `0x13`)
189/// reports `true`. Neither is a framing error, so nothing downstream notices.
190///
191/// Draft-16 later folds the same three choices into a named SUBGROUP_ID_MODE
192/// field with a fourth, reserved value. Draft-15 has no such name and no such
193/// value: `0x16`, `0x17`, `0x1E`, `0x1F`, `0x36`, `0x37`, `0x3E` and `0x3F`
194/// are absent from Table 6 rather than reserved by it, and the word does not
195/// appear here in that sense at all. The two drafts agree on every byte and
196/// differ only in how they say why, so borrowing the later vocabulary reads
197/// as though draft-15 states something it does not.
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct SubgroupHeader {
200 pub header_type: u8,
201 pub track_alias: VarInt,
202 pub group_id: VarInt,
203 pub subgroup_id: VarInt,
204 pub publisher_priority: Option<u8>,
205}
206
207/// Whether draft-15 Section 10.4.2 Table 6 assigns this subgroup stream type.
208///
209/// Twenty-four values are assigned, counted off the table itself: `0x10`-`0x15`
210/// and `0x18`-`0x1D`, then `0x30`-`0x35` and `0x38`-`0x3D`. A type is valid
211/// when its base is `0x10` or `0x30` and its `0x06` bits name one of the three
212/// ways the table carries a Subgroup ID. That leaves `0x16`, `0x17`, `0x1E`,
213/// `0x1F`, `0x36`, `0x37`, `0x3E` and `0x3F` with no row.
214///
215/// They are **unassigned, not reserved.** Draft-16 reserves the same eight by
216/// name; draft-15 reaches them by omission and says nothing about them at all.
217/// The test is the same either way, which is why the distinction is only worth
218/// a sentence — but the sentence keeps the next reader from carrying draft-16's
219/// vocabulary back into a draft that does not use it.
220///
221/// Section 10 requires closing the session on a stream type the draft does not
222/// define, so accepting one and inventing a framing for it — the decoder read a
223/// Subgroup ID field for these, because `0x04` happens to be set — is not a
224/// harmless leniency.
225fn subgroup_type_is_assigned(ty: u64) -> bool {
226 let Ok(ty) = u8::try_from(ty) else {
227 return false;
228 };
229 // The `0x20` bit is the one difference between the two assigned bases, and
230 // masking with `0xD0` rather than `0xF0` drops it — so `0x30` through `0x3F`
231 // fold onto `0x10` and one comparison covers both. Every other high nibble
232 // survives the mask as something other than `0x10` and is refused: `0x20`
233 // folds to `0x00`, `0x50` and `0x70` to `0x50`, and so on up.
234 (ty & 0xD0) == 0x10 && (ty & 0x06) != 0x06
235}
236
237impl SubgroupHeader {
238 pub fn has_extensions(&self) -> bool {
239 self.header_type & 0x01 != 0
240 }
241
242 /// When set, the Subgroup ID is the Object ID of the first object on the
243 /// stream and is not transmitted. The `0x02` row of the `0x06` bits.
244 pub fn subgroup_id_from_first_object(&self) -> bool {
245 self.header_type & 0x06 == 0x02
246 }
247
248 pub fn has_explicit_subgroup_id(&self) -> bool {
249 self.header_type & 0x06 == 0x04
250 }
251
252 pub fn has_end_of_group(&self) -> bool {
253 self.header_type & 0x08 != 0
254 }
255
256 pub fn has_priority(&self) -> bool {
257 self.header_type & 0x20 == 0
258 }
259
260 /// Encode the header, writing whichever fields the type byte announces.
261 ///
262 /// Field presence follows `header_type`, because that is what the peer
263 /// reads. A `publisher_priority` of `None` under a type whose `0x20` bit is
264 /// clear writes a zero rather than dropping the byte: omitting it would
265 /// leave the peer reading the first object's Object ID Delta as a priority
266 /// and desync the whole stream. Use [`encode_checked`](Self::encode_checked)
267 /// to be told about the disagreement instead of having it papered over.
268 pub fn encode(&self, buf: &mut impl BufMut) {
269 VarInt::from_usize(self.header_type as usize).encode(buf);
270 self.track_alias.encode(buf);
271 self.group_id.encode(buf);
272 if self.has_explicit_subgroup_id() {
273 self.subgroup_id.encode(buf);
274 }
275 if self.has_priority() {
276 buf.put_u8(self.publisher_priority.unwrap_or(0));
277 }
278 }
279
280 /// Encode, refusing a header whose fields disagree with its own type byte.
281 ///
282 /// [`encode`](Self::encode) is driven by `header_type` and
283 /// [`decode`](Self::decode) reads the same byte, so the two agree on the
284 /// wire whatever the struct holds —
285 /// but a caller that sets `publisher_priority` beside a type whose `0x20`
286 /// bit is set, or leaves it `None` beside one whose bit is clear, has built
287 /// a header that does not mean what it says. The fetch object writer
288 /// already refuses that shape; this is the same check one layer up.
289 pub fn encode_checked(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
290 if self.has_priority() != self.publisher_priority.is_some() {
291 return Err(CodecError::InvalidField);
292 }
293 if !subgroup_type_is_assigned(self.header_type as u64) {
294 return Err(CodecError::InvalidField);
295 }
296 self.encode(buf);
297 Ok(())
298 }
299
300 pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
301 // Compared as the full varint rather than truncated to `u8`: a type of
302 // `0x110` narrows to `0x10` and would be accepted as a plain subgroup
303 // header, so a stream this draft does not define would be parsed as one
304 // it does instead of closing the session.
305 let type_value = VarInt::decode(buf)?.into_inner();
306 if !subgroup_type_is_assigned(type_value) {
307 return Err(stream_type_error(type_value));
308 }
309 let header_type = type_value as u8;
310 let track_alias = VarInt::decode(buf)?;
311 let group_id = VarInt::decode(buf)?;
312 // `0x04` alone carries the Subgroup ID on the wire. `0x02` takes it
313 // from the first object, which the stream reader resolves, and `0x00`
314 // defines it as zero.
315 let subgroup_id = if type_value as u8 & 0x06 == 0x04 {
316 VarInt::decode(buf)?
317 } else {
318 VarInt::from_usize(0)
319 };
320 let publisher_priority = if header_type & 0x20 == 0 {
321 if buf.remaining() < 1 {
322 return Err(CodecError::UnexpectedEnd);
323 }
324 Some(buf.get_u8())
325 } else {
326 None
327 };
328 Ok(Self { header_type, track_alias, group_id, subgroup_id, publisher_priority })
329 }
330}
331
332// ── Subgroup objects (stateful) ─────────────────────────────
333
334/// One object within a draft-15 subgroup stream with its Object ID
335/// already resolved from the delta encoding.
336///
337/// Draft-15 object framing requires context from the enclosing
338/// [`SubgroupHeader`] (specifically, whether extension headers are
339/// present and the running delta state), so decoding/encoding uses a
340/// stateful [`SubgroupObjectReader`] rather than a standalone method.
341#[derive(Debug, Clone, PartialEq, Eq)]
342pub struct SubgroupObject {
343 /// Resolved absolute Object ID.
344 pub object_id: VarInt,
345 /// Raw extension-header bytes, excluding the byte-length prefix that
346 /// precedes them on the wire. Empty when the stream header does not
347 /// set the extensions-present bit, or when the block is present but
348 /// zero-length. Opaque: [`SubgroupObjectReader::write_object`] re-emits
349 /// the prefix and these bytes verbatim.
350 pub extension_headers: Vec<u8>,
351 /// Payload length as encoded on the wire. Zero when the object is
352 /// a status-only object.
353 pub payload_length: VarInt,
354 /// Object status; `Some` when `payload_length == 0`.
355 ///
356 /// The wire field is a varint, so it can carry any value up to 2^62-1;
357 /// draft-15 Section 10.2.1.1 assigns four of them and says a peer SHOULD
358 /// treat the rest as a protocol error. This field is typed to the assigned
359 /// set, so it refuses to hold the codes the draft leaves unassigned —
360 /// 0x2, and everything from 0x5 up. That makes the refusal a property of
361 /// the struct rather than of any one code path: an encoder cannot be
362 /// handed a status the draft does not define, and does not have to check.
363 ///
364 /// `None` on a zero-length object means the same thing as
365 /// [`ObjectStatus::Normal`] and encodes as it; the wire field is not
366 /// optional once `payload_length` is zero.
367 pub object_status: Option<ObjectStatus>,
368 /// Payload bytes; empty when `object_status` is `Some`.
369 pub payload: Vec<u8>,
370}
371
372impl SubgroupObject {
373 /// The status this object resolves to.
374 ///
375 /// The wire carries a status field only on a zero-length object, so an
376 /// object holding bytes is [`ObjectStatus::Normal`] whatever
377 /// [`Self::object_status`] says — draft-15 Section 10.2.1.1: "This status is
378 /// implicit for any non-zero length object."
379 pub fn status(&self) -> ObjectStatus {
380 if self.payload_length.into_inner() == 0 {
381 self.object_status.unwrap_or(ObjectStatus::Normal)
382 } else {
383 ObjectStatus::Normal
384 }
385 }
386
387 /// Whether this object's status is allowed to carry the extension headers
388 /// it has.
389 ///
390 /// Draft-15 Section 10.2.1.2: "Any Object with status Normal can have
391 /// extension headers. If an endpoint receives extension headers on Objects
392 /// with status that is not Normal, it MUST close the session with a
393 /// PROTOCOL_VIOLATION."
394 ///
395 /// So this is `false` for exactly one shape: a non-empty extension block on
396 /// an object whose status is not [`ObjectStatus::Normal`]. An object with no
397 /// extensions is fine at any status, and an object at Normal may carry any
398 /// extensions.
399 ///
400 /// Neither [`SubgroupObjectReader::read_object`] nor
401 /// [`SubgroupObjectReader::write_object`] applies this itself, which is a
402 /// deliberate contrast with the payload rule beside it. A status next to a
403 /// payload has no encoding — the two share a position on the wire — so the
404 /// writer refuses it as unrepresentable. Extensions next to a status encode
405 /// fine; the frame is well formed and merely non-conforming, and a codec
406 /// that could not read or write it could not reproduce a capture containing
407 /// one. The corpus ships exactly such a frame. The rule addresses an
408 /// endpoint receiving the Object, so the endpoint is where it is enforced,
409 /// and this is what it asks.
410 pub fn extensions_permitted(&self) -> bool {
411 self.extension_headers.is_empty() || self.status() == ObjectStatus::Normal
412 }
413}
414
415/// The framing of one draft-15 subgroup object, without its payload.
416///
417/// Produced by [`SubgroupObjectReader::read_object_meta`] for callers that
418/// forward an object's bytes verbatim and never inspect the payload.
419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
420pub struct SubgroupObjectMeta {
421 /// Resolved absolute Object ID.
422 pub object_id: u64,
423 /// Byte length of the extension-header block's contents, excluding its
424 /// length prefix.
425 pub extension_headers_len: u64,
426 /// Declared payload length. Zero when `status` is `Some`.
427 pub payload_length: u64,
428 /// Object status wire code, present only when the payload is empty.
429 ///
430 /// Kept as the raw code, unlike [`SubgroupObject::object_status`]: a meta
431 /// is produced by [`SubgroupObjectReader::read_object_meta`] and is never
432 /// an encode input, and a relay that reads a status on one draft may hand
433 /// it to a draft that numbers the same value differently. The code is
434 /// still one draft-15 assigns — `read_object_meta` refuses the others.
435 pub status: Option<u64>,
436 /// Total bytes this object occupies on the wire, prefix fields included.
437 pub wire_len: u64,
438}
439
440impl SubgroupObjectMeta {
441 /// Whether this object is permitted a non-empty payload, or `None` when
442 /// [`Self::status`] holds a code draft-15 does not assign.
443 ///
444 /// [`Self::status`] is a raw wire code, so the rule of draft-15 Section
445 /// 10.2.1.1 — "Any object with a status code other than zero MUST have an
446 /// empty payload" — cannot be read off it without first deciding what the
447 /// code means. This does that decision once: a consumer asking whether an
448 /// object may carry bytes gets an answer instead of a number and a rule to
449 /// apply to it.
450 ///
451 /// An absent status answers [`PayloadPermission::Permitted`] rather than
452 /// `None`. A meta has no status only when its payload length is non-zero,
453 /// and Section 10.2.1.1 says of Normal that "This status is implicit for
454 /// any non-zero length object" — the object has a status, the encoding just
455 /// does not spell it.
456 ///
457 /// `None` means the code is one the draft leaves unassigned, and so one it
458 /// gives no payload rule for. That is not reachable through
459 /// [`SubgroupObjectReader::read_object_meta`], which refuses such a code
460 /// before it can reach the field, but the fields here are public and a meta
461 /// assembled by hand — by a relay carrying a status across from a draft that
462 /// numbers them differently, say — can hold anything a varint can. The
463 /// answer there is that draft-15 has none, not that the payload is
464 /// forbidden.
465 pub fn payload_permission(&self) -> Option<PayloadPermission> {
466 match self.status {
467 None => Some(PayloadPermission::Permitted),
468 Some(code) => ObjectStatus::from_u64(code).map(PayloadPermission::for_status),
469 }
470 }
471
472 /// Whether this object's status is allowed to carry the extension block it
473 /// declares.
474 ///
475 /// The same rule [`SubgroupObject::extensions_permitted`] answers, from the
476 /// declared length rather than from the bytes. A relay that forwards an
477 /// object verbatim reads it through
478 /// [`SubgroupObjectReader::read_object_meta`] and never copies the block, so
479 /// asking this must not require having it — draft-15 Section 10.2.1.2 turns
480 /// on whether the block is empty, and the length says that on its own.
481 ///
482 /// A status this draft does not assign answers `None` rather than `false`,
483 /// for the reason [`Self::payload_permission`] gives: the draft states no
484 /// rule for a code it does not define, and a meta assembled by hand can
485 /// hold one. `read_object_meta` refuses such a code before it reaches the
486 /// field.
487 ///
488 /// An absent status answers from Normal. A meta has no status only when its
489 /// payload length is non-zero, and such an object is Normal by Section
490 /// 10.2.1.1, so its extensions are permitted.
491 pub fn extensions_permitted(&self) -> Option<bool> {
492 if self.extension_headers_len == 0 {
493 return Some(true);
494 }
495 match self.status {
496 None => Some(true),
497 Some(code) => ObjectStatus::from_u64(code).map(|s| s == ObjectStatus::Normal),
498 }
499 }
500}
501
502/// Stateful reader/writer for draft-15 subgroup objects.
503///
504/// Carries the running delta state for object IDs and remembers whether
505/// extension headers are present on this stream.
506#[derive(Debug, Clone)]
507pub struct SubgroupObjectReader {
508 extensions_present: bool,
509 prev_object_id: Option<u64>,
510}
511
512impl SubgroupObjectReader {
513 /// Build a reader seeded from the enclosing subgroup header.
514 pub fn new(header: &SubgroupHeader) -> Self {
515 Self { extensions_present: header.has_extensions(), prev_object_id: None }
516 }
517
518 /// Decode the next object from `buf`.
519 pub fn read_object(&mut self, buf: &mut impl Buf) -> Result<SubgroupObject, CodecError> {
520 let delta = VarInt::decode(buf)?.into_inner();
521 // The first object's field is its absolute Object ID; every later
522 // object encodes the gap to its predecessor, biased by one because
523 // two objects on a subgroup stream cannot share an ID.
524 let object_id_val = match self.prev_object_id {
525 None => delta,
526 Some(prev) => prev
527 .checked_add(1)
528 .and_then(|v| v.checked_add(delta))
529 .ok_or(CodecError::InvalidField)?,
530 };
531 self.prev_object_id = Some(object_id_val);
532 let object_id = VarInt::from_u64(object_id_val).map_err(|_| CodecError::InvalidField)?;
533
534 let extension_headers = if self.extensions_present {
535 // Draft-15+: extensions are a byte-length-prefixed opaque
536 // blob. We copy the blob verbatim; callers that want
537 // structured extensions can parse the returned bytes.
538 let ext_len = VarInt::decode(buf)?.into_inner() as usize;
539 crate::types::read_bytes(buf, ext_len)?
540 } else {
541 Vec::new()
542 };
543
544 let payload_length_vi = VarInt::decode(buf)?;
545 let payload_length_val = payload_length_vi.into_inner() as usize;
546 let (object_status, payload) = if payload_length_val == 0 {
547 let status = decoded_status(VarInt::decode(buf)?.into_inner())?;
548 (Some(status), Vec::new())
549 } else {
550 let payload = crate::types::read_bytes(buf, payload_length_val)?;
551 (None, payload)
552 };
553
554 Ok(SubgroupObject {
555 object_id,
556 extension_headers,
557 payload_length: payload_length_vi,
558 object_status,
559 payload,
560 })
561 }
562
563 /// Decode the next object's framing without copying its payload.
564 ///
565 /// Consumes exactly the bytes [`Self::read_object`] consumes and leaves
566 /// the same delta state behind, so the two are interchangeable on a
567 /// given stream.
568 pub fn read_object_meta(
569 &mut self,
570 buf: &mut impl Buf,
571 ) -> Result<SubgroupObjectMeta, CodecError> {
572 let start = buf.remaining();
573 let delta = VarInt::decode(buf)?.into_inner();
574 let object_id_val = match self.prev_object_id {
575 None => delta,
576 Some(prev) => prev
577 .checked_add(1)
578 .and_then(|v| v.checked_add(delta))
579 .ok_or(CodecError::InvalidField)?,
580 };
581 self.prev_object_id = Some(object_id_val);
582 let object_id =
583 VarInt::from_u64(object_id_val).map_err(|_| CodecError::InvalidField)?.into_inner();
584
585 let extension_headers_len = if self.extensions_present {
586 let ext_len = VarInt::decode(buf)?.into_inner();
587 skip(buf, ext_len)?;
588 ext_len
589 } else {
590 0
591 };
592
593 let payload_length = VarInt::decode(buf)?.into_inner();
594 let status = if payload_length == 0 {
595 let code = VarInt::decode(buf)?.into_inner();
596 decoded_status(code)?;
597 Some(code)
598 } else {
599 skip(buf, payload_length)?;
600 None
601 };
602
603 Ok(SubgroupObjectMeta {
604 object_id,
605 extension_headers_len,
606 payload_length,
607 status,
608 wire_len: (start - buf.remaining()) as u64,
609 })
610 }
611
612 /// Serialize an object, producing the correct delta encoding.
613 ///
614 /// Errors with [`CodecError::InvalidField`] when `object.object_id` is
615 /// not strictly greater than the previously written object's ID, since
616 /// no valid delta exists for that case.
617 ///
618 /// A zero `payload_length` makes this a status object, and the status
619 /// field is then mandatory on the wire: an absent
620 /// [`SubgroupObject::object_status`] is written as
621 /// [`ObjectStatus::Normal`]. The code written is always one draft-15
622 /// assigns, because the field cannot hold any other, so the bytes this
623 /// produces are always bytes [`SubgroupObjectReader::read_object`] accepts.
624 ///
625 /// Errors with [`CodecError::InvalidField`] when `payload_length` is not
626 /// exactly `payload.len()`. The declared length is written ahead of the
627 /// payload, so a mismatch is a frame [`Self::read_object`] cannot parse
628 /// and one no caller could fix by appending bytes.
629 ///
630 /// Does NOT refuse extension headers beside a status other than Normal,
631 /// though draft-15 Section 10.2.1.2 forbids them. Such a frame is well
632 /// formed and merely non-conforming, this is the only writer a subgroup
633 /// object has, and the corpus ships one — so refusing here would leave a
634 /// captured violation impossible to reproduce.
635 /// [`SubgroupObject::extensions_permitted`] reports it instead.
636 pub fn write_object(
637 &mut self,
638 object: &SubgroupObject,
639 buf: &mut impl BufMut,
640 ) -> Result<(), CodecError> {
641 // A declared length that disagrees with the payload framed under it
642 // produces bytes no reader can parse and no caller can repair: the
643 // length is already on the wire ahead of the payload. Checked before
644 // anything is written, so a refused object leaves `buf` untouched
645 // rather than half an object the next read would run into.
646 //
647 // Zero is not "an empty payload" here; it is the marker that puts a
648 // status code where the payload would go, so an object carrying bytes
649 // under it is asking for two framings at once.
650 let declared = object.payload_length.into_inner();
651 if declared != object.payload.len() as u64 {
652 return Err(CodecError::InvalidField);
653 }
654
655 // Extension headers beside a non-Normal status are NOT refused here,
656 // though draft-15 Section 10.2.1.2 forbids them. The two rules around a
657 // status differ in kind. A payload beside a status has no encoding at
658 // all — the status field and the payload occupy the same position, so
659 // no sequence of bytes states both — which is why the declared-length
660 // check above refuses it as unrepresentable. Extensions beside a status
661 // encode perfectly well and read back byte for byte; the frame is well
662 // formed and non-conforming, which is a judgement about what a peer may
663 // send rather than about what the bytes mean.
664 //
665 // This is the only writer for a subgroup object, so refusing here would
666 // leave no way to reproduce a capture containing such a frame — and the
667 // corpus ships one. [`SubgroupObject::extensions_permitted`] reports the
668 // violation instead, and the endpoint acts on it.
669 // [`DatagramHeader::encode_checked`] does refuse it, because a plain
670 // [`DatagramHeader::encode`] stands beside it for verbatim reproduction.
671
672 let oid = object.object_id.into_inner();
673 let delta = match self.prev_object_id {
674 None => oid,
675 Some(prev) => oid
676 .checked_sub(prev)
677 .and_then(|v| v.checked_sub(1))
678 .ok_or(CodecError::InvalidField)?,
679 };
680 VarInt::from_u64(delta).map_err(|_| CodecError::InvalidField)?.encode(buf);
681 if self.extensions_present {
682 let ext_len = object.extension_headers.len();
683 VarInt::from_usize(ext_len).encode(buf);
684 buf.put_slice(&object.extension_headers);
685 }
686 object.payload_length.encode(buf);
687 if object.payload_length.into_inner() == 0 {
688 // Zero-length means a status object, and the status is not
689 // optional on the wire; an unset one is Normal.
690 let status = object.object_status.unwrap_or(ObjectStatus::Normal);
691 VarInt::from_usize(status.as_u64() as usize).encode(buf);
692 } else {
693 buf.put_slice(&object.payload);
694 }
695 self.prev_object_id = Some(oid);
696 Ok(())
697 }
698}
699
700// ── Datagram headers ───────────────────────────────────────
701
702/// Whether draft-15 Section 10.3.1 Table 5 assigns this datagram type.
703///
704/// Twenty-four values are assigned: `0x00`-`0x0F`, and `0x20`, `0x21`, `0x24`,
705/// `0x25`, `0x28`, `0x29`, `0x2C`, `0x2D`. Two rules generate that set. A type
706/// may only use the bits the draft defines — `0x01` extensions, `0x02` end of
707/// group, `0x04` no object ID, `0x08` default priority, `0x20` status — so any
708/// other bit makes it undefined. And a status datagram cannot also mark the end
709/// of a group, which rules out every type setting `0x20` and `0x02` together.
710///
711/// Section 10 requires closing the session on a datagram type the draft does
712/// not define. Accepting one means inferring field presence from bits that
713/// carry no meaning, which is how an undefined type gets parsed as a
714/// well-formed object.
715///
716/// Note that Section 10 Table 4 lists only ten datagram types and describes
717/// them as the whole set. That table is draft-14 text left behind: Table 5 in
718/// Section 10.3.1 is the normative field-presence table, it lists twenty-four,
719/// and draft-16 follows it.
720fn datagram_type_is_assigned(ty: u64) -> bool {
721 let Ok(ty) = u8::try_from(ty) else {
722 return false;
723 };
724 ty & 0xD0 == 0 && ty & 0x22 != 0x22
725}
726
727/// Datagram header for draft-15.
728///
729/// The `datagram_type` byte encodes flags:
730/// - `0x02`: end-of-group
731/// - `0x04`: no object_id (object_id = 0 implied)
732/// - `0x20`: status datagram (carries object_status instead of payload)
733#[derive(Debug, Clone, PartialEq, Eq)]
734pub struct DatagramHeader {
735 /// Raw datagram-type byte encoding flags + kind.
736 pub datagram_type: u8,
737 /// Track alias identifying the track.
738 pub track_alias: VarInt,
739 /// Group ID for the contained object.
740 pub group_id: VarInt,
741 /// Object ID (zero when the `no-object-id` flag is set).
742 pub object_id: VarInt,
743 /// Publisher priority, present only when the type byte leaves the
744 /// default-priority bit (`0x08`) clear.
745 ///
746 /// `None` means the object inherits the priority the control message that
747 /// established the subscription specified — draft-15 Section 10.3.1. This
748 /// is optional here and not on draft-14 because draft-15 is the draft that
749 /// made it so.
750 pub publisher_priority: Option<u8>,
751 /// Opaque extension-headers blob (only when the `0x01` flag is set).
752 pub extension_headers: Vec<u8>,
753 /// Object status (only when the `0x20` status flag is set).
754 ///
755 /// The wire field is a varint and can carry any value up to 2^62-1;
756 /// draft-15 Section 10.2.1.1 assigns four of them and says a peer SHOULD
757 /// treat the rest as a protocol error. This field is typed to the assigned
758 /// set, so it cannot hold 0x2 or anything from 0x5 up — [`Self::encode`]
759 /// therefore needs no check and cannot emit a datagram that
760 /// [`Self::decode`] would reject.
761 ///
762 /// `None` while the status flag is set encodes as
763 /// [`ObjectStatus::Normal`]: once the flag is set the field is present on
764 /// the wire, so there is nothing for `None` to mean but the default.
765 pub object_status: Option<ObjectStatus>,
766}
767
768impl DatagramHeader {
769 /// Whether the datagram carries an explicit object_id.
770 pub fn has_object_id(&self) -> bool {
771 self.datagram_type & 0x04 == 0
772 }
773
774 /// Whether this datagram marks the end of its group.
775 pub fn is_end_of_group(&self) -> bool {
776 self.datagram_type & 0x02 != 0
777 }
778
779 /// Whether this datagram carries an object_status instead of payload.
780 pub fn is_status(&self) -> bool {
781 self.datagram_type & 0x20 != 0
782 }
783
784 /// Whether this datagram carries extension headers.
785 pub fn has_extensions(&self) -> bool {
786 self.datagram_type & 0x01 != 0
787 }
788
789 /// Whether the publisher priority is omitted and inherited.
790 ///
791 /// Draft-15 Section 10.3.1: with Priority Present set to No the field is
792 /// absent and "this Object inherits the Publisher Priority specified in the
793 /// control message that established the subscription". New in draft-15;
794 /// draft-14 always carries the byte.
795 pub fn has_default_priority(&self) -> bool {
796 self.datagram_type & 0x08 != 0
797 }
798
799 /// Encode the datagram header, refusing a status the framing cannot carry.
800 ///
801 /// A datagram states a status only when its type byte sets the status flag
802 /// (0x20). With the flag clear there is no status field on the wire, so an
803 /// `object_status` of anything but [`ObjectStatus::Normal`] has nowhere to
804 /// go: [`Self::encode`] drops it, and the datagram parses back as an
805 /// ordinary payload object. An End of Group marker written that way does
806 /// not arrive late or malformed — it does not arrive at all, and the
807 /// receiver sees a normal object in its place.
808 ///
809 /// Draft-15 Section 10.3.1 puts the framing side plainly — "The Object
810 /// Status field and Object Payload are mutually exclusive" — and Section
811 /// 10.2.1.1 the conformance side: "Any object with a status code other than
812 /// zero MUST have an empty payload." Between them there is no datagram that
813 /// carries a non-zero status and a payload, so the pair being refused here
814 /// is not one this encoder merely declines to spell.
815 ///
816 /// [`ObjectStatus::Normal`] with the flag clear is not that case and is
817 /// accepted. It is the status the encoding elides for every datagram that
818 /// carries a payload, so stating it asks for exactly the bytes leaving it
819 /// out asks for, and nothing is lost.
820 ///
821 /// Errors with [`CodecError::InvalidField`] on the lossy combination,
822 /// before any byte is written, so a refused header leaves `buf` untouched.
823 /// It also refuses a type byte Section 10.3.1 Table 5 does not assign, so
824 /// this encoder cannot emit a datagram its own decoder — or a conforming
825 /// peer — must close the session over.
826 ///
827 /// The extension block is refused on the same terms the type byte governs
828 /// it, in both directions. A type announcing extensions must carry some:
829 /// Section 10.3.1 states that "If an endpoint receives a datagram with
830 /// Extensions Present as 'Yes' and a Extension Headers Length of 0, it MUST
831 /// close the session with a PROTOCOL_VIOLATION", so a zero-length block
832 /// under that type is a datagram no peer may accept. A type announcing none
833 /// cannot carry any, because [`Self::encode`] would drop the bytes in
834 /// silence.
835 ///
836 /// That first rule belongs to datagrams alone. Section 10.4.2 says the
837 /// opposite of a subgroup stream — "Objects with no extensions set Extension
838 /// Headers Length to 0" — because there the type byte is fixed for the whole
839 /// stream and a zero-length block is the only way one object among many can
840 /// say it has no extensions. Applying the datagram rule to a subgroup object
841 /// would refuse frames the draft spells out.
842 ///
843 /// Also refused: an extension block beside a status other than Normal, per
844 /// Section 10.2.1.2. [`Self::decode`] still parses such a datagram, so a
845 /// capture containing one stays readable; see
846 /// `check_extensions_against_status_on_encode`.
847 pub fn encode_checked(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
848 if !self.is_status() && matches!(self.object_status, Some(s) if s != ObjectStatus::Normal) {
849 return Err(CodecError::InvalidField);
850 }
851 if !datagram_type_is_assigned(self.datagram_type as u64) {
852 return Err(CodecError::UnknownDatagramType(self.datagram_type as u64));
853 }
854 if self.has_extensions() {
855 if self.extension_headers.is_empty() {
856 return Err(CodecError::InvalidField);
857 }
858 } else if !self.extension_headers.is_empty() {
859 return Err(CodecError::InvalidField);
860 }
861 check_extensions_against_status_on_encode(
862 self.effective_status().map(|s| s.as_u64()),
863 if self.has_extensions() { self.extension_headers.len() as u64 } else { 0 },
864 )?;
865 self.encode(buf);
866 Ok(())
867 }
868
869 /// The status this datagram resolves to, or `None` when its framing gives
870 /// it none to resolve.
871 ///
872 /// The type byte decides. With the status bit set the field is on the wire
873 /// and an unset [`Self::object_status`] is written as
874 /// [`ObjectStatus::Normal`]; with the bit clear the datagram carries a
875 /// payload, and the status of an object that carries a payload is Normal —
876 /// draft-15 Section 10.2.1.1 says "This status is implicit for any non-zero
877 /// length object". `None` is that implicit Normal, which is why a caller
878 /// asking what a datagram's status is may not read [`Self::object_status`]
879 /// directly.
880 fn effective_status(&self) -> Option<ObjectStatus> {
881 if self.is_status() {
882 Some(self.object_status.unwrap_or(ObjectStatus::Normal))
883 } else {
884 None
885 }
886 }
887
888 /// Whether this datagram's status is allowed to carry the extension headers
889 /// it has.
890 ///
891 /// The datagram half of the rule [`SubgroupObject::extensions_permitted`]
892 /// answers for a subgroup object; draft-15 Section 10.3.1 builds the
893 /// datagram's extension block out of the same structure Section 10.2.1.2
894 /// defines, so the rule covers both carriers.
895 ///
896 /// [`Self::decode`] reports this rather than refusing, because the datagram
897 /// is well framed and a codec that could not read one could not reproduce a
898 /// capture containing it. [`Self::encode_checked`] does refuse it — that is
899 /// the one direction with no such excuse, and a plain [`Self::encode`]
900 /// stands beside it when verbatim reproduction is what is wanted.
901 pub fn extensions_permitted(&self) -> bool {
902 if !self.has_extensions() || self.extension_headers.is_empty() {
903 return true;
904 }
905 self.effective_status().unwrap_or(ObjectStatus::Normal) == ObjectStatus::Normal
906 }
907
908 /// Encode the datagram header to `buf`.
909 ///
910 /// When the type byte sets the status flag the status field is written
911 /// unconditionally, defaulting to [`ObjectStatus::Normal`]. Omitting it
912 /// would truncate the datagram: [`Self::decode`] reads a status whenever
913 /// the flag is set, and answers [`CodecError::UnexpectedEnd`] when the
914 /// bytes stop first.
915 ///
916 /// The type byte is taken as the authority on framing, which is what makes
917 /// this infallible — and what makes it lossy when the struct disagrees with
918 /// itself. An `object_status` set while the type byte leaves the status
919 /// flag clear is discarded here without a word. Prefer
920 /// [`Self::encode_checked`], which refuses that combination instead of
921 /// resolving it.
922 pub fn encode(&self, buf: &mut impl BufMut) {
923 VarInt::from_usize(self.datagram_type as usize).encode(buf);
924 self.track_alias.encode(buf);
925 self.group_id.encode(buf);
926 if self.has_object_id() {
927 self.object_id.encode(buf);
928 }
929 if !self.has_default_priority() {
930 // The type byte is the authority on presence. A `None` here under a
931 // type whose `0x08` bit is clear writes a zero rather than dropping
932 // the byte: omitting it would leave the peer reading the first
933 // extension-length or status varint as a priority.
934 buf.put_u8(self.publisher_priority.unwrap_or(0));
935 }
936 if self.has_extensions() {
937 VarInt::from_usize(self.extension_headers.len()).encode(buf);
938 buf.put_slice(&self.extension_headers);
939 }
940 if self.is_status() {
941 let status = self.object_status.unwrap_or(ObjectStatus::Normal);
942 VarInt::from_usize(status.as_u64() as usize).encode(buf);
943 }
944 }
945
946 /// Decode a datagram header from `buf`.
947 pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
948 // Compared as the full varint, not truncated to `u8`: a type of `0x100`
949 // narrows to `0x00` and would be read as an ordinary object datagram, so
950 // a greased or future type would be silently mis-parsed rather than
951 // closing the session.
952 let type_value = VarInt::decode(buf)?.into_inner();
953 if !datagram_type_is_assigned(type_value) {
954 return Err(CodecError::UnknownDatagramType(type_value));
955 }
956 let datagram_type = type_value as u8;
957 let track_alias = VarInt::decode(buf)?;
958 let group_id = VarInt::decode(buf)?;
959 let object_id =
960 if datagram_type & 0x04 == 0 { VarInt::decode(buf)? } else { VarInt::from_usize(0) };
961 let publisher_priority = if datagram_type & 0x08 == 0 {
962 if buf.remaining() < 1 {
963 return Err(CodecError::UnexpectedEnd);
964 }
965 Some(buf.get_u8())
966 } else {
967 None
968 };
969 let extension_headers = if datagram_type & 0x01 != 0 {
970 let ext_len = VarInt::decode(buf)?.into_inner() as usize;
971 // A datagram whose type says extensions are present must actually carry
972 // some: receiving one with an Extension Headers Length of 0 closes the
973 // session. The opposite holds on a subgroup stream, where the type byte is
974 // fixed for the whole stream and an object with no extensions has no other
975 // way to say so, which is why this check belongs to the datagram readers
976 // alone.
977 if ext_len == 0 {
978 return Err(CodecError::InvalidField);
979 }
980 crate::types::read_bytes(buf, ext_len)?
981 } else {
982 Vec::new()
983 };
984 let object_status = if datagram_type & 0x20 != 0 {
985 Some(decoded_status(VarInt::decode(buf)?.into_inner())?)
986 } else {
987 None
988 };
989 Ok(Self {
990 datagram_type,
991 track_alias,
992 group_id,
993 object_id,
994 publisher_priority,
995 extension_headers,
996 object_status,
997 })
998 }
999}
1000
1001// ── Fetch stream headers ───────────────────────────────────
1002
1003/// Fetch stream header for draft-15.
1004///
1005/// Stream type is 0x05. Only contains a request_id.
1006#[derive(Debug, Clone, PartialEq, Eq)]
1007pub struct FetchHeader {
1008 pub request_id: VarInt,
1009}
1010
1011/// The unidirectional stream type draft-15 Section 10.4.4 gives a fetch stream.
1012const FETCH_STREAM_TYPE: u64 = 0x05;
1013
1014/// Which failure a leading unidirectional stream type that is not the one a
1015/// reader wants is.
1016///
1017/// Section 10: "An endpoint that receives an unknown stream or datagram type
1018/// MUST close the session." One sentence, two tables. The stream table assigns
1019/// FETCH_HEADER and the subgroup types [`subgroup_type_is_assigned`] describes;
1020/// everything outside them is unknown at the head of a stream, and the session
1021/// ends.
1022///
1023/// Both assigned kinds are what the [`CodecError::InvalidField`] arm is for: a
1024/// fetch stream reaching the subgroup reader, or a subgroup stream reaching the
1025/// fetch reader, is a value this draft defines, and the disagreement is with
1026/// the reader that was called rather than with the draft. Reporting it as
1027/// unknown would end sessions over streams draft-15 permits.
1028///
1029/// The datagram reader needs no such helper. Its table shares no value with the
1030/// stream table, so every type it rejects is one no table assigns and the
1031/// answer is always [`CodecError::UnknownDatagramType`].
1032fn stream_type_error(raw: u64) -> CodecError {
1033 if raw == FETCH_STREAM_TYPE || subgroup_type_is_assigned(raw) {
1034 CodecError::InvalidField
1035 } else {
1036 CodecError::UnknownStreamType(raw)
1037 }
1038}
1039
1040impl FetchHeader {
1041 pub fn encode(&self, buf: &mut impl BufMut) {
1042 VarInt::from_usize(FETCH_STREAM_TYPE as usize).encode(buf);
1043 self.request_id.encode(buf);
1044 }
1045
1046 /// Decode the header.
1047 ///
1048 /// Errors with [`CodecError::UnknownStreamType`] when the stream table does
1049 /// not assign the leading type, which this draft answers with a close, and
1050 /// with [`CodecError::InvalidField`] for the subgroup types, which it does
1051 /// assign. `stream_type_error` draws that line.
1052 pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
1053 let stream_type = VarInt::decode(buf)?.into_inner();
1054 if stream_type != FETCH_STREAM_TYPE {
1055 return Err(stream_type_error(stream_type));
1056 }
1057 let request_id = VarInt::decode(buf)?;
1058 Ok(Self { request_id })
1059 }
1060}
1061
1062// ── Fetch objects (stateful) ───────────────────────────────
1063
1064/// How a draft-15 fetch object carries its Subgroup ID.
1065///
1066/// The two least significant bits of the Serialization Flags are one field,
1067/// not two independent flags: draft-15 Section 10.4.4, Table 7 gives all four
1068/// of their values a meaning, and only one of them puts a Subgroup ID on the
1069/// wire. Reading either bit on its own gets the wrong answer for half the
1070/// values.
1071#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1072pub enum SubgroupIdEncoding {
1073 /// `0x00`: the Subgroup ID is zero, whatever the preceding object's was.
1074 Zero,
1075 /// `0x01`: the Subgroup ID is the prior object's.
1076 SameAsPrior,
1077 /// `0x02`: the Subgroup ID is the prior object's plus one.
1078 PriorPlusOne,
1079 /// `0x03`: the Subgroup ID follows as a varint.
1080 Present,
1081}
1082
1083impl SubgroupIdEncoding {
1084 /// Read the two-bit field out of a Serialization Flags byte.
1085 pub fn from_flags(flags: u8) -> Self {
1086 match flags & 0x03 {
1087 0x00 => SubgroupIdEncoding::Zero,
1088 0x01 => SubgroupIdEncoding::SameAsPrior,
1089 0x02 => SubgroupIdEncoding::PriorPlusOne,
1090 _ => SubgroupIdEncoding::Present,
1091 }
1092 }
1093
1094 /// The bit pattern this encoding occupies in a Serialization Flags byte.
1095 pub fn as_bits(self) -> u8 {
1096 match self {
1097 SubgroupIdEncoding::Zero => 0x00,
1098 SubgroupIdEncoding::SameAsPrior => 0x01,
1099 SubgroupIdEncoding::PriorPlusOne => 0x02,
1100 SubgroupIdEncoding::Present => 0x03,
1101 }
1102 }
1103
1104 /// Whether resolving a Subgroup ID under this encoding needs the object
1105 /// before it on the stream.
1106 ///
1107 /// [`SubgroupIdEncoding::Zero`] does not, which is what makes it the one
1108 /// implicit form a stream's first object may use.
1109 pub fn references_prior(self) -> bool {
1110 matches!(self, SubgroupIdEncoding::SameAsPrior | SubgroupIdEncoding::PriorPlusOne)
1111 }
1112}
1113
1114/// One object on a draft-15 fetch stream, without its payload.
1115///
1116/// Draft-15 Section 10.4.4 gives fetch objects a leading Serialization Flags
1117/// byte that says which of the object's fields are on the wire; every field it
1118/// omits is taken from, or counted from, the object before it on the same
1119/// stream. An object therefore cannot be decoded on its own, and the fields
1120/// below are the resolved absolute values rather than whatever the wire spelled
1121/// out — reading them needs the running state a [`FetchObjectReader`] carries.
1122///
1123/// `serialization_flags` is kept beside the resolved values so that an object
1124/// re-encodes to the bytes it was decoded from. One object has as many
1125/// encodings as there are flag bytes that resolve to it, and choosing one on
1126/// the caller's behalf would rewrite a stream a relay is meant to forward
1127/// unchanged.
1128///
1129/// The bits, from Section 10.4.4, Tables 7 and 8:
1130/// - `& 0x03`: how the Subgroup ID is carried; see [`SubgroupIdEncoding`]
1131/// - `& 0x04`: Object ID field present, else the prior object's ID plus one
1132/// - `& 0x08`: Group ID field present, else the prior object's Group ID
1133/// - `& 0x10`: Publisher Priority field present, else the prior object's
1134/// - `& 0x20`: Extensions field present
1135/// - `& 0xc0`: unassigned, and Table 8 makes either bit a protocol violation
1136///
1137/// The payload is deliberately not part of this type: an object's declared
1138/// length is the last thing before its bytes, so a caller that forwards
1139/// payloads verbatim can read the framing and then move `payload_length` bytes
1140/// without ever copying them.
1141#[derive(Debug, Clone, PartialEq, Eq)]
1142pub struct FetchObjectHeader {
1143 /// Raw Serialization Flags byte, as decoded or as it is to be written.
1144 pub serialization_flags: u8,
1145 /// Resolved absolute Group ID.
1146 pub group_id: VarInt,
1147 /// Resolved absolute Subgroup ID.
1148 pub subgroup_id: VarInt,
1149 /// Resolved absolute Object ID.
1150 pub object_id: VarInt,
1151 /// Publisher priority for delivery ordering.
1152 pub publisher_priority: u8,
1153 /// Raw extension-header bytes, excluding the byte-length prefix that
1154 /// precedes them on the wire. Empty when the flags do not set the
1155 /// extensions bit, or when the block is present but zero-length. Opaque:
1156 /// [`FetchObjectReader::write_object_header`] re-emits the prefix and these
1157 /// bytes verbatim.
1158 pub extension_headers: Vec<u8>,
1159 /// Payload length as encoded on the wire. Zero when the object is a
1160 /// status-only object.
1161 pub payload_length: VarInt,
1162 /// Object status; `Some` when `payload_length == 0`.
1163 ///
1164 /// Section 10.4.4: "The Object Status field is only present if the Object
1165 /// Payload Length is zero." Typed to the statuses draft-15 assigns for the
1166 /// same reason [`SubgroupObject::object_status`] is: the field cannot hold
1167 /// a code the draft leaves unassigned, so the encoder needs no check and
1168 /// cannot emit an object the decoder would refuse.
1169 ///
1170 /// `None` on a zero-length object means the same as
1171 /// [`ObjectStatus::Normal`] and encodes as it; the wire field is not
1172 /// optional once `payload_length` is zero.
1173 pub object_status: Option<ObjectStatus>,
1174}
1175
1176impl FetchObjectHeader {
1177 /// How this object's Subgroup ID is carried, from the low two flag bits.
1178 pub fn subgroup_id_encoding(&self) -> SubgroupIdEncoding {
1179 SubgroupIdEncoding::from_flags(self.serialization_flags)
1180 }
1181
1182 /// Whether the Object ID is on the wire, rather than the prior object's
1183 /// ID plus one.
1184 pub fn has_object_id(&self) -> bool {
1185 self.serialization_flags & 0x04 != 0
1186 }
1187
1188 /// Whether the Group ID is on the wire, rather than the prior object's.
1189 pub fn has_group_id(&self) -> bool {
1190 self.serialization_flags & 0x08 != 0
1191 }
1192
1193 /// Whether the Publisher Priority is on the wire, rather than the prior
1194 /// object's.
1195 pub fn has_priority(&self) -> bool {
1196 self.serialization_flags & 0x10 != 0
1197 }
1198
1199 /// Whether an extensions block is on the wire.
1200 pub fn has_extensions(&self) -> bool {
1201 self.serialization_flags & 0x20 != 0
1202 }
1203
1204 /// Whether any of this object's fields is taken from the object before it
1205 /// on the stream.
1206 ///
1207 /// Draft-15 Section 10.4.4: "If the first Object in the FETCH response uses
1208 /// a flag that references fields in the prior Object, the Subscriber MUST
1209 /// close the session with a PROTOCOL_VIOLATION." Four of the flags do so —
1210 /// two of the Subgroup ID encodings, and the cleared state of the Object
1211 /// ID, Group ID and Priority bits, each of which means "the prior
1212 /// object's". The extensions bit does not: it is present or it is not, and
1213 /// nothing is inherited either way.
1214 pub fn references_prior_object(&self) -> bool {
1215 self.subgroup_id_encoding().references_prior()
1216 || !self.has_object_id()
1217 || !self.has_group_id()
1218 || !self.has_priority()
1219 }
1220
1221 /// The status this object resolves to.
1222 ///
1223 /// Section 10.4.4: "The Object Status field is only present if the Object
1224 /// Payload Length is zero." An object declaring a length is therefore
1225 /// [`ObjectStatus::Normal`] whatever [`Self::object_status`] holds, on the
1226 /// reading Section 10.2.1.1 gives Normal: "This status is implicit for any
1227 /// non-zero length object."
1228 pub fn status(&self) -> ObjectStatus {
1229 if self.payload_length.into_inner() == 0 {
1230 self.object_status.unwrap_or(ObjectStatus::Normal)
1231 } else {
1232 ObjectStatus::Normal
1233 }
1234 }
1235
1236 /// Whether this object's status is allowed to carry the extension headers
1237 /// it has.
1238 ///
1239 /// The fetch half of the rule [`SubgroupObject::extensions_permitted`]
1240 /// answers, and the same one: Section 10.4.4 builds a fetch object's
1241 /// Extensions field out of the structure Section 10.2.1.2 defines, and that
1242 /// section is where the rule sits — "Any Object with status Normal can have
1243 /// extension headers. If an endpoint receives extension headers on Objects
1244 /// with status that is not Normal, it MUST close the session with a
1245 /// PROTOCOL_VIOLATION."
1246 /// Draft-15 is the only draft where a fetch object can state this
1247 /// violation, which is why no counterpart to this exists on drafts 16 and
1248 /// later rather than one that is always `true`. Section 10.4.4 gives this
1249 /// draft's fetch object an Object Status field — "The Object Status field
1250 /// is only present if the Object Payload Length is zero" — and its
1251 /// extensions bit, Table 8's `0x20`, is independent of every other flag, so
1252 /// the two can appear together. Drafts 16 and later remove the field
1253 /// outright, draft-16 Section 10.2.1.1: "The Object Status is a field that
1254 /// is only present in objects that are delivered via a SUBSCRIPTION, and is
1255 /// absent in Objects delivered via a FETCH." A fetch object there has no
1256 /// status to disagree with, so the rule has nothing to bite on.
1257 ///
1258 /// [`FetchObjectReader`] reports this rather than refusing it, for the
1259 /// reason [`SubgroupObject::extensions_permitted`] sets out: the frame is
1260 /// well formed and merely non-conforming, and a reader that refused it
1261 /// could not reproduce a capture containing one.
1262 pub fn extensions_permitted(&self) -> bool {
1263 self.extension_headers.is_empty() || self.status() == ObjectStatus::Normal
1264 }
1265
1266 /// Serialize this Object's framing, writing only the fields its own
1267 /// Serialization Flags announce.
1268 ///
1269 /// The inverse of [`FetchObjectReader::read_object_header`], and the reason
1270 /// it is fallible where the subgroup form's is not: this type holds every
1271 /// field resolved, so the flags rather than the values decide what goes on
1272 /// the wire. A Group ID held with the 0x08 bit clear is *not* written and
1273 /// the reader takes the Object as sharing its predecessor's group — which
1274 /// is silent data loss when the two differ, and correct when they do not.
1275 /// Nothing here can tell those apart, so the caller settles it by choosing
1276 /// the flags, and [`FetchObjectWriter`] is what chooses them against a
1277 /// predecessor.
1278 ///
1279 /// # Errors
1280 ///
1281 /// [`CodecError::InvalidField`] for a flags byte with either of the two
1282 /// bits Section 10.4.4 leaves unassigned, which is the same value
1283 /// [`FetchObjectReader::read_object_header`] refuses to read — the field is
1284 /// one fixed byte and not a variable-length integer, so 0x40 and 0x80 are
1285 /// not wider spellings of anything.
1286 pub fn encode(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
1287 if self.serialization_flags & 0xc0 != 0 {
1288 return Err(CodecError::InvalidField);
1289 }
1290 buf.put_u8(self.serialization_flags);
1291 // Field order is Section 10.4.4's, Figure 30: Group ID, Subgroup ID,
1292 // Object ID, Priority, Extensions.
1293 if self.has_group_id() {
1294 self.group_id.encode(buf);
1295 }
1296 if self.subgroup_id_encoding() == SubgroupIdEncoding::Present {
1297 self.subgroup_id.encode(buf);
1298 }
1299 if self.has_object_id() {
1300 self.object_id.encode(buf);
1301 }
1302 if self.has_priority() {
1303 buf.put_u8(self.publisher_priority);
1304 }
1305 if self.has_extensions() {
1306 VarInt::from_usize(self.extension_headers.len()).encode(buf);
1307 buf.put_slice(&self.extension_headers);
1308 }
1309 self.payload_length.encode(buf);
1310 // A zero-length Object carries a status varint after the length, and a
1311 // non-zero-length one carries none: the reader reads the field on
1312 // exactly that test, so writing it on any other would put a byte on the
1313 // wire the reader would take for payload.
1314 if self.payload_length.into_inner() == 0 {
1315 VarInt::from_u64(self.object_status.unwrap_or(ObjectStatus::Normal).as_u64())
1316 .map_err(|_| CodecError::InvalidField)?
1317 .encode(buf);
1318 }
1319 Ok(())
1320 }
1321}
1322
1323/// Re-encodes resolved fetch Objects onto one FETCH stream.
1324///
1325/// The exact inverse of [`FetchObjectReader`], and it exists for one caller:
1326/// something that has read a stream and is writing a different stream from the
1327/// same Objects. Draft-15 Section 10.4.4 lets an Object leave out its Group
1328/// ID, Object ID, Subgroup ID and Priority and take the prior Object's, so
1329/// removing an Object changes what the Objects after it are read against: a
1330/// field the survivor left off has to appear, and a flag bit with it.
1331///
1332/// Every field draft-15 puts on the wire is the absolute value rather than a
1333/// difference — the deltas arrive at draft-18. What is stateful is the
1334/// *omission*, and that alone makes removal a re-encode rather than a deletion.
1335///
1336/// # Why this is not a general encoder
1337///
1338/// Every Object it writes came off a stream, so the caller holds the Object's
1339/// own header with both its resolved values and the flags it arrived under.
1340/// Those flags are the preference: wherever the original shape still says the
1341/// same thing against the new predecessor it is kept, so a stream with nothing
1342/// removed is reproduced byte for byte.
1343#[derive(Debug, Clone, Default)]
1344pub struct FetchObjectWriter {
1345 prior: Option<PriorFetchObject>,
1346}
1347
1348impl FetchObjectWriter {
1349 /// A writer positioned before the first Object of a fetch stream, with no
1350 /// prior Object for anything to be written against.
1351 pub fn new() -> Self {
1352 Self::default()
1353 }
1354
1355 /// The header that encodes `original`'s resolved values against everything
1356 /// written so far.
1357 ///
1358 /// Does not advance the writer — [`Self::write_object_header`] is the call
1359 /// that does both.
1360 ///
1361 /// # Errors
1362 ///
1363 /// [`CodecError::InvalidField`] for a flags byte with an unassigned bit.
1364 pub fn header_for(
1365 &self,
1366 original: &FetchObjectHeader,
1367 ) -> Result<FetchObjectHeader, CodecError> {
1368 if original.serialization_flags & 0xc0 != 0 {
1369 return Err(CodecError::InvalidField);
1370 }
1371 let group_id = original.group_id.into_inner();
1372 let subgroup_id = original.subgroup_id.into_inner();
1373 let object_id = original.object_id.into_inner();
1374
1375 // Each field is written when the Object wrote it, and written anyway
1376 // when leaving it off would now say something else. Keeping the
1377 // Object's own choice is what reproduces an untouched stream byte for
1378 // byte; the second half is what a removal forces.
1379 let mut flags = 0u8;
1380 if original.has_group_id() || self.prior.map(|p| p.group_id) != Some(group_id) {
1381 flags |= 0x08;
1382 }
1383 if original.has_object_id()
1384 || self.prior.and_then(|p| p.object_id.checked_add(1)) != Some(object_id)
1385 {
1386 flags |= 0x04;
1387 }
1388 if original.has_priority()
1389 || self.prior.map(|p| p.publisher_priority) != Some(original.publisher_priority)
1390 {
1391 flags |= 0x10;
1392 }
1393 if original.has_extensions() {
1394 flags |= 0x20;
1395 }
1396 flags |= self.subgroup_bits(original, subgroup_id).as_bits();
1397
1398 Ok(FetchObjectHeader {
1399 serialization_flags: flags,
1400 group_id: original.group_id,
1401 subgroup_id: original.subgroup_id,
1402 object_id: original.object_id,
1403 publisher_priority: original.publisher_priority,
1404 extension_headers: original.extension_headers.clone(),
1405 payload_length: original.payload_length,
1406 object_status: original.object_status,
1407 })
1408 }
1409
1410 /// Which Subgroup ID encoding says `subgroup_id` against this predecessor.
1411 ///
1412 /// The Object's own encoding is tried first, so a run that inherited its
1413 /// Subgroup ID keeps inheriting it and its bytes do not move. Only when the
1414 /// predecessor changed under it is a different one chosen, and then the
1415 /// cheapest that names the right number.
1416 fn subgroup_bits(&self, original: &FetchObjectHeader, subgroup_id: u64) -> SubgroupIdEncoding {
1417 let inherits = self.prior.map(|p| p.subgroup_id) == Some(subgroup_id);
1418 let successor =
1419 self.prior.is_some_and(|p| p.subgroup_id.checked_add(1) == Some(subgroup_id));
1420 let kept = match original.subgroup_id_encoding() {
1421 SubgroupIdEncoding::Zero if subgroup_id == 0 => Some(SubgroupIdEncoding::Zero),
1422 SubgroupIdEncoding::SameAsPrior if inherits => Some(SubgroupIdEncoding::SameAsPrior),
1423 SubgroupIdEncoding::PriorPlusOne if successor => Some(SubgroupIdEncoding::PriorPlusOne),
1424 SubgroupIdEncoding::Present => Some(SubgroupIdEncoding::Present),
1425 _ => None,
1426 };
1427 match kept {
1428 Some(encoding) => encoding,
1429 None if subgroup_id == 0 => SubgroupIdEncoding::Zero,
1430 None if inherits => SubgroupIdEncoding::SameAsPrior,
1431 None if successor => SubgroupIdEncoding::PriorPlusOne,
1432 None => SubgroupIdEncoding::Present,
1433 }
1434 }
1435
1436 /// Encode `original`'s resolved values against everything written so far
1437 /// and advance.
1438 ///
1439 /// Writes the framing only. The payload is `payload_length` bytes and is
1440 /// the caller's to copy, unchanged.
1441 ///
1442 /// # Errors
1443 ///
1444 /// [`CodecError::InvalidField`] for a flags byte with an unassigned bit;
1445 /// the writer is left untouched when this happens.
1446 pub fn write_object_header(
1447 &mut self,
1448 original: &FetchObjectHeader,
1449 out: &mut impl BufMut,
1450 ) -> Result<FetchObjectHeader, CodecError> {
1451 let header = self.header_for(original)?;
1452 header.encode(out)?;
1453 self.advance(&header);
1454 Ok(header)
1455 }
1456
1457 /// Record what was written as the predecessor of whatever comes next.
1458 ///
1459 /// Every field draft-15 puts on a fetch object's wire is the absolute
1460 /// value, so this reads them straight off the header rather than resolving
1461 /// anything.
1462 ///
1463 /// Public because a re-emitting caller has a second way of putting a frame
1464 /// on the wire: when the framing it arrived in still encodes the same
1465 /// meaning against the frame before it, its own bytes are forwarded
1466 /// untouched — no header is produced and nothing is copied. The writer
1467 /// still has to move, or the frame after it is encoded against a
1468 /// predecessor one frame stale. `written` is then the Object's own header, which is
1469 /// what was put on the wire.
1470 pub fn advance(&mut self, written: &FetchObjectHeader) {
1471 self.prior = Some(PriorFetchObject {
1472 group_id: written.group_id.into_inner(),
1473 subgroup_id: written.subgroup_id.into_inner(),
1474 object_id: written.object_id.into_inner(),
1475 publisher_priority: written.publisher_priority,
1476 });
1477 }
1478}
1479
1480/// The fields a draft-15 fetch object leaves to its successor to inherit.
1481#[derive(Debug, Clone, Copy)]
1482struct PriorFetchObject {
1483 group_id: u64,
1484 subgroup_id: u64,
1485 object_id: u64,
1486 publisher_priority: u8,
1487}
1488
1489/// Stateful reader/writer for the objects on one draft-15 fetch stream.
1490///
1491/// Holds the fields of the object last read or written, which is what the next
1492/// object's Serialization Flags may refer to. One reader belongs to one stream:
1493/// draft-15 Section 10.4.4 counts "the prior Object" along the stream, so
1494/// sharing a reader between streams, or restarting one mid-stream, resolves
1495/// later objects onto the wrong group, subgroup, ID or priority without
1496/// producing an error anywhere.
1497///
1498/// A fresh reader has no prior object, which is exactly the state in which the
1499/// draft's protocol violation applies — see
1500/// [`FetchObjectHeader::references_prior_object`].
1501#[derive(Debug, Clone, Default)]
1502pub struct FetchObjectReader {
1503 prior: Option<PriorFetchObject>,
1504}
1505
1506impl FetchObjectReader {
1507 /// Build a reader for the objects following a fetch stream's header.
1508 pub fn new() -> Self {
1509 Self::default()
1510 }
1511
1512 /// Decode the next object's framing, leaving its payload in `buf`.
1513 ///
1514 /// Consumes the Serialization Flags byte, whichever of the Group ID,
1515 /// Subgroup ID, Object ID, Priority and Extensions fields that byte says are
1516 /// present, the Object Payload Length, and the Object Status when that
1517 /// length is zero. The declared payload bytes are left where they are, so a
1518 /// caller can forward them without a copy; skipping them is the caller's
1519 /// job, and skipping the wrong number of them desynchronises every later
1520 /// object on the stream.
1521 ///
1522 /// Errors with [`CodecError::InvalidField`] when the flags set either bit
1523 /// draft-15 Section 10.4.4, Table 8 leaves unassigned, when the stream's
1524 /// first object inherits from an object that does not exist, when an
1525 /// inherited value cannot be represented, or when the Object Status is a
1526 /// code the draft does not assign.
1527 pub fn read_object_header(
1528 &mut self,
1529 buf: &mut impl Buf,
1530 ) -> Result<FetchObjectHeader, CodecError> {
1531 if buf.remaining() < 1 {
1532 return Err(CodecError::UnexpectedEnd);
1533 }
1534 // Section 10.4.4 writes the field as "Serialization Flags (8)": one
1535 // fixed byte, not a varint. The two readings only part on the values
1536 // Table 8 forbids, which is what makes the difference easy to miss —
1537 // 0x40 and 0x80 are the two- and four-byte varint prefixes, so a varint
1538 // reader consumes the fields after them as part of the flags and
1539 // reports a plausible object instead of the violation.
1540 let serialization_flags = buf.get_u8();
1541 if serialization_flags & 0xc0 != 0 {
1542 return Err(CodecError::InvalidField);
1543 }
1544
1545 let subgroup_encoding = SubgroupIdEncoding::from_flags(serialization_flags);
1546 let has_object_id = serialization_flags & 0x04 != 0;
1547 let has_group_id = serialization_flags & 0x08 != 0;
1548 let has_priority = serialization_flags & 0x10 != 0;
1549 let has_extensions = serialization_flags & 0x20 != 0;
1550
1551 // The first object on the stream has nothing to inherit from, and the
1552 // draft's answer to being asked anyway is to close the session rather
1553 // than to invent a zero.
1554 let inherits = subgroup_encoding.references_prior()
1555 || !has_object_id
1556 || !has_group_id
1557 || !has_priority;
1558 if inherits && self.prior.is_none() {
1559 return Err(CodecError::InvalidField);
1560 }
1561 let prior = self.prior;
1562
1563 // Field order is Section 10.4.4's, Figure 30: Group ID, then Subgroup
1564 // ID, then Object ID, then Priority, then Extensions. Only the fields
1565 // the flags announce are on the wire, so resolving out of order would
1566 // read one field's bytes as another's.
1567 let group_id = if has_group_id {
1568 VarInt::decode(buf)?
1569 } else {
1570 let prior = prior.ok_or(CodecError::InvalidField)?;
1571 VarInt::from_u64(prior.group_id).map_err(|_| CodecError::InvalidField)?
1572 };
1573
1574 let subgroup_id = match subgroup_encoding {
1575 SubgroupIdEncoding::Zero => VarInt::from_usize(0),
1576 SubgroupIdEncoding::SameAsPrior => {
1577 let prior = prior.ok_or(CodecError::InvalidField)?;
1578 VarInt::from_u64(prior.subgroup_id).map_err(|_| CodecError::InvalidField)?
1579 }
1580 SubgroupIdEncoding::PriorPlusOne => {
1581 let prior = prior.ok_or(CodecError::InvalidField)?;
1582 let next = prior.subgroup_id.checked_add(1).ok_or(CodecError::InvalidField)?;
1583 VarInt::from_u64(next).map_err(|_| CodecError::InvalidField)?
1584 }
1585 SubgroupIdEncoding::Present => VarInt::decode(buf)?,
1586 };
1587
1588 let object_id = if has_object_id {
1589 VarInt::decode(buf)?
1590 } else {
1591 let prior = prior.ok_or(CodecError::InvalidField)?;
1592 let next = prior.object_id.checked_add(1).ok_or(CodecError::InvalidField)?;
1593 VarInt::from_u64(next).map_err(|_| CodecError::InvalidField)?
1594 };
1595
1596 let publisher_priority = if has_priority {
1597 if buf.remaining() < 1 {
1598 return Err(CodecError::UnexpectedEnd);
1599 }
1600 buf.get_u8()
1601 } else {
1602 prior.ok_or(CodecError::InvalidField)?.publisher_priority
1603 };
1604
1605 let extension_headers = if has_extensions {
1606 // Section 10.4.4 defers to Section 10.2.1.2 here, so the block is
1607 // the same byte-length-prefixed opaque blob subgroup objects carry.
1608 let ext_len = VarInt::decode(buf)?.into_inner() as usize;
1609 crate::types::read_bytes(buf, ext_len)?
1610 } else {
1611 Vec::new()
1612 };
1613
1614 let payload_length = VarInt::decode(buf)?;
1615 let object_status = if payload_length.into_inner() == 0 {
1616 Some(decoded_status(VarInt::decode(buf)?.into_inner())?)
1617 } else {
1618 None
1619 };
1620 self.prior = Some(PriorFetchObject {
1621 group_id: group_id.into_inner(),
1622 subgroup_id: subgroup_id.into_inner(),
1623 object_id: object_id.into_inner(),
1624 publisher_priority,
1625 });
1626
1627 Ok(FetchObjectHeader {
1628 serialization_flags,
1629 group_id,
1630 subgroup_id,
1631 object_id,
1632 publisher_priority,
1633 extension_headers,
1634 payload_length,
1635 object_status,
1636 })
1637 }
1638
1639 /// Serialize an object's framing, leaving its payload to the caller.
1640 ///
1641 /// Writes exactly the fields `header.serialization_flags` announces, and
1642 /// stops after the Object Payload Length — or, when that length is zero,
1643 /// after the Object Status. The caller appends `payload_length` payload
1644 /// bytes; the length is already on the wire by then, so appending a
1645 /// different number of them frames an object no reader can parse.
1646 ///
1647 /// The flags are taken as the authority on what reaches the wire, which
1648 /// means a header whose resolved fields disagree with its own flags cannot
1649 /// be written faithfully. Every such disagreement is refused with
1650 /// [`CodecError::InvalidField`] rather than resolved:
1651 ///
1652 /// - a field the flags omit whose value is not the one the omission
1653 /// implies — a Group ID that is not the prior object's, a Subgroup ID
1654 /// that is not what the two-bit encoding resolves to, an Object ID that
1655 /// is not the prior object's plus one, a Priority that is not the prior
1656 /// object's. Written anyway, each would arrive as the value the flags
1657 /// imply, and the object the peer sees would be a different object.
1658 /// - extension bytes with the extensions bit clear, which would be dropped
1659 /// in silence. Draft-15 Section 10.2.1.2 requires relays to forward
1660 /// extensions they do not understand unchanged, so dropping them is not
1661 /// a smaller loss than mis-stating an ID.
1662 /// - a status other than [`ObjectStatus::Normal`] on an object with a
1663 /// non-zero payload length. Section 10.4.4 puts the status field on the
1664 /// wire only when that length is zero, and Section 10.2.1.1 the
1665 /// conformance side: "Any object with a status code other than zero MUST
1666 /// have an empty payload." There is no such object to write.
1667 /// [`ObjectStatus::Normal`] alongside a payload is not that case and is
1668 /// accepted: it is the status the encoding elides for every object that
1669 /// carries bytes, so stating it asks for exactly the bytes leaving it out
1670 /// asks for.
1671 ///
1672 /// Not refused: extension bytes on an object whose resolved status is not
1673 /// [`ObjectStatus::Normal`], which Section 10.2.1.2 forbids. As on a
1674 /// subgroup stream this is the only writer a fetch object has, and the
1675 /// frame encodes and reads back exactly, so refusing it would cost the
1676 /// ability to reproduce a capture rather than prevent anything.
1677 ///
1678 /// Also refused: flags setting either bit Table 8 leaves unassigned, and a
1679 /// first object on the stream that inherits from an object that does not
1680 /// exist.
1681 ///
1682 /// Every check runs before a byte is written, so a refused header leaves
1683 /// `buf` untouched rather than half an object the next write would run
1684 /// into, and leaves the reader's prior-object state as it was.
1685 pub fn write_object_header(
1686 &mut self,
1687 header: &FetchObjectHeader,
1688 buf: &mut impl BufMut,
1689 ) -> Result<(), CodecError> {
1690 if header.serialization_flags & 0xc0 != 0 {
1691 return Err(CodecError::InvalidField);
1692 }
1693 if header.payload_length.into_inner() != 0
1694 && matches!(header.object_status, Some(s) if s != ObjectStatus::Normal)
1695 {
1696 return Err(CodecError::InvalidField);
1697 }
1698 if !header.has_extensions() && !header.extension_headers.is_empty() {
1699 return Err(CodecError::InvalidField);
1700 }
1701 // As on a subgroup stream, extension headers beside a non-Normal status
1702 // are not refused here: they encode and read back exactly, this is the
1703 // only writer a fetch object has, and a capture containing one has to
1704 // stay reproducible. See `SubgroupObjectReader::write_object`.
1705 if header.references_prior_object() && self.prior.is_none() {
1706 return Err(CodecError::InvalidField);
1707 }
1708 let prior = self.prior;
1709 if !header.has_group_id() {
1710 let prior = prior.ok_or(CodecError::InvalidField)?;
1711 if header.group_id.into_inner() != prior.group_id {
1712 return Err(CodecError::InvalidField);
1713 }
1714 }
1715 let subgroup_id = header.subgroup_id.into_inner();
1716 match header.subgroup_id_encoding() {
1717 SubgroupIdEncoding::Zero => {
1718 if subgroup_id != 0 {
1719 return Err(CodecError::InvalidField);
1720 }
1721 }
1722 SubgroupIdEncoding::SameAsPrior => {
1723 let prior = prior.ok_or(CodecError::InvalidField)?;
1724 if subgroup_id != prior.subgroup_id {
1725 return Err(CodecError::InvalidField);
1726 }
1727 }
1728 SubgroupIdEncoding::PriorPlusOne => {
1729 let prior = prior.ok_or(CodecError::InvalidField)?;
1730 let next = prior.subgroup_id.checked_add(1).ok_or(CodecError::InvalidField)?;
1731 if subgroup_id != next {
1732 return Err(CodecError::InvalidField);
1733 }
1734 }
1735 SubgroupIdEncoding::Present => {}
1736 }
1737 if !header.has_object_id() {
1738 let prior = prior.ok_or(CodecError::InvalidField)?;
1739 let next = prior.object_id.checked_add(1).ok_or(CodecError::InvalidField)?;
1740 if header.object_id.into_inner() != next {
1741 return Err(CodecError::InvalidField);
1742 }
1743 }
1744 if !header.has_priority() {
1745 let prior = prior.ok_or(CodecError::InvalidField)?;
1746 if header.publisher_priority != prior.publisher_priority {
1747 return Err(CodecError::InvalidField);
1748 }
1749 }
1750
1751 buf.put_u8(header.serialization_flags);
1752 if header.has_group_id() {
1753 header.group_id.encode(buf);
1754 }
1755 if header.subgroup_id_encoding() == SubgroupIdEncoding::Present {
1756 header.subgroup_id.encode(buf);
1757 }
1758 if header.has_object_id() {
1759 header.object_id.encode(buf);
1760 }
1761 if header.has_priority() {
1762 buf.put_u8(header.publisher_priority);
1763 }
1764 if header.has_extensions() {
1765 VarInt::from_usize(header.extension_headers.len()).encode(buf);
1766 buf.put_slice(&header.extension_headers);
1767 }
1768 header.payload_length.encode(buf);
1769 if header.payload_length.into_inner() == 0 {
1770 // Zero length means a status object, and the status is not optional
1771 // on the wire; an unset one is Normal.
1772 let status = header.object_status.unwrap_or(ObjectStatus::Normal);
1773 VarInt::from_usize(status.as_u64() as usize).encode(buf);
1774 }
1775
1776 self.prior = Some(PriorFetchObject {
1777 group_id: header.group_id.into_inner(),
1778 subgroup_id,
1779 object_id: header.object_id.into_inner(),
1780 publisher_priority: header.publisher_priority,
1781 });
1782 Ok(())
1783 }
1784}
1785
1786#[cfg(test)]
1787mod tests {
1788 use super::*;
1789
1790 /// Canonically encoded subgroup stream vectors from
1791 /// `test-vectors/transport/draft15/codec/data-streams/subgroup.json`.
1792 /// `subgroup-explicit-subgroup-id` is omitted: it encodes group_id 100 as a
1793 /// two-byte varint, which does not survive a minimal-width re-encode.
1794 const VECTORS: &[&str] = &[
1795 // subgroup-single-object
1796 "100100800004deadbeef",
1797 // subgroup-two-objects
1798 "100100800004deadbeef0002cafe",
1799 // subgroup-no-priority
1800 "3001000004deadbeef",
1801 // subgroup-with-extensions
1802 "11010080000004deadbeef",
1803 // subgroup-nonempty-extensions
1804 "1101008000023c0104deadbeef",
1805 // subgroup-status-end-of-group
1806 "100105800004deadbeef000003",
1807 // subgroup-status-end-of-track
1808 "10010a800004deadbeef000004",
1809 // subgroup-extensions-two-objects-empty
1810 "11010080000004deadbeef000002cafe",
1811 // subgroup-extensions-two-objects-nonempty
1812 "1101008000023c0204deadbeef00023c0302cafe",
1813 // subgroup-extensions-status-object
1814 "1101008000023c010003",
1815 // subgroup-end-of-group
1816 "120105800004deadbeef",
1817 ];
1818
1819 fn vi(v: u64) -> VarInt {
1820 VarInt::from_u64(v).unwrap()
1821 }
1822
1823 fn hex(s: &str) -> Vec<u8> {
1824 (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
1825 }
1826
1827 /// Decode a whole subgroup stream: the header, then every object up to
1828 /// the end of the buffer.
1829 fn decode_all(bytes: &[u8]) -> (SubgroupHeader, Vec<SubgroupObject>) {
1830 let mut cursor = bytes;
1831 let header = SubgroupHeader::decode(&mut cursor)
1832 .unwrap_or_else(|e| panic!("header decode failed: {e:?}"));
1833 let mut reader = SubgroupObjectReader::new(&header);
1834 let mut objects = Vec::new();
1835 while cursor.has_remaining() {
1836 objects.push(
1837 reader
1838 .read_object(&mut cursor)
1839 .unwrap_or_else(|e| panic!("object {} decode failed: {e:?}", objects.len())),
1840 );
1841 }
1842 (header, objects)
1843 }
1844
1845 fn encode_all(header: &SubgroupHeader, objects: &[SubgroupObject]) -> Vec<u8> {
1846 let mut buf = Vec::new();
1847 header.encode(&mut buf);
1848 let mut writer = SubgroupObjectReader::new(header);
1849 for o in objects {
1850 writer.write_object(o, &mut buf).unwrap_or_else(|e| panic!("write failed: {e:?}"));
1851 }
1852 buf
1853 }
1854
1855 fn object(id: u64, extensions: Vec<u8>, payload: Vec<u8>) -> SubgroupObject {
1856 SubgroupObject {
1857 object_id: vi(id),
1858 extension_headers: extensions,
1859 payload_length: vi(payload.len() as u64),
1860 object_status: None,
1861 payload,
1862 }
1863 }
1864
1865 // ── Object ID deltas ────────────────────────────────────
1866
1867 #[test]
1868 fn two_objects_with_extensions_have_distinct_ids() {
1869 // Vector `subgroup-extensions-two-objects-empty`: two objects, each
1870 // carrying an empty extensions block and a delta of 0. The delta is
1871 // biased by one whether or not the extensions bit is set, so the IDs
1872 // are 0 and 1 — not 0 and 0.
1873 let bytes = hex("11010080000004deadbeef000002cafe");
1874 let (header, objects) = decode_all(&bytes);
1875 assert!(header.has_extensions());
1876 assert_eq!(objects.len(), 2);
1877 assert_eq!(objects[0].object_id.into_inner(), 0);
1878 assert_eq!(objects[1].object_id.into_inner(), 1);
1879 assert_eq!(objects[0].payload, hex("deadbeef"));
1880 assert_eq!(objects[1].payload, hex("cafe"));
1881 assert!(objects.iter().all(|o| o.extension_headers.is_empty()));
1882 }
1883
1884 #[test]
1885 fn deltas_resolve_sparse_ids() {
1886 let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
1887 let objects: Vec<_> =
1888 [3u64, 4, 40].iter().map(|&id| object(id, vec![], vec![0xAA, id as u8])).collect();
1889 let (_, decoded) = decode_all(&encode_all(&header, &objects));
1890 let ids: Vec<u64> = decoded.iter().map(|o| o.object_id.into_inner()).collect();
1891 assert_eq!(ids, vec![3, 4, 40]);
1892 }
1893
1894 #[test]
1895 fn write_rejects_non_increasing_ids() {
1896 let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
1897 let mut writer = SubgroupObjectReader::new(&header);
1898 let mut buf = Vec::new();
1899 writer.write_object(&object(7, vec![], vec![0x01]), &mut buf).unwrap();
1900 for id in [7u64, 6, 0] {
1901 let err = writer.write_object(&object(id, vec![], vec![0x01]), &mut buf).unwrap_err();
1902 assert!(matches!(err, CodecError::InvalidField), "id {id} gave {err:?}");
1903 }
1904 }
1905
1906 #[test]
1907 fn eliding_an_object_renumbers_its_successor() {
1908 let header = SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap();
1909 let all: Vec<_> = (0..5u64).map(|id| object(id, vec![], vec![id as u8])).collect();
1910 for elided in 0..5u64 {
1911 let kept: Vec<_> =
1912 all.iter().filter(|o| o.object_id.into_inner() != elided).cloned().collect();
1913 let (_, decoded) = decode_all(&encode_all(&header, &kept));
1914 let ids: Vec<u64> = decoded.iter().map(|o| o.object_id.into_inner()).collect();
1915 let expected: Vec<u64> = (0..5u64).filter(|&i| i != elided).collect();
1916 assert_eq!(ids, expected, "eliding object {elided}");
1917 }
1918 }
1919
1920 // ── Extension blocks ──────────────────────────────
1921
1922 #[test]
1923 fn extensions_blob_excludes_its_length_prefix() {
1924 // Vector `subgroup-extensions-two-objects-nonempty`: each
1925 // object carries a two-byte block, so the blob is those two bytes
1926 // with the `02` length prefix stripped.
1927 let bytes = hex("1101008000023c0204deadbeef00023c0302cafe");
1928 let (_, objects) = decode_all(&bytes);
1929 assert_eq!(objects.len(), 2);
1930 assert_eq!(objects[0].object_id.into_inner(), 0);
1931 assert_eq!(objects[1].object_id.into_inner(), 1);
1932 assert_eq!(objects[0].extension_headers, hex("3c02"));
1933 assert_eq!(objects[1].extension_headers, hex("3c03"));
1934 assert_eq!(objects[0].payload, hex("deadbeef"));
1935 assert_eq!(objects[1].payload, hex("cafe"));
1936 }
1937
1938 #[test]
1939 fn status_object_carries_its_extensions_block() {
1940 let (_, objects) = decode_all(&hex("1101008000023c010003"));
1941 assert_eq!(objects.len(), 1);
1942 assert_eq!(objects[0].extension_headers, hex("3c01"));
1943 assert_eq!(objects[0].payload_length.into_inner(), 0);
1944 assert_eq!(objects[0].object_status.map(ObjectStatus::as_u64), Some(3));
1945 assert!(objects[0].payload.is_empty());
1946 }
1947
1948 // ── Object status ───────────────────────────────────────
1949
1950 /// A subgroup header with neither extensions nor an explicit subgroup ID,
1951 /// so an object on this stream is just `delta, payload_length, [status]`.
1952 fn plain_header() -> SubgroupHeader {
1953 SubgroupHeader::decode(&mut &hex("100100800004deadbeef")[..]).unwrap()
1954 }
1955
1956 /// A status object on a [`plain_header`] stream: object 0, empty payload,
1957 /// `code` as the status. Every code used here is one wire byte.
1958 fn subgroup_status_body(code: u64) -> Vec<u8> {
1959 let mut buf = vec![0x00, 0x00];
1960 VarInt::from_u64(code).unwrap().encode(&mut buf);
1961 buf
1962 }
1963
1964 fn status_object(status: Option<ObjectStatus>) -> SubgroupObject {
1965 SubgroupObject {
1966 object_id: vi(0),
1967 extension_headers: vec![],
1968 payload_length: vi(0),
1969 object_status: status,
1970 payload: vec![],
1971 }
1972 }
1973
1974 /// A status datagram carrying `code`. Type 0x20 sets the status flag and
1975 /// leaves the object-id (0x04) and extensions (0x01) flags clear, so the
1976 /// layout is `type, track_alias, group_id, object_id, priority, status`.
1977 fn datagram_status_bytes(code: u64) -> Vec<u8> {
1978 let mut buf = vec![0x20, 0x01, 0x02, 0x03, 0x80];
1979 VarInt::from_u64(code).unwrap().encode(&mut buf);
1980 buf
1981 }
1982
1983 fn status_datagram(status: Option<ObjectStatus>) -> DatagramHeader {
1984 DatagramHeader {
1985 datagram_type: 0x20,
1986 track_alias: vi(1),
1987 group_id: vi(2),
1988 object_id: vi(3),
1989 publisher_priority: Some(0x80),
1990 extension_headers: vec![],
1991 object_status: status,
1992 }
1993 }
1994
1995 /// Every status the type can hold reaches the wire as its own code and
1996 /// comes back unchanged, through both subgroup readers and the datagram.
1997 ///
1998 /// Observed by making `write_object` encode `ObjectStatus::Normal` instead
1999 /// of the object's own status, which fails this with:
2000 ///
2001 /// ```text
2002 /// assertion `left == right` failed: ObjectDoesNotExist on the subgroup wire
2003 /// left: [0, 0, 0]
2004 /// right: [0, 0, 1]
2005 /// ```
2006 #[test]
2007 fn assigned_statuses_round_trip() {
2008 let header = plain_header();
2009 for &status in ObjectStatus::ALL {
2010 let mut bytes = Vec::new();
2011 SubgroupObjectReader::new(&header)
2012 .write_object(&status_object(Some(status)), &mut bytes)
2013 .unwrap();
2014 assert_eq!(
2015 bytes,
2016 subgroup_status_body(status.as_u64()),
2017 "{status:?} on the subgroup wire"
2018 );
2019
2020 let decoded = SubgroupObjectReader::new(&header)
2021 .read_object(&mut &bytes[..])
2022 .unwrap_or_else(|e| panic!("{status:?} was written and then refused: {e:?}"));
2023 assert_eq!(decoded.object_status, Some(status), "{status:?} through read_object");
2024
2025 let meta = SubgroupObjectReader::new(&header)
2026 .read_object_meta(&mut &bytes[..])
2027 .unwrap_or_else(|e| panic!("{status:?} was written and then refused: {e:?}"));
2028 assert_eq!(meta.status, Some(status.as_u64()), "{status:?} through read_object_meta");
2029
2030 let datagram = status_datagram(Some(status));
2031 let mut bytes = Vec::new();
2032 datagram.encode(&mut bytes);
2033 assert_eq!(
2034 bytes,
2035 datagram_status_bytes(status.as_u64()),
2036 "{status:?} on the datagram wire"
2037 );
2038 let decoded = DatagramHeader::decode(&mut &bytes[..]).unwrap_or_else(|e| {
2039 panic!("{status:?} datagram was written and then refused: {e:?}")
2040 });
2041 assert_eq!(decoded, datagram, "{status:?} datagram round trip");
2042 }
2043 }
2044
2045 /// A datagram whose type byte sets the status flag always carries a status
2046 /// field, because the flag is what puts the field on the wire — an unset
2047 /// `object_status` writes Normal rather than nothing.
2048 ///
2049 /// Observed by putting back the `if let Some(s) = &self.object_status`
2050 /// with no `else`, which emits a datagram that stops before its status
2051 /// field and fails this with:
2052 ///
2053 /// ```text
2054 /// assertion `left == right` failed
2055 /// left: [32, 1, 2, 3, 128]
2056 /// right: [32, 1, 2, 3, 128, 0]
2057 /// ```
2058 ///
2059 /// and, with the byte comparison removed, fails the decode of its own
2060 /// output with `own output refused: VarInt(UnexpectedEnd)`.
2061 #[test]
2062 fn a_status_datagram_without_a_status_encodes_normal() {
2063 let mut bytes = Vec::new();
2064 status_datagram(None).encode(&mut bytes);
2065 assert_eq!(bytes, datagram_status_bytes(ObjectStatus::Normal.as_u64()));
2066 let decoded = DatagramHeader::decode(&mut &bytes[..])
2067 .unwrap_or_else(|e| panic!("own output refused: {e:?}"));
2068 assert_eq!(decoded.object_status, Some(ObjectStatus::Normal));
2069 }
2070
2071 /// The codes this draft's decoders accept are exactly the codes its
2072 /// encoders can emit.
2073 ///
2074 /// The sweep covers `0x00..=0x3f`, the whole one-byte varint range, so it
2075 /// contains every code draft-15 assigns, the gap inside that range (0x2)
2076 /// and the codes later drafts moved (0x1, 0x5). The expected set is read
2077 /// from [`ObjectStatus::ALL`] rather than written out here, so reassigning
2078 /// a code moves both halves of the test at once.
2079 ///
2080 /// Observed by teaching `ObjectStatus::from_u64` to answer `Some` for a
2081 /// code that is not in `ALL` — 0x05, which drafts 07-10 assigned and
2082 /// draft-15 does not, mapped onto an existing variant — which fails this
2083 /// with:
2084 ///
2085 /// ```text
2086 /// assertion `left == right` failed: subgroup read_object on status 0x5: Ok(SubgroupObject { object_id: VarInt(0), extension_headers: [], payload_length: VarInt(0), object_status: Some(EndOfTrack), payload: [] })
2087 /// left: true
2088 /// right: false
2089 /// ```
2090 #[test]
2091 fn the_wire_accepts_exactly_what_the_type_can_hold() {
2092 let header = plain_header();
2093 for code in 0x00u64..=0x3f {
2094 let assigned = ObjectStatus::ALL.iter().any(|s| s.as_u64() == code);
2095 let body = subgroup_status_body(code);
2096
2097 let read = SubgroupObjectReader::new(&header).read_object(&mut &body[..]);
2098 assert_eq!(
2099 read.is_ok(),
2100 assigned,
2101 "subgroup read_object on status {code:#x}: {read:?}"
2102 );
2103
2104 let meta = SubgroupObjectReader::new(&header).read_object_meta(&mut &body[..]);
2105 assert_eq!(
2106 meta.is_ok(),
2107 assigned,
2108 "subgroup read_object_meta on status {code:#x}: {meta:?}"
2109 );
2110
2111 let datagram = DatagramHeader::decode(&mut &datagram_status_bytes(code)[..]);
2112 assert_eq!(
2113 datagram.is_ok(),
2114 assigned,
2115 "status datagram on status {code:#x}: {datagram:?}"
2116 );
2117
2118 // The other direction: an accepted code is one the encoders can
2119 // reach, and they reach it with exactly these bytes.
2120 if assigned {
2121 let status = ObjectStatus::from_u64(code).unwrap();
2122 let mut bytes = Vec::new();
2123 SubgroupObjectReader::new(&header)
2124 .write_object(&status_object(Some(status)), &mut bytes)
2125 .unwrap();
2126 assert_eq!(bytes, body, "write_object on status {code:#x}");
2127 let mut bytes = Vec::new();
2128 status_datagram(Some(status)).encode(&mut bytes);
2129 assert_eq!(
2130 bytes,
2131 datagram_status_bytes(code),
2132 "datagram encode on status {code:#x}"
2133 );
2134 }
2135 }
2136 }
2137
2138 // ── Re-encoding ─────────────────────────────────────────
2139
2140 #[test]
2141 fn vectors_re_encode_byte_identically() {
2142 for vector in VECTORS {
2143 let bytes = hex(vector);
2144 let (header, objects) = decode_all(&bytes);
2145 assert_eq!(encode_all(&header, &objects), bytes, "[{vector}] re-encode");
2146 }
2147 }
2148
2149 // ── Payload-free framing ────────────────────────────────
2150
2151 #[test]
2152 fn meta_matches_read_object() {
2153 for vector in VECTORS {
2154 let bytes = hex(vector);
2155 let mut cursor = &bytes[..];
2156 let header = SubgroupHeader::decode(&mut cursor).unwrap();
2157 let mut full_reader = SubgroupObjectReader::new(&header);
2158 let mut meta_reader = SubgroupObjectReader::new(&header);
2159 let mut full_cursor = cursor;
2160 let mut meta_cursor = cursor;
2161 while meta_cursor.has_remaining() {
2162 let before = meta_cursor.remaining();
2163 let object = full_reader.read_object(&mut full_cursor).unwrap();
2164 let meta = meta_reader.read_object_meta(&mut meta_cursor).unwrap();
2165 assert_eq!(meta.object_id, object.object_id.into_inner(), "[{vector}]");
2166 assert_eq!(
2167 meta.extension_headers_len,
2168 object.extension_headers.len() as u64,
2169 "[{vector}]"
2170 );
2171 assert_eq!(meta.payload_length, object.payload_length.into_inner(), "[{vector}]");
2172 assert_eq!(
2173 meta.status,
2174 object.object_status.map(ObjectStatus::as_u64),
2175 "[{vector}]"
2176 );
2177 assert_eq!(meta.wire_len, (before - meta_cursor.remaining()) as u64, "[{vector}]");
2178 assert_eq!(full_cursor.remaining(), meta_cursor.remaining(), "[{vector}]");
2179 }
2180 }
2181 }
2182
2183 /// A fetch object reports the extensions-beside-a-status rule the subgroup
2184 /// and datagram carriers already reported.
2185 /// Draft-15 is the only draft on which this is expressible: from
2186 /// draft-16 Section 10.2.1.1 the Object Status field is "absent in Objects
2187 /// delivered via a FETCH", so there is no status for extensions to sit
2188 /// beside. All four objects below are well formed and all four decode — the
2189 /// predicate is the only thing that separates them, which is the whole
2190 /// point of reporting rather than refusing.
2191 ///
2192 /// Inverting the predicate's `||` to `&&` fails this at the second case,
2193 /// `[1c0000000003] End of Group with no extensions is fine: expected true,
2194 /// got false`, and fails
2195 /// `fetch_object_status_is_normal_whenever_a_payload_is_declared` beside
2196 /// it.
2197 #[test]
2198 fn fetch_objects_report_extensions_beside_a_non_normal_status() {
2199 // flags 0x3c: Subgroup ID zero, Group ID / Object ID / Priority and an
2200 // extensions block all present. flags 0x1c is the same without the
2201 // extensions block.
2202 let cases: [(&str, bool, &str); 4] = [
2203 (
2204 "3c000000023c010003",
2205 false,
2206 "a two-byte extension block beside End of Group is the violation",
2207 ),
2208 ("1c0000000003", true, "End of Group with no extensions is fine"),
2209 (
2210 "3c000000000003",
2211 true,
2212 "a present but zero-length block carries nothing, so nothing is beside the status",
2213 ),
2214 ("3c000000023c0104", true, "a normal object may carry extensions"),
2215 ];
2216
2217 for (vector, permitted, why) in cases {
2218 let bytes = hex(vector);
2219 let mut cursor = &bytes[..];
2220 let header = FetchObjectReader::new()
2221 .read_object_header(&mut cursor)
2222 .unwrap_or_else(|e| panic!("[{vector}] {why}: decode failed with {e:?}"));
2223 assert_eq!(
2224 header.extensions_permitted(),
2225 permitted,
2226 "[{vector}] {why}: expected {permitted}, got {}",
2227 header.extensions_permitted(),
2228 );
2229 }
2230 }
2231
2232 /// A fetch object's status resolves the way a subgroup object's does: the
2233 /// field is on the wire only under a zero payload length, so an object
2234 /// declaring bytes is Normal whatever the field would have said.
2235 #[test]
2236 fn fetch_object_status_is_normal_whenever_a_payload_is_declared() {
2237 let bytes = hex("3c000000023c0104");
2238 let mut cursor = &bytes[..];
2239 let header = FetchObjectReader::new().read_object_header(&mut cursor).unwrap();
2240 assert_eq!(header.object_status, None, "no status field follows a non-zero length");
2241 assert_eq!(header.status(), ObjectStatus::Normal);
2242
2243 // Assembled by hand rather than decoded: the wire cannot put a status
2244 // beside a payload, but the struct's fields are public and a caller
2245 // porting an object across drafts can set both.
2246 let contradictory =
2247 FetchObjectHeader { object_status: Some(ObjectStatus::EndOfGroup), ..header };
2248 assert_eq!(
2249 contradictory.status(),
2250 ObjectStatus::Normal,
2251 "a declared payload wins over a status the wire could not have carried",
2252 );
2253 assert!(contradictory.extensions_permitted());
2254 }
2255
2256 #[test]
2257 fn short_buffers_report_unexpected_end() {
2258 let bytes = hex("1101008000023c0204deadbeef00023c0302cafe");
2259 let mut cursor = &bytes[..];
2260 let header = SubgroupHeader::decode(&mut cursor).unwrap();
2261 let objects_start = bytes.len() - cursor.len();
2262 for cut in objects_start..bytes.len() {
2263 let mut reader = SubgroupObjectReader::new(&header);
2264 let mut meta_reader = SubgroupObjectReader::new(&header);
2265 let mut cursor = &bytes[objects_start..cut];
2266 let mut meta_cursor = cursor;
2267 while cursor.has_remaining() {
2268 if let Err(err) = reader.read_object(&mut cursor) {
2269 assert!(
2270 matches!(err, CodecError::UnexpectedEnd | CodecError::VarInt(_)),
2271 "cut {cut} gave {err:?}"
2272 );
2273 break;
2274 }
2275 }
2276 while meta_cursor.has_remaining() {
2277 if let Err(err) = meta_reader.read_object_meta(&mut meta_cursor) {
2278 assert!(
2279 matches!(err, CodecError::UnexpectedEnd | CodecError::VarInt(_)),
2280 "cut {cut} gave {err:?}"
2281 );
2282 break;
2283 }
2284 }
2285 }
2286 }
2287}