1use alloc::format;
41use alloc::string::{String, ToString};
42use alloc::vec::Vec;
43
44use crate::message::{ContentType, MessagePriority};
45use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
46use serde::{Deserialize, Serialize};
47
48pub const WIRE_V1_MAGIC: u8 = 0xF5;
56
57pub const WIRE_VERSION_V1: u8 = 1;
62
63pub(crate) const EXT_TAG_B64_TAIL: u16 = 1;
88
89pub(crate) const EXT_TAG_REPLY_CONTEXT: u16 = 2;
103
104const B64_TAIL_MIN_LEN: usize = 64;
109
110pub(crate) fn split_b64_tail(content: &str) -> Option<(&str, Vec<u8>)> {
123 let bytes = content.as_bytes();
124 if bytes.len() < B64_TAIL_MIN_LEN {
125 return None;
126 }
127 let mut run_end = bytes.len();
129 let mut pad = 0;
130 while pad < 2 && run_end > 0 && bytes[run_end - 1] == b'=' {
131 run_end -= 1;
132 pad += 1;
133 }
134 let is_b64 = |b: u8| b.is_ascii_alphanumeric() || b == b'+' || b == b'/';
135 let mut start = run_end;
136 while start > 0 && is_b64(bytes[start - 1]) {
137 start -= 1;
138 }
139 start += (bytes.len() - start) % 4;
143 let tail = &content[start..];
144 if tail.len() < B64_TAIL_MIN_LEN {
145 return None;
146 }
147 let raw = BASE64.decode(tail).ok()?;
148 if BASE64.encode(&raw) != tail {
149 return None;
150 }
151 Some((&content[..start], raw))
152}
153
154pub(crate) fn encode_b64_tail(raw: &[u8]) -> String {
157 BASE64.encode(raw)
158}
159
160#[derive(Serialize, Deserialize)]
166pub(crate) struct WireMessageV1 {
167 pub(crate) id: [u8; 16],
168 pub(crate) sender: String,
169 pub(crate) recipient: String,
170 pub(crate) app_id: String,
171 pub(crate) priority: u8,
172 pub(crate) ttl: u8,
173 pub(crate) hop_count: u8,
174 pub(crate) timestamp: i64,
175 pub(crate) lamport_clock: u64,
176 pub(crate) content_type: u8,
177 pub(crate) content: String,
178 pub(crate) binary_content: Option<Vec<u8>>,
179 pub(crate) media_metadata_json: Option<Vec<u8>>,
183 pub(crate) metadata: Vec<(String, String)>,
185 pub(crate) requires_ack: bool,
186 pub(crate) reply_to_msg: Option<[u8; 16]>,
187 pub(crate) forwarded_from_json: Option<Vec<u8>>,
190 pub(crate) ext: Vec<(u16, Vec<u8>)>,
192}
193
194pub(crate) fn priority_to_u8(p: MessagePriority) -> u8 {
196 match p {
197 MessagePriority::Low => 0,
198 MessagePriority::Medium => 1,
199 MessagePriority::High => 2,
200 MessagePriority::Critical => 3,
201 }
202}
203
204pub(crate) fn priority_from_u8(v: u8) -> MessagePriority {
207 match v {
208 0 => MessagePriority::Low,
209 1 => MessagePriority::Medium,
210 2 => MessagePriority::High,
211 3 => MessagePriority::Critical,
212 _ => MessagePriority::Medium,
213 }
214}
215
216pub(crate) fn content_type_to_u8(c: ContentType) -> u8 {
219 match c {
220 ContentType::Text => 0,
221 ContentType::Image => 1,
222 ContentType::Video => 2,
223 ContentType::Audio => 3,
224 ContentType::VoiceNote => 4,
225 ContentType::VideoNote => 5,
226 ContentType::File => 6,
227 ContentType::FileChunk => 7,
228 ContentType::Poll => 8,
229 }
230}
231
232pub(crate) fn content_type_from_u8(v: u8) -> ContentType {
235 match v {
236 0 => ContentType::Text,
237 1 => ContentType::Image,
238 2 => ContentType::Video,
239 3 => ContentType::Audio,
240 4 => ContentType::VoiceNote,
241 5 => ContentType::VideoNote,
242 6 => ContentType::File,
243 7 => ContentType::FileChunk,
244 8 => ContentType::Poll,
245 _ => ContentType::File,
246 }
247}
248
249pub(crate) fn encode(wire: &WireMessageV1) -> crate::Result<Vec<u8>> {
251 let body = postcard::to_allocvec(wire)
252 .map_err(|e| crate::Error::SerializationError(format!("wire v1 encode: {e}")))?;
253 let mut out = Vec::with_capacity(body.len() + 1);
254 out.push(WIRE_V1_MAGIC);
255 out.extend_from_slice(&body);
256 Ok(out)
257}
258
259pub(crate) fn decode(data: &[u8]) -> crate::Result<WireMessageV1> {
264 match data.split_first() {
265 Some((&WIRE_V1_MAGIC, body)) => postcard::from_bytes::<WireMessageV1>(body)
266 .map_err(|e| crate::Error::DeserializationError(format!("wire v1 decode: {e}"))),
267 Some((other, _)) => Err(crate::Error::DeserializationError(format!(
268 "not a wire v1 frame: unexpected leading byte 0x{other:02X}"
269 ))),
270 None => Err(crate::Error::DeserializationError(
271 "not a wire v1 frame: empty input".to_string(),
272 )),
273 }
274}
275
276#[cfg(all(test, feature = "std"))]
277mod tests {
278 use super::*;
279 use crate::types::LAMPORT_CLOCK_MAX;
280 use crate::{
281 AppId, ForwardInfo, LamportClock, MediaMetadata, Message, MessageId, ReplyContext,
282 Timestamp, UserId, MAX_ID_LEN, TTL,
283 };
284
285 fn sample_message() -> Message {
286 let mut m = Message::new(
287 UserId::new("alice").unwrap(),
288 UserId::new("bob").unwrap(),
289 AppId::new("com.example.chat").unwrap(),
290 "hello world",
291 );
292 m.priority = MessagePriority::High;
293 m.ttl = TTL::new(5).unwrap();
294 m.content_type = ContentType::Image;
295 m.binary_content = Some(vec![0, 1, 2, 3, 255, 254]);
296 m.lamport_clock = LamportClock::from_value(42);
297 m.metadata.insert("k1".into(), "v1".into());
298 m.metadata.insert("k2".into(), "v2".into());
299 m.requires_ack = false;
300 m.reply_to_msg = Some(MessageId::new());
301 m.media_metadata = Some(MediaMetadata {
302 mime_type: "image/png".into(),
303 file_name: "x.png".into(),
304 file_size: 1234,
305 duration_ms: None,
306 width: Some(640),
307 height: Some(480),
308 thumbnail_base64: None,
309 media_id: Some("media-1".into()),
310 download_url: Some("https://cdn.example/media-1".into()),
311 thumbnail_url: None,
312 encryption_key: Some("a2V5".into()),
313 iv: None,
314 ciphertext_hash: None,
315 sticker_provider: None,
316 sticker_remote_id: None,
317 sticker_kind: None,
318 });
319 m.forwarded_from = Some(ForwardInfo {
320 original_sender: UserId::new("carol").unwrap(),
321 original_message_id: MessageId::new(),
322 original_timestamp: Timestamp::from_millis(1_700_000_000_000),
323 forward_count: 2,
324 });
325 m
326 }
327
328 fn base_dto() -> WireMessageV1 {
330 WireMessageV1 {
331 id: [7u8; 16],
332 sender: "alice".into(),
333 recipient: "bob".into(),
334 app_id: "app".into(),
335 priority: 1,
336 ttl: 8,
337 hop_count: 0,
338 timestamp: 1,
339 lamport_clock: 0,
340 content_type: 0,
341 content: "hi".into(),
342 binary_content: None,
343 media_metadata_json: None,
344 metadata: Vec::new(),
345 requires_ack: true,
346 reply_to_msg: None,
347 forwarded_from_json: None,
348 ext: Vec::new(),
349 }
350 }
351
352 #[test]
353 fn wire_v1_round_trips_all_fields() {
354 let original = sample_message();
355 let bytes = original.to_wire_v1_bytes().unwrap();
356 assert_eq!(bytes[0], WIRE_V1_MAGIC);
357 let decoded = Message::from_wire_v1_bytes(&bytes).unwrap();
358
359 assert_eq!(decoded.id, original.id);
360 assert_eq!(decoded.sender, original.sender);
361 assert_eq!(decoded.recipient, original.recipient);
362 assert_eq!(decoded.app_id, original.app_id);
363 assert_eq!(decoded.priority, original.priority);
364 assert_eq!(decoded.ttl, original.ttl);
365 assert_eq!(decoded.hop_count, original.hop_count);
366 assert_eq!(decoded.timestamp, original.timestamp);
367 assert_eq!(decoded.lamport_clock, original.lamport_clock);
368 assert_eq!(decoded.content_type, original.content_type);
369 assert_eq!(decoded.content, original.content);
370 assert_eq!(decoded.binary_content, original.binary_content);
371 assert_eq!(decoded.metadata, original.metadata);
372 assert_eq!(decoded.requires_ack, original.requires_ack);
373 assert_eq!(decoded.reply_to_msg, original.reply_to_msg);
374 assert_eq!(decoded.forwarded_from, original.forwarded_from);
375 assert_eq!(
377 serde_json::to_vec(&decoded.media_metadata).unwrap(),
378 serde_json::to_vec(&original.media_metadata).unwrap(),
379 );
380 assert!(decoded.transport_peer_id().is_none());
382 assert_eq!(decoded.wire_codec(), crate::WireCodec::Json);
386 }
387
388 #[test]
389 fn wire_v1_round_trips_minimal_defaults() {
390 let original = Message::new(
391 UserId::new("a").unwrap(),
392 UserId::new("b").unwrap(),
393 AppId::new("c").unwrap(),
394 "hi",
395 );
396 let decoded = Message::from_wire_v1_bytes(&original.to_wire_v1_bytes().unwrap()).unwrap();
397 assert_eq!(decoded.id, original.id);
398 assert!(decoded.requires_ack);
400 assert!(decoded.binary_content.is_none());
401 assert!(decoded.media_metadata.is_none());
402 assert!(decoded.forwarded_from.is_none());
403 assert!(decoded.reply_to_msg.is_none());
404 assert!(decoded.metadata.is_empty());
405 }
406
407 #[test]
408 fn wire_v1_is_much_smaller_than_json() {
409 let m = Message::new(
410 UserId::new("a3f8c2d1-user-alice").unwrap(),
411 UserId::new("b7e4d9a2-user-bob").unwrap(),
412 AppId::new("com.example.chat").unwrap(),
413 "hi",
414 );
415 let json = m.to_bytes().unwrap();
416 let binary = m.to_wire_v1_bytes().unwrap();
417 assert!(
418 binary.len() * 2 < json.len(),
419 "binary {} should be < half of json {}",
420 binary.len(),
421 json.len()
422 );
423 }
424
425 #[test]
426 fn wire_v1_preserves_identifier_length_validation() {
427 let mut wire = base_dto();
430 wire.sender = "a".repeat(MAX_ID_LEN + 1);
431 let bytes = encode(&wire).unwrap();
432 assert!(Message::from_wire_v1_bytes(&bytes).is_err());
433 }
434
435 #[test]
436 fn wire_v1_preserves_identifier_char_validation() {
437 let mut wire = base_dto();
438 wire.recipient = "evil/path".into();
439 let bytes = encode(&wire).unwrap();
440 assert!(Message::from_wire_v1_bytes(&bytes).is_err());
441 }
442
443 #[test]
444 fn wire_v1_clamps_adversarial_lamport_clock() {
445 let mut wire = base_dto();
446 wire.lamport_clock = u64::MAX;
447 let bytes = encode(&wire).unwrap();
448 let decoded = Message::from_wire_v1_bytes(&bytes).unwrap();
449 assert_eq!(decoded.lamport_clock.value(), LAMPORT_CLOCK_MAX);
450 }
451
452 #[test]
453 fn wire_v1_tolerates_unknown_enum_discriminants() {
454 let mut wire = base_dto();
455 wire.content_type = 200;
456 wire.priority = 200;
457 let bytes = encode(&wire).unwrap();
458 let decoded = Message::from_wire_v1_bytes(&bytes).unwrap();
459 assert_eq!(decoded.content_type, ContentType::File);
460 assert_eq!(decoded.priority, MessagePriority::Medium);
461 }
462
463 #[test]
464 fn wire_v1_round_trips_poll_content_type() {
465 let mut wire = base_dto();
466 wire.content_type = content_type_to_u8(ContentType::Poll);
467 let bytes = encode(&wire).unwrap();
468 let decoded = Message::from_wire_v1_bytes(&bytes).unwrap();
469 assert_eq!(decoded.content_type, ContentType::Poll);
470 }
471
472 #[test]
473 fn wire_v1_decode_rejects_non_v1_frames() {
474 assert!(decode(&[0x7B, 0x00]).is_err()); assert!(decode(&[]).is_err()); }
477
478 #[test]
479 fn b64_tail_split_and_reconstruct_is_byte_identical() {
480 let raw: Vec<u8> = (0u8..50).collect();
482 let content = format!("__MLS_ENC__{}", BASE64.encode(&raw));
483 let (head, tail_raw) = split_b64_tail(&content).expect("tail must split");
484 assert_eq!(head, "__MLS_ENC__");
485 assert_eq!(tail_raw, raw);
486 assert_eq!(format!("{head}{}", encode_b64_tail(&tail_raw)), content);
487 }
488
489 #[test]
490 fn b64_tail_split_sheds_run_remainder_into_head_and_handles_headless() {
491 let content = format!("!{}", "A".repeat(66));
494 let (head, raw) = split_b64_tail(&content).expect("aligned suffix splits");
495 assert_eq!(head, "!AA");
496 assert_eq!(format!("{head}{}", encode_b64_tail(&raw)), content);
497
498 let content = "A".repeat(64);
500 let (head, raw) = split_b64_tail(&content).expect("headless splits");
501 assert!(head.is_empty());
502 assert_eq!(encode_b64_tail(&raw), content);
503 }
504
505 #[test]
506 fn b64_tail_split_is_utf8_boundary_safe() {
507 let content = format!("héllo → {}", "A".repeat(64));
508 let (head, raw) = split_b64_tail(&content).expect("ascii tail after unicode head");
509 assert_eq!(head, "héllo → ");
510 assert_eq!(format!("{head}{}", encode_b64_tail(&raw)), content);
511 }
512
513 #[test]
514 fn b64_tail_split_rejects_short_nonaligned_and_noncanonical() {
515 assert!(split_b64_tail("AAAA").is_none());
517 assert!(split_b64_tail(&format!("!{}", "A".repeat(63))).is_none());
519 let json_like = format!("__MLS_ENC__{{\"ciphertext\":[{}]}}", "57,".repeat(60));
521 assert!(split_b64_tail(&json_like).is_none());
522 assert!(split_b64_tail(&format!("!{}=", "B".repeat(63))).is_none());
525 assert!(split_b64_tail(&format!("{}={}", "A".repeat(64), "A".repeat(3))).is_none());
527 assert!(split_b64_tail(&"=".repeat(64)).is_none());
529 }
530
531 #[test]
532 fn wire_v1_round_trips_b64_tail_content_through_message() {
533 let mut m = Message::new(
534 UserId::new("alice").unwrap(),
535 UserId::new("bob").unwrap(),
536 AppId::new("app").unwrap(),
537 format!("__MLS_ENC__{}", BASE64.encode(vec![7u8; 90])),
538 );
539 m.requires_ack = true;
540 let bytes = m.to_wire_v1_bytes().unwrap();
541
542 let wire = decode(&bytes).unwrap();
544 assert_eq!(wire.content, "__MLS_ENC__");
545 assert_eq!(wire.ext.len(), 1);
546 assert_eq!(wire.ext[0].0, EXT_TAG_B64_TAIL);
547 assert_eq!(wire.ext[0].1, vec![7u8; 90]);
548
549 let decoded = Message::from_wire_v1_bytes(&bytes).unwrap();
551 assert_eq!(decoded.content, m.content);
552 assert!(bytes.len() < m.to_bytes().unwrap().len());
553 }
554
555 #[test]
556 fn wire_v1_plain_content_emits_no_ext_entries() {
557 let m = sample_message();
558 let wire = decode(&m.to_wire_v1_bytes().unwrap()).unwrap();
559 assert!(wire.ext.is_empty());
560 }
561
562 #[test]
563 fn wire_v1_decode_ignores_unknown_ext_tags_and_extra_tag1_entries() {
564 let mut wire = base_dto();
567 wire.content = "head:".into();
568 wire.ext = vec![
569 (99, vec![1, 2, 3]),
570 (EXT_TAG_B64_TAIL, b"AB".to_vec()),
571 (EXT_TAG_B64_TAIL, b"ZZ".to_vec()),
572 ];
573 let bytes = encode(&wire).unwrap();
574 let decoded = Message::from_wire_v1_bytes(&bytes).unwrap();
575 assert_eq!(decoded.content, format!("head:{}", BASE64.encode(b"AB")));
576 }
577
578 fn sample_reply_context() -> ReplyContext {
579 ReplyContext {
580 sender: UserId::new("carol").unwrap(),
581 text: "original text".into(),
582 timestamp: Some(Timestamp::from_millis(1_700_000_000_000)),
583 reply_media_label: Some("x.png".into()),
584 reply_content_type: Some("image".into()),
585 }
586 }
587
588 #[test]
589 fn wire_v1_round_trips_reply_context() {
590 let mut m = sample_message();
592 m.reply_context = Some(sample_reply_context());
593 let decoded = Message::from_wire_v1_bytes(&m.to_wire_v1_bytes().unwrap()).unwrap();
594 assert_eq!(decoded.reply_context, m.reply_context);
595
596 let mut m = Message::new(
598 UserId::new("a").unwrap(),
599 UserId::new("b").unwrap(),
600 AppId::new("c").unwrap(),
601 "hi",
602 );
603 m.reply_context = Some(ReplyContext {
604 sender: UserId::new("carol").unwrap(),
605 text: "quoted".into(),
606 timestamp: None,
607 reply_media_label: None,
608 reply_content_type: None,
609 });
610 let decoded = Message::from_wire_v1_bytes(&m.to_wire_v1_bytes().unwrap()).unwrap();
611 assert_eq!(decoded.reply_context, m.reply_context);
612 }
613
614 #[test]
615 fn wire_v1_reply_context_coexists_with_b64_tail() {
616 let mut m = Message::new(
617 UserId::new("alice").unwrap(),
618 UserId::new("bob").unwrap(),
619 AppId::new("app").unwrap(),
620 format!("__MLS_ENC__{}", BASE64.encode(vec![7u8; 90])),
621 );
622 m.reply_context = Some(sample_reply_context());
623 let bytes = m.to_wire_v1_bytes().unwrap();
624
625 let wire = decode(&bytes).unwrap();
626 assert_eq!(wire.ext.len(), 2);
627 assert_eq!(wire.ext[0].0, EXT_TAG_B64_TAIL);
628 assert_eq!(wire.ext[1].0, EXT_TAG_REPLY_CONTEXT);
629
630 let decoded = Message::from_wire_v1_bytes(&bytes).unwrap();
631 assert_eq!(decoded.content, m.content);
632 assert_eq!(decoded.reply_context, m.reply_context);
633 }
634
635 #[test]
636 fn wire_v1_honors_only_first_reply_context_entry() {
637 let mut wire = base_dto();
640 wire.ext = vec![
641 (
642 EXT_TAG_REPLY_CONTEXT,
643 br#"{"sender":"carol","text":"first"}"#.to_vec(),
644 ),
645 (
646 EXT_TAG_REPLY_CONTEXT,
647 br#"{"sender":"mallory","text":"second"}"#.to_vec(),
648 ),
649 ];
650 let decoded = Message::from_wire_v1_bytes(&encode(&wire).unwrap()).unwrap();
651 let rc = decoded.reply_context.expect("first entry honored");
652 assert_eq!(rc.sender.as_str(), "carol");
653 assert_eq!(rc.text, "first");
654 }
655
656 #[test]
657 fn wire_v1_rejects_malformed_reply_context_ext() {
658 let mut wire = base_dto();
661 wire.ext = vec![(EXT_TAG_REPLY_CONTEXT, b"not json".to_vec())];
662 assert!(Message::from_wire_v1_bytes(&encode(&wire).unwrap()).is_err());
663
664 let mut wire = base_dto();
666 wire.ext = vec![(
667 EXT_TAG_REPLY_CONTEXT,
668 br#"{"sender":"evil/path","text":"t"}"#.to_vec(),
669 )];
670 assert!(Message::from_wire_v1_bytes(&encode(&wire).unwrap()).is_err());
671 }
672
673 #[test]
674 fn wire_v1_golden_layout_with_reply_context_ext_is_frozen() {
675 let rc_json: &[u8] = br#"{"sender":"c","text":"t"}"#;
679 let mut wire = base_dto();
680 wire.content = String::new();
681 wire.ext = vec![(EXT_TAG_REPLY_CONTEXT, rc_json.to_vec())];
682 let mut expected_tail = vec![
683 0x01, 0x02, 0x19, ];
687 expected_tail.extend_from_slice(rc_json); let encoded = encode(&wire).unwrap();
689 assert_eq!(
690 &encoded[encoded.len() - expected_tail.len()..],
691 expected_tail
692 );
693 let decoded = Message::from_wire_v1_bytes(&encoded).unwrap();
695 let rc = decoded.reply_context.expect("reply context decoded");
696 assert_eq!(rc.sender.as_str(), "c");
697 assert_eq!(rc.text, "t");
698 }
699
700 #[test]
701 fn wire_v1_golden_layout_with_b64_tail_ext_is_frozen() {
702 let mut wire = base_dto();
706 wire.content = String::new();
707 wire.ext = vec![(EXT_TAG_B64_TAIL, vec![0xDE, 0xAD])];
708 let expected_tail: &[u8] = &[
709 0x01, 0x01, 0x02, 0xDE, 0xAD, ];
713 let encoded = encode(&wire).unwrap();
714 assert_eq!(
715 &encoded[encoded.len() - expected_tail.len()..],
716 expected_tail
717 );
718 let decoded = Message::from_wire_v1_bytes(&encoded).unwrap();
720 assert_eq!(decoded.content, BASE64.encode([0xDE, 0xAD]));
721 }
722
723 #[test]
724 fn wire_v1_golden_layout_is_frozen() {
725 let wire = WireMessageV1 {
729 id: [0u8; 16],
730 sender: "a".into(),
731 recipient: "b".into(),
732 app_id: "c".into(),
733 priority: 1,
734 ttl: 8,
735 hop_count: 0,
736 timestamp: 0,
737 lamport_clock: 0,
738 content_type: 0,
739 content: String::new(),
740 binary_content: None,
741 media_metadata_json: None,
742 metadata: Vec::new(),
743 requires_ack: true,
744 reply_to_msg: None,
745 forwarded_from_json: None,
746 ext: Vec::new(),
747 };
748 let expected: &[u8] = &[
749 0xF5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01, b'a', 0x01, b'b', 0x01, b'c', 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, ];
769 assert_eq!(encode(&wire).unwrap(), expected);
770 }
771}
772
773#[cfg(all(test, feature = "std"))]
787mod spec_vectors {
788 use super::*;
789 use crate::types::LAMPORT_CLOCK_MAX;
790 use crate::{AppId, ContentType, MessageId, MessagePriority, Timestamp, UserId, MAX_ID_LEN};
791 use crate::{HopCount, LamportClock, Message, ReplyContext, TTL};
792
793 const VECTORS: &str = include_str!("../tests/data/wire-v1.vectors.json");
794
795 fn vectors() -> serde_json::Value {
796 serde_json::from_str(VECTORS).expect("the vector file is JSON")
797 }
798
799 fn unhex(s: &str) -> Vec<u8> {
800 assert!(s.len() % 2 == 0, "hex has an even length");
801 (0..s.len())
802 .step_by(2)
803 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex digit"))
804 .collect()
805 }
806
807 fn hex(bytes: &[u8]) -> String {
808 bytes.iter().map(|b| format!("{b:02x}")).collect()
809 }
810
811 fn message_from(spec: &serde_json::Value) -> Message {
817 let s = |k: &str| spec[k].as_str().expect("a string field").to_string();
818 let mut m = Message::from_parts(
819 MessageId::from_str(&s("id")).expect("a uuid"),
820 UserId::new(s("sender")).expect("a sender"),
821 UserId::new(s("recipient")).expect("a recipient"),
822 AppId::new(s("app_id")).expect("an app id"),
823 s("content"),
824 Timestamp::from_millis(spec["timestamp_ms"].as_i64().expect("an instant")),
825 );
826
827 m.priority = match spec["priority"].as_str().expect("a priority") {
828 "low" => MessagePriority::Low,
829 "medium" => MessagePriority::Medium,
830 "high" => MessagePriority::High,
831 "critical" => MessagePriority::Critical,
832 other => panic!("the vector names a priority that does not exist: {other}"),
833 };
834 m.ttl = TTL::from_value(spec["ttl"].as_u64().expect("a ttl") as u8);
835 m.hop_count = HopCount::from_value(spec["hop_count"].as_u64().expect("a hop count") as u8);
836 m.lamport_clock =
837 LamportClock::from_value(spec["lamport_clock"].as_u64().expect("a lamport clock"));
838 m.content_type = ContentType::parse(spec["content_type"].as_str().expect("a content type"));
839 m.requires_ack = spec["requires_ack"].as_bool().expect("requires_ack");
840
841 if let Some(h) = spec["binary_content_hex"].as_str() {
842 m.binary_content = Some(unhex(h));
843 }
844 if let Some(j) = spec["media_metadata_json"].as_str() {
845 m.media_metadata = Some(serde_json::from_str(j).expect("media metadata"));
846 }
847 if let Some(j) = spec["forwarded_from_json"].as_str() {
848 m.forwarded_from = Some(serde_json::from_str(j).expect("forward attribution"));
849 }
850 if let Some(j) = spec["reply_context_json"].as_str() {
851 m.reply_context = Some(serde_json::from_str::<ReplyContext>(j).expect("reply context"));
852 }
853 if let Some(id) = spec["reply_to_msg"].as_str() {
854 m.reply_to_msg = Some(MessageId::from_str(id).expect("a reply id"));
855 }
856 for (k, v) in spec["metadata"].as_object().expect("a metadata map") {
857 m.metadata
858 .insert(k.clone(), v.as_str().expect("a metadata value").to_string());
859 }
860 m
861 }
862
863 #[test]
870 fn the_vector_file_is_the_size_it_was() {
871 let v = vectors();
872 assert_eq!(v["magic"], "f5");
873 assert_eq!(v["frames"].as_array().expect("frames").len(), 18);
874 assert_eq!(v["decode_only"].as_array().expect("decode_only").len(), 6);
875 assert_eq!(v["rejects"].as_array().expect("rejects").len(), 5);
876 }
877
878 #[test]
879 fn every_frame_encodes_to_its_vector() {
880 for case in vectors()["frames"].as_array().expect("frames") {
881 let name = case["name"].as_str().expect("a name");
882 let encoded = message_from(&case["message"])
883 .to_wire_v1_bytes()
884 .unwrap_or_else(|e| panic!("[{name}] did not encode: {e}"));
885 assert_eq!(
886 hex(&encoded),
887 case["hex"].as_str().expect("expected hex"),
888 "[{name}] encoded to different bytes than the chapter specifies"
889 );
890 }
891 }
892
893 #[test]
899 fn every_frame_decodes_back_into_its_message() {
900 for case in vectors()["frames"].as_array().expect("frames") {
901 let name = case["name"].as_str().expect("a name");
902 let want = message_from(&case["message"]);
903 let got = Message::from_wire_v1_bytes(&unhex(case["hex"].as_str().expect("hex")))
904 .unwrap_or_else(|e| panic!("[{name}] did not decode: {e}"));
905
906 assert_eq!(got.id, want.id, "[{name}] id");
907 assert_eq!(got.sender, want.sender, "[{name}] sender");
908 assert_eq!(got.recipient, want.recipient, "[{name}] recipient");
909 assert_eq!(got.app_id, want.app_id, "[{name}] app_id");
910 assert_eq!(got.priority, want.priority, "[{name}] priority");
911 assert_eq!(got.ttl.value(), want.ttl.value(), "[{name}] ttl");
912 assert_eq!(
913 got.hop_count.value(),
914 want.hop_count.value(),
915 "[{name}] hop_count"
916 );
917 assert_eq!(
918 got.timestamp.as_millis(),
919 want.timestamp.as_millis(),
920 "[{name}] timestamp"
921 );
922 assert_eq!(
923 got.lamport_clock.value(),
924 want.lamport_clock.value(),
925 "[{name}] lamport_clock"
926 );
927 assert_eq!(got.content_type, want.content_type, "[{name}] content_type");
928 assert_eq!(got.content, want.content, "[{name}] content");
929 assert_eq!(
930 got.binary_content, want.binary_content,
931 "[{name}] binary_content"
932 );
933 assert_eq!(got.metadata, want.metadata, "[{name}] metadata");
934 assert_eq!(got.requires_ack, want.requires_ack, "[{name}] requires_ack");
935 assert_eq!(got.reply_to_msg, want.reply_to_msg, "[{name}] reply_to_msg");
936 assert_eq!(
946 serde_json::to_vec(&got.media_metadata).expect("it serializes"),
947 serde_json::to_vec(&want.media_metadata).expect("it serializes"),
948 "[{name}] media_metadata"
949 );
950 assert_eq!(
951 got.forwarded_from, want.forwarded_from,
952 "[{name}] forwarded_from"
953 );
954 assert_eq!(
955 got.reply_context, want.reply_context,
956 "[{name}] reply_context"
957 );
958 }
959 }
960
961 #[test]
966 fn decode_only_vectors_decode_as_specified() {
967 for case in vectors()["decode_only"].as_array().expect("decode_only") {
968 let name = case["name"].as_str().expect("a name");
969 let got = Message::from_wire_v1_bytes(&unhex(case["hex"].as_str().expect("hex")))
970 .unwrap_or_else(|e| panic!("[{name}] did not decode: {e}"));
971 let expect = &case["expect"];
972
973 if let Some(want) = expect["content_type"].as_str() {
974 assert_eq!(
975 got.content_type.as_wire_str(),
976 want,
977 "[{name}] content_type"
978 );
979 }
980 if let Some(want) = expect["priority"].as_str() {
981 let got = match got.priority {
982 MessagePriority::Low => "low",
983 MessagePriority::Medium => "medium",
984 MessagePriority::High => "high",
985 MessagePriority::Critical => "critical",
986 };
987 assert_eq!(got, want, "[{name}] priority");
988 }
989 if let Some(want) = expect["lamport_clock"].as_u64() {
990 assert_eq!(got.lamport_clock.value(), want, "[{name}] lamport_clock");
991 }
992 if let Some(want) = expect["content"].as_str() {
993 assert_eq!(got.content, want, "[{name}] content");
994 }
995 }
996 }
997
998 #[test]
999 fn reject_vectors_are_refused() {
1000 for case in vectors()["rejects"].as_array().expect("rejects") {
1001 let name = case["name"].as_str().expect("a name");
1002 let reason = case["reason"].as_str().expect("a reason");
1003 let bytes = unhex(case["hex"].as_str().expect("hex"));
1004 assert!(
1005 Message::from_wire_v1_bytes(&bytes).is_err(),
1006 "[{name}] was accepted, but the chapter refuses it: {reason}"
1007 );
1008 }
1009 }
1010
1011 fn chapter() -> Option<String> {
1018 let path =
1019 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/spec/wire-format.md");
1020 std::fs::read_to_string(&path).ok().or_else(|| {
1021 eprintln!("spec tree not present, skipping the wire-format chapter drift checks");
1022 None
1023 })
1024 }
1025
1026 #[test]
1032 fn the_chapter_states_the_constants_the_code_uses() {
1033 let Some(text) = chapter() else { return };
1034
1035 assert!(
1036 text.contains(&format!("`0x{:02X}` | Binary wire v1", WIRE_V1_MAGIC)),
1037 "the chapter's encoding-detection table does not state the magic byte as 0x{WIRE_V1_MAGIC:02X}"
1038 );
1039 assert!(
1040 text.contains(&format!(
1041 "{B64_TAIL_MIN_LEN} base64 characters"
1042 )),
1043 "the chapter does not state the {B64_TAIL_MIN_LEN}-character minimum for the tag 1 split"
1044 );
1045 assert!(
1046 text.contains(&format!("`{MAX_ID_LEN}`"))
1047 || text.contains(&format!("{MAX_ID_LEN}-byte")),
1048 "the chapter does not state the {MAX_ID_LEN}-byte identifier cap"
1049 );
1050 }
1051
1052 #[test]
1061 fn the_chapter_states_how_a_primitive_becomes_bytes() {
1062 let Some(text) = chapter() else { return };
1063
1064 for required in [
1065 "Zigzag",
1066 "varint",
1067 "little-endian base 128",
1068 "no length prefix",
1069 "UTF-16 code-unit order",
1074 ] {
1075 assert!(
1076 text.contains(required),
1077 "the binary chapter no longer states {required:?}, without which \
1078 the field-order block cannot be implemented"
1079 );
1080 }
1081 }
1082
1083 #[test]
1085 fn the_lamport_clamp_the_vectors_pin_is_the_types_own() {
1086 let v = vectors();
1087 let case = v["decode_only"]
1088 .as_array()
1089 .expect("decode_only")
1090 .iter()
1091 .find(|c| c["expect"]["lamport_clock"].is_u64())
1092 .expect("a clamp vector");
1093 assert_eq!(
1094 case["expect"]["lamport_clock"].as_u64().expect("a clamp"),
1095 LAMPORT_CLOCK_MAX,
1096 "the vector pins a ceiling the type does not enforce"
1097 );
1098 }
1099}