Skip to main content

kipuka_coap/
server.rs

1//! CoAP message parsing, encoding, and EST-coaps URI routing.
2//!
3//! This module implements the CoAP message format (RFC 7252 §3) and maps
4//! CoAP URI paths to EST operations per RFC 9483 §5.1.
5//!
6//! # CoAP Message Format
7//!
8//! ```text
9//!  0                   1                   2                   3
10//!  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
11//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
12//! |Ver| T |  TKL  |      Code     |          Message ID           |
13//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
14//! |   Token (if any, TKL bytes) ...
15//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
16//! |   Options (if any) ...
17//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
18//! |1 1 1 1 1 1 1 1|    Payload (if any) ...
19//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
20//! ```
21//!
22//! # EST-coaps URI Mapping
23//!
24//! RFC 9483 §5.1 defines abbreviated URI paths for EST operations:
25//!
26//! | CoAP Path | EST Operation | HTTP Method |
27//! |-----------|---------------|-------------|
28//! | `/sen`    | simpleenroll  | POST        |
29//! | `/sren`   | simplereenroll| POST        |
30//! | `/skg`    | serverkeygen  | POST        |
31//! | `/att`    | csrattrs      | GET         |
32//! | `/cacerts`| cacerts       | GET         |
33//! | `/crts`   | cacerts       | GET (alias) |
34
35use crate::{CoapError, CoapResult};
36
37/// CoAP protocol version (RFC 7252 §3).
38///
39/// The only defined version is 1. Other values are reserved.
40pub const COAP_VERSION: u8 = 1;
41
42/// Payload marker byte (RFC 7252 §3).
43///
44/// The byte 0xFF separates CoAP options from the payload.
45const PAYLOAD_MARKER: u8 = 0xFF;
46
47// --- CoAP Option Numbers (RFC 7252 §5.10) ---
48
49/// Uri-Host option (RFC 7252 §5.10.1).
50pub const OPTION_URI_HOST: u16 = 3;
51
52/// Uri-Port option (RFC 7252 §5.10.1).
53pub const OPTION_URI_PORT: u16 = 7;
54
55/// Uri-Path option (RFC 7252 §5.10.1).
56///
57/// Each Uri-Path option contains one path segment. Multiple options
58/// are concatenated with `/` to form the full URI path.
59pub const OPTION_URI_PATH: u16 = 11;
60
61/// Content-Format option (RFC 7252 §5.10.3).
62///
63/// Contains the CoAP content-format ID (see [`crate::content_format`]).
64pub const OPTION_CONTENT_FORMAT: u16 = 12;
65
66/// Uri-Query option (RFC 7252 §5.10.1).
67pub const OPTION_URI_QUERY: u16 = 15;
68
69/// Block2 option (RFC 7959 §2.1).
70///
71/// Controls response payload block-wise transfer (server to client).
72pub const OPTION_BLOCK2: u16 = 23;
73
74/// Block1 option (RFC 7959 §2.1).
75///
76/// Controls request payload block-wise transfer (client to server).
77pub const OPTION_BLOCK1: u16 = 27;
78
79/// Size2 option (RFC 7959 §4).
80///
81/// Indicates the total size of the response payload.
82pub const OPTION_SIZE2: u16 = 28;
83
84/// Size1 option (RFC 7959 §4).
85///
86/// Indicates the total size of the request payload.
87pub const OPTION_SIZE1: u16 = 60;
88
89/// CoAP request/response method codes (RFC 7252 §5.8, §12.1).
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91pub enum CoapMethod {
92    /// 0.01 GET
93    Get,
94    /// 0.02 POST
95    Post,
96    /// 0.03 PUT
97    Put,
98    /// 0.04 DELETE
99    Delete,
100    /// 0.05 FETCH (RFC 8132)
101    Fetch,
102    /// 0.06 PATCH (RFC 8132)
103    Patch,
104    /// 0.07 iPATCH (RFC 8132)
105    IPatch,
106}
107
108impl CoapMethod {
109    /// Converts a CoAP code byte to a method, if it represents a request.
110    ///
111    /// Request codes have class 0 (code byte 0.01 through 0.07).
112    pub fn from_code(code: &CoapCode) -> Option<Self> {
113        if code.class != 0 {
114            return None;
115        }
116        match code.detail {
117            1 => Some(Self::Get),
118            2 => Some(Self::Post),
119            3 => Some(Self::Put),
120            4 => Some(Self::Delete),
121            5 => Some(Self::Fetch),
122            6 => Some(Self::Patch),
123            7 => Some(Self::IPatch),
124            _ => None,
125        }
126    }
127
128    /// Returns the CoAP code for this method.
129    pub fn to_code(&self) -> CoapCode {
130        let detail = match self {
131            Self::Get => 1,
132            Self::Post => 2,
133            Self::Put => 3,
134            Self::Delete => 4,
135            Self::Fetch => 5,
136            Self::Patch => 6,
137            Self::IPatch => 7,
138        };
139        CoapCode { class: 0, detail }
140    }
141}
142
143/// CoAP message type (RFC 7252 §3).
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
145pub enum CoapMessageType {
146    /// Confirmable (CON): requires acknowledgement.
147    Confirmable,
148    /// Non-confirmable (NON): fire-and-forget.
149    NonConfirmable,
150    /// Acknowledgement (ACK): confirms receipt of CON.
151    Acknowledgement,
152    /// Reset (RST): indicates message was received but cannot be processed.
153    Reset,
154}
155
156impl CoapMessageType {
157    /// Decodes the message type from the 2-bit T field.
158    fn from_bits(bits: u8) -> CoapResult<Self> {
159        match bits {
160            0 => Ok(Self::Confirmable),
161            1 => Ok(Self::NonConfirmable),
162            2 => Ok(Self::Acknowledgement),
163            3 => Ok(Self::Reset),
164            _ => Err(CoapError::InvalidMessage(format!(
165                "Invalid message type: {bits}"
166            ))),
167        }
168    }
169
170    /// Encodes the message type to the 2-bit T field.
171    fn to_bits(&self) -> u8 {
172        match self {
173            Self::Confirmable => 0,
174            Self::NonConfirmable => 1,
175            Self::Acknowledgement => 2,
176            Self::Reset => 3,
177        }
178    }
179}
180
181/// CoAP response/request code (RFC 7252 §3, §5.9).
182///
183/// Encoded as a single byte: upper 3 bits = class, lower 5 bits = detail.
184/// Conventionally written as `class.detail` (e.g., 2.05 = Content).
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
186pub struct CoapCode {
187    /// Code class (0 = request, 2 = success, 4 = client error, 5 = server error).
188    pub class: u8,
189    /// Code detail within the class.
190    pub detail: u8,
191}
192
193impl CoapCode {
194    /// 2.01 Created — enrollment succeeded.
195    pub const CREATED: Self = Self {
196        class: 2,
197        detail: 1,
198    };
199    /// 2.05 Content — response with payload.
200    pub const CONTENT: Self = Self {
201        class: 2,
202        detail: 5,
203    };
204    /// 4.04 Not Found — unknown URI path.
205    pub const NOT_FOUND: Self = Self {
206        class: 4,
207        detail: 4,
208    };
209    /// 4.05 Method Not Allowed — wrong method for resource.
210    pub const METHOD_NOT_ALLOWED: Self = Self {
211        class: 4,
212        detail: 5,
213    };
214    /// 4.15 Unsupported Content-Format.
215    pub const UNSUPPORTED_CONTENT_FORMAT: Self = Self {
216        class: 4,
217        detail: 15,
218    };
219    /// 5.00 Internal Server Error.
220    pub const INTERNAL_SERVER_ERROR: Self = Self {
221        class: 5,
222        detail: 0,
223    };
224
225    /// Encodes the code as a single byte: class in bits 7-5, detail in bits 4-0.
226    pub fn to_byte(&self) -> u8 {
227        ((self.class & 0x07) << 5) | (self.detail & 0x1F)
228    }
229
230    /// Decodes a code from a single byte.
231    pub fn from_byte(byte: u8) -> Self {
232        Self {
233            class: (byte >> 5) & 0x07,
234            detail: byte & 0x1F,
235        }
236    }
237
238    /// Returns whether this code represents a request (class 0).
239    pub fn is_request(&self) -> bool {
240        self.class == 0
241    }
242
243    /// Returns whether this code represents a success response (class 2).
244    pub fn is_success(&self) -> bool {
245        self.class == 2
246    }
247
248    /// Returns whether this code represents a client error (class 4).
249    pub fn is_client_error(&self) -> bool {
250        self.class == 4
251    }
252
253    /// Returns whether this code represents a server error (class 5).
254    pub fn is_server_error(&self) -> bool {
255        self.class == 5
256    }
257}
258
259impl std::fmt::Display for CoapCode {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        write!(f, "{}.{:02}", self.class, self.detail)
262    }
263}
264
265/// A single CoAP option (RFC 7252 §3.1).
266///
267/// Options are TLV-encoded in messages, sorted by option number.
268/// Repeated option numbers create multiple values for that option.
269#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct CoapOption {
271    /// Option number (determines semantics).
272    pub number: u16,
273    /// Option value (interpretation depends on the option number).
274    pub value: Vec<u8>,
275}
276
277impl CoapOption {
278    /// Creates a new option with the given number and value.
279    pub fn new(number: u16, value: Vec<u8>) -> Self {
280        Self { number, value }
281    }
282
283    /// Creates an empty option (zero-length value).
284    pub fn empty(number: u16) -> Self {
285        Self {
286            number,
287            value: Vec::new(),
288        }
289    }
290
291    /// Interprets the option value as a variable-length unsigned integer.
292    ///
293    /// RFC 7252 §3.2: Options with format "uint" use 0-4 bytes in
294    /// network byte order.
295    pub fn value_as_uint(&self) -> u32 {
296        let mut result: u32 = 0;
297        for &byte in &self.value {
298            result = (result << 8) | u32::from(byte);
299        }
300        result
301    }
302
303    /// Creates an option with a uint value encoded in the minimum number of bytes.
304    pub fn from_uint(number: u16, value: u32) -> Self {
305        let bytes = if value == 0 {
306            Vec::new()
307        } else if value <= 0xFF {
308            vec![value as u8]
309        } else if value <= 0xFFFF {
310            vec![(value >> 8) as u8, value as u8]
311        } else if value <= 0xFF_FFFF {
312            vec![(value >> 16) as u8, (value >> 8) as u8, value as u8]
313        } else {
314            vec![
315                (value >> 24) as u8,
316                (value >> 16) as u8,
317                (value >> 8) as u8,
318                value as u8,
319            ]
320        };
321        Self {
322            number,
323            value: bytes,
324        }
325    }
326
327    /// Interprets the option value as a UTF-8 string.
328    pub fn value_as_str(&self) -> CoapResult<&str> {
329        std::str::from_utf8(&self.value)
330            .map_err(|e| CoapError::InvalidMessage(format!("Invalid UTF-8 in option: {e}")))
331    }
332}
333
334/// A parsed CoAP message (RFC 7252 §3).
335///
336/// Represents a complete CoAP datagram including the fixed header, token,
337/// options, and optional payload.
338#[derive(Debug, Clone, PartialEq, Eq)]
339pub struct CoapMessage {
340    /// Protocol version (must be 1).
341    pub version: u8,
342    /// Message type (CON, NON, ACK, RST).
343    pub msg_type: CoapMessageType,
344    /// Request/response code.
345    pub code: CoapCode,
346    /// Message ID for matching requests to responses.
347    pub message_id: u16,
348    /// Token for correlating requests and responses (0-8 bytes).
349    pub token: Vec<u8>,
350    /// CoAP options, sorted by option number.
351    pub options: Vec<CoapOption>,
352    /// Message payload (after the 0xFF marker).
353    pub payload: Vec<u8>,
354}
355
356impl CoapMessage {
357    /// Parses a raw UDP datagram into a CoAP message.
358    ///
359    /// RFC 7252 §3: The message format is a compact binary encoding with
360    /// a 4-byte fixed header followed by a variable-length token, options,
361    /// and payload.
362    pub fn parse(data: &[u8]) -> CoapResult<Self> {
363        if data.len() < 4 {
364            return Err(CoapError::InvalidMessage(format!(
365                "Message too short: {} bytes (minimum 4)",
366                data.len()
367            )));
368        }
369
370        // Byte 0: Ver (2 bits) | T (2 bits) | TKL (4 bits)
371        let version = (data[0] >> 6) & 0x03;
372        if version != COAP_VERSION {
373            return Err(CoapError::InvalidMessage(format!(
374                "Unsupported CoAP version: {version}"
375            )));
376        }
377
378        let msg_type = CoapMessageType::from_bits((data[0] >> 4) & 0x03)?;
379        let tkl = (data[0] & 0x0F) as usize;
380
381        if tkl > 8 {
382            return Err(CoapError::InvalidMessage(format!(
383                "Token length {tkl} exceeds maximum of 8"
384            )));
385        }
386
387        // Byte 1: Code
388        let code = CoapCode::from_byte(data[1]);
389
390        // Bytes 2-3: Message ID
391        let message_id = u16::from_be_bytes([data[2], data[3]]);
392
393        // Token
394        let token_end = 4 + tkl;
395        if data.len() < token_end {
396            return Err(CoapError::InvalidMessage(format!(
397                "Message truncated in token: need {} bytes, have {}",
398                token_end,
399                data.len()
400            )));
401        }
402        let token = data[4..token_end].to_vec();
403
404        // Options and payload
405        let mut pos = token_end;
406        let mut options = Vec::new();
407        let mut current_option_number: u16 = 0;
408
409        while pos < data.len() {
410            // Check for payload marker
411            if data[pos] == PAYLOAD_MARKER {
412                pos += 1;
413                break;
414            }
415
416            // Parse option delta and length (RFC 7252 §3.1)
417            let option_byte = data[pos];
418            pos += 1;
419
420            let mut delta = u16::from((option_byte >> 4) & 0x0F);
421            let mut length = u16::from(option_byte & 0x0F);
422
423            // Extended delta
424            match delta {
425                13 => {
426                    if pos >= data.len() {
427                        return Err(CoapError::InvalidMessage(
428                            "Truncated option delta (13)".to_string(),
429                        ));
430                    }
431                    delta = u16::from(data[pos]) + 13;
432                    pos += 1;
433                }
434                14 => {
435                    if pos + 1 >= data.len() {
436                        return Err(CoapError::InvalidMessage(
437                            "Truncated option delta (14)".to_string(),
438                        ));
439                    }
440                    delta = u16::from_be_bytes([data[pos], data[pos + 1]]) + 269;
441                    pos += 2;
442                }
443                15 => {
444                    return Err(CoapError::InvalidMessage(
445                        "Reserved option delta value 15".to_string(),
446                    ));
447                }
448                _ => {}
449            }
450
451            // Extended length
452            match length {
453                13 => {
454                    if pos >= data.len() {
455                        return Err(CoapError::InvalidMessage(
456                            "Truncated option length (13)".to_string(),
457                        ));
458                    }
459                    length = u16::from(data[pos]) + 13;
460                    pos += 1;
461                }
462                14 => {
463                    if pos + 1 >= data.len() {
464                        return Err(CoapError::InvalidMessage(
465                            "Truncated option length (14)".to_string(),
466                        ));
467                    }
468                    length = u16::from_be_bytes([data[pos], data[pos + 1]]) + 269;
469                    pos += 2;
470                }
471                15 => {
472                    return Err(CoapError::InvalidMessage(
473                        "Reserved option length value 15".to_string(),
474                    ));
475                }
476                _ => {}
477            }
478
479            current_option_number += delta;
480            let length = length as usize;
481
482            if pos + length > data.len() {
483                return Err(CoapError::InvalidMessage(format!(
484                    "Truncated option value: need {} bytes at offset {}, have {}",
485                    length,
486                    pos,
487                    data.len() - pos
488                )));
489            }
490
491            let value = data[pos..pos + length].to_vec();
492            pos += length;
493
494            options.push(CoapOption {
495                number: current_option_number,
496                value,
497            });
498        }
499
500        // Remaining bytes after payload marker are the payload.
501        let payload = if pos < data.len() {
502            data[pos..].to_vec()
503        } else {
504            Vec::new()
505        };
506
507        Ok(Self {
508            version,
509            msg_type,
510            code,
511            message_id,
512            token,
513            options,
514            payload,
515        })
516    }
517
518    /// Serializes this CoAP message to bytes suitable for UDP transmission.
519    ///
520    /// RFC 7252 §3: Options are encoded using delta compression relative
521    /// to the previous option number.
522    pub fn encode(&self) -> Vec<u8> {
523        let mut buf = Vec::with_capacity(4 + self.token.len() + self.payload.len() + 32);
524
525        // Byte 0: Ver (2) | T (2) | TKL (4)
526        let tkl = self.token.len().min(8) as u8;
527        buf.push((COAP_VERSION << 6) | (self.msg_type.to_bits() << 4) | tkl);
528
529        // Byte 1: Code
530        buf.push(self.code.to_byte());
531
532        // Bytes 2-3: Message ID
533        buf.extend_from_slice(&self.message_id.to_be_bytes());
534
535        // Token
536        buf.extend_from_slice(&self.token[..tkl as usize]);
537
538        // Options (must be sorted by option number for delta encoding)
539        let mut sorted_options = self.options.clone();
540        sorted_options.sort_by_key(|o| o.number);
541
542        let mut prev_number: u16 = 0;
543        for opt in &sorted_options {
544            let delta = opt.number - prev_number;
545            let length = opt.value.len() as u16;
546            prev_number = opt.number;
547
548            // Encode delta
549            let (delta_nibble, delta_ext) = encode_option_header_value(delta);
550            // Encode length
551            let (length_nibble, length_ext) = encode_option_header_value(length);
552
553            buf.push((delta_nibble << 4) | length_nibble);
554            buf.extend_from_slice(&delta_ext);
555            buf.extend_from_slice(&length_ext);
556            buf.extend_from_slice(&opt.value);
557        }
558
559        // Payload (preceded by 0xFF marker if non-empty)
560        if !self.payload.is_empty() {
561            buf.push(PAYLOAD_MARKER);
562            buf.extend_from_slice(&self.payload);
563        }
564
565        buf
566    }
567
568    /// Extracts the URI path from Uri-Path options.
569    ///
570    /// RFC 7252 §5.10.1: Multiple Uri-Path options are joined with `/`
571    /// to reconstruct the full path.
572    pub fn uri_path(&self) -> String {
573        let segments: Vec<&str> = self
574            .options
575            .iter()
576            .filter(|o| o.number == OPTION_URI_PATH)
577            .filter_map(|o| std::str::from_utf8(&o.value).ok())
578            .collect();
579
580        if segments.is_empty() {
581            "/".to_string()
582        } else {
583            format!("/{}", segments.join("/"))
584        }
585    }
586
587    /// Returns the Content-Format option value, if present.
588    pub fn content_format(&self) -> Option<u16> {
589        self.options
590            .iter()
591            .find(|o| o.number == OPTION_CONTENT_FORMAT)
592            .map(|o| o.value_as_uint() as u16)
593    }
594
595    /// Returns the Block1 option, if present.
596    pub fn block1(&self) -> Option<crate::block::BlockOption> {
597        self.options
598            .iter()
599            .find(|o| o.number == OPTION_BLOCK1)
600            .map(|o| crate::block::BlockOption::decode(o.value_as_uint()))
601    }
602
603    /// Returns the Block2 option, if present.
604    pub fn block2(&self) -> Option<crate::block::BlockOption> {
605        self.options
606            .iter()
607            .find(|o| o.number == OPTION_BLOCK2)
608            .map(|o| crate::block::BlockOption::decode(o.value_as_uint()))
609    }
610}
611
612/// Encodes a delta or length value into the option header nibble and
613/// optional extended bytes per RFC 7252 §3.1.
614fn encode_option_header_value(value: u16) -> (u8, Vec<u8>) {
615    if value < 13 {
616        (value as u8, Vec::new())
617    } else if value < 269 {
618        (13, vec![(value - 13) as u8])
619    } else {
620        let extended = value - 269;
621        (14, extended.to_be_bytes().to_vec())
622    }
623}
624
625/// EST operations per RFC 7030, reused from the `kipuka-est` crate.
626///
627/// Duplicated here to avoid a dependency on `kipuka-est` from the CoAP
628/// transport layer. The router maps CoAP paths to these operations.
629#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
630pub enum EstOperation {
631    /// Retrieve CA certificates (RFC 7030 §4.1).
632    CaCerts,
633    /// Simple enrollment (RFC 7030 §4.2).
634    SimpleEnroll,
635    /// Simple re-enrollment (RFC 7030 §4.2.2).
636    SimpleReenroll,
637    /// Server-side key generation (RFC 7030 §4.4).
638    ServerKeygen,
639    /// CSR attributes (RFC 7030 §4.5).
640    CsrAttrs,
641}
642
643/// CoAP request representing an EST-coaps operation.
644///
645/// RFC 9483 §5.1: EST-coaps URIs follow the pattern:
646///   coaps://host/.well-known/est/{operation}
647/// where {operation} uses the abbreviated names from RFC 9483 §5.1:
648///   - /cacerts   -> /cacerts (GET)
649///   - /sen       -> /simpleenroll (POST)
650///   - /sren      -> /simplereenroll (POST)
651///   - /skg       -> /serverkeygen (POST)
652///   - /att       -> /csrattrs (GET)
653///   - /crts      -> /cacerts (GET, alias)
654#[derive(Debug)]
655pub struct CoapEstRequest {
656    /// The decoded EST operation.
657    pub operation: EstOperation,
658    /// The CoAP method used.
659    pub method: CoapMethod,
660    /// The original CoAP message.
661    pub message: CoapMessage,
662}
663
664/// Routes CoAP URI paths to EST operations.
665///
666/// RFC 9483 §5.1: EST-coaps uses abbreviated path names under
667/// `/.well-known/est/` to reduce URI size for constrained devices.
668///
669/// The router strips the well-known prefix and maps the final path
670/// segment to an [`EstOperation`].
671pub struct CoapEstRouter;
672
673impl CoapEstRouter {
674    /// Maps a CoAP URI path to an EST operation.
675    ///
676    /// Recognizes both the abbreviated RFC 9483 paths and the full-length
677    /// path segments from the well-known prefix.
678    ///
679    /// # Path Recognition
680    ///
681    /// The router accepts paths with or without the `/.well-known/est/`
682    /// prefix, recognizing these final segments:
683    /// - `sen` → SimpleEnroll
684    /// - `sren` → SimpleReenroll
685    /// - `skg` → ServerKeygen
686    /// - `att` → CsrAttrs
687    /// - `cacerts` or `crts` → CaCerts
688    pub fn route(path: &str) -> CoapResult<EstOperation> {
689        // Extract the final path segment, stripping the well-known prefix.
690        let segment = path
691            .trim_start_matches('/')
692            .trim_start_matches(".well-known/est/")
693            .trim_start_matches(".well-known/est")
694            .trim_start_matches('/')
695            .trim_end_matches('/');
696
697        match segment {
698            "sen" | "simpleenroll" => Ok(EstOperation::SimpleEnroll),
699            "sren" | "simplereenroll" => Ok(EstOperation::SimpleReenroll),
700            "skg" | "serverkeygen" => Ok(EstOperation::ServerKeygen),
701            "att" | "csrattrs" => Ok(EstOperation::CsrAttrs),
702            "cacerts" | "crts" => Ok(EstOperation::CaCerts),
703            _ => Err(CoapError::ResourceNotFound(format!(
704                "Unknown EST-coaps path: {path}"
705            ))),
706        }
707    }
708
709    /// Routes a full CoAP message to an EST operation.
710    ///
711    /// Extracts the URI path from the message options and resolves the
712    /// corresponding EST operation. Also validates that the CoAP method
713    /// is appropriate for the operation.
714    pub fn route_message(message: CoapMessage) -> CoapResult<CoapEstRequest> {
715        let path = message.uri_path();
716        let operation = Self::route(&path)?;
717
718        let method = CoapMethod::from_code(&message.code).ok_or_else(|| {
719            CoapError::UnsupportedMethod(format!("Code {} is not a request", message.code))
720        })?;
721
722        // Validate method against operation.
723        match operation {
724            EstOperation::CaCerts | EstOperation::CsrAttrs => {
725                if method != CoapMethod::Get && method != CoapMethod::Fetch {
726                    return Err(CoapError::UnsupportedMethod(format!(
727                        "{path} requires GET or FETCH, got {:?}",
728                        method
729                    )));
730                }
731            }
732            EstOperation::SimpleEnroll
733            | EstOperation::SimpleReenroll
734            | EstOperation::ServerKeygen => {
735                if method != CoapMethod::Post {
736                    return Err(CoapError::UnsupportedMethod(format!(
737                        "{path} requires POST, got {:?}",
738                        method
739                    )));
740                }
741            }
742        }
743
744        Ok(CoapEstRequest {
745            operation,
746            method,
747            message,
748        })
749    }
750}
751
752#[cfg(test)]
753mod tests {
754    use super::*;
755
756    // --- CoapCode tests ---
757
758    #[test]
759    fn test_code_byte_roundtrip() {
760        let codes = [
761            CoapCode::CREATED,
762            CoapCode::CONTENT,
763            CoapCode::NOT_FOUND,
764            CoapCode::METHOD_NOT_ALLOWED,
765            CoapCode::UNSUPPORTED_CONTENT_FORMAT,
766            CoapCode::INTERNAL_SERVER_ERROR,
767        ];
768
769        for code in codes {
770            let byte = code.to_byte();
771            let decoded = CoapCode::from_byte(byte);
772            assert_eq!(decoded, code, "roundtrip failed for {code}");
773        }
774    }
775
776    #[test]
777    fn test_code_classification() {
778        assert!(CoapCode::from_byte(0x01).is_request()); // 0.01 GET
779        assert!(CoapCode::CONTENT.is_success());
780        assert!(CoapCode::NOT_FOUND.is_client_error());
781        assert!(CoapCode::INTERNAL_SERVER_ERROR.is_server_error());
782    }
783
784    #[test]
785    fn test_code_display() {
786        assert_eq!(CoapCode::CONTENT.to_string(), "2.05");
787        assert_eq!(CoapCode::NOT_FOUND.to_string(), "4.04");
788        assert_eq!(CoapCode::CREATED.to_string(), "2.01");
789    }
790
791    // --- CoapOption tests ---
792
793    #[test]
794    fn test_option_uint_encoding() {
795        let opt = CoapOption::from_uint(OPTION_CONTENT_FORMAT, 285);
796        assert_eq!(opt.value_as_uint(), 285);
797
798        let opt_zero = CoapOption::from_uint(OPTION_CONTENT_FORMAT, 0);
799        assert_eq!(opt_zero.value_as_uint(), 0);
800        assert!(opt_zero.value.is_empty());
801    }
802
803    #[test]
804    fn test_option_string_value() {
805        let opt = CoapOption::new(OPTION_URI_PATH, b"sen".to_vec());
806        assert_eq!(opt.value_as_str().unwrap(), "sen");
807    }
808
809    // --- CoapMessage parse/encode tests ---
810
811    #[test]
812    fn test_parse_minimal_message() {
813        // Minimal CON GET with no token, no options, no payload
814        // Ver=1, T=0 (CON), TKL=0, Code=0.01 (GET), MID=0x1234
815        let data = [0x40, 0x01, 0x12, 0x34];
816        let msg = CoapMessage::parse(&data).unwrap();
817
818        assert_eq!(msg.version, 1);
819        assert_eq!(msg.msg_type, CoapMessageType::Confirmable);
820        assert_eq!(
821            msg.code,
822            CoapCode {
823                class: 0,
824                detail: 1
825            }
826        );
827        assert_eq!(msg.message_id, 0x1234);
828        assert!(msg.token.is_empty());
829        assert!(msg.options.is_empty());
830        assert!(msg.payload.is_empty());
831    }
832
833    #[test]
834    fn test_parse_message_with_token() {
835        // CON GET, TKL=4, token=[0xDE,0xAD,0xBE,0xEF]
836        let data = [0x44, 0x01, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF];
837        let msg = CoapMessage::parse(&data).unwrap();
838
839        assert_eq!(msg.token, vec![0xDE, 0xAD, 0xBE, 0xEF]);
840    }
841
842    #[test]
843    fn test_parse_message_with_payload() {
844        // CON POST, TKL=0, no options, payload "hello"
845        let mut data = vec![0x40, 0x02, 0x00, 0x01];
846        data.push(PAYLOAD_MARKER);
847        data.extend_from_slice(b"hello");
848
849        let msg = CoapMessage::parse(&data).unwrap();
850        assert_eq!(msg.payload, b"hello");
851    }
852
853    #[test]
854    fn test_parse_encode_roundtrip() {
855        let original = CoapMessage {
856            version: 1,
857            msg_type: CoapMessageType::Confirmable,
858            code: CoapMethod::Post.to_code(),
859            message_id: 0xABCD,
860            token: vec![0x01, 0x02],
861            options: vec![
862                CoapOption::new(OPTION_URI_PATH, b".well-known".to_vec()),
863                CoapOption::new(OPTION_URI_PATH, b"est".to_vec()),
864                CoapOption::new(OPTION_URI_PATH, b"sen".to_vec()),
865                CoapOption::from_uint(OPTION_CONTENT_FORMAT, 285),
866            ],
867            payload: vec![0x30, 0x82, 0x01, 0x00],
868        };
869
870        let encoded = original.encode();
871        let decoded = CoapMessage::parse(&encoded).unwrap();
872
873        assert_eq!(decoded.version, original.version);
874        assert_eq!(decoded.msg_type, original.msg_type);
875        assert_eq!(decoded.code, original.code);
876        assert_eq!(decoded.message_id, original.message_id);
877        assert_eq!(decoded.token, original.token);
878        assert_eq!(decoded.payload, original.payload);
879        assert_eq!(decoded.options.len(), original.options.len());
880    }
881
882    #[test]
883    fn test_parse_too_short() {
884        let err = CoapMessage::parse(&[0x40, 0x01]).unwrap_err();
885        assert!(matches!(err, CoapError::InvalidMessage(_)));
886    }
887
888    #[test]
889    fn test_parse_bad_version() {
890        // Version 2 (invalid)
891        let data = [0x80, 0x01, 0x00, 0x01];
892        let err = CoapMessage::parse(&data).unwrap_err();
893        assert!(matches!(err, CoapError::InvalidMessage(_)));
894    }
895
896    #[test]
897    fn test_parse_tkl_too_large() {
898        // TKL=9 (invalid, max is 8)
899        let data = [0x49, 0x01, 0x00, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0];
900        let err = CoapMessage::parse(&data).unwrap_err();
901        assert!(matches!(err, CoapError::InvalidMessage(_)));
902    }
903
904    // --- URI path extraction ---
905
906    #[test]
907    fn test_uri_path_extraction() {
908        let msg = CoapMessage {
909            version: 1,
910            msg_type: CoapMessageType::Confirmable,
911            code: CoapMethod::Post.to_code(),
912            message_id: 1,
913            token: vec![],
914            options: vec![
915                CoapOption::new(OPTION_URI_PATH, b".well-known".to_vec()),
916                CoapOption::new(OPTION_URI_PATH, b"est".to_vec()),
917                CoapOption::new(OPTION_URI_PATH, b"sen".to_vec()),
918            ],
919            payload: vec![],
920        };
921
922        assert_eq!(msg.uri_path(), "/.well-known/est/sen");
923    }
924
925    #[test]
926    fn test_uri_path_empty() {
927        let msg = CoapMessage {
928            version: 1,
929            msg_type: CoapMessageType::Confirmable,
930            code: CoapMethod::Get.to_code(),
931            message_id: 1,
932            token: vec![],
933            options: vec![],
934            payload: vec![],
935        };
936
937        assert_eq!(msg.uri_path(), "/");
938    }
939
940    // --- EST routing tests ---
941
942    #[test]
943    fn test_route_abbreviated_paths() {
944        assert_eq!(
945            CoapEstRouter::route("/sen").unwrap(),
946            EstOperation::SimpleEnroll
947        );
948        assert_eq!(
949            CoapEstRouter::route("/sren").unwrap(),
950            EstOperation::SimpleReenroll
951        );
952        assert_eq!(
953            CoapEstRouter::route("/skg").unwrap(),
954            EstOperation::ServerKeygen
955        );
956        assert_eq!(
957            CoapEstRouter::route("/att").unwrap(),
958            EstOperation::CsrAttrs
959        );
960        assert_eq!(
961            CoapEstRouter::route("/cacerts").unwrap(),
962            EstOperation::CaCerts
963        );
964        assert_eq!(
965            CoapEstRouter::route("/crts").unwrap(),
966            EstOperation::CaCerts
967        );
968    }
969
970    #[test]
971    fn test_route_well_known_prefix() {
972        assert_eq!(
973            CoapEstRouter::route("/.well-known/est/sen").unwrap(),
974            EstOperation::SimpleEnroll
975        );
976        assert_eq!(
977            CoapEstRouter::route("/.well-known/est/cacerts").unwrap(),
978            EstOperation::CaCerts
979        );
980    }
981
982    #[test]
983    fn test_route_full_names() {
984        assert_eq!(
985            CoapEstRouter::route("/simpleenroll").unwrap(),
986            EstOperation::SimpleEnroll
987        );
988        assert_eq!(
989            CoapEstRouter::route("/simplereenroll").unwrap(),
990            EstOperation::SimpleReenroll
991        );
992        assert_eq!(
993            CoapEstRouter::route("/serverkeygen").unwrap(),
994            EstOperation::ServerKeygen
995        );
996        assert_eq!(
997            CoapEstRouter::route("/csrattrs").unwrap(),
998            EstOperation::CsrAttrs
999        );
1000    }
1001
1002    #[test]
1003    fn test_route_unknown_path() {
1004        let err = CoapEstRouter::route("/unknown").unwrap_err();
1005        assert!(matches!(err, CoapError::ResourceNotFound(_)));
1006    }
1007
1008    // --- Block option accessors ---
1009
1010    #[test]
1011    fn test_message_block1_option() {
1012        let block = crate::block::BlockOption {
1013            num: 3,
1014            more: true,
1015            szx: 5,
1016        };
1017
1018        let msg = CoapMessage {
1019            version: 1,
1020            msg_type: CoapMessageType::Confirmable,
1021            code: CoapMethod::Post.to_code(),
1022            message_id: 1,
1023            token: vec![],
1024            options: vec![CoapOption::from_uint(OPTION_BLOCK1, block.encode())],
1025            payload: vec![],
1026        };
1027
1028        let extracted = msg.block1().unwrap();
1029        assert_eq!(extracted, block);
1030    }
1031
1032    #[test]
1033    fn test_message_content_format() {
1034        let msg = CoapMessage {
1035            version: 1,
1036            msg_type: CoapMessageType::Confirmable,
1037            code: CoapMethod::Post.to_code(),
1038            message_id: 1,
1039            token: vec![],
1040            options: vec![CoapOption::from_uint(
1041                OPTION_CONTENT_FORMAT,
1042                u32::from(crate::content_format::APPLICATION_PKCS10),
1043            )],
1044            payload: vec![],
1045        };
1046
1047        assert_eq!(
1048            msg.content_format(),
1049            Some(crate::content_format::APPLICATION_PKCS10)
1050        );
1051    }
1052
1053    // --- Route message integration test ---
1054
1055    #[test]
1056    fn test_route_message_post_simpleenroll() {
1057        let msg = CoapMessage {
1058            version: 1,
1059            msg_type: CoapMessageType::Confirmable,
1060            code: CoapMethod::Post.to_code(),
1061            message_id: 42,
1062            token: vec![0x01],
1063            options: vec![
1064                CoapOption::new(OPTION_URI_PATH, b".well-known".to_vec()),
1065                CoapOption::new(OPTION_URI_PATH, b"est".to_vec()),
1066                CoapOption::new(OPTION_URI_PATH, b"sen".to_vec()),
1067            ],
1068            payload: vec![0x30],
1069        };
1070
1071        let req = CoapEstRouter::route_message(msg).unwrap();
1072        assert_eq!(req.operation, EstOperation::SimpleEnroll);
1073        assert_eq!(req.method, CoapMethod::Post);
1074    }
1075
1076    #[test]
1077    fn test_route_message_get_cacerts() {
1078        let msg = CoapMessage {
1079            version: 1,
1080            msg_type: CoapMessageType::Confirmable,
1081            code: CoapMethod::Get.to_code(),
1082            message_id: 43,
1083            token: vec![],
1084            options: vec![CoapOption::new(OPTION_URI_PATH, b"cacerts".to_vec())],
1085            payload: vec![],
1086        };
1087
1088        let req = CoapEstRouter::route_message(msg).unwrap();
1089        assert_eq!(req.operation, EstOperation::CaCerts);
1090        assert_eq!(req.method, CoapMethod::Get);
1091    }
1092
1093    #[test]
1094    fn test_route_message_wrong_method() {
1095        let msg = CoapMessage {
1096            version: 1,
1097            msg_type: CoapMessageType::Confirmable,
1098            code: CoapMethod::Get.to_code(), // GET on /sen is wrong
1099            message_id: 44,
1100            token: vec![],
1101            options: vec![CoapOption::new(OPTION_URI_PATH, b"sen".to_vec())],
1102            payload: vec![],
1103        };
1104
1105        let err = CoapEstRouter::route_message(msg).unwrap_err();
1106        assert!(matches!(err, CoapError::UnsupportedMethod(_)));
1107    }
1108
1109    // --- Encode/parse round-trip with options ---
1110
1111    #[test]
1112    fn test_encode_parse_roundtrip_with_large_option_delta() {
1113        let msg = CoapMessage {
1114            version: 1,
1115            msg_type: CoapMessageType::Acknowledgement,
1116            code: CoapCode::CONTENT,
1117            message_id: 999,
1118            token: vec![0xAA, 0xBB, 0xCC],
1119            options: vec![
1120                CoapOption::new(OPTION_URI_PATH, b"test".to_vec()), // 11
1121                CoapOption::from_uint(OPTION_CONTENT_FORMAT, 285),  // 12
1122                CoapOption::from_uint(OPTION_SIZE1, 4096),          // 60
1123            ],
1124            payload: b"response-body".to_vec(),
1125        };
1126
1127        let bytes = msg.encode();
1128        let parsed = CoapMessage::parse(&bytes).unwrap();
1129
1130        assert_eq!(parsed.version, 1);
1131        assert_eq!(parsed.msg_type, CoapMessageType::Acknowledgement);
1132        assert_eq!(parsed.code, CoapCode::CONTENT);
1133        assert_eq!(parsed.message_id, 999);
1134        assert_eq!(parsed.token, vec![0xAA, 0xBB, 0xCC]);
1135        assert_eq!(parsed.payload, b"response-body");
1136
1137        // Verify option values
1138        assert_eq!(parsed.content_format(), Some(285));
1139        let size1 = parsed
1140            .options
1141            .iter()
1142            .find(|o| o.number == OPTION_SIZE1)
1143            .unwrap();
1144        assert_eq!(size1.value_as_uint(), 4096);
1145    }
1146}