Skip to main content

zerodds_coap_bridge/
message.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! CoAP-Message Modell — RFC 7252 §3 + §12.1.
5
6use alloc::vec::Vec;
7use core::fmt;
8
9use crate::option::CoapOption;
10
11/// `Type` field (RFC 7252 §3, S. 16).
12///
13/// 2-bit unsigned integer.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum MessageType {
16    /// `0` — Confirmable.
17    Confirmable,
18    /// `1` — Non-Confirmable.
19    NonConfirmable,
20    /// `2` — Acknowledgement.
21    Acknowledgement,
22    /// `3` — Reset.
23    Reset,
24}
25
26impl MessageType {
27    /// Konvertiert vom 2-Bit-Wire-Wert.
28    ///
29    /// # Errors
30    /// Liefert `None` wenn `v > 3`.
31    #[must_use]
32    pub const fn from_bits(v: u8) -> Option<Self> {
33        match v {
34            0 => Some(Self::Confirmable),
35            1 => Some(Self::NonConfirmable),
36            2 => Some(Self::Acknowledgement),
37            3 => Some(Self::Reset),
38            _ => None,
39        }
40    }
41
42    /// Wire-Wert (2 Bit).
43    #[must_use]
44    pub const fn to_bits(self) -> u8 {
45        match self {
46            Self::Confirmable => 0,
47            Self::NonConfirmable => 1,
48            Self::Acknowledgement => 2,
49            Self::Reset => 3,
50        }
51    }
52}
53
54/// CoAP-Code (RFC 7252 §3 + §12.1) — 8-bit, 3-bit class + 5-bit detail.
55///
56/// Format `c.dd` mit `c` ∈ 0..=7 und `dd` ∈ 0..=31.
57///
58/// Code-Class:
59/// * 0 — Request (0.00 = empty, 0.01-0.04 = method codes).
60/// * 2 — Success Response.
61/// * 4 — Client Error.
62/// * 5 — Server Error.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub struct CoapCode {
65    /// 3-bit class.
66    pub class: u8,
67    /// 5-bit detail.
68    pub detail: u8,
69}
70
71impl CoapCode {
72    /// Konstruktor; class+detail werden auf gueltige Bit-Bereiche
73    /// (3-bit, 5-bit) gemaskt.
74    #[must_use]
75    pub const fn new(class: u8, detail: u8) -> Self {
76        Self {
77            class: class & 0b111,
78            detail: detail & 0b1_1111,
79        }
80    }
81
82    /// Decodiert vom Wire-Byte (RFC 7252 §3 Code-Field).
83    #[must_use]
84    pub const fn from_byte(b: u8) -> Self {
85        Self {
86            class: (b >> 5) & 0b111,
87            detail: b & 0b1_1111,
88        }
89    }
90
91    /// Encodiert ins Wire-Byte.
92    #[must_use]
93    pub const fn to_byte(self) -> u8 {
94        (self.class << 5) | (self.detail & 0b1_1111)
95    }
96
97    /// `EMPTY` (0.00, RFC 7252 §3) — Spec-Sonderfall fuer Reset/ACK.
98    pub const EMPTY: Self = Self::new(0, 0);
99    /// `GET` (0.01).
100    pub const GET: Self = Self::new(0, 1);
101    /// `POST` (0.02).
102    pub const POST: Self = Self::new(0, 2);
103    /// `PUT` (0.03).
104    pub const PUT: Self = Self::new(0, 3);
105    /// `DELETE` (0.04).
106    pub const DELETE: Self = Self::new(0, 4);
107    /// `2.01 Created`.
108    pub const CREATED: Self = Self::new(2, 1);
109    /// `2.02 Deleted`.
110    pub const DELETED: Self = Self::new(2, 2);
111    /// `2.03 Valid`.
112    pub const VALID: Self = Self::new(2, 3);
113    /// `2.04 Changed`.
114    pub const CHANGED: Self = Self::new(2, 4);
115    /// `2.05 Content`.
116    pub const CONTENT: Self = Self::new(2, 5);
117    /// `4.00 Bad Request`.
118    pub const BAD_REQUEST: Self = Self::new(4, 0);
119    /// `4.04 Not Found`.
120    pub const NOT_FOUND: Self = Self::new(4, 4);
121    /// `5.00 Internal Server Error`.
122    pub const INTERNAL_SERVER_ERROR: Self = Self::new(5, 0);
123
124    /// `true` wenn class == 0 und detail > 0 — Request-Method (Spec
125    /// §3 + §12.1.1).
126    #[must_use]
127    pub const fn is_request(self) -> bool {
128        self.class == 0 && self.detail > 0
129    }
130
131    /// `true` wenn class == 2 — Success-Response (Spec §12.1.2).
132    #[must_use]
133    pub const fn is_success(self) -> bool {
134        self.class == 2
135    }
136
137    /// `true` wenn class == 4 — Client-Error (Spec §12.1.2).
138    #[must_use]
139    pub const fn is_client_error(self) -> bool {
140        self.class == 4
141    }
142
143    /// `true` wenn class == 5 — Server-Error (Spec §12.1.2).
144    #[must_use]
145    pub const fn is_server_error(self) -> bool {
146        self.class == 5
147    }
148
149    /// `true` wenn Empty-Code (0.00).
150    #[must_use]
151    pub const fn is_empty(self) -> bool {
152        self.class == 0 && self.detail == 0
153    }
154}
155
156impl fmt::Display for CoapCode {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        write!(f, "{}.{:02}", self.class, self.detail)
159    }
160}
161
162/// CoAP-Message — RFC 7252 §3.
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct CoapMessage {
165    /// Spec §3 — `Ver`. Wir setzen immer 1 beim Encode.
166    pub version: u8,
167    /// Spec §3 — `Type`.
168    pub message_type: MessageType,
169    /// Spec §3 — `Code`.
170    pub code: CoapCode,
171    /// Spec §3 — `Message ID` (16 bit).
172    pub message_id: u16,
173    /// Spec §3 — `Token`. Length 0..=8 (TKL field).
174    pub token: Vec<u8>,
175    /// Spec §3.1 — Optionen, sortiert nach Number (Spec verlangt
176    /// Delta-Encoding-Reihenfolge).
177    pub options: Vec<CoapOption>,
178    /// Spec §3 — Payload (nach 0xFF-Marker).
179    pub payload: Vec<u8>,
180}
181
182impl CoapMessage {
183    /// Konstruiert eine neue Message mit Default `version=1` und
184    /// leeren Options/Payload.
185    #[must_use]
186    pub const fn new(message_type: MessageType, code: CoapCode, message_id: u16) -> Self {
187        Self {
188            version: 1,
189            message_type,
190            code,
191            message_id,
192            token: Vec::new(),
193            options: Vec::new(),
194            payload: Vec::new(),
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn message_type_round_trips_via_bits() {
205        // RFC 7252 §3.
206        for t in [
207            MessageType::Confirmable,
208            MessageType::NonConfirmable,
209            MessageType::Acknowledgement,
210            MessageType::Reset,
211        ] {
212            assert_eq!(MessageType::from_bits(t.to_bits()), Some(t));
213        }
214    }
215
216    #[test]
217    fn message_type_rejects_out_of_range() {
218        for v in 4..=7 {
219            assert_eq!(MessageType::from_bits(v), None);
220        }
221    }
222
223    #[test]
224    fn code_byte_round_trip() {
225        // RFC 7252 §3 + §12.1.
226        for class in 0..8 {
227            for detail in 0..32 {
228                let c = CoapCode::new(class, detail);
229                assert_eq!(CoapCode::from_byte(c.to_byte()), c);
230            }
231        }
232    }
233
234    #[test]
235    fn well_known_codes_match_spec_values() {
236        // RFC 7252 §12.1.
237        assert_eq!(CoapCode::GET.to_byte(), 0b000_00001);
238        assert_eq!(CoapCode::POST.to_byte(), 0b000_00010);
239        assert_eq!(CoapCode::PUT.to_byte(), 0b000_00011);
240        assert_eq!(CoapCode::DELETE.to_byte(), 0b000_00100);
241        // 2.05 Content = class 2 detail 5 = 0100_0101 = 0x45.
242        assert_eq!(CoapCode::CONTENT.to_byte(), 0x45);
243        // 4.04 Not Found.
244        assert_eq!(CoapCode::NOT_FOUND.to_byte(), 0x84);
245        // 5.00 Internal Server Error.
246        assert_eq!(CoapCode::INTERNAL_SERVER_ERROR.to_byte(), 0xA0);
247    }
248
249    #[test]
250    fn code_display_uses_dotted_format() {
251        // Spec §3 — "documented as c.dd".
252        assert_eq!(alloc::format!("{}", CoapCode::GET), "0.01");
253        assert_eq!(alloc::format!("{}", CoapCode::CONTENT), "2.05");
254        assert_eq!(alloc::format!("{}", CoapCode::NOT_FOUND), "4.04");
255        assert_eq!(
256            alloc::format!("{}", CoapCode::INTERNAL_SERVER_ERROR),
257            "5.00"
258        );
259    }
260
261    #[test]
262    fn code_classification_predicates() {
263        assert!(CoapCode::GET.is_request());
264        assert!(!CoapCode::GET.is_success());
265        assert!(CoapCode::CONTENT.is_success());
266        assert!(CoapCode::NOT_FOUND.is_client_error());
267        assert!(CoapCode::INTERNAL_SERVER_ERROR.is_server_error());
268        assert!(CoapCode::EMPTY.is_empty());
269        assert!(!CoapCode::GET.is_empty());
270    }
271
272    #[test]
273    fn new_message_defaults_version_to_1() {
274        // Spec §3 — Implementations MUST set version to 1.
275        let m = CoapMessage::new(MessageType::Confirmable, CoapCode::GET, 42);
276        assert_eq!(m.version, 1);
277        assert_eq!(m.message_id, 42);
278        assert!(m.token.is_empty());
279        assert!(m.options.is_empty());
280        assert!(m.payload.is_empty());
281    }
282}