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}