1use alloc::vec::Vec;
14use core::fmt;
15
16use crate::message::{CoapCode, CoapMessage, MessageType};
17use crate::option::{CoapOption, OptionValue};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum CodecError {
23 HeaderTooShort,
25 UnsupportedVersion(u8),
30 ReservedTokenLength(u8),
33 TokenTruncated,
35 OptionHeaderTruncated,
37 OptionDeltaIs15,
41 OptionLengthIs15,
43 OptionValueTruncated,
45 PayloadMarkerWithoutPayload,
48 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
74pub 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 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 out.extend_from_slice(&msg.token);
95
96 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 if !msg.payload.is_empty() {
119 out.push(0xFF);
120 out.extend_from_slice(&msg.payload);
121 }
122
123 Ok(out)
124}
125
126fn 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 #[allow(clippy::cast_possible_truncation)]
135 let ext = (value - 13) as u8;
136 (13, alloc::vec![ext])
137 } else {
138 #[allow(clippy::cast_possible_truncation)]
140 let ext = (value - 269) as u16;
141 (14, ext.to_be_bytes().to_vec())
142 }
143}
144
145pub 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 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 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 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 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
233fn 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 _ => 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 let m = CoapMessage::new(MessageType::Confirmable, CoapCode::GET, 0x1234);
274 let bytes = encode(&m).expect("encode");
275 assert_eq!(bytes.len(), 4);
276 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 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 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 let bytes = [0b1000_0000_u8, 0, 0, 0]; assert_eq!(decode(&bytes), Err(CodecError::UnsupportedVersion(2)));
322 }
323
324 #[test]
325 fn reserved_token_length_9_through_15_decode_fails() {
326 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 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 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 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 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 assert_eq!(bytes[4], 0xD0);
366 assert_eq!(bytes[5], 7);
367 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 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); 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 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 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 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 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 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 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 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 }
476
477 #[test]
478 fn options_are_sorted_on_encode() {
479 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 assert_eq!(numbers, alloc::vec![6, 11, 12]);
492 }
493
494 #[test]
495 fn empty_message_round_trip() {
496 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}