1use crate::{Result, SealedError};
15use alloc::format;
16use alloc::string::{String, ToString};
17use alloc::vec::Vec;
18use core::fmt;
19use serde::{Deserialize, Serialize};
20
21const MAX_STRING_FIELD_LEN: usize = 4096;
32
33#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
43pub struct GroupId(String);
44
45impl GroupId {
46 pub const MAX_LEN: usize = MAX_STRING_FIELD_LEN;
52
53 pub fn new(id: impl Into<String>) -> Result<Self> {
55 let id = id.into();
56 Self::validate(&id)?;
57 Ok(Self(id))
58 }
59
60 fn validate(id: &str) -> Result<()> {
61 if id.is_empty() {
62 return Err(SealedError::InvalidGroupId(
63 "Group ID cannot be empty".to_string(),
64 ));
65 }
66 if id.len() > Self::MAX_LEN {
67 return Err(SealedError::InvalidGroupId(format!(
68 "Group ID length {} exceeds maximum {}",
69 id.len(),
70 Self::MAX_LEN
71 )));
72 }
73 for segment in id.split(':') {
74 offline_protocol_core::validate_id_chars(segment, "Group ID segment")
75 .map_err(|e| SealedError::InvalidGroupId(e.to_string()))?;
76 }
77 Ok(())
78 }
79
80 pub fn as_str(&self) -> &str {
82 &self.0
83 }
84
85 pub fn for_session(user_a: &str, user_b: &str) -> Result<Self> {
100 use offline_protocol_core::Address;
101
102 let ordered = match (user_a.parse::<Address>(), user_b.parse::<Address>()) {
103 (Ok(addr_a), Ok(addr_b)) if addr_b < addr_a => [user_b, user_a],
104 (Ok(_), Ok(_)) => [user_a, user_b],
105 _ => {
106 let mut users = [user_a, user_b];
107 users.sort();
108 users
109 }
110 };
111
112 Self::new(format!("session:{}:{}", ordered[0], ordered[1]))
113 }
114}
115
116impl fmt::Display for GroupId {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 write!(f, "{}", self.0)
119 }
120}
121
122impl<'de> Deserialize<'de> for GroupId {
123 fn deserialize<D: serde::Deserializer<'de>>(
124 deserializer: D,
125 ) -> core::result::Result<Self, D::Error> {
126 let s = String::deserialize(deserializer)?;
127 GroupId::new(s).map_err(serde::de::Error::custom)
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
133pub enum MlsMessageType {
134 Application,
136
137 Welcome,
139
140 Commit,
142
143 Proposal,
145}
146
147impl MlsMessageType {
148 pub fn as_str(&self) -> &'static str {
150 match self {
151 Self::Application => "Application",
152 Self::Welcome => "Welcome",
153 Self::Commit => "Commit",
154 Self::Proposal => "Proposal",
155 }
156 }
157
158 pub fn from_str_opt(s: &str) -> Option<Self> {
160 match s {
161 "Application" => Some(Self::Application),
162 "Welcome" => Some(Self::Welcome),
163 "Commit" => Some(Self::Commit),
164 "Proposal" => Some(Self::Proposal),
165 _ => None,
166 }
167 }
168
169 pub fn as_u8(&self) -> u8 {
171 match self {
172 Self::Application => 0,
173 Self::Welcome => 1,
174 Self::Commit => 2,
175 Self::Proposal => 3,
176 }
177 }
178
179 pub fn from_u8(tag: u8) -> Option<Self> {
181 match tag {
182 0 => Some(Self::Application),
183 1 => Some(Self::Welcome),
184 2 => Some(Self::Commit),
185 3 => Some(Self::Proposal),
186 _ => None,
187 }
188 }
189}
190
191impl fmt::Display for MlsMessageType {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 f.write_str(self.as_str())
194 }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct EncryptedMessage {
200 pub group_id: GroupId,
202
203 pub message_type: MlsMessageType,
205
206 pub epoch: u64,
208
209 pub ciphertext: Vec<u8>,
211
212 pub sender_id: String,
214
215 pub timestamp_ms: u64,
217}
218
219impl EncryptedMessage {
220 pub fn to_base64(&self) -> Result<String> {
222 let json =
223 serde_json::to_vec(self).map_err(|e| SealedError::Serialization(e.to_string()))?;
224 Ok(base64::Engine::encode(
225 &base64::engine::general_purpose::STANDARD,
226 &json,
227 ))
228 }
229
230 pub fn from_base64(encoded: &str) -> Result<Self> {
232 let json = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encoded)
233 .map_err(|e| SealedError::Deserialization(e.to_string()))?;
234 serde_json::from_slice(&json).map_err(|e| SealedError::Deserialization(e.to_string()))
235 }
236
237 const MAX_CIPHERTEXT_LEN: usize = 128 * 1024 * 1024; pub fn to_bytes(&self) -> Vec<u8> {
252 let group_id = self.group_id.as_str().as_bytes();
253 let sender_id = self.sender_id.as_bytes();
254
255 let capacity =
256 4 + group_id.len() + 4 + sender_id.len() + 1 + 8 + 8 + 4 + self.ciphertext.len();
257 let mut buf = Vec::with_capacity(capacity);
258
259 buf.extend_from_slice(&(group_id.len() as u32).to_le_bytes());
260 buf.extend_from_slice(group_id);
261 buf.extend_from_slice(&(sender_id.len() as u32).to_le_bytes());
262 buf.extend_from_slice(sender_id);
263 buf.push(self.message_type.as_u8());
264 buf.extend_from_slice(&self.epoch.to_le_bytes());
265 buf.extend_from_slice(&self.timestamp_ms.to_le_bytes());
266 buf.extend_from_slice(&(self.ciphertext.len() as u32).to_le_bytes());
267 buf.extend_from_slice(&self.ciphertext);
268
269 buf
270 }
271
272 pub fn from_bytes(data: &[u8]) -> Result<Self> {
274 let err = SealedError::Deserialization;
275 let mut pos = 0;
276
277 let read_u32 = |pos: &mut usize| -> Result<u32> {
278 if *pos + 4 > data.len() {
279 return Err(err("unexpected end of data reading u32".to_string()));
280 }
281 let mut bytes = [0u8; 4];
282 bytes.copy_from_slice(&data[*pos..*pos + 4]);
283 let val = u32::from_le_bytes(bytes);
284 *pos += 4;
285 Ok(val)
286 };
287
288 let read_u64 = |pos: &mut usize| -> Result<u64> {
289 if *pos + 8 > data.len() {
290 return Err(err("unexpected end of data reading u64".to_string()));
291 }
292 let mut bytes = [0u8; 8];
293 bytes.copy_from_slice(&data[*pos..*pos + 8]);
294 let val = u64::from_le_bytes(bytes);
295 *pos += 8;
296 Ok(val)
297 };
298
299 let read_bytes = |pos: &mut usize, len: usize| -> Result<Vec<u8>> {
300 if *pos + len > data.len() {
301 return Err(err("unexpected end of data reading bytes".to_string()));
302 }
303 let val = data[*pos..*pos + len].to_vec();
304 *pos += len;
305 Ok(val)
306 };
307
308 let group_id_len = read_u32(&mut pos)? as usize;
309 if group_id_len > MAX_STRING_FIELD_LEN {
310 return Err(err(format!(
311 "group_id_len {} exceeds maximum {}",
312 group_id_len, MAX_STRING_FIELD_LEN
313 )));
314 }
315 let group_id = String::from_utf8(read_bytes(&mut pos, group_id_len)?)
316 .map_err(|e| err(format!("invalid group_id UTF-8: {}", e)))?;
317
318 let sender_len = read_u32(&mut pos)? as usize;
319 if sender_len > MAX_STRING_FIELD_LEN {
320 return Err(err(format!(
321 "sender_len {} exceeds maximum {}",
322 sender_len, MAX_STRING_FIELD_LEN
323 )));
324 }
325 let sender_id = String::from_utf8(read_bytes(&mut pos, sender_len)?)
326 .map_err(|e| err(format!("invalid sender_id UTF-8: {}", e)))?;
327
328 if pos + 1 > data.len() {
329 return Err(err(
330 "unexpected end of data reading message_type".to_string()
331 ));
332 }
333 let message_type = MlsMessageType::from_u8(data[pos])
334 .ok_or_else(|| err(format!("unknown message_type tag {}", data[pos])))?;
335 pos += 1;
336
337 let epoch = read_u64(&mut pos)?;
338 let timestamp_ms = read_u64(&mut pos)?;
339
340 let ciphertext_len = read_u32(&mut pos)? as usize;
341 if ciphertext_len > Self::MAX_CIPHERTEXT_LEN {
342 return Err(err(format!(
343 "ciphertext_len {} exceeds maximum {}",
344 ciphertext_len,
345 Self::MAX_CIPHERTEXT_LEN
346 )));
347 }
348 let ciphertext = read_bytes(&mut pos, ciphertext_len)?;
349
350 Ok(Self {
351 group_id: GroupId::new(group_id)?,
352 message_type,
353 epoch,
354 ciphertext,
355 sender_id,
356 timestamp_ms,
357 })
358 }
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct WelcomeMessage {
367 pub group_id: GroupId,
369
370 pub welcome_data: Vec<u8>,
372
373 pub inviter_id: String,
375
376 pub group_name: Option<String>,
378
379 pub timestamp_ms: u64,
381}
382
383impl WelcomeMessage {
384 pub fn to_base64(&self) -> Result<String> {
386 let json =
387 serde_json::to_vec(self).map_err(|e| SealedError::Serialization(e.to_string()))?;
388 Ok(base64::Engine::encode(
389 &base64::engine::general_purpose::STANDARD,
390 &json,
391 ))
392 }
393
394 pub fn from_base64(encoded: &str) -> Result<Self> {
396 let json = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encoded)
397 .map_err(|e| SealedError::Deserialization(e.to_string()))?;
398 serde_json::from_slice(&json).map_err(|e| SealedError::Deserialization(e.to_string()))
399 }
400}
401
402#[cfg(all(test, feature = "std"))]
403mod tests {
404 use super::*;
405 use offline_protocol_core::Address;
406
407 fn disagreeing_addresses() -> (Address, Address) {
417 let mut low_bytes = [0u8; Address::HASH_LEN];
418 low_bytes[0] = 0x00;
419 let mut high_bytes = [0u8; Address::HASH_LEN];
420 high_bytes[0] = 0x40;
421 (
422 Address::from_hash_bytes(low_bytes),
423 Address::from_hash_bytes(high_bytes),
424 )
425 }
426
427 #[test]
428 fn session_id_orders_addresses_by_hash_not_by_string() {
429 let (low, high) = disagreeing_addresses();
430
431 assert!(low < high, "hash-byte order");
433 assert!(
434 low.to_string() > high.to_string(),
435 "rendered-string order must be the opposite, or this test proves nothing"
436 );
437
438 let expected = format!("session:{}:{}", low, high);
439 assert_eq!(
440 GroupId::for_session(&low.to_string(), &high.to_string())
441 .unwrap()
442 .as_str(),
443 expected,
444 "address slots must follow Address order, not string order"
445 );
446 }
447
448 #[test]
449 fn session_id_is_symmetric_for_addresses() {
450 let (low, high) = disagreeing_addresses();
451 let (a, b) = (low.to_string(), high.to_string());
452
453 assert_eq!(
454 GroupId::for_session(&a, &b).unwrap(),
455 GroupId::for_session(&b, &a).unwrap(),
456 "both peers must derive the same slot regardless of argument order"
457 );
458 }
459
460 #[test]
463 fn session_id_falls_back_to_string_order_for_non_addresses() {
464 assert_eq!(
465 GroupId::for_session("bob", "alice").unwrap().as_str(),
466 "session:alice:bob"
467 );
468
469 let (address, _) = disagreeing_addresses();
470 let mixed = GroupId::for_session("alice", &address.to_string()).unwrap();
471 let mixed_reversed = GroupId::for_session(&address.to_string(), "alice").unwrap();
472 assert_eq!(mixed, mixed_reversed, "mixed pairs must stay symmetric");
473 }
474
475 fn sample_encrypted_message() -> EncryptedMessage {
476 EncryptedMessage {
477 group_id: GroupId::for_session("alice", "bob").unwrap(),
478 message_type: MlsMessageType::Application,
479 epoch: 42,
480 ciphertext: vec![0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01],
481 sender_id: "alice".to_string(),
482 timestamp_ms: 1_700_000_000_123,
483 }
484 }
485
486 #[test]
487 fn test_encrypted_message_bytes_roundtrip() {
488 let msg = sample_encrypted_message();
489 let bytes = msg.to_bytes();
490 let decoded = EncryptedMessage::from_bytes(&bytes).unwrap();
491
492 assert_eq!(decoded.group_id, msg.group_id);
493 assert_eq!(decoded.message_type, msg.message_type);
494 assert_eq!(decoded.epoch, msg.epoch);
495 assert_eq!(decoded.ciphertext, msg.ciphertext);
496 assert_eq!(decoded.sender_id, msg.sender_id);
497 assert_eq!(decoded.timestamp_ms, msg.timestamp_ms);
498 }
499
500 #[test]
501 fn test_encrypted_message_bytes_roundtrip_empty_ciphertext() {
502 let mut msg = sample_encrypted_message();
503 msg.ciphertext = Vec::new();
504 let decoded = EncryptedMessage::from_bytes(&msg.to_bytes()).unwrap();
505 assert!(decoded.ciphertext.is_empty());
506 }
507
508 #[test]
509 fn test_encrypted_message_from_bytes_truncated() {
510 let bytes = sample_encrypted_message().to_bytes();
511 for len in 0..bytes.len() {
513 assert!(
514 EncryptedMessage::from_bytes(&bytes[..len]).is_err(),
515 "truncation at {} unexpectedly succeeded",
516 len
517 );
518 }
519 }
520
521 #[test]
522 fn test_encrypted_message_from_bytes_oversized_group_id_rejected() {
523 let mut bytes = Vec::new();
525 bytes.extend_from_slice(&(u32::MAX).to_le_bytes());
526 let err = EncryptedMessage::from_bytes(&bytes).unwrap_err();
527 assert!(err.to_string().contains("group_id_len"));
528 }
529
530 #[test]
534 fn test_encrypted_message_from_bytes_oversized_sender_id_rejected() {
535 let mut bytes = Vec::new();
536 let group_id = b"session:alice:bob";
537 bytes.extend_from_slice(&(group_id.len() as u32).to_le_bytes());
538 bytes.extend_from_slice(group_id);
539 bytes.extend_from_slice(&(u32::MAX).to_le_bytes());
540 let err = EncryptedMessage::from_bytes(&bytes).unwrap_err();
541 assert!(
542 err.to_string().contains("sender_len"),
543 "expected the sender_len cap to refuse it, got: {err}"
544 );
545 }
546
547 #[test]
548 fn test_encrypted_message_from_bytes_oversized_ciphertext_rejected() {
549 let mut msg = sample_encrypted_message();
550 msg.ciphertext = vec![0u8; 4];
551 let mut bytes = msg.to_bytes();
552 let ct_len_offset = bytes.len() - 4 - 4;
554 bytes[ct_len_offset..ct_len_offset + 4].copy_from_slice(&(u32::MAX).to_le_bytes());
555 let err = EncryptedMessage::from_bytes(&bytes).unwrap_err();
556 assert!(err.to_string().contains("ciphertext_len"));
557 }
558
559 #[test]
560 fn test_encrypted_message_from_bytes_unknown_message_type_rejected() {
561 let msg = sample_encrypted_message();
562 let mut bytes = msg.to_bytes();
563 let tag_offset = 4 + msg.group_id.as_str().len() + 4 + msg.sender_id.len();
564 bytes[tag_offset] = 0xFF;
565 let err = EncryptedMessage::from_bytes(&bytes).unwrap_err();
566 assert!(err.to_string().contains("message_type"));
567 }
568
569 #[test]
577 fn json_bodies_are_refused_by_the_compact_parser() {
578 let json = serde_json::to_vec(&sample_encrypted_message()).unwrap();
579 assert!(json.starts_with(b"{\""), "premise: JSON starts with {{\"");
580
581 let err = EncryptedMessage::from_bytes(&json).unwrap_err();
582 assert!(
583 err.to_string().contains("group_id_len"),
584 "a JSON body must be refused by the string-field cap, got: {err}"
585 );
586
587 let claimed = u32::from_le_bytes([json[0], json[1], json[2], json[3]]) as usize;
588 assert!(
589 claimed > MAX_STRING_FIELD_LEN,
590 "the cap only disambiguates while it stays below {claimed}"
591 );
592 }
593
594 #[test]
595 fn test_group_id_accepts_legitimate_formats() {
596 assert!(GroupId::new("group:0bd6e5f2-3a70-4a4e-9c3f-1c1f2a3b4c5d").is_ok());
597 assert!(GroupId::new("session:alice:bob").is_ok());
598 assert!(GroupId::new("plain-segment_1.x@y").is_ok());
599 }
600
601 #[test]
602 fn test_group_id_rejects_storage_hostile_chars() {
603 assert!(GroupId::new("").is_err());
604 assert!(GroupId::new("group/evil").is_err());
605 assert!(GroupId::new("group\\evil").is_err());
606 assert!(GroupId::new("group\0evil").is_err());
607 assert!(GroupId::new("group\nevil").is_err());
608 assert!(GroupId::new("group\x7Fevil").is_err()); assert!(GroupId::new(".").is_err());
610 assert!(GroupId::new("..").is_err());
611 assert!(GroupId::new("session:..:bob").is_err());
613 assert!(GroupId::new("group:../../etc").is_err());
614 assert!(GroupId::new("session:").is_err());
616 assert!(GroupId::new(":session").is_err());
617 assert!(GroupId::new("a::b").is_err());
618 }
619
620 #[test]
621 fn test_group_id_length_cap() {
622 assert!(GroupId::new("a".repeat(GroupId::MAX_LEN)).is_ok());
623 assert!(GroupId::new("a".repeat(GroupId::MAX_LEN + 1)).is_err());
624 }
625
626 #[test]
627 fn test_group_id_deserialize_rejects_hostile() {
628 assert!(serde_json::from_str::<GroupId>(r#""evil/path""#).is_err());
629 assert!(serde_json::from_str::<GroupId>(r#""..""#).is_err());
630 assert!(serde_json::from_str::<GroupId>(r#""session:alice:bob""#).is_ok());
631 }
632
633 #[test]
634 fn test_encrypted_message_from_bytes_rejects_hostile_group_id() {
635 let mut bytes = Vec::new();
638 let hostile = b"evil/../path";
639 bytes.extend_from_slice(&(hostile.len() as u32).to_le_bytes());
640 bytes.extend_from_slice(hostile);
641 let sender = b"alice";
642 bytes.extend_from_slice(&(sender.len() as u32).to_le_bytes());
643 bytes.extend_from_slice(sender);
644 bytes.push(0); bytes.extend_from_slice(&42u64.to_le_bytes()); bytes.extend_from_slice(&1u64.to_le_bytes()); bytes.extend_from_slice(&0u32.to_le_bytes()); let err = EncryptedMessage::from_bytes(&bytes).unwrap_err();
649 assert!(matches!(err, SealedError::InvalidGroupId(_)));
650 }
651
652 #[test]
653 fn test_mls_message_type_u8_roundtrip() {
654 for msg_type in [
655 MlsMessageType::Application,
656 MlsMessageType::Welcome,
657 MlsMessageType::Commit,
658 MlsMessageType::Proposal,
659 ] {
660 assert_eq!(MlsMessageType::from_u8(msg_type.as_u8()), Some(msg_type));
661 }
662 assert_eq!(MlsMessageType::from_u8(4), None);
663 }
664}
665
666#[cfg(all(test, feature = "std"))]
677mod spec_vectors {
678 use super::*;
679
680 const VECTORS: &str = include_str!("../tests/data/mls-envelope-v1.vectors.json");
681
682 fn vectors() -> serde_json::Value {
683 serde_json::from_str(VECTORS).expect("the vector file is JSON")
684 }
685
686 fn hex(bytes: &[u8]) -> String {
687 bytes.iter().map(|b| format!("{b:02x}")).collect()
688 }
689
690 fn unhex(s: &str) -> Vec<u8> {
691 (0..s.len())
692 .step_by(2)
693 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex digit"))
694 .collect()
695 }
696
697 fn envelope_from(spec: &serde_json::Value) -> EncryptedMessage {
698 EncryptedMessage {
699 group_id: GroupId::new(spec["group_id"].as_str().expect("a group id"))
700 .expect("a valid group id"),
701 sender_id: spec["sender_id"].as_str().expect("a sender").to_string(),
702 message_type: match spec["message_type"].as_str().expect("a type") {
703 "application" => MlsMessageType::Application,
704 "welcome" => MlsMessageType::Welcome,
705 "commit" => MlsMessageType::Commit,
706 "proposal" => MlsMessageType::Proposal,
707 other => panic!("the vector names a message type that does not exist: {other}"),
708 },
709 epoch: spec["epoch"].as_u64().expect("an epoch"),
710 timestamp_ms: spec["timestamp_ms"].as_u64().expect("a timestamp"),
711 ciphertext: unhex(spec["ciphertext_hex"].as_str().expect("a ciphertext")),
712 }
713 }
714
715 #[test]
718 fn the_vector_file_is_the_size_it_was() {
719 let v = vectors();
720 assert_eq!(v["envelopes"].as_array().expect("envelopes").len(), 6);
721 assert_eq!(v["max_string_field_len"], MAX_STRING_FIELD_LEN);
722 }
723
724 #[test]
725 fn every_envelope_encodes_to_its_vector() {
726 for case in vectors()["envelopes"].as_array().expect("envelopes") {
727 let name = case["name"].as_str().expect("a name");
728 assert_eq!(
729 hex(&envelope_from(&case["envelope"]).to_bytes()),
730 case["hex"].as_str().expect("expected hex"),
731 "[{name}] encoded to different bytes than the chapter specifies"
732 );
733 }
734 }
735
736 #[test]
743 fn every_vector_decodes_back_into_its_envelope() {
744 for case in vectors()["envelopes"].as_array().expect("envelopes") {
745 let name = case["name"].as_str().expect("a name");
746 let want = envelope_from(&case["envelope"]);
747 let got = EncryptedMessage::from_bytes(&unhex(case["hex"].as_str().expect("hex")))
748 .unwrap_or_else(|e| panic!("[{name}] did not decode: {e}"));
749
750 assert_eq!(
751 got.group_id.as_str(),
752 want.group_id.as_str(),
753 "[{name}] group_id"
754 );
755 assert_eq!(got.sender_id, want.sender_id, "[{name}] sender_id");
756 assert_eq!(got.message_type, want.message_type, "[{name}] message_type");
757 assert_eq!(got.epoch, want.epoch, "[{name}] epoch");
758 assert_eq!(got.timestamp_ms, want.timestamp_ms, "[{name}] timestamp_ms");
759 assert_eq!(got.ciphertext, want.ciphertext, "[{name}] ciphertext");
760 }
761 }
762
763 #[test]
769 fn a_json_body_is_refused_by_the_compact_parser() {
770 let v = vectors();
771 let case = &v["json_disambiguation"];
772 let prefix = case["prefix_utf8"].as_str().expect("a prefix");
773 let declared = case["min_as_le_u32"]
774 .as_u64()
775 .expect("the smallest length it can read as");
776
777 let mut probe = prefix.as_bytes().to_vec();
783 probe.extend_from_slice(&[0u8; 2]);
784 assert_eq!(
785 u32::from_le_bytes(probe[..4].try_into().expect("four bytes")) as u64,
786 declared,
787 "the vector's arithmetic no longer describes what a decoder reads"
788 );
789 for tail in [[0x00, 0x00], [0x67, 0x22], [0xff, 0xff]] {
790 let mut probe = prefix.as_bytes().to_vec();
791 probe.extend_from_slice(&tail);
792 assert!(
793 u32::from_le_bytes(probe[..4].try_into().expect("four bytes")) as u64 >= declared,
794 "the vector calls {declared} a minimum, but a continuation read below it"
795 );
796 }
797 assert!(
798 declared > MAX_STRING_FIELD_LEN as u64,
799 "a JSON body no longer overflows the cap, so the two forms are \
800 ambiguous and the fallback is a guess"
801 );
802
803 let json = br#"{"group_id":"session:a:b","ciphertext":[1,2,3]}"#;
804 assert!(
805 EncryptedMessage::from_bytes(json).is_err(),
806 "the compact parser accepted a JSON envelope"
807 );
808 }
809
810 fn chapter() -> Option<String> {
813 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
814 .join("../../docs/spec/encryption-envelopes.md");
815 std::fs::read_to_string(&path).ok().or_else(|| {
816 eprintln!("spec tree not present, skipping the envelope chapter drift checks");
817 None
818 })
819 }
820
821 #[test]
822 fn the_chapter_states_the_layout_the_code_writes() {
823 let Some(text) = chapter() else { return };
824
825 for required in [
826 "u32le len(group_id)",
827 "u8 message_type",
828 "u64le epoch",
829 "All integers are little-endian",
830 ] {
831 assert!(
832 text.contains(required),
833 "the chapter no longer states {required:?}, without which the \
834 envelope cannot be implemented from it"
835 );
836 }
837 }
838}