1use crate::{CoapError, CoapResult};
36
37pub const COAP_VERSION: u8 = 1;
41
42const PAYLOAD_MARKER: u8 = 0xFF;
46
47pub const OPTION_URI_HOST: u16 = 3;
51
52pub const OPTION_URI_PORT: u16 = 7;
54
55pub const OPTION_URI_PATH: u16 = 11;
60
61pub const OPTION_CONTENT_FORMAT: u16 = 12;
65
66pub const OPTION_URI_QUERY: u16 = 15;
68
69pub const OPTION_BLOCK2: u16 = 23;
73
74pub const OPTION_BLOCK1: u16 = 27;
78
79pub const OPTION_SIZE2: u16 = 28;
83
84pub const OPTION_SIZE1: u16 = 60;
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
91pub enum CoapMethod {
92 Get,
94 Post,
96 Put,
98 Delete,
100 Fetch,
102 Patch,
104 IPatch,
106}
107
108impl CoapMethod {
109 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
145pub enum CoapMessageType {
146 Confirmable,
148 NonConfirmable,
150 Acknowledgement,
152 Reset,
154}
155
156impl CoapMessageType {
157 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
186pub struct CoapCode {
187 pub class: u8,
189 pub detail: u8,
191}
192
193impl CoapCode {
194 pub const CREATED: Self = Self {
196 class: 2,
197 detail: 1,
198 };
199 pub const CONTENT: Self = Self {
201 class: 2,
202 detail: 5,
203 };
204 pub const NOT_FOUND: Self = Self {
206 class: 4,
207 detail: 4,
208 };
209 pub const METHOD_NOT_ALLOWED: Self = Self {
211 class: 4,
212 detail: 5,
213 };
214 pub const UNSUPPORTED_CONTENT_FORMAT: Self = Self {
216 class: 4,
217 detail: 15,
218 };
219 pub const INTERNAL_SERVER_ERROR: Self = Self {
221 class: 5,
222 detail: 0,
223 };
224
225 pub fn to_byte(&self) -> u8 {
227 ((self.class & 0x07) << 5) | (self.detail & 0x1F)
228 }
229
230 pub fn from_byte(byte: u8) -> Self {
232 Self {
233 class: (byte >> 5) & 0x07,
234 detail: byte & 0x1F,
235 }
236 }
237
238 pub fn is_request(&self) -> bool {
240 self.class == 0
241 }
242
243 pub fn is_success(&self) -> bool {
245 self.class == 2
246 }
247
248 pub fn is_client_error(&self) -> bool {
250 self.class == 4
251 }
252
253 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#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct CoapOption {
271 pub number: u16,
273 pub value: Vec<u8>,
275}
276
277impl CoapOption {
278 pub fn new(number: u16, value: Vec<u8>) -> Self {
280 Self { number, value }
281 }
282
283 pub fn empty(number: u16) -> Self {
285 Self {
286 number,
287 value: Vec::new(),
288 }
289 }
290
291 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
339pub struct CoapMessage {
340 pub version: u8,
342 pub msg_type: CoapMessageType,
344 pub code: CoapCode,
346 pub message_id: u16,
348 pub token: Vec<u8>,
350 pub options: Vec<CoapOption>,
352 pub payload: Vec<u8>,
354}
355
356impl CoapMessage {
357 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 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 let code = CoapCode::from_byte(data[1]);
389
390 let message_id = u16::from_be_bytes([data[2], data[3]]);
392
393 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 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 if data[pos] == PAYLOAD_MARKER {
412 pos += 1;
413 break;
414 }
415
416 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 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 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 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 pub fn encode(&self) -> Vec<u8> {
523 let mut buf = Vec::with_capacity(4 + self.token.len() + self.payload.len() + 32);
524
525 let tkl = self.token.len().min(8) as u8;
527 buf.push((COAP_VERSION << 6) | (self.msg_type.to_bits() << 4) | tkl);
528
529 buf.push(self.code.to_byte());
531
532 buf.extend_from_slice(&self.message_id.to_be_bytes());
534
535 buf.extend_from_slice(&self.token[..tkl as usize]);
537
538 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 let (delta_nibble, delta_ext) = encode_option_header_value(delta);
550 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 if !self.payload.is_empty() {
561 buf.push(PAYLOAD_MARKER);
562 buf.extend_from_slice(&self.payload);
563 }
564
565 buf
566 }
567
568 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 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 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 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
612fn 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
630pub enum EstOperation {
631 CaCerts,
633 SimpleEnroll,
635 SimpleReenroll,
637 ServerKeygen,
639 CsrAttrs,
641}
642
643#[derive(Debug)]
655pub struct CoapEstRequest {
656 pub operation: EstOperation,
658 pub method: CoapMethod,
660 pub message: CoapMessage,
662}
663
664pub struct CoapEstRouter;
672
673impl CoapEstRouter {
674 pub fn route(path: &str) -> CoapResult<EstOperation> {
689 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 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 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 #[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()); 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 #[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 #[test]
812 fn test_parse_minimal_message() {
813 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 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 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 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 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 #[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 #[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 #[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 #[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(), 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 #[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()), CoapOption::from_uint(OPTION_CONTENT_FORMAT, 285), CoapOption::from_uint(OPTION_SIZE1, 4096), ],
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 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}