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}