mesh.rs (19399B)
1 use radroots_mesh::{ 2 RADROOTS_MESH_RETICULUM_POLICY_ID, RADROOTS_MESH_UNAVAILABLE_MESSAGE, 3 RadrootsMeshAdmissionDecision, RadrootsMeshAdmissionInput, RadrootsMeshCompressionPolicy, 4 RadrootsMeshError, RadrootsMeshFrame, RadrootsMeshFrameType, RadrootsMeshPayload, 5 RadrootsMeshPayloadPolicy, RadrootsMeshPolicyDenyReason, RadrootsMeshPrivacyClass, 6 RadrootsMeshScope, decode_mesh_frame_cbor, encode_mesh_frame_cbor, 7 }; 8 use serde_json::Value; 9 10 fn default_frame() -> RadrootsMeshFrame { 11 RadrootsMeshFrame::new( 12 RadrootsMeshFrameType::Hello, 13 RadrootsMeshScope::Local, 14 "message-1", 15 42, 16 60_000, 17 ) 18 } 19 20 fn default_encoded_with_replacement( 21 start: usize, 22 end: usize, 23 replacement: impl IntoIterator<Item = u8>, 24 ) -> Vec<u8> { 25 let mut encoded = encode_mesh_frame_cbor(&default_frame()).expect("encode default"); 26 encoded.splice(start..end, replacement); 27 encoded 28 } 29 30 #[test] 31 fn default_frame_encodes_as_mesh_frame_v1_cddl_cbor() { 32 let frame = default_frame(); 33 let encoded = encode_mesh_frame_cbor(&frame).expect("encode frame"); 34 35 assert_eq!( 36 encoded, 37 [ 38 0xa7, 0x00, 0x01, 0x01, 0x00, 0x02, 0x65, b'l', b'o', b'c', b'a', b'l', 0x03, 0x69, 39 b'm', b'e', b's', b's', b'a', b'g', b'e', b'-', b'1', 0x04, 0x18, 0x2a, 0x05, 0x19, 40 0xea, 0x60, 0x06, 0xa0, 41 ] 42 ); 43 assert_eq!( 44 decode_mesh_frame_cbor(&encoded).expect("decode frame"), 45 frame 46 ); 47 } 48 49 #[test] 50 fn all_frame_types_round_trip_with_stable_codes_and_labels() { 51 let cases = [ 52 (RadrootsMeshFrameType::Hello, 0, "hello"), 53 ( 54 RadrootsMeshFrameType::EventHeadAnnounce, 55 1, 56 "event_head_announce", 57 ), 58 (RadrootsMeshFrameType::EventRequest, 2, "event_request"), 59 (RadrootsMeshFrameType::EventChunk, 3, "event_chunk"), 60 (RadrootsMeshFrameType::EventAck, 4, "event_ack"), 61 (RadrootsMeshFrameType::RouteProbe, 5, "route_probe"), 62 ]; 63 64 for (frame_type, code, label) in cases { 65 let frame = RadrootsMeshFrame::new( 66 frame_type, 67 RadrootsMeshScope::Community, 68 format!("{label}-message"), 69 code + 1, 70 1_000, 71 ); 72 let encoded = encode_mesh_frame_cbor(&frame).expect("encode frame"); 73 let decoded = decode_mesh_frame_cbor(&encoded).expect("decode frame"); 74 75 assert_eq!(decoded, frame); 76 assert_eq!(encode_mesh_frame_cbor(&decoded).expect("reencode"), encoded); 77 assert_eq!(frame_type.code(), code); 78 assert_eq!(frame_type.label(), label); 79 } 80 } 81 82 #[test] 83 fn reticulum_policy_has_zero_delivery_budgets_and_disabled_compression() { 84 let policy = RadrootsMeshPayloadPolicy::reticulum_unavailable(); 85 86 assert_eq!(policy.policy_id(), RADROOTS_MESH_RETICULUM_POLICY_ID); 87 assert_eq!(policy.max_payload_bytes, 0); 88 assert_eq!(policy.max_frame_bytes, 0); 89 assert_eq!(policy.compression, RadrootsMeshCompressionPolicy::Disabled); 90 assert_eq!(policy.compression.label(), "disabled"); 91 assert!(!policy.custom_scopes_enabled); 92 assert!(!policy.usable_for_delivery()); 93 } 94 95 #[test] 96 fn reticulum_policy_denies_real_payload_admission_deterministically() { 97 let policy = RadrootsMeshPayloadPolicy::reticulum_unavailable(); 98 let input = RadrootsMeshAdmissionInput::new( 99 RadrootsMeshScope::Local, 100 RadrootsMeshPrivacyClass::PublicEvent, 101 256, 102 512, 103 ); 104 let decision = policy.evaluate(&input); 105 106 assert_eq!( 107 decision, 108 RadrootsMeshAdmissionDecision::Denied { 109 reason: RadrootsMeshPolicyDenyReason::DeliveryUnavailable 110 } 111 ); 112 assert_eq!(decision.label(), "denied"); 113 assert_eq!( 114 decision.deny_reason(), 115 Some(RadrootsMeshPolicyDenyReason::DeliveryUnavailable) 116 ); 117 assert_eq!(decision.message(), RADROOTS_MESH_UNAVAILABLE_MESSAGE); 118 assert!(!decision.usable_for_delivery()); 119 assert_eq!( 120 RadrootsMeshPrivacyClass::PublicEvent.label(), 121 "public_event" 122 ); 123 assert_eq!( 124 RadrootsMeshPrivacyClass::PrivateEvent.label(), 125 "private_event" 126 ); 127 assert_eq!( 128 RadrootsMeshPolicyDenyReason::DeliveryUnavailable.label(), 129 "delivery_unavailable" 130 ); 131 } 132 133 #[test] 134 fn enabled_policy_evaluates_all_admission_decisions() { 135 let mut policy = RadrootsMeshPayloadPolicy::reticulum_unavailable(); 136 policy.max_payload_bytes = 10; 137 assert!(!policy.usable_for_delivery()); 138 policy.max_frame_bytes = 20; 139 assert!(policy.usable_for_delivery()); 140 141 let local = |payload_bytes, frame_bytes| { 142 RadrootsMeshAdmissionInput::new( 143 RadrootsMeshScope::Local, 144 RadrootsMeshPrivacyClass::PublicEvent, 145 payload_bytes, 146 frame_bytes, 147 ) 148 }; 149 let accepted = policy.evaluate(&local(10, 20)); 150 assert_eq!(accepted, RadrootsMeshAdmissionDecision::Accepted); 151 assert_eq!(accepted.label(), "accepted"); 152 assert_eq!(accepted.deny_reason(), None); 153 assert_eq!(accepted.message(), "mesh payload delivery is admitted"); 154 assert!(accepted.usable_for_delivery()); 155 156 for input in [local(11, 20), local(10, 21)] { 157 let denied = policy.evaluate(&input); 158 assert_eq!( 159 denied.deny_reason(), 160 Some(RadrootsMeshPolicyDenyReason::PayloadBudgetExceeded) 161 ); 162 } 163 assert_eq!( 164 RadrootsMeshPolicyDenyReason::PayloadBudgetExceeded.label(), 165 "payload_budget_exceeded" 166 ); 167 assert_eq!( 168 RadrootsMeshPolicyDenyReason::PayloadBudgetExceeded.message(), 169 "mesh payload exceeds the configured delivery budget" 170 ); 171 172 let custom_input = RadrootsMeshAdmissionInput::new( 173 RadrootsMeshScope::custom("farm-north").unwrap(), 174 RadrootsMeshPrivacyClass::PrivateEvent, 175 1, 176 1, 177 ); 178 let custom_denied = policy.evaluate(&custom_input); 179 assert_eq!( 180 custom_denied.deny_reason(), 181 Some(RadrootsMeshPolicyDenyReason::CustomScopeUnavailable) 182 ); 183 assert_eq!( 184 RadrootsMeshPolicyDenyReason::CustomScopeUnavailable.label(), 185 "custom_scope_unavailable" 186 ); 187 assert_eq!( 188 RadrootsMeshPolicyDenyReason::CustomScopeUnavailable.message(), 189 "mesh custom scopes are not enabled for the configured policy" 190 ); 191 192 policy.custom_scopes_enabled = true; 193 assert_eq!( 194 policy.evaluate(&custom_input), 195 RadrootsMeshAdmissionDecision::Accepted 196 ); 197 } 198 199 #[test] 200 fn custom_scope_has_explicit_namespace() { 201 let scope = RadrootsMeshScope::custom("farm-north.mesh_1").expect("custom scope"); 202 assert_eq!(scope.label(), "farm-north.mesh_1"); 203 let frame = RadrootsMeshFrame::new( 204 RadrootsMeshFrameType::RouteProbe, 205 scope, 206 "route-probe-1", 207 10, 208 1, 209 ); 210 let encoded = encode_mesh_frame_cbor(&frame).expect("encode custom scope"); 211 let decoded = decode_mesh_frame_cbor(&encoded).expect("decode custom scope"); 212 213 assert_eq!(decoded.scope_id.cbor_label(), "custom:farm-north.mesh_1"); 214 assert_eq!(encode_mesh_frame_cbor(&decoded).expect("reencode"), encoded); 215 } 216 217 #[test] 218 fn mesh_parsers_and_validation_reject_unknown_empty_or_invalid_values() { 219 assert_eq!( 220 RadrootsMeshScope::custom("").expect_err("empty custom scope"), 221 RadrootsMeshError::EmptyCustomScope 222 ); 223 for invalid in [ 224 " ", 225 " farm-north", 226 "farm-north ", 227 "farm north", 228 "farm/north", 229 "farm:north", 230 "farm\nnorth", 231 ] { 232 assert_eq!( 233 RadrootsMeshScope::custom(invalid).expect_err("invalid custom scope"), 234 RadrootsMeshError::InvalidCustomScope 235 ); 236 } 237 assert_eq!( 238 RadrootsMeshScope::parse("custom:").expect_err("empty parsed custom scope"), 239 RadrootsMeshError::EmptyCustomScope 240 ); 241 assert_eq!( 242 RadrootsMeshScope::parse("custom:farm north").expect_err("invalid parsed custom scope"), 243 RadrootsMeshError::InvalidCustomScope 244 ); 245 assert_eq!( 246 RadrootsMeshScope::parse("unscoped").expect_err("unknown scope"), 247 RadrootsMeshError::UnknownScope 248 ); 249 assert_eq!( 250 RadrootsMeshFrameType::parse_code(6).expect_err("unknown frame type"), 251 RadrootsMeshError::UnknownFrameType 252 ); 253 assert_eq!(RadrootsMeshScope::Local.label(), "local"); 254 assert_eq!(RadrootsMeshScope::Community.label(), "community"); 255 256 let empty_message = RadrootsMeshFrame::new( 257 RadrootsMeshFrameType::Hello, 258 RadrootsMeshScope::Local, 259 " ", 260 1, 261 1, 262 ); 263 assert_eq!( 264 empty_message.validate().expect_err("empty message id"), 265 RadrootsMeshError::EmptyMessageId 266 ); 267 268 let zero_ttl = RadrootsMeshFrame::new( 269 RadrootsMeshFrameType::Hello, 270 RadrootsMeshScope::Local, 271 "message-1", 272 1, 273 0, 274 ); 275 assert_eq!( 276 zero_ttl.validate().expect_err("zero ttl"), 277 RadrootsMeshError::InvalidTtl 278 ); 279 280 let mut unsupported_version = default_frame(); 281 unsupported_version.version += 1; 282 assert_eq!( 283 unsupported_version 284 .validate() 285 .expect_err("unsupported version"), 286 RadrootsMeshError::UnsupportedVersion 287 ); 288 } 289 290 #[test] 291 fn mesh_errors_have_stable_display_strings() { 292 let cases = [ 293 ( 294 RadrootsMeshError::EmptyCustomScope, 295 "mesh custom scope is empty", 296 ), 297 ( 298 RadrootsMeshError::InvalidCustomScope, 299 "mesh custom scope is invalid", 300 ), 301 ( 302 RadrootsMeshError::EmptyMessageId, 303 "mesh message id is empty", 304 ), 305 (RadrootsMeshError::InvalidTtl, "mesh frame TTL is invalid"), 306 ( 307 RadrootsMeshError::PayloadTransmissionForbidden, 308 "mesh payload transmission is forbidden", 309 ), 310 (RadrootsMeshError::InvalidCbor, "mesh frame CBOR is invalid"), 311 ( 312 RadrootsMeshError::InvalidUtf8, 313 "mesh frame text is invalid UTF-8", 314 ), 315 ( 316 RadrootsMeshError::UnknownFrameType, 317 "mesh frame type is unknown", 318 ), 319 (RadrootsMeshError::UnknownScope, "mesh scope is unknown"), 320 ( 321 RadrootsMeshError::UnsupportedVersion, 322 "mesh frame version is unsupported", 323 ), 324 ]; 325 326 for (error, message) in cases { 327 assert_eq!(error.to_string(), message); 328 } 329 } 330 331 #[test] 332 fn payload_transmission_is_forbidden_in_mvp_frames() { 333 let mut frame = default_frame(); 334 frame.payload = RadrootsMeshPayload::Bytes(vec![1, 2, 3]); 335 336 assert_eq!( 337 encode_mesh_frame_cbor(&frame).expect_err("payload must fail"), 338 RadrootsMeshError::PayloadTransmissionForbidden 339 ); 340 341 let mut encoded_payload = encode_mesh_frame_cbor(&default_frame()).expect("encode default"); 342 let payload_offset = encoded_payload.len() - 1; 343 encoded_payload[payload_offset] = 0x43; 344 encoded_payload.extend_from_slice(&[1, 2, 3]); 345 assert_eq!( 346 decode_mesh_frame_cbor(&encoded_payload).expect_err("decode payload"), 347 RadrootsMeshError::PayloadTransmissionForbidden 348 ); 349 } 350 351 #[test] 352 fn cbor_codec_covers_extended_integer_widths() { 353 let frame = RadrootsMeshFrame::new( 354 RadrootsMeshFrameType::EventAck, 355 RadrootsMeshScope::Community, 356 "wide-created-at", 357 u64::MAX, 358 u64::MAX, 359 ); 360 let encoded = encode_mesh_frame_cbor(&frame).expect("encode wide frame"); 361 let decoded = decode_mesh_frame_cbor(&encoded).expect("decode wide frame"); 362 363 assert_eq!(decoded, frame); 364 365 let frame_32 = RadrootsMeshFrame::new( 366 RadrootsMeshFrameType::EventAck, 367 RadrootsMeshScope::Community, 368 "32-bit-created-at", 369 0x1_0000, 370 0x1_0000, 371 ); 372 let encoded_32 = encode_mesh_frame_cbor(&frame_32).expect("encode 32-bit frame"); 373 assert_eq!( 374 decode_mesh_frame_cbor(&encoded_32).expect("decode 32-bit frame"), 375 frame_32 376 ); 377 } 378 379 #[test] 380 fn decoder_rejects_noncanonical_cbor_widths() { 381 let cases = [ 382 ( 383 "over-wide map length", 384 default_encoded_with_replacement(0, 1, [0xb8, 0x07]), 385 ), 386 ( 387 "over-wide key", 388 default_encoded_with_replacement(1, 2, [0x18, 0x00]), 389 ), 390 ( 391 "over-wide version", 392 default_encoded_with_replacement(2, 3, [0x18, 0x01]), 393 ), 394 ( 395 "over-wide frame type", 396 default_encoded_with_replacement(4, 5, [0x1a, 0x00, 0x00, 0x00, 0x00]), 397 ), 398 ( 399 "over-wide text length", 400 default_encoded_with_replacement(6, 7, [0x78, 0x05]), 401 ), 402 ( 403 "over-wide created-at", 404 default_encoded_with_replacement(24, 26, [0x19, 0x00, 0x2a]), 405 ), 406 ( 407 "over-wide ttl", 408 default_encoded_with_replacement( 409 27, 410 30, 411 [0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x60], 412 ), 413 ), 414 ( 415 "over-wide forbidden byte payload length", 416 default_encoded_with_replacement(31, 32, [0x58, 0x03, 1, 2, 3]), 417 ), 418 ]; 419 420 for (label, encoded) in cases { 421 assert_eq!( 422 decode_mesh_frame_cbor(&encoded).expect_err(label), 423 RadrootsMeshError::InvalidCbor 424 ); 425 } 426 } 427 428 #[test] 429 fn decoder_rejects_previous_five_field_frame_shape() { 430 let previous_shape = vec![ 431 0xa5, 0x01, 0x01, 0x02, 0x65, b'l', b'o', b'c', b'a', b'l', 0x03, 0x71, b'p', b'a', b'y', 432 b'l', b'o', b'a', b'd', b'-', b'f', b'o', b'r', b'b', b'i', b'd', b'd', b'e', b'n', 0x04, 433 0x80, 0x05, 0xf6, 434 ]; 435 436 assert_eq!( 437 decode_mesh_frame_cbor(&previous_shape).expect_err("old frame shape"), 438 RadrootsMeshError::InvalidCbor 439 ); 440 } 441 442 #[test] 443 fn decoder_rejects_malformed_cbor_shapes() { 444 let encoded = encode_mesh_frame_cbor(&default_frame()).expect("encode default"); 445 let invalid_custom_scope = default_encoded_with_replacement( 446 6, 447 12, 448 [ 449 0x70, b'c', b'u', b's', b't', b'o', b'm', b':', b'f', b'a', b'r', b'm', b' ', b'n', 450 b'o', b'r', b't', 451 ], 452 ); 453 assert_eq!( 454 decode_mesh_frame_cbor(&invalid_custom_scope).expect_err("invalid custom scope"), 455 RadrootsMeshError::InvalidCustomScope 456 ); 457 458 let mut unsupported_version = encoded.clone(); 459 unsupported_version[2] = 2; 460 assert_eq!( 461 decode_mesh_frame_cbor(&unsupported_version).expect_err("unsupported version"), 462 RadrootsMeshError::UnsupportedVersion 463 ); 464 465 let mut out_of_range_version = encoded.clone(); 466 out_of_range_version.splice(2..3, [0x1a, 0x00, 0x01, 0x00, 0x01]); 467 assert_eq!( 468 decode_mesh_frame_cbor(&out_of_range_version).expect_err("out of range version"), 469 RadrootsMeshError::InvalidCbor 470 ); 471 472 let mut unknown_frame_type = encoded.clone(); 473 unknown_frame_type[4] = 6; 474 assert_eq!( 475 decode_mesh_frame_cbor(&unknown_frame_type).expect_err("unknown frame type"), 476 RadrootsMeshError::UnknownFrameType 477 ); 478 479 let mut zero_ttl = encoded.clone(); 480 zero_ttl.splice(27..30, [0x00]); 481 assert_eq!( 482 decode_mesh_frame_cbor(&zero_ttl).expect_err("zero ttl"), 483 RadrootsMeshError::InvalidTtl 484 ); 485 486 let mut wrong_key_order = encoded.clone(); 487 wrong_key_order[3] = 2; 488 assert_eq!( 489 decode_mesh_frame_cbor(&wrong_key_order).expect_err("wrong key order"), 490 RadrootsMeshError::InvalidCbor 491 ); 492 493 let cases = [ 494 vec![0x80], 495 vec![0xbc], 496 vec![0xa6], 497 vec![0xa7, 0x01], 498 vec![0xa7, 0x00, 0x01, 0x01, 0x00, 0x02, 0x61, 0xff], 499 vec![ 500 0xa7, 0x00, 0x01, 0x01, 0x00, 0x02, 0x63, b'b', b'a', b'd', 0x03, 0x69, b'm', b'e', 501 b's', b's', b'a', b'g', b'e', b'-', b'1', 0x04, 0x18, 0x2a, 0x05, 0x19, 0xea, 0x60, 502 0x06, 0xa0, 503 ], 504 vec![ 505 0xa7, 0x00, 0x01, 0x01, 0x00, 0x02, 0x7b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 506 0xff, 507 ], 508 vec![ 509 0xa7, 0x00, 0x01, 0x01, 0x00, 0x02, 0x65, b'l', b'o', b'c', b'a', b'l', 0x03, 0x60, 510 0x04, 0x18, 0x2a, 0x05, 0x19, 0xea, 0x60, 0x06, 0xa0, 511 ], 512 vec![ 513 0xa7, 0x00, 0x01, 0x01, 0x00, 0x02, 0x65, b'l', b'o', b'c', b'a', b'l', 0x03, 0x69, 514 b'm', b'e', b's', b's', b'a', b'g', b'e', b'-', b'1', 0x04, 0x18, 0x2a, 0x05, 0x19, 515 0xea, 0x60, 0x06, 0xa1, 516 ], 517 vec![ 518 0xa7, 0x00, 0x01, 0x01, 0x00, 0x02, 0x65, b'l', b'o', b'c', b'a', b'l', 0x03, 0x69, 519 b'm', b'e', b's', b's', b'a', b'g', b'e', b'-', b'1', 0x04, 0x18, 0x2a, 0x05, 0x19, 520 0xea, 0x60, 0x06, 0xf6, 521 ], 522 ]; 523 524 for malformed in cases { 525 assert!(decode_mesh_frame_cbor(&malformed).is_err()); 526 } 527 528 let mut trailing = encoded; 529 trailing.push(0x00); 530 assert_eq!( 531 decode_mesh_frame_cbor(&trailing).expect_err("trailing byte"), 532 RadrootsMeshError::InvalidCbor 533 ); 534 } 535 536 #[test] 537 fn checked_in_mesh_cbor_vectors_match_decoder_behavior() { 538 let vectors = include_str!("../../../contracts/conformance/vectors/mesh/frame_cbor.v1.json"); 539 let document: Value = serde_json::from_str(vectors).expect("mesh cbor vector json"); 540 let entries = document 541 .get("vectors") 542 .and_then(Value::as_array) 543 .expect("mesh cbor vectors"); 544 545 for entry in entries { 546 let kind = entry.get("kind").and_then(Value::as_str).expect("kind"); 547 let hex = entry 548 .get("input") 549 .and_then(|input| input.get("hex")) 550 .and_then(Value::as_str) 551 .expect("input hex"); 552 let bytes = decode_hex(hex); 553 match kind { 554 "mesh.frame_cbor.valid" => { 555 let frame = decode_mesh_frame_cbor(bytes.as_slice()).expect("mesh frame"); 556 let expected = entry.get("expected").expect("expected"); 557 assert_eq!( 558 frame.message_id, 559 expected 560 .get("message_id") 561 .and_then(Value::as_str) 562 .expect("message id") 563 ); 564 assert_eq!( 565 frame.scope_id.label(), 566 expected 567 .get("scope") 568 .and_then(Value::as_str) 569 .expect("scope") 570 ); 571 } 572 "mesh.frame_cbor.invalid" => { 573 assert!(decode_mesh_frame_cbor(bytes.as_slice()).is_err()); 574 } 575 other => panic!("unknown mesh cbor vector kind {other}"), 576 } 577 } 578 } 579 580 fn decode_hex(value: &str) -> Vec<u8> { 581 assert_eq!(value.len() % 2, 0, "hex fixture length must be even"); 582 value 583 .as_bytes() 584 .chunks_exact(2) 585 .map(|chunk| { 586 let high = hex_nibble(chunk[0]); 587 let low = hex_nibble(chunk[1]); 588 (high << 4) | low 589 }) 590 .collect() 591 } 592 593 fn hex_nibble(byte: u8) -> u8 { 594 match byte { 595 b'0'..=b'9' => byte - b'0', 596 b'a'..=b'f' => byte - b'a' + 10, 597 _ => panic!("hex fixture contains non-lowercase-hex byte"), 598 } 599 }