Skip to main content

netget/server/coap/
codec.rs

1//! CoAP (RFC 7252) message codec.
2//!
3//! Hand-rolled on purpose: the server's encoder and the `coap` / `coap-lite` crates used
4//! as the test peer are then genuinely independent implementations, so a round trip
5//! through them is evidence rather than a tautology.
6//!
7//! Covers the 4-byte binary header, the four message types, tokens, the option
8//! delta/length encoding with both extension forms, the payload marker, and the
9//! request/response code space, and the option values Observe (RFC 7641) and Block-wise
10//! transfer (RFC 7959) carry: [`BlockValue`] and the Observe sequence. What those options
11//! *mean* — registrations, reassembly, the transfer cache — is `observe.rs` and
12//! `blockwise.rs`; this file only reads and writes them. Not covered: DTLS.
13
14use std::fmt;
15
16/// Fixed CoAP header length: Ver/T/TKL, Code, Message ID.
17pub const HEADER_LEN: usize = 4;
18
19/// Marks the start of the payload, after any options.
20pub const PAYLOAD_MARKER: u8 = 0xFF;
21
22/// Largest **inbound** datagram this server will process, in bytes.
23///
24/// RFC 7252 §4.6 names this number: an endpoint that does not know the path MTU assumes
25/// `MAX_MESSAGE_SIZE` of 1152 bytes, and a constrained endpoint is entitled to refuse
26/// anything larger. The legal way for a peer to send more is Block1 (RFC 7959), whose largest
27/// block is 1024 bytes and fits, so a bigger datagram is either a client that ignored the
28/// specification or somebody probing for a buffer to grow.
29///
30/// Enforced in `mod.rs::handle_datagram`, **before** the message is decoded and before any
31/// model call, and answered in CoAP's own vocabulary with 4.13 Request Entity Too Large
32/// (RFC 7252 §5.9.2.9). This is the number `ProtocolMetadataV2::max_inbound_bytes`
33/// declares.
34///
35/// It used to declare [`MAX_PAYLOAD_LEN`] instead, which is the bound on what the server
36/// *writes*. Nothing inbound was bounded at all: the receive buffer was the only ceiling
37/// and a 2000-byte request was decoded and handed to the model. A declaration naming a
38/// number the inbound path never checks is the exact shape
39/// `max_inbound_bytes`' own doc comment warns about — "the number must be the one the code
40/// enforces, at the point the length is decided".
41pub const MAX_MESSAGE_LEN: usize = 1152;
42
43/// Largest payload this server will put in **one message**, in bytes — the largest Block2
44/// block (SZX 6).
45///
46/// RFC 7252 §4.6: absent any knowledge of the path MTU, a CoAP endpoint must assume
47/// `MAX_MESSAGE_SIZE` of 1152 bytes, "leading to a maximum payload size of 1024 bytes". A
48/// representation larger than this is split with Block2 (RFC 7959) rather than sent whole;
49/// the bound on the representation itself is [`MAX_REPRESENTATION_LEN`].
50pub const MAX_PAYLOAD_LEN: usize = 1024;
51
52/// Largest representation the model may return, in bytes — and the largest request body a
53/// Block1 upload may reassemble to.
54///
55/// Over [`MAX_PAYLOAD_LEN`] a representation goes out as Block2 blocks, so this is not a
56/// datagram limit but a memory one: every block of a transfer is served from one cached copy
57/// (`blockwise::Transfers`), and every upload is held until its last block arrives. 64 KiB is
58/// sixty-four blocks — far past anything a constrained device serves — and with the table
59/// caps in `blockwise.rs` bounds what one server holds to a few MiB.
60///
61/// Enforced on the model's side in `actions.rs::decode_payload`, so an oversize answer is a
62/// legible error the model can act on rather than an `EMSGSIZE` the peer never hears about,
63/// and on the peer's side in `blockwise::Uploads`, which answers 4.13 with Size1.
64pub const MAX_REPRESENTATION_LEN: usize = 64 * 1024;
65
66/// CoAP version implemented here.
67pub const VERSION: u8 = 1;
68
69/// Longest token a message may carry, in octets (RFC 7252 §3).
70///
71/// TKL is four bits and 9-15 are reserved, so this is a format limit. It is stated once and
72/// enforced in **both** directions: `decode` refuses `tkl > 8` from a peer, and `encode`
73/// refuses an over-long token from the caller. The encoder used to cut instead, which is the
74/// worst available outcome on this protocol — CoAP's entire request/response matching *is*
75/// token equality, so a truncated token produces a reply that is discarded silently at the
76/// client with no error at either end.
77pub const MAX_TOKEN_LEN: usize = 8;
78
79/// Longest value a single option may carry, in octets.
80///
81/// The extended length form is `269 + a big-endian u16`, but `read_extended` saturates that
82/// addition at `u16::MAX`, so 65535 is the largest length this codec can read back — and an
83/// encoder must not produce a length its own decoder cannot. Stated so the encoder refuses
84/// rather than narrowing an option length with an `as u16`.
85pub const MAX_OPTION_LEN: usize = u16::MAX as usize;
86
87// ---------------------------------------------------------------------------
88// Option numbers (RFC 7252 §5.10, §12.2)
89// ---------------------------------------------------------------------------
90
91pub const OPT_IF_MATCH: u16 = 1;
92pub const OPT_URI_HOST: u16 = 3;
93pub const OPT_ETAG: u16 = 4;
94pub const OPT_IF_NONE_MATCH: u16 = 5;
95/// Observe (RFC 7641 §2): 0 registers, 1 deregisters, and in a notification it is the
96/// sequence number.
97pub const OPT_OBSERVE: u16 = 6;
98pub const OPT_URI_PORT: u16 = 7;
99pub const OPT_LOCATION_PATH: u16 = 8;
100pub const OPT_URI_PATH: u16 = 11;
101pub const OPT_CONTENT_FORMAT: u16 = 12;
102pub const OPT_MAX_AGE: u16 = 14;
103pub const OPT_URI_QUERY: u16 = 15;
104pub const OPT_ACCEPT: u16 = 17;
105pub const OPT_LOCATION_QUERY: u16 = 20;
106/// Block2 (RFC 7959 §2.1): which block of the *response* this is, or which one is wanted.
107pub const OPT_BLOCK2: u16 = 23;
108/// Block1 (RFC 7959 §2.1): which block of the *request* body this is.
109pub const OPT_BLOCK1: u16 = 27;
110/// Size2 (RFC 7959 §4): the whole representation's length.
111pub const OPT_SIZE2: u16 = 28;
112/// Size1 (RFC 7252 §5.10.9, RFC 7959 §4): the whole request body's length, or in a 4.13
113/// the largest one the server will take.
114pub const OPT_SIZE1: u16 = 60;
115
116/// A CoAP unsigned integer option value: minimum-length big-endian (RFC 7252 §3.2), so zero
117/// is the empty string rather than a zero byte.
118pub fn uint_option(value: u32) -> Vec<u8> {
119    let bytes = value.to_be_bytes();
120    let first = bytes.iter().position(|&b| b != 0).unwrap_or(bytes.len());
121    bytes[first..].to_vec()
122}
123
124// ---------------------------------------------------------------------------
125// Codes
126// ---------------------------------------------------------------------------
127
128/// The empty code, 0.00 — used by ACK and RST, and by a CoAP ping.
129pub const CODE_EMPTY: u8 = 0x00;
130pub const CODE_GET: u8 = 0x01;
131pub const CODE_POST: u8 = 0x02;
132pub const CODE_PUT: u8 = 0x03;
133pub const CODE_DELETE: u8 = 0x04;
134
135/// Build a code byte from the human `class.detail` pair, e.g. `2.05` -> `code(2, 5)`.
136pub const fn code(class: u8, detail: u8) -> u8 {
137    (class << 5) | (detail & 0x1F)
138}
139
140pub const CODE_CONTENT: u8 = code(2, 5); // 2.05
141pub const CODE_CONTINUE: u8 = code(2, 31); // 2.31 (RFC 7959 §2.9.1)
142pub const CODE_BAD_REQUEST: u8 = code(4, 0); // 4.00
143pub const CODE_BAD_OPTION: u8 = code(4, 2); // 4.02
144pub const CODE_NOT_FOUND: u8 = code(4, 4); // 4.04
145pub const CODE_METHOD_NOT_ALLOWED: u8 = code(4, 5); // 4.05
146pub const CODE_REQUEST_ENTITY_INCOMPLETE: u8 = code(4, 8); // 4.08 (RFC 7959 §2.9.2)
147pub const CODE_REQUEST_ENTITY_TOO_LARGE: u8 = code(4, 13); // 4.13
148pub const CODE_INTERNAL_SERVER_ERROR: u8 = code(5, 0); // 5.00
149pub const CODE_SERVICE_UNAVAILABLE: u8 = code(5, 3); // 5.03
150
151/// Class portion of a code byte (the digit before the dot).
152pub const fn code_class(code: u8) -> u8 {
153    code >> 5
154}
155
156/// Detail portion of a code byte (the two digits after the dot).
157pub const fn code_detail(code: u8) -> u8 {
158    code & 0x1F
159}
160
161/// Render a code byte in the conventional `c.dd` notation.
162pub fn code_to_string(code: u8) -> String {
163    format!("{}.{:02}", code_class(code), code_detail(code))
164}
165
166/// Parse `"2.05"` (or `"2.05 Content"`) into a code byte.
167pub fn parse_code_string(s: &str) -> Option<u8> {
168    let token = s.split_whitespace().next()?;
169    let (class, detail) = token.split_once('.')?;
170    let class: u8 = class.trim().parse().ok()?;
171    let detail: u8 = detail.trim().parse().ok()?;
172    if class > 7 || detail > 31 {
173        return None;
174    }
175    Some(code(class, detail))
176}
177
178/// Method name for a request code, or `None` if this is not a request code.
179pub fn method_name(code: u8) -> Option<&'static str> {
180    match code {
181        CODE_GET => Some("GET"),
182        CODE_POST => Some("POST"),
183        CODE_PUT => Some("PUT"),
184        CODE_DELETE => Some("DELETE"),
185        _ => None,
186    }
187}
188
189// ---------------------------------------------------------------------------
190// Content formats (RFC 7252 §12.3)
191// ---------------------------------------------------------------------------
192
193/// Media type name for a Content-Format identifier.
194pub fn content_format_name(id: u16) -> Option<&'static str> {
195    match id {
196        0 => Some("text/plain"),
197        40 => Some("application/link-format"),
198        41 => Some("application/xml"),
199        42 => Some("application/octet-stream"),
200        47 => Some("application/exi"),
201        50 => Some("application/json"),
202        60 => Some("application/cbor"),
203        _ => None,
204    }
205}
206
207/// Content-Format identifier for a media type name.
208pub fn content_format_id(name: &str) -> Option<u16> {
209    let n = name.trim().to_ascii_lowercase();
210    // Tolerate the charset parameter clients and models both like to add.
211    let n = n.split(';').next().unwrap_or(&n).trim().to_string();
212    match n.as_str() {
213        "text/plain" | "text" | "plain" => Some(0),
214        "application/link-format" | "link-format" => Some(40),
215        "application/xml" | "xml" => Some(41),
216        "application/octet-stream" | "octet-stream" | "binary" => Some(42),
217        "application/exi" | "exi" => Some(47),
218        "application/json" | "json" => Some(50),
219        "application/cbor" | "cbor" => Some(60),
220        _ => None,
221    }
222}
223
224// ---------------------------------------------------------------------------
225// Message
226// ---------------------------------------------------------------------------
227
228/// CoAP message type (the `T` field).
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub enum MessageType {
231    Confirmable,
232    NonConfirmable,
233    Acknowledgement,
234    Reset,
235}
236
237impl MessageType {
238    pub fn from_bits(bits: u8) -> MessageType {
239        match bits & 0x03 {
240            0 => MessageType::Confirmable,
241            1 => MessageType::NonConfirmable,
242            2 => MessageType::Acknowledgement,
243            _ => MessageType::Reset,
244        }
245    }
246
247    pub fn to_bits(self) -> u8 {
248        match self {
249            MessageType::Confirmable => 0,
250            MessageType::NonConfirmable => 1,
251            MessageType::Acknowledgement => 2,
252            MessageType::Reset => 3,
253        }
254    }
255
256    /// Short name used in event data and logs.
257    pub fn as_str(self) -> &'static str {
258        match self {
259            MessageType::Confirmable => "CON",
260            MessageType::NonConfirmable => "NON",
261            MessageType::Acknowledgement => "ACK",
262            MessageType::Reset => "RST",
263        }
264    }
265}
266
267/// A decoded CoAP message.
268#[derive(Debug, Clone, PartialEq, Eq)]
269pub struct CoapMessage {
270    pub mtype: MessageType,
271    pub code: u8,
272    pub message_id: u16,
273    pub token: Vec<u8>,
274    /// Options as `(number, value)`, in ascending order of number, repeats allowed.
275    pub options: Vec<(u16, Vec<u8>)>,
276    pub payload: Vec<u8>,
277}
278
279/// Why a datagram is not a CoAP message.
280#[derive(Debug, Clone, PartialEq, Eq)]
281pub enum DecodeError {
282    TooShort {
283        len: usize,
284    },
285    BadVersion {
286        version: u8,
287    },
288    BadTokenLength {
289        tkl: u8,
290    },
291    Truncated {
292        what: &'static str,
293    },
294    /// Option delta or length nibble 15, which is reserved (0xFF is the payload marker).
295    ReservedOptionNibble,
296    /// A payload marker with nothing after it (RFC 7252 §3: this is a format error).
297    EmptyPayloadAfterMarker,
298}
299
300impl fmt::Display for DecodeError {
301    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302        match self {
303            DecodeError::TooShort { len } => {
304                write!(
305                    f,
306                    "datagram of {len} bytes is shorter than the 4-byte CoAP header"
307                )
308            }
309            DecodeError::BadVersion { version } => {
310                write!(f, "CoAP version {version} is not supported (expected 1)")
311            }
312            DecodeError::BadTokenLength { tkl } => {
313                write!(f, "token length {tkl} is reserved (must be 0-8)")
314            }
315            DecodeError::Truncated { what } => write!(f, "message truncated inside {what}"),
316            DecodeError::ReservedOptionNibble => {
317                write!(f, "option delta/length nibble 15 is reserved")
318            }
319            DecodeError::EmptyPayloadAfterMarker => {
320                write!(f, "payload marker present but payload is empty")
321            }
322        }
323    }
324}
325
326impl std::error::Error for DecodeError {}
327
328/// Why a message cannot be put on the wire.
329///
330/// The decode side above is bounded against a hostile peer; this is the same bound stated
331/// against the *caller*, which is the direction that had none. Both refusals are total: a
332/// message this codec would not accept back is never written in a shortened form.
333#[derive(Debug, Clone, PartialEq, Eq)]
334pub enum EncodeError {
335    /// A token longer than the 4-bit TKL field can describe (RFC 7252 §3: 0-8 octets).
336    TokenTooLong { len: usize },
337    /// An option value longer than the extended length form can describe.
338    OptionTooLong { number: u16, len: usize },
339}
340
341impl fmt::Display for EncodeError {
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        match self {
344            EncodeError::TokenTooLong { len } => write!(
345                f,
346                "token is {len} octets; RFC 7252 §3 allows 0-{MAX_TOKEN_LEN}, and CoAP matches \
347                 a response to its request by token equality, so a shortened one matches nothing"
348            ),
349            EncodeError::OptionTooLong { number, len } => write!(
350                f,
351                "option {number} carries {len} octets; the option length field tops out at \
352                 {MAX_OPTION_LEN}"
353            ),
354        }
355    }
356}
357
358impl std::error::Error for EncodeError {}
359
360impl CoapMessage {
361    /// Decode a datagram.
362    pub fn decode(buf: &[u8]) -> Result<CoapMessage, DecodeError> {
363        if buf.len() < HEADER_LEN {
364            return Err(DecodeError::TooShort { len: buf.len() });
365        }
366
367        let version = buf[0] >> 6;
368        if version != VERSION {
369            return Err(DecodeError::BadVersion { version });
370        }
371        let mtype = MessageType::from_bits(buf[0] >> 4);
372        let tkl = buf[0] & 0x0F;
373        if tkl > 8 {
374            return Err(DecodeError::BadTokenLength { tkl });
375        }
376        let code = buf[1];
377        let message_id = u16::from_be_bytes([buf[2], buf[3]]);
378
379        let mut pos = HEADER_LEN;
380        if buf.len() < pos + tkl as usize {
381            return Err(DecodeError::Truncated { what: "token" });
382        }
383        let token = buf[pos..pos + tkl as usize].to_vec();
384        pos += tkl as usize;
385
386        let mut options: Vec<(u16, Vec<u8>)> = Vec::new();
387        let mut current_number: u16 = 0;
388        let mut payload = Vec::new();
389
390        while pos < buf.len() {
391            if buf[pos] == PAYLOAD_MARKER {
392                pos += 1;
393                if pos >= buf.len() {
394                    return Err(DecodeError::EmptyPayloadAfterMarker);
395                }
396                payload = buf[pos..].to_vec();
397                break;
398            }
399
400            let delta_nibble = buf[pos] >> 4;
401            let length_nibble = buf[pos] & 0x0F;
402            pos += 1;
403
404            let delta = read_extended(buf, &mut pos, delta_nibble)?;
405            let length = read_extended(buf, &mut pos, length_nibble)?;
406
407            if buf.len() < pos + length as usize {
408                return Err(DecodeError::Truncated {
409                    what: "option value",
410                });
411            }
412            current_number = current_number.saturating_add(delta);
413            options.push((current_number, buf[pos..pos + length as usize].to_vec()));
414            pos += length as usize;
415        }
416
417        Ok(CoapMessage {
418            mtype,
419            code,
420            message_id,
421            token,
422            options,
423            payload,
424        })
425    }
426
427    /// Encode to a datagram.
428    ///
429    /// Refuses anything `decode` would not accept back, rather than shortening it to fit. See
430    /// [`MAX_TOKEN_LEN`] for why truncating a token is the worst outcome available here.
431    pub fn encode(&self) -> Result<Vec<u8>, EncodeError> {
432        if self.token.len() > MAX_TOKEN_LEN {
433            return Err(EncodeError::TokenTooLong {
434                len: self.token.len(),
435            });
436        }
437        for (number, value) in &self.options {
438            if value.len() > MAX_OPTION_LEN {
439                return Err(EncodeError::OptionTooLong {
440                    number: *number,
441                    len: value.len(),
442                });
443            }
444        }
445
446        let mut out = Vec::with_capacity(HEADER_LEN + self.token.len() + self.payload.len() + 16);
447        let tkl = self.token.len() as u8;
448        out.push((VERSION << 6) | (self.mtype.to_bits() << 4) | tkl);
449        out.push(self.code);
450        out.extend_from_slice(&self.message_id.to_be_bytes());
451        out.extend_from_slice(&self.token);
452
453        let mut sorted = self.options.clone();
454        // Options must be emitted in ascending order; `sort_by_key` is stable, so
455        // repeated options (Uri-Path segments) keep the order they were given in.
456        sorted.sort_by_key(|(n, _)| *n);
457
458        let mut last: u16 = 0;
459        for (number, value) in &sorted {
460            let delta = number.saturating_sub(last);
461            last = *number;
462
463            // Range-checked above, so neither cast can lose a bit.
464            let (delta_nibble, delta_ext) = split_extended(delta);
465            let (length_nibble, length_ext) = split_extended(value.len() as u16);
466
467            out.push((delta_nibble << 4) | length_nibble);
468            out.extend_from_slice(&delta_ext);
469            out.extend_from_slice(&length_ext);
470            out.extend_from_slice(value);
471        }
472
473        if !self.payload.is_empty() {
474            out.push(PAYLOAD_MARKER);
475            out.extend_from_slice(&self.payload);
476        }
477
478        Ok(out)
479    }
480
481    /// Values of every occurrence of an option, in order.
482    pub fn option_values(&self, number: u16) -> Vec<&[u8]> {
483        self.options
484            .iter()
485            .filter(|(n, _)| *n == number)
486            .map(|(_, v)| v.as_slice())
487            .collect()
488    }
489
490    /// First value of an option, decoded as a big-endian unsigned integer.
491    pub fn option_uint(&self, number: u16) -> Option<u32> {
492        self.option_values(number)
493            .first()
494            .map(|v| v.iter().take(4).fold(0u32, |acc, b| (acc << 8) | *b as u32))
495    }
496
497    /// Request path assembled from the Uri-Path options, always leading-slashed.
498    pub fn uri_path(&self) -> String {
499        let segments: Vec<String> = self
500            .option_values(OPT_URI_PATH)
501            .iter()
502            .map(|v| String::from_utf8_lossy(v).to_string())
503            .collect();
504        if segments.is_empty() {
505            "/".to_string()
506        } else {
507            format!("/{}", segments.join("/"))
508        }
509    }
510
511    /// Uri-Path options as individual segments.
512    pub fn path_segments(&self) -> Vec<String> {
513        self.option_values(OPT_URI_PATH)
514            .iter()
515            .map(|v| String::from_utf8_lossy(v).to_string())
516            .collect()
517    }
518
519    /// Query string assembled from the Uri-Query options, `&`-joined, or `None`.
520    pub fn uri_query(&self) -> Option<String> {
521        let parts: Vec<String> = self
522            .option_values(OPT_URI_QUERY)
523            .iter()
524            .map(|v| String::from_utf8_lossy(v).to_string())
525            .collect();
526        if parts.is_empty() {
527            None
528        } else {
529            Some(parts.join("&"))
530        }
531    }
532
533    /// True when this message is empty (code 0.00 and no token, options or payload).
534    pub fn is_empty_message(&self) -> bool {
535        self.code == CODE_EMPTY
536    }
537
538    /// True when the code is one of the four defined request methods.
539    pub fn is_request(&self) -> bool {
540        method_name(self.code).is_some()
541    }
542}
543
544/// A Block1 or Block2 option value: `NUM << 4 | M << 3 | SZX` (RFC 7959 §2.2).
545#[derive(Debug, Clone, Copy, PartialEq, Eq)]
546pub struct BlockValue {
547    /// Block number, 0-based. At most 20 bits.
548    pub num: u32,
549    /// More blocks follow.
550    pub more: bool,
551    /// Size exponent: the block is `2^(szx + 4)` bytes, 16 to 1024.
552    pub szx: u8,
553}
554
555/// Largest block number a three-byte option value can carry.
556pub const MAX_BLOCK_NUM: u32 = (1 << 20) - 1;
557
558/// The largest size exponent: SZX 6 is 1024 bytes. 7 is reserved (BERT, CoAP over TCP only).
559pub const MAX_SZX: u8 = 6;
560
561impl BlockValue {
562    /// Read an option value. `None` for more than three bytes or the reserved SZX 7.
563    pub fn parse(value: &[u8]) -> Option<BlockValue> {
564        if value.len() > 3 {
565            return None;
566        }
567        let v = value.iter().fold(0u32, |acc, b| (acc << 8) | u32::from(*b));
568        let szx = (v & 0x07) as u8;
569        if szx > MAX_SZX {
570            return None;
571        }
572        Some(BlockValue {
573            num: v >> 4,
574            more: v & 0x08 != 0,
575            szx,
576        })
577    }
578
579    /// The option value, minimum-length big-endian. `num` is masked to 20 bits.
580    pub fn encode(self) -> Vec<u8> {
581        let v = ((self.num & MAX_BLOCK_NUM) << 4)
582            | if self.more { 0x08 } else { 0 }
583            | u32::from(self.szx.min(MAX_SZX));
584        let bytes = v.to_be_bytes();
585        let first = bytes.iter().position(|&b| b != 0).unwrap_or(bytes.len());
586        bytes[first..].to_vec()
587    }
588
589    /// Block size in bytes.
590    pub fn size(self) -> usize {
591        szx_size(self.szx)
592    }
593
594    /// Byte offset of this block in the whole body.
595    pub fn offset(self) -> usize {
596        self.num as usize * self.size()
597    }
598}
599
600/// Block size for a size exponent, `2^(szx + 4)`, clamped to SZX 6.
601pub const fn szx_size(szx: u8) -> usize {
602    let szx = if szx > MAX_SZX { MAX_SZX } else { szx };
603    1 << (szx as u32 + 4)
604}
605
606impl CoapMessage {
607    /// The Block1 option, if present and readable. `Some(None)` when present but malformed.
608    pub fn block1(&self) -> Option<Option<BlockValue>> {
609        self.option_values(OPT_BLOCK1)
610            .first()
611            .map(|v| BlockValue::parse(v))
612    }
613
614    /// The Block2 option, if present and readable. `Some(None)` when present but malformed.
615    pub fn block2(&self) -> Option<Option<BlockValue>> {
616        self.option_values(OPT_BLOCK2)
617            .first()
618            .map(|v| BlockValue::parse(v))
619    }
620
621    /// The Observe option's value, if present. Values are at most three bytes (24 bits).
622    pub fn observe(&self) -> Option<u32> {
623        self.option_values(OPT_OBSERVE)
624            .first()
625            .filter(|v| v.len() <= 3)
626            .map(|v| v.iter().fold(0u32, |acc, b| (acc << 8) | u32::from(*b)))
627    }
628}
629
630/// The part of a message that lives at fixed offsets: the header and the token.
631///
632/// Everything here can be read without walking a single option, which is what makes it
633/// usable for a datagram the server has decided **not** to parse. A refusal still has to
634/// carry the request's message id and token or the client discards it as unsolicited and
635/// the refusal is indistinguishable from silence.
636#[derive(Debug, Clone, PartialEq, Eq)]
637pub struct MessagePrefix {
638    pub mtype: MessageType,
639    pub code: u8,
640    pub message_id: u16,
641    pub token: Vec<u8>,
642}
643
644/// Read the header and token of a datagram without touching its options or payload.
645///
646/// `None` when the bytes are not a CoAP message at all by the parts this reads — too
647/// short, wrong version, a reserved token length, or a token that runs off the end. In
648/// that case there is nothing to echo and no reply can be matched to anything.
649pub fn message_prefix(buf: &[u8]) -> Option<MessagePrefix> {
650    if buf.len() < HEADER_LEN || buf[0] >> 6 != VERSION {
651        return None;
652    }
653    let tkl = (buf[0] & 0x0F) as usize;
654    if tkl > MAX_TOKEN_LEN || buf.len() < HEADER_LEN + tkl {
655        return None;
656    }
657    Some(MessagePrefix {
658        mtype: MessageType::from_bits(buf[0] >> 4),
659        code: buf[1],
660        message_id: u16::from_be_bytes([buf[2], buf[3]]),
661        token: buf[HEADER_LEN..HEADER_LEN + tkl].to_vec(),
662    })
663}
664
665/// Build the piggybacked/separate response shell for a request.
666///
667/// The message id and token are taken from the request, which is why the model never
668/// sees or supplies them: reliability matching is the transport's job, not a decision.
669/// A Confirmable request is answered with an Acknowledgement carrying the same message
670/// id (RFC 7252 §5.2.1); a Non-confirmable request is answered with a Non-confirmable
671/// message carrying a fresh message id.
672pub fn response_to(request: &CoapMessage, fresh_message_id: u16, code: u8) -> CoapMessage {
673    response_shell(
674        request.mtype,
675        request.message_id,
676        request.token.clone(),
677        fresh_message_id,
678        code,
679    )
680}
681
682/// The same shell, built from a [`MessagePrefix`] — for a datagram that was refused
683/// before it was decoded, which therefore has no [`CoapMessage`] to answer from.
684pub fn response_to_prefix(prefix: &MessagePrefix, fresh_message_id: u16, code: u8) -> CoapMessage {
685    response_shell(
686        prefix.mtype,
687        prefix.message_id,
688        prefix.token.clone(),
689        fresh_message_id,
690        code,
691    )
692}
693
694fn response_shell(
695    request_type: MessageType,
696    request_message_id: u16,
697    token: Vec<u8>,
698    fresh_message_id: u16,
699    code: u8,
700) -> CoapMessage {
701    let (mtype, message_id) = match request_type {
702        MessageType::Confirmable => (MessageType::Acknowledgement, request_message_id),
703        _ => (MessageType::NonConfirmable, fresh_message_id),
704    };
705    CoapMessage {
706        mtype,
707        code,
708        message_id,
709        token,
710        options: Vec::new(),
711        payload: Vec::new(),
712    }
713}
714
715/// An empty Reset for the given message id (RFC 7252 §4.2).
716pub fn reset_for(message_id: u16) -> CoapMessage {
717    CoapMessage {
718        mtype: MessageType::Reset,
719        code: CODE_EMPTY,
720        message_id,
721        token: Vec::new(),
722        options: Vec::new(),
723        payload: Vec::new(),
724    }
725}
726
727// ---------------------------------------------------------------------------
728// Option delta/length extension encoding
729// ---------------------------------------------------------------------------
730
731/// Read the extended value that a nibble of 13 or 14 introduces.
732fn read_extended(buf: &[u8], pos: &mut usize, nibble: u8) -> Result<u16, DecodeError> {
733    match nibble {
734        0..=12 => Ok(nibble as u16),
735        13 => {
736            let b = *buf.get(*pos).ok_or(DecodeError::Truncated {
737                what: "option extension byte",
738            })?;
739            *pos += 1;
740            Ok(b as u16 + 13)
741        }
742        14 => {
743            if buf.len() < *pos + 2 {
744                return Err(DecodeError::Truncated {
745                    what: "option extension bytes",
746                });
747            }
748            let v = u16::from_be_bytes([buf[*pos], buf[*pos + 1]]);
749            *pos += 2;
750            Ok(v.saturating_add(269))
751        }
752        _ => Err(DecodeError::ReservedOptionNibble),
753    }
754}
755
756/// Split a delta or length into its nibble and extension bytes.
757fn split_extended(value: u16) -> (u8, Vec<u8>) {
758    if value < 13 {
759        (value as u8, Vec::new())
760    } else if value < 269 {
761        (13, vec![(value - 13) as u8])
762    } else {
763        (14, (value - 269).to_be_bytes().to_vec())
764    }
765}