Skip to main content

moqtap_codec/draft07/
data_stream.rs

1use super::types::ObjectStatus;
2use crate::error::CodecError;
3use crate::varint::VarInt;
4use bytes::{Buf, BufMut};
5
6/// Every type ID this draft's data plane assigns, from the single table it
7/// keeps them in.
8///
9/// Draft-07 Section 7 numbers streams and datagrams together. Its Table 5 is
10/// headed "Stream Type" and holds all three assignments — 0x1 OBJECT_DATAGRAM,
11/// 0x4 STREAM_HEADER_SUBGROUP, 0x5 FETCH_HEADER — under one sentence: "All
12/// unidirectional MOQT streams, as well as all datagrams, start with a
13/// variable-length integer indicating the type of the stream in question."
14/// Draft-08 is where the two split into tables of their own, after which the
15/// numbers are reused across them independently.
16///
17/// One shared space is what makes 0x1 an assigned value at the head of a
18/// unidirectional stream rather than an unknown one. Such a stream is still
19/// refused — a datagram type says nothing about how to read a stream — but it
20/// is refused as a stream this reader cannot read, not under the
21/// unknown-stream-type rule, which would end the session.
22///
23/// [`StreamType::from_id`] answers over that one table. Callers that need the
24/// narrower question, which of these a particular reader will accept, ask
25/// `stream_type_error` instead.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[repr(u64)]
28pub enum StreamType {
29    /// Datagram stream type (0x01).
30    Datagram = 0x01,
31    /// Subgroup stream type (0x04).
32    Subgroup = 0x04,
33    /// Fetch stream type (0x05).
34    Fetch = 0x05,
35}
36
37impl StreamType {
38    /// Convert a raw stream type ID to a `StreamType`, if valid.
39    pub fn from_id(id: u64) -> Option<Self> {
40        match id {
41            0x01 => Some(StreamType::Datagram),
42            0x04 => Some(StreamType::Subgroup),
43            0x05 => Some(StreamType::Fetch),
44            _ => None,
45        }
46    }
47}
48
49// ============================================================
50// Subgroup stream (type 0x04)
51// ============================================================
52
53/// Subgroup stream header (follows the stream type varint).
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct SubgroupHeader {
56    /// Track alias identifying the subscription.
57    pub track_alias: VarInt,
58    /// Group identifier.
59    pub group_id: VarInt,
60    /// Subgroup identifier within the group.
61    pub subgroup_id: VarInt,
62    /// Publisher priority for delivery ordering.
63    pub publisher_priority: u8,
64}
65
66/// Which failure a leading type that is not the one a reader wants is.
67///
68/// Section 7: "An endpoint that receives an unknown stream type MUST close the
69/// session." That sentence is about values Table 5 does not assign, and Table 5
70/// assigns three: OBJECT_DATAGRAM, STREAM_HEADER_SUBGROUP and FETCH_HEADER.
71/// Everything outside those three is unknown, and the session ends.
72///
73/// All three of them, on the other hand, are values this draft defines. A
74/// reader handed one it was not written for — a fetch stream at the subgroup
75/// reader, or a datagram type at either — is refused, but the disagreement is
76/// with the caller rather than with the draft, and the session survives it.
77///
78/// Draft-07 is the only draft where the datagram types fall on this side of the
79/// split, and the reason is the shared table described on [`StreamType`]. From
80/// draft-08 on the two tables are separate, and a datagram type at the head of
81/// a stream is then genuinely unknown there.
82fn stream_type_error(raw: u64) -> CodecError {
83    if StreamType::from_id(raw).is_some() {
84        CodecError::InvalidField
85    } else {
86        CodecError::UnknownStreamType(raw)
87    }
88}
89
90/// Object within a subgroup stream.
91///
92/// Encoding: object_id(vi), payload_length(vi),
93///   if payload_length == 0: object_status(vi)
94///   else: payload bytes (status is implicitly Normal)
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct ObjectHeader {
97    /// Object identifier within the subgroup.
98    pub object_id: VarInt,
99    /// Length of the object payload in bytes.
100    pub payload_length: VarInt,
101    /// Status of this object.
102    pub object_status: ObjectStatus,
103}
104
105impl SubgroupHeader {
106    /// Encode a subgroup stream header including its leading stream-type
107    /// field, so the bytes form the start of a data stream a peer can read.
108    ///
109    /// [`Self::encode`] writes the body alone, which is what a caller wants
110    /// once the stream is already open and what a caller must not use for its
111    /// first write.
112    pub fn encode_stream(&self, buf: &mut impl BufMut) {
113        VarInt::from_usize(StreamType::Subgroup as usize).encode(buf);
114        self.encode(buf);
115    }
116
117    /// Encode the subgroup header into the buffer.
118    pub fn encode(&self, buf: &mut impl BufMut) {
119        self.track_alias.encode(buf);
120        self.group_id.encode(buf);
121        self.subgroup_id.encode(buf);
122        buf.put_u8(self.publisher_priority);
123    }
124
125    /// Decode a subgroup header from the buffer.
126    pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
127        let track_alias = VarInt::decode(buf)?;
128        let group_id = VarInt::decode(buf)?;
129        let subgroup_id = VarInt::decode(buf)?;
130        if buf.remaining() < 1 {
131            return Err(CodecError::UnexpectedEnd);
132        }
133        let publisher_priority = buf.get_u8();
134        Ok(Self { track_alias, group_id, subgroup_id, publisher_priority })
135    }
136
137    /// Decode a subgroup header from the start of a data stream, consuming
138    /// the leading stream type varint.
139    ///
140    /// Errors with [`CodecError::UnknownStreamType`] when the leading type is
141    /// not one this draft's stream table assigns, and with
142    /// [`CodecError::InvalidField`] when it is the other assigned type — a
143    /// stream this reader cannot read, but not one the draft asks a session to
144    /// be closed over.
145    pub fn decode_stream(buf: &mut impl Buf) -> Result<Self, CodecError> {
146        let stream_type = VarInt::decode(buf)?.into_inner();
147        if stream_type != StreamType::Subgroup as u64 {
148            return Err(stream_type_error(stream_type));
149        }
150        Self::decode(buf)
151    }
152}
153
154impl ObjectHeader {
155    /// Encode the object header into the buffer.
156    pub fn encode(&self, buf: &mut impl BufMut) {
157        self.object_id.encode(buf);
158        self.payload_length.encode(buf);
159        if self.payload_length.into_inner() == 0 {
160            VarInt::from_usize(self.object_status as usize).encode(buf);
161        }
162    }
163
164    /// Encode the header, refusing a status the framing cannot carry.
165    ///
166    /// Section 7.3.1 puts the Object Status field on the wire only when the
167    /// Object Payload Length is zero, and Section 7.1.1.1 says "Any object
168    /// with a status code other than zero MUST have an empty payload". A
169    /// non-zero status paired with a non-zero payload length therefore has no
170    /// encoding at all: [`Self::encode`] drops the status and the peer reads an
171    /// ordinary object, which is a different object from the one the caller
172    /// described. This refuses instead.
173    ///
174    /// The datagram types on this draft already refuse the same pairing. These
175    /// two did not, and they are the ones a publisher writes on every stream.
176    ///
177    /// # Errors
178    ///
179    /// [`CodecError::InvalidField`] if a non-zero Object Status is paired with
180    /// a non-zero Object Payload Length.
181    pub fn encode_checked(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
182        if self.payload_length.into_inner() != 0 && self.object_status as usize != 0 {
183            return Err(CodecError::InvalidField);
184        }
185        self.encode(buf);
186        Ok(())
187    }
188
189    /// Decode an object header from the buffer.
190    pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
191        let object_id = VarInt::decode(buf)?;
192        let payload_length = VarInt::decode(buf)?;
193        let object_status = if payload_length.into_inner() == 0 {
194            let status_val = VarInt::decode(buf)?.into_inner();
195            ObjectStatus::from_u64(status_val).ok_or(CodecError::InvalidField)?
196        } else {
197            ObjectStatus::Normal
198        };
199        Ok(Self { object_id, payload_length, object_status })
200    }
201}
202
203// ============================================================
204// Datagram (type 0x01)
205// ============================================================
206
207/// Datagram header (draft-07).
208///
209/// Encoding (after the type varint):
210///   track_alias(vi), group_id(vi), object_id(vi),
211///   publisher_priority(u8), payload_length(vi),
212///   [object_status(vi) if payload_length==0],
213///   payload bytes
214#[derive(Debug, Clone, PartialEq, Eq)]
215pub struct DatagramHeader {
216    /// Track alias identifying the subscription.
217    pub track_alias: VarInt,
218    /// Group identifier.
219    pub group_id: VarInt,
220    /// Object identifier within the group.
221    pub object_id: VarInt,
222    /// Publisher priority for delivery ordering.
223    pub publisher_priority: u8,
224    /// Status of this object.
225    pub object_status: ObjectStatus,
226    /// Length of the object payload in bytes.
227    pub payload_length: VarInt,
228}
229
230impl DatagramHeader {
231    /// Encode the datagram header into the buffer.
232    ///
233    /// The declared payload length is taken as the authority on framing: the
234    /// status field is written exactly when that length is zero, because that
235    /// is the condition under which the OBJECT_DATAGRAM layout in draft-07
236    /// Section 7.2 carries one. That is what makes this infallible — and what
237    /// makes it lossy when the struct disagrees with itself. An
238    /// `object_status` set alongside a non-zero `payload_length` is discarded
239    /// here without a word. Prefer [`Self::encode_checked`], which refuses that
240    /// combination instead of resolving it.
241    pub fn encode(&self, buf: &mut impl BufMut) {
242        self.track_alias.encode(buf);
243        self.group_id.encode(buf);
244        self.object_id.encode(buf);
245        buf.put_u8(self.publisher_priority);
246        self.payload_length.encode(buf);
247        if self.payload_length.into_inner() == 0 {
248            VarInt::from_usize(self.object_status as usize).encode(buf);
249        }
250    }
251
252    /// Encode the datagram header, refusing a status the framing cannot carry.
253    ///
254    /// A datagram states its Object Status only when its Object Payload Length
255    /// is zero. With a non-zero length there is no status field on the wire, so
256    /// an `object_status` of anything but [`ObjectStatus::Normal`] has nowhere
257    /// to go: [`Self::encode`] drops it and the datagram parses back as an
258    /// ordinary payload object. An End of Group marker written that way does
259    /// not arrive late or malformed — it does not arrive at all, and the
260    /// receiver sees a normal object in its place.
261    ///
262    /// That pair is also the frame draft-07 Section 7.1.1.1 forbids outright:
263    /// "Any object with a status code other than zero MUST have an empty
264    /// payload." So the refusal here is not merely about what this encoder can
265    /// express; there is no conforming datagram to express.
266    ///
267    /// [`ObjectStatus::Normal`] beside a payload is not that case and is
268    /// accepted. It is the status every payload-bearing object has under the
269    /// rule above, and the one the encoding elides, so stating it asks for
270    /// exactly the bytes leaving it out asks for and nothing is lost.
271    ///
272    /// Errors with [`CodecError::InvalidField`] on the lossy combination,
273    /// before any byte is written, so a refused header leaves `buf` untouched.
274    pub fn encode_checked(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
275        if self.payload_length.into_inner() != 0 && self.object_status != ObjectStatus::Normal {
276            return Err(CodecError::InvalidField);
277        }
278        self.encode(buf);
279        Ok(())
280    }
281
282    /// Decode a datagram header from the buffer.
283    pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
284        let track_alias = VarInt::decode(buf)?;
285        let group_id = VarInt::decode(buf)?;
286        let object_id = VarInt::decode(buf)?;
287        if buf.remaining() < 1 {
288            return Err(CodecError::UnexpectedEnd);
289        }
290        let publisher_priority = buf.get_u8();
291        let payload_length = VarInt::decode(buf)?;
292        let object_status = if payload_length.into_inner() == 0 {
293            let status_val = VarInt::decode(buf)?.into_inner();
294            ObjectStatus::from_u64(status_val).ok_or(CodecError::InvalidField)?
295        } else {
296            ObjectStatus::Normal
297        };
298        Ok(Self {
299            track_alias,
300            group_id,
301            object_id,
302            publisher_priority,
303            object_status,
304            payload_length,
305        })
306    }
307}
308
309// ============================================================
310// Datagram framing
311// ============================================================
312
313/// One datagram, of whichever shape its type field names.
314///
315/// A MoQT datagram opens with a variable-length integer naming its type, and
316/// that integer is what says which of the layouts above follows it — draft-07 names one datagram type, and this is it.
317/// Neither [`DatagramHeader`] nor its type field reads or writes it, so neither can be handed
318/// the first byte of a datagram a peer sent, and neither produces bytes a peer
319/// can read. This is the entry point that does both.
320///
321/// The payload of a payload-bearing datagram runs to the end of the QUIC
322/// datagram, so it is not part of this value: [`Self::decode`] stops at the end
323/// of the header and leaves the payload in the buffer, and a caller appends the
324/// payload after [`Self::encode`].
325#[derive(Debug, Clone, PartialEq, Eq)]
326pub enum Datagram {
327    /// An object carrying a payload.
328    Payload(DatagramHeader),
329}
330
331impl Datagram {
332    /// Whether this datagram states an Object Status instead of carrying a
333    /// payload.
334    ///
335    /// Draft-07 has one datagram layout and hangs the status off a declared
336    /// payload length of zero, so the answer is in the body rather than in the
337    /// type field.
338    pub fn is_status(&self) -> bool {
339        match self {
340            Self::Payload(header) => header.payload_length.into_inner() == 0,
341        }
342    }
343
344    /// The type field this value writes.
345    pub fn datagram_type(&self) -> StreamType {
346        match self {
347            Self::Payload(_) => StreamType::Datagram,
348        }
349    }
350
351    /// Decode a datagram from its first byte, type field included.
352    ///
353    /// Errors with [`CodecError::UnknownStreamType`] when the leading type is
354    /// one Table 5 does not assign, which Section 7 answers with a close, and
355    /// with [`CodecError::InvalidField`] when it assigns the value but to a
356    /// stream rather than a datagram. `stream_type_error` draws that line, and
357    /// names the stream rule for both because draft-07 numbers datagrams in the
358    /// stream table.
359    pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
360        let raw = VarInt::decode(buf)?.into_inner();
361        match StreamType::from_id(raw) {
362            Some(StreamType::Datagram) => Ok(Self::Payload(DatagramHeader::decode(buf)?)),
363            _ => Err(stream_type_error(raw)),
364        }
365    }
366
367    /// Encode the datagram, type field included.
368    pub fn encode(&self, buf: &mut impl BufMut) {
369        VarInt::from_usize(self.datagram_type() as usize).encode(buf);
370        match self {
371            Self::Payload(header) => header.encode(buf),
372        }
373    }
374
375    /// Encode the datagram, refusing a header the framing it names cannot
376    /// carry.
377    ///
378    /// The body is built before anything reaches `buf`, so a refused datagram
379    /// leaves `buf` untouched rather than a type field with no body under it.
380    pub fn encode_checked(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
381        let mut body = Vec::with_capacity(64);
382        match self {
383            Self::Payload(header) => header.encode_checked(&mut body)?,
384        }
385        VarInt::from_usize(self.datagram_type() as usize).encode(buf);
386        buf.put_slice(&body);
387        Ok(())
388    }
389}
390
391// ============================================================
392// Fetch stream (type 0x05)
393// ============================================================
394
395/// Fetch stream header (follows the stream type varint).
396#[derive(Debug, Clone, PartialEq, Eq)]
397pub struct FetchHeader {
398    /// Subscribe ID this fetch responds to.
399    pub subscribe_id: VarInt,
400}
401
402/// Object within a fetch stream.
403///
404/// Encoding: group_id(vi), subgroup_id(vi), object_id(vi),
405///   publisher_priority(u8), payload_length(vi),
406///   [object_status(vi) if payload_length==0],
407///   payload bytes
408#[derive(Debug, Clone, PartialEq, Eq)]
409pub struct FetchObjectHeader {
410    /// Group identifier.
411    pub group_id: VarInt,
412    /// Subgroup identifier within the group.
413    pub subgroup_id: VarInt,
414    /// Object identifier within the subgroup.
415    pub object_id: VarInt,
416    /// Publisher priority for delivery ordering.
417    pub publisher_priority: u8,
418    /// Status of this object.
419    pub object_status: ObjectStatus,
420    /// Length of the object payload in bytes.
421    pub payload_length: VarInt,
422}
423
424impl FetchHeader {
425    /// Encode a fetch stream header including its leading stream-type field,
426    /// so the bytes form the start of a data stream a peer can read.
427    ///
428    /// [`Self::encode`] writes the body alone, which is what a caller wants
429    /// once the stream is already open and what a caller must not use for its
430    /// first write. The read side has had [`Self::decode_stream`] all along,
431    /// so without this the codec could not round-trip its own fetch stream
432    /// through its own reader.
433    pub fn encode_stream(&self, buf: &mut impl BufMut) {
434        VarInt::from_usize(StreamType::Fetch as usize).encode(buf);
435        self.encode(buf);
436    }
437
438    /// Encode the fetch header into the buffer.
439    pub fn encode(&self, buf: &mut impl BufMut) {
440        self.subscribe_id.encode(buf);
441    }
442
443    /// Decode a fetch header from the buffer.
444    pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
445        let subscribe_id = VarInt::decode(buf)?;
446        Ok(Self { subscribe_id })
447    }
448
449    /// Decode a fetch header from the start of a data stream, consuming the
450    /// leading stream type varint.
451    ///
452    /// Errors with [`CodecError::UnknownStreamType`] when the leading type is
453    /// not one this draft's stream table assigns, and with
454    /// [`CodecError::InvalidField`] when it is the other assigned type — a
455    /// stream this reader cannot read, but not one the draft asks a session to
456    /// be closed over.
457    pub fn decode_stream(buf: &mut impl Buf) -> Result<Self, CodecError> {
458        let stream_type = VarInt::decode(buf)?.into_inner();
459        if stream_type != StreamType::Fetch as u64 {
460            return Err(stream_type_error(stream_type));
461        }
462        Self::decode(buf)
463    }
464}
465
466impl FetchObjectHeader {
467    /// Encode the fetch object header into the buffer.
468    pub fn encode(&self, buf: &mut impl BufMut) {
469        self.group_id.encode(buf);
470        self.subgroup_id.encode(buf);
471        self.object_id.encode(buf);
472        buf.put_u8(self.publisher_priority);
473        self.payload_length.encode(buf);
474        if self.payload_length.into_inner() == 0 {
475            VarInt::from_usize(self.object_status as usize).encode(buf);
476        }
477    }
478
479    /// Encode the header, refusing a status the framing cannot carry.
480    ///
481    /// Section 7.3.2 puts the Object Status field on the wire only when the
482    /// Object Payload Length is zero, and Section 7.1.1.1 says "Any object
483    /// with a status code other than zero MUST have an empty payload". A
484    /// non-zero status paired with a non-zero payload length therefore has no
485    /// encoding at all: [`Self::encode`] drops the status and the peer reads an
486    /// ordinary object, which is a different object from the one the caller
487    /// described. This refuses instead.
488    ///
489    /// The datagram types on this draft already refuse the same pairing. These
490    /// two did not, and they are the ones a publisher writes on every stream.
491    ///
492    /// # Errors
493    ///
494    /// [`CodecError::InvalidField`] if a non-zero Object Status is paired with
495    /// a non-zero Object Payload Length.
496    pub fn encode_checked(&self, buf: &mut impl BufMut) -> Result<(), CodecError> {
497        if self.payload_length.into_inner() != 0 && self.object_status as usize != 0 {
498            return Err(CodecError::InvalidField);
499        }
500        self.encode(buf);
501        Ok(())
502    }
503
504    /// Decode a fetch object header from the buffer.
505    pub fn decode(buf: &mut impl Buf) -> Result<Self, CodecError> {
506        let group_id = VarInt::decode(buf)?;
507        let subgroup_id = VarInt::decode(buf)?;
508        let object_id = VarInt::decode(buf)?;
509        if buf.remaining() < 1 {
510            return Err(CodecError::UnexpectedEnd);
511        }
512        let publisher_priority = buf.get_u8();
513        let payload_length = VarInt::decode(buf)?;
514        let object_status = if payload_length.into_inner() == 0 {
515            let status_val = VarInt::decode(buf)?.into_inner();
516            ObjectStatus::from_u64(status_val).ok_or(CodecError::InvalidField)?
517        } else {
518            ObjectStatus::Normal
519        };
520        Ok(Self {
521            group_id,
522            subgroup_id,
523            object_id,
524            publisher_priority,
525            object_status,
526            payload_length,
527        })
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534
535    /// A datagram header for track 1, group 0, object 0, priority 128, holding
536    /// `status` and declaring `payload_length` bytes of payload after it.
537    fn datagram(status: ObjectStatus, payload_length: u64) -> DatagramHeader {
538        DatagramHeader {
539            track_alias: VarInt::from_usize(1),
540            group_id: VarInt::from_usize(0),
541            object_id: VarInt::from_usize(0),
542            publisher_priority: 128,
543            object_status: status,
544            payload_length: VarInt::from_usize(payload_length as usize),
545        }
546    }
547
548    /// A status a payload-bearing datagram cannot state is refused, not
549    /// dropped.
550    ///
551    /// Draft-07 puts the Object Status field on a datagram only when its Object
552    /// Payload Length is zero, so a header holding End of Group under a
553    /// non-zero length asks for two framings at once. [`DatagramHeader::encode`]
554    /// resolves that by writing the length and leaving the status out, which is
555    /// the loss this gate exists for: the datagram that comes back is an
556    /// ordinary object and the marker is simply gone, indistinguishable from
557    /// one that never carried a status. The second half of the test observes
558    /// exactly that, so the gate states the old behaviour as well as the new.
559    ///
560    /// The statuses are read from `ObjectStatus::ALL` rather than listed here,
561    /// so the sweep follows the draft's registry instead of a copy of it.
562    /// Normal is exempt and checked separately: draft-07 Section 7.1.1.1 says
563    /// "Any object with a status code other than zero MUST have an empty
564    /// payload", so Normal is the status every payload-bearing object already
565    /// has, and naming it asks for the same bytes as leaving it out.
566    ///
567    /// # What this catches, observed by making each change and running it
568    ///
569    /// Dropping the check from `encode_checked`, leaving the payload length to
570    /// decide on its own as it did before:
571    ///
572    /// ```text
573    /// encode_checked must refuse ObjectDoesNotExist beside a payload; got Ok(())
574    /// ```
575    ///
576    /// Widening the check to refuse every payload-bearing header, Normal
577    /// included:
578    ///
579    /// ```text
580    /// encode_checked refused a Normal object carrying a payload: InvalidField
581    /// ```
582    #[test]
583    fn encode_checked_refuses_a_status_a_payload_hides() {
584        for &status in ObjectStatus::ALL {
585            if status == ObjectStatus::Normal {
586                continue;
587            }
588
589            let header = datagram(status, 4);
590            let mut refused = Vec::new();
591            let result = header.encode_checked(&mut refused);
592            assert!(
593                matches!(result, Err(CodecError::InvalidField)),
594                "encode_checked must refuse {status:?} beside a payload; got {result:?}"
595            );
596            assert!(refused.is_empty(), "a refused {status:?} header still wrote {refused:?}");
597
598            // What the refusal replaces: the infallible encode writes the
599            // header without the status, and it decodes back as Normal.
600            let mut dropped = Vec::new();
601            header.encode(&mut dropped);
602            let decoded = DatagramHeader::decode(&mut &dropped[..])
603                .unwrap_or_else(|e| panic!("the lossy encoding of {status:?} must parse: {e:?}"));
604            assert_eq!(
605                decoded.object_status,
606                ObjectStatus::Normal,
607                "{status:?} beside a payload is exactly the status `encode` loses"
608            );
609
610            // The same status with an empty payload is representable, so it is
611            // written and read back unchanged.
612            let mut empty = Vec::new();
613            datagram(status, 0)
614                .encode_checked(&mut empty)
615                .unwrap_or_else(|e| panic!("encode_checked refused an empty {status:?}: {e:?}"));
616            let decoded = DatagramHeader::decode(&mut &empty[..]).unwrap();
617            assert_eq!(decoded.object_status, status, "empty {status:?} lost its status");
618        }
619
620        // Normal beside a payload asks for the bytes the encoding already
621        // writes for an object with no status field, so it is accepted.
622        let mut normal = Vec::new();
623        datagram(ObjectStatus::Normal, 4).encode_checked(&mut normal).unwrap_or_else(|e| {
624            panic!("encode_checked refused a Normal object carrying a payload: {e:?}")
625        });
626        let mut plain = Vec::new();
627        datagram(ObjectStatus::Normal, 4).encode(&mut plain);
628        assert_eq!(normal, plain, "a permitted header must encode exactly as `encode` writes it");
629        let decoded = DatagramHeader::decode(&mut &normal[..]).unwrap();
630        assert_eq!(decoded.payload_length.into_inner(), 4);
631        assert_eq!(decoded.object_status, ObjectStatus::Normal);
632    }
633}