Skip to main content

liminal/protocol/
codec.rs

1mod known;
2mod payload;
3
4#[cfg(test)]
5mod tests_support;
6
7use super::causal::MessageId;
8use super::envelope::{MessageEnvelope, SchemaId};
9use super::error::ProtocolError;
10use super::frame::{
11    DISCONNECT_REASON_BODY_VERSION, DeliveredId, Frame, FrameType, HEADER_LEN, LINK_END_EXPORT,
12    LINK_END_IMPORT, LINK_MACHINE_LEN, LINK_OPEN_ACK_HELD, LINK_OPEN_ACK_OPENED,
13    LINK_OPEN_ACK_REFUSED, LinkEnd, LinkOffer, LinkOpenOutcome, WORKER_REGISTER_ACK_ACCEPTED,
14    WORKER_REGISTER_ACK_REJECTED, WorkerActivityDescriptor, WorkerRegisterOutcome,
15    WorkerRegistration, validate_stream,
16};
17use super::version::ProtocolVersion;
18use known::decode_known_payload;
19use payload::{
20    PayloadReader, PayloadWriter, U16_LEN, U32_LEN, U64_LEN, bytes_field_len, checked_u32_len,
21    option_string_len, option_u16_len, schema_ids_field_len, string_field_len,
22    string_vec_field_len, sum_lengths,
23};
24
25/// Wire length of the one-byte fields the worker-registration frames use (the
26/// optional-node presence byte folds into [`option_string_len`]; this covers the
27/// ack's status byte).
28const U8_FIELD_LEN: usize = 1;
29
30/// Return the number of bytes needed to encode a frame.
31///
32/// # Errors
33///
34/// Returns [`ProtocolError`] when the frame violates stream invariants or its
35/// payload cannot fit in the protocol's `u32` length fields.
36pub fn encoded_len(frame: &Frame) -> Result<usize, ProtocolError> {
37    frame.validate()?;
38    let payload_len = encoded_payload_len(frame)?;
39    HEADER_LEN
40        .checked_add(payload_len)
41        .ok_or_else(|| ProtocolError::codec("encoded frame length overflowed usize"))
42}
43
44/// Encode a frame into the provided byte buffer, returning bytes written.
45///
46/// The buffer must be at least [`encoded_len`] bytes long. Encoding writes the
47/// fixed 10-byte header followed by the serialized payload and performs no heap
48/// allocation.
49///
50/// # Errors
51///
52/// Returns [`ProtocolError`] when the frame violates stream invariants, its
53/// payload cannot fit in the protocol's length fields, or the provided buffer is
54/// too small.
55pub fn encode(frame: &Frame, buffer: &mut [u8]) -> Result<usize, ProtocolError> {
56    frame.validate()?;
57    let payload_len = encoded_payload_len(frame)?;
58    let payload_length = u32::try_from(payload_len)
59        .map_err(|_| ProtocolError::codec("payload length exceeded u32::MAX"))?;
60    let total_len = HEADER_LEN
61        .checked_add(payload_len)
62        .ok_or_else(|| ProtocolError::codec("encoded frame length overflowed usize"))?;
63
64    if buffer.len() < total_len {
65        return Err(ProtocolError::codec("output buffer is too small"));
66    }
67
68    let Some(header) = buffer.get_mut(..HEADER_LEN) else {
69        return Err(ProtocolError::codec(
70            "output buffer is too small for header",
71        ));
72    };
73    write_header(frame, payload_length, header)?;
74
75    let Some(payload) = buffer.get_mut(HEADER_LEN..total_len) else {
76        return Err(ProtocolError::codec(
77            "output buffer is too small for payload",
78        ));
79    };
80    write_payload(frame, payload)?;
81
82    Ok(total_len)
83}
84
85/// Decode one complete frame from a byte buffer.
86///
87/// Returns the decoded frame and the number of bytes consumed. Unknown frame
88/// types are length-delimited and returned as [`Frame::Unknown`] without
89/// producing an error.
90///
91/// # Errors
92///
93/// Returns [`ProtocolError::IncompleteHeader`] for buffers shorter than the
94/// fixed header, [`ProtocolError::TruncatedPayload`] when the declared payload
95/// is not fully present, and [`ProtocolError`] for malformed known-frame
96/// payloads or invalid stream placement.
97pub fn decode(buffer: &[u8]) -> Result<(Frame, usize), ProtocolError> {
98    if buffer.len() < HEADER_LEN {
99        return Err(ProtocolError::IncompleteHeader {
100            message: Some("buffer shorter than fixed frame header".to_owned()),
101        });
102    }
103
104    let Some(header) = buffer.get(..HEADER_LEN) else {
105        return Err(ProtocolError::IncompleteHeader {
106            message: Some("buffer shorter than fixed frame header".to_owned()),
107        });
108    };
109    let mut header_reader = PayloadReader::new(header);
110    let type_id = header_reader.read_u8()?;
111    let flags = header_reader.read_u8()?;
112    let stream_id = header_reader.read_u32()?;
113    let payload_length = header_reader.read_u32()?;
114    header_reader.finish()?;
115
116    let payload_len = usize::try_from(payload_length)
117        .map_err(|_| ProtocolError::codec("payload length cannot fit usize"))?;
118    let total_len = HEADER_LEN
119        .checked_add(payload_len)
120        .ok_or_else(|| ProtocolError::codec("decoded frame length overflowed usize"))?;
121
122    if buffer.len() < total_len {
123        return Err(ProtocolError::TruncatedPayload {
124            message: Some("buffer shorter than declared payload length".to_owned()),
125        });
126    }
127
128    let Some(payload) = buffer.get(HEADER_LEN..total_len) else {
129        return Err(ProtocolError::TruncatedPayload {
130            message: Some("buffer shorter than declared payload length".to_owned()),
131        });
132    };
133
134    let frame_type = FrameType::from(type_id);
135    let frame = decode_payload(frame_type, flags, stream_id, payload)?;
136    Ok((frame, total_len))
137}
138
139fn write_header(
140    frame: &Frame,
141    payload_length: u32,
142    buffer: &mut [u8],
143) -> Result<(), ProtocolError> {
144    let mut writer = PayloadWriter::new(buffer);
145    writer.write_u8(u8::from(frame.frame_type()))?;
146    writer.write_u8(frame.wire_flags())?;
147    writer.write_u32(frame.stream_id())?;
148    writer.write_u32(payload_length)?;
149    writer.finish()
150}
151
152fn encoded_payload_len(frame: &Frame) -> Result<usize, ProtocolError> {
153    match frame {
154        Frame::Connect { auth_token, .. } => sum_lengths(&[
155            ProtocolVersion::WIRE_LEN,
156            ProtocolVersion::WIRE_LEN,
157            bytes_field_len(auth_token)?,
158        ]),
159        Frame::ConnectAck { .. } => sum_lengths(&[ProtocolVersion::WIRE_LEN, U32_LEN]),
160        Frame::ConnectError { message, .. }
161        | Frame::SubscribeError { message, .. }
162        | Frame::PublishError { message, .. } => {
163            sum_lengths(&[U16_LEN, option_string_len(message.as_deref())?])
164        }
165        Frame::Disconnect { .. } | Frame::Ping { .. } | Frame::Pong { .. } => Ok(0),
166        Frame::DisconnectReason { reason, .. } => {
167            sum_lengths(&[U8_FIELD_LEN, U16_LEN, string_field_len(reason.name())?])
168        }
169        Frame::Subscribe {
170            channel,
171            accepted_schemas,
172            ..
173        } => sum_lengths(&[
174            string_field_len(channel)?,
175            schema_ids_field_len(accepted_schemas)?,
176            U32_LEN,
177        ]),
178        Frame::SubscribeAck { .. } => sum_lengths(&[U64_LEN, SchemaId::WIRE_LEN]),
179        Frame::Unsubscribe { .. } => Ok(U64_LEN),
180        Frame::PublishAck {
181            durable_position, ..
182        } => sum_lengths(&[U64_LEN, durable_position.map_or(0, |_| U64_LEN)]),
183        Frame::Publish {
184            channel,
185            envelope,
186            idempotency_key,
187            ..
188        } => {
189            let mut parts = vec![
190                string_field_len(channel)?,
191                envelope_bytes_field_len(envelope.encoded_len()?)?,
192            ];
193            if let Some(key) = idempotency_key {
194                parts.push(string_field_len(key)?);
195            }
196            sum_lengths(&parts)
197        }
198        Frame::ConversationOpen { .. }
199        | Frame::ConversationMessage { .. }
200        | Frame::ConversationClose { .. }
201        | Frame::ConversationError { .. } => conversation_payload_len(frame),
202        // Conversation-message and delivery frames share a wire shape: a `u64`
203        // prefix (conversation id / delivery seq) followed by a length-prefixed
204        // envelope. A delivery carrying its message id adds the boot and the id.
205        Frame::Deliver {
206            envelope, message, ..
207        } => sum_lengths(&[
208            U64_LEN,
209            envelope_bytes_field_len(envelope.encoded_len()?)?,
210            message.map_or(0, |_| U64_LEN + U64_LEN),
211        ]),
212        Frame::LinkOpen { offer, .. } => link_open_payload_len(offer),
213        Frame::LinkOpenAck { outcome, .. } => link_open_ack_payload_len(outcome),
214        Frame::LinkMessage {
215            subject, sealed, ..
216        } => sum_lengths(&[string_field_len(subject)?, bytes_field_len(sealed)?]),
217        Frame::Accept {
218            referenced_message_id,
219            ..
220        } => message_id_field_len(referenced_message_id),
221        Frame::Defer {
222            referenced_message_id,
223            reason,
224            ..
225        }
226        | Frame::Reject {
227            referenced_message_id,
228            reason,
229            ..
230        } => sum_lengths(&[
231            message_id_field_len(referenced_message_id)?,
232            option_string_len(reason.as_deref())?,
233        ]),
234        Frame::Push { payload, .. } | Frame::PushReply { payload, .. } => {
235            sum_lengths(&[U64_LEN, bytes_field_len(payload)?])
236        }
237        Frame::WorkerRegister { registration, .. } => worker_register_payload_len(registration),
238        Frame::WorkerRegisterAck { outcome, .. } => worker_register_ack_payload_len(outcome),
239        Frame::Unknown { payload, .. } => checked_u32_len(payload.len()).map(|()| payload.len()),
240    }
241}
242
243fn envelope_bytes_field_len(envelope_len: usize) -> Result<usize, ProtocolError> {
244    checked_u32_len(envelope_len)?;
245    sum_lengths(&[U32_LEN, envelope_len])
246}
247
248fn message_id_field_len(message_id: &MessageId) -> Result<usize, ProtocolError> {
249    string_field_len(message_id.as_str())
250}
251
252fn conversation_payload_len(frame: &Frame) -> Result<usize, ProtocolError> {
253    match frame {
254        Frame::ConversationOpen { subject, .. } => {
255            sum_lengths(&[U64_LEN, string_field_len(subject)?])
256        }
257        // A conversation message shares the delivery frame's wire shape: a
258        // `u64` conversation id followed by a length-prefixed envelope.
259        Frame::ConversationMessage { envelope, .. } => {
260            sum_lengths(&[U64_LEN, envelope_bytes_field_len(envelope.encoded_len()?)?])
261        }
262        Frame::ConversationClose {
263            reason_code,
264            message,
265            ..
266        } => sum_lengths(&[
267            U64_LEN,
268            option_u16_len(*reason_code),
269            option_string_len(message.as_deref())?,
270        ]),
271        Frame::ConversationError { message, .. } => {
272            sum_lengths(&[U64_LEN, U16_LEN, option_string_len(message.as_deref())?])
273        }
274        _ => Err(ProtocolError::codec(
275            "frame type was not a conversation frame",
276        )),
277    }
278}
279
280fn link_open_payload_len(offer: &LinkOffer) -> Result<usize, ProtocolError> {
281    sum_lengths(&[
282        string_field_len(&offer.link_id)?,
283        U8_FIELD_LEN,
284        LINK_MACHINE_LEN,
285        bytes_field_len(&offer.handshake)?,
286    ])
287}
288
289fn link_open_ack_payload_len(outcome: &LinkOpenOutcome) -> Result<usize, ProtocolError> {
290    match outcome {
291        LinkOpenOutcome::Opened { handshake } => {
292            sum_lengths(&[U8_FIELD_LEN, bytes_field_len(handshake)?])
293        }
294        LinkOpenOutcome::Held { grant, draft } => sum_lengths(&[
295            U8_FIELD_LEN,
296            string_field_len(grant)?,
297            string_field_len(draft)?,
298        ]),
299        LinkOpenOutcome::Refused { reason } => {
300            sum_lengths(&[U8_FIELD_LEN, string_field_len(reason)?])
301        }
302    }
303}
304
305fn worker_register_payload_len(registration: &WorkerRegistration) -> Result<usize, ProtocolError> {
306    sum_lengths(&[
307        string_vec_field_len(&registration.namespaces)?,
308        string_field_len(&registration.task_queue)?,
309        option_string_len(registration.node.as_deref())?,
310        string_vec_field_len(&registration.activity_types)?,
311        string_field_len(&registration.identity)?,
312        activity_descriptors_field_len(&registration.activities)?,
313    ])
314}
315
316fn activity_descriptors_field_len(
317    activities: &[WorkerActivityDescriptor],
318) -> Result<usize, ProtocolError> {
319    checked_u32_len(activities.len())?;
320    let mut total = U32_LEN;
321    for activity in activities {
322        total = sum_lengths(&[
323            total,
324            string_field_len(&activity.name)?,
325            string_field_len(&activity.input_schema_json)?,
326            string_field_len(&activity.output_schema_json)?,
327        ])?;
328    }
329    Ok(total)
330}
331
332fn worker_register_ack_payload_len(
333    outcome: &WorkerRegisterOutcome,
334) -> Result<usize, ProtocolError> {
335    match outcome {
336        WorkerRegisterOutcome::Accepted => Ok(U8_FIELD_LEN),
337        WorkerRegisterOutcome::Rejected { reason } => {
338            sum_lengths(&[U8_FIELD_LEN, string_field_len(reason)?])
339        }
340    }
341}
342
343fn write_handshake_payload(
344    frame: &Frame,
345    writer: &mut PayloadWriter<'_>,
346) -> Result<(), ProtocolError> {
347    match frame {
348        Frame::Connect {
349            min_version,
350            max_version,
351            auth_token,
352            ..
353        } => {
354            writer.write_slice(&min_version.to_wire_bytes())?;
355            writer.write_slice(&max_version.to_wire_bytes())?;
356            writer.write_bytes_field(auth_token)
357        }
358        Frame::ConnectAck {
359            selected_version,
360            capabilities,
361            ..
362        } => {
363            writer.write_slice(&selected_version.to_wire_bytes())?;
364            writer.write_u32(*capabilities)
365        }
366        _ => Err(ProtocolError::codec("frame type was not a handshake frame")),
367    }
368}
369
370fn write_pressure_payload(
371    frame: &Frame,
372    writer: &mut PayloadWriter<'_>,
373) -> Result<(), ProtocolError> {
374    match frame {
375        Frame::Accept {
376            referenced_message_id,
377            ..
378        } => writer.write_string_field(referenced_message_id.as_str()),
379        Frame::Defer {
380            referenced_message_id,
381            reason,
382            ..
383        }
384        | Frame::Reject {
385            referenced_message_id,
386            reason,
387            ..
388        } => {
389            writer.write_string_field(referenced_message_id.as_str())?;
390            writer.write_optional_string(reason.as_deref())
391        }
392        _ => Err(ProtocolError::codec("frame type was not a pressure frame")),
393    }
394}
395
396fn write_publish(frame: &Frame, writer: &mut PayloadWriter<'_>) -> Result<(), ProtocolError> {
397    match frame {
398        Frame::Publish {
399            channel,
400            envelope,
401            idempotency_key,
402            ..
403        } => {
404            writer.write_string_field(channel)?;
405            writer.write_bytes_field(&envelope.serialize()?)?;
406            // The trailing key is written ONLY when present (its flag bit tells the
407            // decoder), so a no-key publish stays byte-identical to pre-13-L1.
408            if let Some(key) = idempotency_key {
409                writer.write_string_field(key)?;
410            }
411            Ok(())
412        }
413        Frame::PublishAck {
414            message_id,
415            durable_position,
416            ..
417        } => {
418            writer.write_u64(*message_id)?;
419            writer.write_flagged_u64(*durable_position)
420        }
421        _ => Err(ProtocolError::codec("frame type was not a publish frame")),
422    }
423}
424
425fn write_conversation_payload(
426    frame: &Frame,
427    writer: &mut PayloadWriter<'_>,
428) -> Result<(), ProtocolError> {
429    match frame {
430        Frame::ConversationOpen {
431            conversation_id,
432            subject,
433            ..
434        } => {
435            writer.write_u64(*conversation_id)?;
436            writer.write_string_field(subject)
437        }
438        Frame::ConversationMessage {
439            conversation_id,
440            envelope,
441            ..
442        } => write_u64_prefixed_envelope(writer, *conversation_id, envelope),
443        Frame::ConversationClose {
444            conversation_id,
445            reason_code,
446            message,
447            ..
448        } => {
449            writer.write_u64(*conversation_id)?;
450            writer.write_optional_u16(*reason_code)?;
451            writer.write_optional_string(message.as_deref())
452        }
453        Frame::ConversationError {
454            conversation_id,
455            reason_code,
456            message,
457            ..
458        } => {
459            writer.write_u64(*conversation_id)?;
460            writer.write_u16(*reason_code)?;
461            writer.write_optional_string(message.as_deref())
462        }
463        _ => Err(ProtocolError::codec(
464            "frame type was not a conversation frame",
465        )),
466    }
467}
468
469/// Writes a `u64` prefix followed by a length-prefixed serialized envelope — the
470/// shared body of conversation-message and delivery frames.
471fn write_u64_prefixed_envelope(
472    writer: &mut PayloadWriter<'_>,
473    value: u64,
474    envelope: &MessageEnvelope,
475) -> Result<(), ProtocolError> {
476    writer.write_u64(value)?;
477    writer.write_bytes_field(&envelope.serialize()?)
478}
479
480/// Writes a delivery's body: the `u64` delivery sequence and the envelope, then
481/// the boot and the id ONLY when present (its flag bit tells the decoder), so a
482/// Deliver without them stays byte-identical to 1.1.
483fn write_deliver_payload(
484    writer: &mut PayloadWriter<'_>,
485    delivery_seq: u64,
486    envelope: &MessageEnvelope,
487    message: Option<DeliveredId>,
488) -> Result<(), ProtocolError> {
489    write_u64_prefixed_envelope(writer, delivery_seq, envelope)?;
490    if let Some(DeliveredId { boot, id }) = message {
491        writer.write_u64(boot)?;
492        writer.write_u64(id)?;
493    }
494    Ok(())
495}
496
497fn write_push_payload(frame: &Frame, writer: &mut PayloadWriter<'_>) -> Result<(), ProtocolError> {
498    match frame {
499        Frame::Push {
500            correlation_id,
501            payload,
502            ..
503        }
504        | Frame::PushReply {
505            correlation_id,
506            payload,
507            ..
508        } => {
509            writer.write_u64(*correlation_id)?;
510            writer.write_bytes_field(payload)
511        }
512        _ => Err(ProtocolError::codec("frame type was not a push frame")),
513    }
514}
515
516fn write_worker_register_payload(
517    registration: &WorkerRegistration,
518    writer: &mut PayloadWriter<'_>,
519) -> Result<(), ProtocolError> {
520    writer.write_string_vec_field(&registration.namespaces)?;
521    writer.write_string_field(&registration.task_queue)?;
522    // `node` is optional locality: a presence byte distinguishes `None` from
523    // `Some("")` so an absent node never collapses to an empty string.
524    writer.write_optional_string(registration.node.as_deref())?;
525    writer.write_string_vec_field(&registration.activity_types)?;
526    writer.write_string_field(&registration.identity)?;
527    let count = u32::try_from(registration.activities.len())
528        .map_err(|_| ProtocolError::codec("activity descriptor count exceeded u32::MAX"))?;
529    writer.write_u32(count)?;
530    for activity in &registration.activities {
531        writer.write_string_field(&activity.name)?;
532        writer.write_string_field(&activity.input_schema_json)?;
533        writer.write_string_field(&activity.output_schema_json)?;
534    }
535    Ok(())
536}
537
538fn write_worker_register_ack_payload(
539    outcome: &WorkerRegisterOutcome,
540    writer: &mut PayloadWriter<'_>,
541) -> Result<(), ProtocolError> {
542    match outcome {
543        WorkerRegisterOutcome::Accepted => writer.write_u8(WORKER_REGISTER_ACK_ACCEPTED),
544        WorkerRegisterOutcome::Rejected { reason } => {
545            writer.write_u8(WORKER_REGISTER_ACK_REJECTED)?;
546            writer.write_string_field(reason)
547        }
548    }
549}
550
551/// The wire byte of a link end.
552const fn link_end_byte(end: LinkEnd) -> u8 {
553    match end {
554        LinkEnd::Export => LINK_END_EXPORT,
555        LinkEnd::Import => LINK_END_IMPORT,
556    }
557}
558
559fn write_link_open_payload(
560    offer: &LinkOffer,
561    writer: &mut PayloadWriter<'_>,
562) -> Result<(), ProtocolError> {
563    writer.write_string_field(&offer.link_id)?;
564    writer.write_u8(link_end_byte(offer.end))?;
565    // The machine key is fixed-length: raw, no length prefix.
566    writer.write_slice(&offer.machine)?;
567    writer.write_bytes_field(&offer.handshake)
568}
569
570fn write_link_open_ack_payload(
571    outcome: &LinkOpenOutcome,
572    writer: &mut PayloadWriter<'_>,
573) -> Result<(), ProtocolError> {
574    match outcome {
575        LinkOpenOutcome::Opened { handshake } => {
576            writer.write_u8(LINK_OPEN_ACK_OPENED)?;
577            writer.write_bytes_field(handshake)
578        }
579        LinkOpenOutcome::Held { grant, draft } => {
580            writer.write_u8(LINK_OPEN_ACK_HELD)?;
581            writer.write_string_field(grant)?;
582            writer.write_string_field(draft)
583        }
584        LinkOpenOutcome::Refused { reason } => {
585            writer.write_u8(LINK_OPEN_ACK_REFUSED)?;
586            writer.write_string_field(reason)
587        }
588    }
589}
590
591fn write_payload(frame: &Frame, buffer: &mut [u8]) -> Result<(), ProtocolError> {
592    let mut writer = PayloadWriter::new(buffer);
593    match frame {
594        Frame::Connect { .. } | Frame::ConnectAck { .. } => {
595            write_handshake_payload(frame, &mut writer)?;
596        }
597        Frame::ConnectError {
598            reason_code,
599            message,
600            ..
601        }
602        | Frame::SubscribeError {
603            reason_code,
604            message,
605            ..
606        }
607        | Frame::PublishError {
608            reason_code,
609            message,
610            ..
611        } => {
612            writer.write_u16(*reason_code)?;
613            writer.write_optional_string(message.as_deref())?;
614        }
615        Frame::Disconnect { .. } | Frame::Ping { .. } | Frame::Pong { .. } => {}
616        Frame::DisconnectReason { reason, .. } => {
617            // ACCESS-005 R5: body version, then the reason's code and name.
618            // The typed reason makes a code/name disagreement unwritable.
619            writer.write_u8(DISCONNECT_REASON_BODY_VERSION)?;
620            writer.write_u16(reason.code())?;
621            writer.write_string_field(reason.name())?;
622        }
623        Frame::Subscribe {
624            channel,
625            accepted_schemas,
626            max_in_flight,
627            ..
628        } => {
629            writer.write_string_field(channel)?;
630            writer.write_schema_ids_field(accepted_schemas)?;
631            writer.write_u32(*max_in_flight)?;
632        }
633        Frame::SubscribeAck {
634            subscription_id,
635            selected_schema,
636            ..
637        } => {
638            writer.write_u64(*subscription_id)?;
639            writer.write_schema_id(*selected_schema)?;
640        }
641        Frame::Unsubscribe {
642            subscription_id, ..
643        } => writer.write_u64(*subscription_id)?,
644        Frame::Publish { .. } | Frame::PublishAck { .. } => write_publish(frame, &mut writer)?,
645        Frame::ConversationOpen { .. }
646        | Frame::ConversationMessage { .. }
647        | Frame::ConversationClose { .. }
648        | Frame::ConversationError { .. } => write_conversation_payload(frame, &mut writer)?,
649        Frame::Accept { .. } | Frame::Defer { .. } | Frame::Reject { .. } => {
650            write_pressure_payload(frame, &mut writer)?;
651        }
652        Frame::Push { .. } | Frame::PushReply { .. } => {
653            write_push_payload(frame, &mut writer)?;
654        }
655        Frame::Deliver {
656            delivery_seq,
657            envelope,
658            message,
659            ..
660        } => write_deliver_payload(&mut writer, *delivery_seq, envelope, *message)?,
661        Frame::WorkerRegister { registration, .. } => {
662            write_worker_register_payload(registration, &mut writer)?;
663        }
664        Frame::WorkerRegisterAck { outcome, .. } => {
665            write_worker_register_ack_payload(outcome, &mut writer)?;
666        }
667        Frame::LinkOpen { offer, .. } => write_link_open_payload(offer, &mut writer)?,
668        Frame::LinkOpenAck { outcome, .. } => write_link_open_ack_payload(outcome, &mut writer)?,
669        Frame::LinkMessage {
670            subject, sealed, ..
671        } => {
672            writer.write_string_field(subject)?;
673            writer.write_bytes_field(sealed)?;
674        }
675        Frame::Unknown { payload, .. } => writer.write_slice(payload)?,
676    }
677    writer.finish()
678}
679
680fn decode_payload(
681    frame_type: FrameType,
682    flags: u8,
683    stream_id: u32,
684    payload: &[u8],
685) -> Result<Frame, ProtocolError> {
686    if let FrameType::Unknown(type_id) = frame_type {
687        return Ok(Frame::Unknown {
688            type_id,
689            flags,
690            stream_id,
691            payload: payload.to_vec(),
692        });
693    }
694
695    validate_stream(frame_type, stream_id)?;
696    decode_known_payload(frame_type, flags, stream_id, payload)
697}
698
699#[cfg(test)]
700mod tests;