Skip to main content

zerodds_coap_bridge/
codec.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! CoAP-Wire-Codec — RFC 7252 §3 + §3.1.
5//!
6//! Implementiert encode/decode der vollstaendigen Message inkl.:
7//! * 4-Byte-Fixed-Header (Ver/T/TKL/Code/MID).
8//! * Token (0..=8 Bytes).
9//! * Options mit Delta-Encoding + Extended-Length-Mechanik (13/14).
10//! * 0xFF Payload-Marker.
11//! * Payload bis Ende des Datagramms.
12
13use alloc::vec::Vec;
14use core::fmt;
15
16use crate::message::{CoapCode, CoapMessage, MessageType};
17use crate::option::{CoapOption, OptionValue};
18
19/// Codec-Fehler — Spec-konform "MUST be processed as a message format
20/// error" Faelle.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum CodecError {
23    /// Header zu kurz (< 4 Bytes).
24    HeaderTooShort,
25    /// Spec §3 — "Implementations MUST set this field to 1 (01 binary).
26    /// Other values are reserved [...]. Messages with unknown version
27    /// numbers MUST be silently ignored." Wir liefern dem Caller einen
28    /// expliziten Fehler.
29    UnsupportedVersion(u8),
30    /// Spec §3 — "Lengths 9-15 are reserved, MUST NOT be sent, and
31    /// MUST be processed as a message format error."
32    ReservedTokenLength(u8),
33    /// Token reicht nicht in den verfuegbaren Bytes.
34    TokenTruncated,
35    /// Option-Header reicht nicht in die verfuegbaren Bytes.
36    OptionHeaderTruncated,
37    /// Spec §3.1 — "15: Reserved for the Payload Marker. If the field
38    /// is set to this value but the entire byte is not the payload
39    /// marker, this MUST be processed as a message format error."
40    OptionDeltaIs15,
41    /// Spec §3.1 — Option-Length 15 reserved.
42    OptionLengthIs15,
43    /// Option-Value reicht nicht in den verfuegbaren Bytes.
44    OptionValueTruncated,
45    /// Spec §3 — "The presence of a marker followed by a zero-length
46    /// payload MUST be processed as a message format error."
47    PayloadMarkerWithoutPayload,
48    /// Token-Length ist > 8 beim Encode (Caller-Fehler).
49    EncodeTokenTooLong,
50}
51
52impl fmt::Display for CodecError {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            Self::HeaderTooShort => f.write_str("header < 4 bytes"),
56            Self::UnsupportedVersion(v) => write!(f, "unsupported CoAP version {v}"),
57            Self::ReservedTokenLength(l) => write!(f, "reserved token length {l}"),
58            Self::TokenTruncated => f.write_str("token truncated"),
59            Self::OptionHeaderTruncated => f.write_str("option header truncated"),
60            Self::OptionDeltaIs15 => f.write_str("option delta 15 (reserved)"),
61            Self::OptionLengthIs15 => f.write_str("option length 15 (reserved)"),
62            Self::OptionValueTruncated => f.write_str("option value truncated"),
63            Self::PayloadMarkerWithoutPayload => {
64                f.write_str("payload marker with zero-length payload")
65            }
66            Self::EncodeTokenTooLong => f.write_str("token length > 8"),
67        }
68    }
69}
70
71#[cfg(feature = "std")]
72impl std::error::Error for CodecError {}
73
74/// Encodiert eine [`CoapMessage`] zum Wire-Format-Byte-Slice.
75///
76/// # Errors
77/// Liefert [`CodecError::EncodeTokenTooLong`] wenn `token.len() > 8`.
78pub fn encode(msg: &CoapMessage) -> Result<Vec<u8>, CodecError> {
79    if msg.token.len() > 8 {
80        return Err(CodecError::EncodeTokenTooLong);
81    }
82    let mut out = Vec::with_capacity(4 + msg.token.len() + 16 + msg.payload.len());
83
84    // Spec §3 Fixed-Header.
85    let ver = 1u8;
86    let t = msg.message_type.to_bits();
87    #[allow(clippy::cast_possible_truncation)]
88    let tkl = msg.token.len() as u8;
89    out.push((ver << 6) | (t << 4) | (tkl & 0x0F));
90    out.push(msg.code.to_byte());
91    out.extend_from_slice(&msg.message_id.to_be_bytes());
92
93    // Token.
94    out.extend_from_slice(&msg.token);
95
96    // Options — Spec §3.1 Delta-Encoding. Sort first.
97    let mut opts: Vec<&CoapOption> = msg.options.iter().collect();
98    opts.sort_by_key(|o| o.number);
99
100    let mut prev_number: u16 = 0;
101    for opt in opts {
102        let delta = opt.number - prev_number;
103        prev_number = opt.number;
104        let value_bytes = opt.value.to_wire_bytes();
105        #[allow(clippy::cast_possible_truncation)]
106        let length = value_bytes.len() as u32;
107
108        let (delta_nibble, delta_extended) = encode_extended(delta as u32);
109        let (length_nibble, length_extended) = encode_extended(length);
110
111        out.push((delta_nibble << 4) | (length_nibble & 0x0F));
112        out.extend_from_slice(&delta_extended);
113        out.extend_from_slice(&length_extended);
114        out.extend_from_slice(&value_bytes);
115    }
116
117    // Payload — Spec §3 0xFF-Marker.
118    if !msg.payload.is_empty() {
119        out.push(0xFF);
120        out.extend_from_slice(&msg.payload);
121    }
122
123    Ok(out)
124}
125
126/// Berechnet `(nibble, extended_bytes)` fuer Delta- oder Length-
127/// Encoding (RFC 7252 §3.1).
128fn encode_extended(value: u32) -> (u8, Vec<u8>) {
129    if value < 13 {
130        #[allow(clippy::cast_possible_truncation)]
131        (value as u8, Vec::new())
132    } else if value < 269 {
133        // 13: 1 byte Extended = value - 13.
134        #[allow(clippy::cast_possible_truncation)]
135        let ext = (value - 13) as u8;
136        (13, alloc::vec![ext])
137    } else {
138        // 14: 2 byte Extended = value - 269 (network byte order).
139        #[allow(clippy::cast_possible_truncation)]
140        let ext = (value - 269) as u16;
141        (14, ext.to_be_bytes().to_vec())
142    }
143}
144
145/// Decodiert eine [`CoapMessage`] aus dem Wire-Format-Byte-Slice.
146///
147/// # Errors
148/// Siehe [`CodecError`].
149pub fn decode(bytes: &[u8]) -> Result<CoapMessage, CodecError> {
150    if bytes.len() < 4 {
151        return Err(CodecError::HeaderTooShort);
152    }
153    let h0 = bytes[0];
154    let version = (h0 >> 6) & 0b11;
155    if version != 1 {
156        return Err(CodecError::UnsupportedVersion(version));
157    }
158    let t_bits = (h0 >> 4) & 0b11;
159    // 2-Bit-Wert ist immer 0..=3 ⇒ from_bits liefert immer Some.
160    let message_type = MessageType::from_bits(t_bits).ok_or(CodecError::HeaderTooShort)?;
161    let tkl = h0 & 0x0F;
162    if tkl > 8 {
163        return Err(CodecError::ReservedTokenLength(tkl));
164    }
165    let code = CoapCode::from_byte(bytes[1]);
166    let message_id = u16::from_be_bytes([bytes[2], bytes[3]]);
167
168    let mut cursor = 4usize;
169    let token_end = cursor.saturating_add(tkl as usize);
170    if token_end > bytes.len() {
171        return Err(CodecError::TokenTruncated);
172    }
173    let token = bytes[cursor..token_end].to_vec();
174    cursor = token_end;
175
176    // Options bis 0xFF oder Ende.
177    let mut options: Vec<CoapOption> = Vec::new();
178    let mut current_number: u16 = 0;
179    while cursor < bytes.len() {
180        let b = bytes[cursor];
181        if b == 0xFF {
182            // Payload-Marker.
183            cursor += 1;
184            if cursor >= bytes.len() {
185                return Err(CodecError::PayloadMarkerWithoutPayload);
186            }
187            break;
188        }
189        let delta_nibble = (b >> 4) & 0x0F;
190        let length_nibble = b & 0x0F;
191        cursor += 1;
192        if delta_nibble == 15 {
193            return Err(CodecError::OptionDeltaIs15);
194        }
195        if length_nibble == 15 {
196            return Err(CodecError::OptionLengthIs15);
197        }
198        let delta = decode_extended(delta_nibble, bytes, &mut cursor)?;
199        let length = decode_extended(length_nibble, bytes, &mut cursor)?;
200        current_number = current_number
201            .checked_add(u16::try_from(delta).map_err(|_| CodecError::OptionHeaderTruncated)?)
202            .ok_or(CodecError::OptionHeaderTruncated)?;
203        let len_us = length as usize;
204        if cursor.saturating_add(len_us) > bytes.len() {
205            return Err(CodecError::OptionValueTruncated);
206        }
207        let value_bytes = bytes[cursor..cursor + len_us].to_vec();
208        cursor += len_us;
209        options.push(CoapOption {
210            number: current_number,
211            value: OptionValue::Opaque(value_bytes),
212        });
213    }
214
215    // Payload (falls Marker konsumiert wurde).
216    let payload = if cursor < bytes.len() {
217        bytes[cursor..].to_vec()
218    } else {
219        Vec::new()
220    };
221
222    Ok(CoapMessage {
223        version,
224        message_type,
225        code,
226        message_id,
227        token,
228        options,
229        payload,
230    })
231}
232
233/// Liest Extended-Bytes nach Spec §3.1 (Delta/Length Nibble 13/14).
234fn decode_extended(nibble: u8, bytes: &[u8], cursor: &mut usize) -> Result<u32, CodecError> {
235    match nibble {
236        v if v < 13 => Ok(u32::from(v)),
237        13 => {
238            if *cursor >= bytes.len() {
239                return Err(CodecError::OptionHeaderTruncated);
240            }
241            let v = u32::from(bytes[*cursor]) + 13;
242            *cursor += 1;
243            Ok(v)
244        }
245        14 => {
246            if *cursor + 1 >= bytes.len() {
247                return Err(CodecError::OptionHeaderTruncated);
248            }
249            let v = u32::from(u16::from_be_bytes([bytes[*cursor], bytes[*cursor + 1]])) + 269;
250            *cursor += 2;
251            Ok(v)
252        }
253        // 15 wurde vom Caller bereits abgefangen.
254        _ => Err(CodecError::OptionHeaderTruncated),
255    }
256}
257
258#[cfg(test)]
259#[allow(
260    clippy::expect_used,
261    clippy::unwrap_used,
262    clippy::panic,
263    clippy::unreachable
264)]
265mod tests {
266    use super::*;
267    use crate::option::{CoapOption, numbers};
268
269    #[test]
270    fn encodes_minimum_get_request_to_4_byte_header() {
271        // RFC 7252 §3 — kleinste Message: GET ohne Token / Options /
272        // Payload.
273        let m = CoapMessage::new(MessageType::Confirmable, CoapCode::GET, 0x1234);
274        let bytes = encode(&m).expect("encode");
275        assert_eq!(bytes.len(), 4);
276        // Ver=1, T=0 (CON), TKL=0.
277        assert_eq!(bytes[0], 0b0100_0000);
278        assert_eq!(bytes[1], CoapCode::GET.to_byte());
279        assert_eq!(bytes[2], 0x12);
280        assert_eq!(bytes[3], 0x34);
281    }
282
283    #[test]
284    fn encode_decode_round_trip_preserves_header_fields() {
285        // Header-Round-Trip.
286        for t in [
287            MessageType::Confirmable,
288            MessageType::NonConfirmable,
289            MessageType::Acknowledgement,
290            MessageType::Reset,
291        ] {
292            let mut m = CoapMessage::new(t, CoapCode::POST, 0xAA55);
293            m.token = alloc::vec![1, 2, 3, 4];
294            let bytes = encode(&m).expect("encode");
295            let parsed = decode(&bytes).expect("decode");
296            assert_eq!(parsed.message_type, t);
297            assert_eq!(parsed.code, CoapCode::POST);
298            assert_eq!(parsed.message_id, 0xAA55);
299            assert_eq!(parsed.token, alloc::vec![1, 2, 3, 4]);
300        }
301    }
302
303    #[test]
304    fn token_length_above_8_is_rejected_on_encode() {
305        // Spec §3 TKL 0..=8.
306        let mut m = CoapMessage::new(MessageType::Confirmable, CoapCode::GET, 0);
307        m.token = alloc::vec![0; 9];
308        assert_eq!(encode(&m), Err(CodecError::EncodeTokenTooLong));
309    }
310
311    #[test]
312    fn header_too_short_decode_fails() {
313        assert_eq!(decode(&[]), Err(CodecError::HeaderTooShort));
314        assert_eq!(decode(&[0; 3]), Err(CodecError::HeaderTooShort));
315    }
316
317    #[test]
318    fn unsupported_version_decode_fails() {
319        // Spec §3 — Version != 1.
320        let bytes = [0b1000_0000_u8, 0, 0, 0]; // Ver=2.
321        assert_eq!(decode(&bytes), Err(CodecError::UnsupportedVersion(2)));
322    }
323
324    #[test]
325    fn reserved_token_length_9_through_15_decode_fails() {
326        // Spec §3.
327        for tkl in 9..=15u8 {
328            let bytes = [0b0100_0000 | tkl, 0, 0, 0];
329            assert_eq!(decode(&bytes), Err(CodecError::ReservedTokenLength(tkl)));
330        }
331    }
332
333    #[test]
334    fn token_truncated_decode_fails() {
335        // TKL=4 aber nur 2 Token-Bytes vorhanden.
336        let bytes = [0b0100_0100_u8, 1, 0, 0, 0xAA, 0xBB];
337        assert_eq!(decode(&bytes), Err(CodecError::TokenTruncated));
338    }
339
340    #[test]
341    fn single_short_option_encodes_in_one_byte_header() {
342        // RFC 7252 §3.1 — Delta=1, Length=2 → 1 byte.
343        let mut m = CoapMessage::new(MessageType::Confirmable, CoapCode::GET, 1);
344        m.options = alloc::vec![CoapOption {
345            number: 1,
346            value: OptionValue::Opaque(alloc::vec![0xAB, 0xCD]),
347        }];
348        let bytes = encode(&m).expect("encode");
349        // Header (4) + Option-Header (1) + Value (2) = 7.
350        assert_eq!(bytes.len(), 7);
351        assert_eq!(bytes[4], (1 << 4) | 2);
352        assert_eq!(&bytes[5..7], &[0xAB, 0xCD]);
353    }
354
355    #[test]
356    fn option_delta_extended_13_uses_one_extra_byte() {
357        // Spec §3.1 — Delta = 20 → nibble=13, ext=7.
358        let mut m = CoapMessage::new(MessageType::Confirmable, CoapCode::GET, 1);
359        m.options = alloc::vec![CoapOption {
360            number: 20,
361            value: OptionValue::Empty,
362        }];
363        let bytes = encode(&m).expect("encode");
364        // [hdr 4][nibble (13<<4|0)=0xD0][delta-ext = 7]
365        assert_eq!(bytes[4], 0xD0);
366        assert_eq!(bytes[5], 7);
367        // Round-Trip.
368        let parsed = decode(&bytes).expect("decode");
369        assert_eq!(parsed.options.len(), 1);
370        assert_eq!(parsed.options[0].number, 20);
371    }
372
373    #[test]
374    fn option_delta_extended_14_uses_two_extra_bytes() {
375        // Spec §3.1 — Delta = 1000 → nibble=14, ext = 1000-269 = 731.
376        let mut m = CoapMessage::new(MessageType::Confirmable, CoapCode::GET, 1);
377        m.options = alloc::vec![CoapOption {
378            number: 1000,
379            value: OptionValue::Empty,
380        }];
381        let bytes = encode(&m).expect("encode");
382        assert_eq!(bytes[4], 0xE0); // nibble=14, length=0.
383        assert_eq!(&bytes[5..7], &731u16.to_be_bytes());
384        let parsed = decode(&bytes).expect("decode");
385        assert_eq!(parsed.options[0].number, 1000);
386    }
387
388    #[test]
389    fn option_length_extended_13_uses_one_extra_byte() {
390        let mut m = CoapMessage::new(MessageType::Confirmable, CoapCode::GET, 1);
391        m.options = alloc::vec![CoapOption {
392            number: 1,
393            value: OptionValue::Opaque(alloc::vec![0; 50]),
394        }];
395        let bytes = encode(&m).expect("encode");
396        // delta=1 nibble=1, length nibble=13 → 0x1D, then ext = 50-13=37.
397        assert_eq!(bytes[4], 0x1D);
398        assert_eq!(bytes[5], 37);
399        let parsed = decode(&bytes).expect("decode");
400        assert_eq!(parsed.options[0].number, 1);
401        if let OptionValue::Opaque(v) = &parsed.options[0].value {
402            assert_eq!(v.len(), 50);
403        } else {
404            panic!("expected opaque");
405        }
406    }
407
408    #[test]
409    fn delta_encoding_sums_across_multiple_options() {
410        // Spec §3.1 — Delta-Encoding-Basis.
411        let mut m = CoapMessage::new(MessageType::Confirmable, CoapCode::GET, 1);
412        m.options = alloc::vec![
413            CoapOption {
414                number: 1,
415                value: OptionValue::Empty,
416            },
417            CoapOption {
418                number: 5,
419                value: OptionValue::Empty,
420            },
421            CoapOption {
422                number: 11,
423                value: OptionValue::Empty,
424            },
425        ];
426        let bytes = encode(&m).expect("encode");
427        let parsed = decode(&bytes).expect("decode");
428        assert_eq!(
429            parsed.options.iter().map(|o| o.number).collect::<Vec<_>>(),
430            alloc::vec![1, 5, 11]
431        );
432    }
433
434    #[test]
435    fn payload_marker_separates_options_from_payload() {
436        // Spec §3 — 0xFF Payload-Marker.
437        let mut m = CoapMessage::new(MessageType::Confirmable, CoapCode::CONTENT, 1);
438        m.payload = alloc::vec![1, 2, 3, 4];
439        let bytes = encode(&m).expect("encode");
440        // Letzte 5 Bytes = 0xFF + payload.
441        let n = bytes.len();
442        assert_eq!(bytes[n - 5], 0xFF);
443        assert_eq!(&bytes[n - 4..], &[1, 2, 3, 4]);
444        let parsed = decode(&bytes).expect("decode");
445        assert_eq!(parsed.payload, alloc::vec![1, 2, 3, 4]);
446    }
447
448    #[test]
449    fn payload_marker_without_payload_is_format_error() {
450        // Spec §3 — "MUST be processed as a message format error".
451        let bytes = [0b0100_0000_u8, CoapCode::GET.to_byte(), 0, 0, 0xFFu8];
452        assert_eq!(decode(&bytes), Err(CodecError::PayloadMarkerWithoutPayload));
453    }
454
455    #[test]
456    fn full_observe_request_encode_decode_round_trip() {
457        // RFC 7641 §2 — Observe-Register-Request mit Uri-Path.
458        let mut m = CoapMessage::new(MessageType::Confirmable, CoapCode::GET, 0xBEEF);
459        m.token = alloc::vec![0x42];
460        m.options = alloc::vec![
461            CoapOption::observe(0),
462            CoapOption::uri_path("sensors"),
463            CoapOption::uri_path("temp"),
464        ];
465        let bytes = encode(&m).expect("encode");
466        let parsed = decode(&bytes).expect("decode");
467        assert_eq!(parsed.token, alloc::vec![0x42]);
468        assert_eq!(parsed.options.len(), 3);
469        // Reihenfolge: Observe (6), Uri-Path (11), Uri-Path (11).
470        assert_eq!(parsed.options[0].number, numbers::OBSERVE);
471        assert_eq!(parsed.options[1].number, numbers::URI_PATH);
472        assert_eq!(parsed.options[2].number, numbers::URI_PATH);
473        // Decoded values are Opaque (Decoder kennt nicht die format-
474        // semantik); Caller konvertiert.
475    }
476
477    #[test]
478    fn options_are_sorted_on_encode() {
479        // Spec §3.1 — "instances MUST appear in order of their Option
480        // Numbers".
481        let mut m = CoapMessage::new(MessageType::Confirmable, CoapCode::GET, 1);
482        m.options = alloc::vec![
483            CoapOption::content_format(50),
484            CoapOption::observe(0),
485            CoapOption::uri_path("a"),
486        ];
487        let bytes = encode(&m).expect("encode");
488        let parsed = decode(&bytes).expect("decode");
489        let numbers: Vec<u16> = parsed.options.iter().map(|o| o.number).collect();
490        // Observe(6) < Uri-Path(11) < Content-Format(12).
491        assert_eq!(numbers, alloc::vec![6, 11, 12]);
492    }
493
494    #[test]
495    fn empty_message_round_trip() {
496        // Spec §4.1 — Code 0.00 = Empty Message (Reset/ACK).
497        let m = CoapMessage::new(MessageType::Acknowledgement, CoapCode::EMPTY, 0xABCD);
498        let bytes = encode(&m).expect("encode");
499        assert_eq!(bytes.len(), 4);
500        let parsed = decode(&bytes).expect("decode");
501        assert_eq!(parsed, m);
502    }
503}