1use alloc::string::String;
68use alloc::sync::Arc;
69use alloc::vec::Vec;
70
71use brink_format::{DefinitionId, LineFlags, MAX_DECODE_DEPTH, MapKey, NameId, OrderedMap, Value};
72
73use crate::output::{OutputPart, resolve_lines};
74use crate::program::Program;
75
76const MAGIC: &[u8; 4] = b"BRKT";
79const VERSION: u16 = 1;
80const HEADER_SIZE: usize = 16;
81
82const TAG_TEXT: u8 = 0x01;
84const TAG_LINE_REF: u8 = 0x02;
85const TAG_VALUE_REF: u8 = 0x03;
86const TAG_NEWLINE: u8 = 0x04;
87const TAG_SPRING: u8 = 0x05;
88const TAG_GLUE: u8 = 0x06;
89const TAG_TAG: u8 = 0x07;
90
91const VAL_INT: u8 = 0x00;
93const VAL_FLOAT: u8 = 0x01;
94const VAL_BOOL: u8 = 0x02;
95const VAL_STRING: u8 = 0x03;
96const VAL_LIST: u8 = 0x04;
97const VAL_DIVERT_TARGET: u8 = 0x05;
98const VAL_NULL: u8 = 0x06;
99const VAL_VAR_POINTER: u8 = 0x07;
105const VAL_FRAGMENT_REF: u8 = 0x08;
106const VAL_ARRAY: u8 = 0x09;
111const VAL_MAP: u8 = 0x0A;
112const VAL_FN_REF: u8 = 0x0B;
118const VAL_CLOSURE: u8 = 0x0C;
119const VAL_HANDLE: u8 = 0x0D;
126const VAL_RECORD: u8 = 0x0F;
129const VAL_PROJECTION: u8 = 0x0E;
135const VAL_OPTION: u8 = 0x10;
139const VAL_RANGE: u8 = 0x11;
144const VAL_VEC2: u8 = 0x12;
150const VAL_VEC3: u8 = 0x13;
151const VAL_VEC4: u8 = 0x14;
152const VAL_QUAT: u8 = 0x15;
153const VAL_MAT2: u8 = 0x16;
154const VAL_MAT3: u8 = 0x17;
155const VAL_MAT4: u8 = 0x18;
156const VAL_WEIGHTED: u8 = 0x19;
162const PROJ_SEG_INDEX: u8 = 0x00;
163const PROJ_SEG_KEY: u8 = 0x01;
164
165#[derive(Debug, thiserror::Error)]
169pub enum TranscriptError {
170 #[error("invalid magic: expected BRKT")]
171 InvalidMagic,
172 #[error("unsupported version: {0}")]
173 UnsupportedVersion(u16),
174 #[error("checksum mismatch: transcript {transcript:#010x} != program {program:#010x}")]
175 ChecksumMismatch { transcript: u32, program: u32 },
176 #[error("integrity check failed: content CRC-32 mismatch")]
177 IntegrityCheckFailed,
178 #[error("unexpected end of data")]
179 UnexpectedEof,
180 #[error("invalid part tag: {0:#04x}")]
181 InvalidPartTag(u8),
182 #[error("invalid value tag: {0:#04x}")]
183 InvalidValueTag(u8),
184 #[error("invalid UTF-8")]
185 InvalidUtf8,
186 #[error("invalid definition ID")]
187 InvalidDefinitionId,
188 #[error("value nesting exceeded max decode depth ({0})")]
189 MaxDepthExceeded(usize),
190 #[error("duplicate key in map value")]
197 DuplicateMapKey,
198}
199
200#[expect(clippy::cast_possible_truncation)]
207pub fn write_transcript(
208 parts: &[OutputPart],
209 source_checksum: u32,
210 fragments: &[crate::output::Fragment],
211) -> Vec<u8> {
212 let mut body = Vec::new();
213
214 let count = parts.iter().filter(|p| is_persisted(p)).count() as u32;
216 write_u32(&mut body, count);
217
218 for part in parts {
219 encode_part(part, &mut body);
220 }
221
222 write_u32(&mut body, fragments.len() as u32);
224 for fragment in fragments {
225 let filtered_count = fragment.parts.iter().filter(|p| is_persisted(p)).count() as u32;
226 write_u32(&mut body, filtered_count);
227 for part in &fragment.parts {
228 encode_part(part, &mut body);
229 }
230 }
231
232 for fragment in fragments {
242 write_u32(&mut body, fragment.tags.len() as u32);
243 for tag in &fragment.tags {
244 write_str(&mut body, tag);
245 }
246 }
247
248 let content_crc = crc32(&body);
250 let mut buf = Vec::with_capacity(HEADER_SIZE + body.len());
251 buf.extend_from_slice(MAGIC);
252 write_u16(&mut buf, VERSION);
253 write_u16(&mut buf, 0); write_u32(&mut buf, source_checksum);
255 write_u32(&mut buf, content_crc);
256 buf.extend(body);
257 buf
258}
259
260#[derive(Debug, Clone)]
270pub struct TranscriptData {
271 pub parts: Vec<OutputPart>,
272 pub source_checksum: u32,
273 pub fragments: Vec<crate::output::Fragment>,
274}
275
276pub fn read_transcript(bytes: &[u8]) -> Result<TranscriptData, TranscriptError> {
278 if bytes.len() < HEADER_SIZE {
279 return Err(TranscriptError::UnexpectedEof);
280 }
281
282 if &bytes[0..4] != MAGIC {
284 return Err(TranscriptError::InvalidMagic);
285 }
286 let mut off = 4;
287 let version = read_u16(bytes, &mut off)?;
288 if version != VERSION {
289 return Err(TranscriptError::UnsupportedVersion(version));
290 }
291 let _reserved = read_u16(bytes, &mut off)?;
292 let source_checksum = read_u32(bytes, &mut off)?;
293 let expected_crc = read_u32(bytes, &mut off)?;
294
295 let body = &bytes[HEADER_SIZE..];
297 if crc32(body) != expected_crc {
298 return Err(TranscriptError::IntegrityCheckFailed);
299 }
300
301 let mut off = HEADER_SIZE;
303 let count = read_u32(bytes, &mut off)? as usize;
304 let mut parts = Vec::with_capacity(count);
305
306 for _ in 0..count {
307 parts.push(decode_part(bytes, &mut off)?);
308 }
309
310 let fragment_count = if off < bytes.len() {
312 read_u32(bytes, &mut off)? as usize
313 } else {
314 0 };
316 let mut fragments = Vec::with_capacity(fragment_count);
317 for _ in 0..fragment_count {
318 let frag_part_count = read_u32(bytes, &mut off)? as usize;
319 let mut frag_parts = Vec::with_capacity(frag_part_count);
320 for _ in 0..frag_part_count {
321 frag_parts.push(decode_part(bytes, &mut off)?);
322 }
323 fragments.push(crate::output::Fragment {
324 parts: frag_parts,
325 tags: Vec::new(),
326 });
327 }
328
329 if off < bytes.len() {
337 for fragment in &mut fragments {
338 let tag_count = read_u32(bytes, &mut off)? as usize;
339 let mut tags = Vec::with_capacity(tag_count.min(bytes.len().saturating_sub(off)));
340 for _ in 0..tag_count {
341 tags.push(read_str(bytes, &mut off)?);
342 }
343 fragment.tags = tags;
344 }
345 }
346
347 Ok(TranscriptData {
348 parts,
349 source_checksum,
350 fragments,
351 })
352}
353
354pub fn render_transcript(
361 parts: &[OutputPart],
362 program: &Program,
363 line_tables: &[Vec<brink_format::LineEntry>],
364 resolver: Option<&dyn brink_format::PluralResolver>,
365 fragments: &[crate::output::Fragment],
366) -> Vec<(String, Vec<String>)> {
367 resolve_lines(parts, program, line_tables, resolver, fragments)
373 .into_iter()
374 .map(|(text, tags, _element)| (text, tags))
375 .collect()
376}
377
378fn is_persisted(part: &OutputPart) -> bool {
402 !matches!(
403 part,
404 OutputPart::Checkpoint | OutputPart::ElementAttach(..) | OutputPart::ElementAttachEnd
405 )
406}
407
408#[expect(clippy::cast_possible_truncation)]
413fn encode_part(part: &OutputPart, buf: &mut Vec<u8>) {
414 match part {
415 OutputPart::Text(s) => {
416 write_u8(buf, TAG_TEXT);
417 write_str(buf, s);
418 }
419 OutputPart::LineRef {
420 container_idx,
421 line_idx,
422 slots,
423 flags,
424 } => {
425 write_u8(buf, TAG_LINE_REF);
426 write_u32(buf, *container_idx);
427 write_u16(buf, *line_idx);
428 write_u8(buf, flags.bits());
429 write_u16(buf, slots.len() as u16);
430 for val in slots {
431 encode_value(val, buf);
432 }
433 }
434 OutputPart::ValueRef(val) => {
435 write_u8(buf, TAG_VALUE_REF);
436 encode_value(val, buf);
437 }
438 OutputPart::Newline => write_u8(buf, TAG_NEWLINE),
439 OutputPart::Spring => write_u8(buf, TAG_SPRING),
440 OutputPart::Glue => write_u8(buf, TAG_GLUE),
441 OutputPart::Tag(s) => {
442 write_u8(buf, TAG_TAG);
443 write_str(buf, s);
444 }
445 OutputPart::Checkpoint | OutputPart::ElementAttach(..) | OutputPart::ElementAttachEnd => {}
451 }
452}
453
454fn decode_part(bytes: &[u8], off: &mut usize) -> Result<OutputPart, TranscriptError> {
457 let tag = read_u8(bytes, off)?;
458 let part = match tag {
459 TAG_TEXT => OutputPart::Text(read_str(bytes, off)?),
460 TAG_LINE_REF => {
461 let container_idx = read_u32(bytes, off)?;
462 let line_idx = read_u16(bytes, off)?;
463 let flags_bits = read_u8(bytes, off)?;
464 let flags = LineFlags::from_bits_truncate(flags_bits);
465 let slot_count = read_u16(bytes, off)? as usize;
466 let mut slots = Vec::with_capacity(slot_count);
467 for _ in 0..slot_count {
468 slots.push(decode_value(bytes, off, 0)?);
469 }
470 OutputPart::LineRef {
471 container_idx,
472 line_idx,
473 slots,
474 flags,
475 }
476 }
477 TAG_VALUE_REF => OutputPart::ValueRef(decode_value(bytes, off, 0)?),
478 TAG_NEWLINE => OutputPart::Newline,
479 TAG_SPRING => OutputPart::Spring,
480 TAG_GLUE => OutputPart::Glue,
481 TAG_TAG => OutputPart::Tag(read_str(bytes, off)?),
482 _ => return Err(TranscriptError::InvalidPartTag(tag)),
483 };
484 Ok(part)
485}
486
487fn write_u8(buf: &mut Vec<u8>, v: u8) {
490 buf.push(v);
491}
492
493fn write_u16(buf: &mut Vec<u8>, v: u16) {
494 buf.extend_from_slice(&v.to_le_bytes());
495}
496
497fn write_u32(buf: &mut Vec<u8>, v: u32) {
498 buf.extend_from_slice(&v.to_le_bytes());
499}
500
501fn write_u64(buf: &mut Vec<u8>, v: u64) {
502 buf.extend_from_slice(&v.to_le_bytes());
503}
504
505fn write_i32(buf: &mut Vec<u8>, v: i32) {
506 buf.extend_from_slice(&v.to_le_bytes());
507}
508
509#[expect(clippy::cast_possible_truncation)]
510fn write_str(buf: &mut Vec<u8>, s: &str) {
511 write_u32(buf, s.len() as u32);
512 buf.extend_from_slice(s.as_bytes());
513}
514
515fn write_def_id(buf: &mut Vec<u8>, id: DefinitionId) {
516 write_u64(buf, id.to_raw());
517}
518
519fn read_u8(buf: &[u8], off: &mut usize) -> Result<u8, TranscriptError> {
520 if *off >= buf.len() {
521 return Err(TranscriptError::UnexpectedEof);
522 }
523 let v = buf[*off];
524 *off += 1;
525 Ok(v)
526}
527
528fn read_u16(buf: &[u8], off: &mut usize) -> Result<u16, TranscriptError> {
529 if *off + 2 > buf.len() {
530 return Err(TranscriptError::UnexpectedEof);
531 }
532 let v = u16::from_le_bytes([buf[*off], buf[*off + 1]]);
533 *off += 2;
534 Ok(v)
535}
536
537fn read_u32(buf: &[u8], off: &mut usize) -> Result<u32, TranscriptError> {
538 if *off + 4 > buf.len() {
539 return Err(TranscriptError::UnexpectedEof);
540 }
541 let v = u32::from_le_bytes([buf[*off], buf[*off + 1], buf[*off + 2], buf[*off + 3]]);
542 *off += 4;
543 Ok(v)
544}
545
546fn read_i32(buf: &[u8], off: &mut usize) -> Result<i32, TranscriptError> {
547 if *off + 4 > buf.len() {
548 return Err(TranscriptError::UnexpectedEof);
549 }
550 let v = i32::from_le_bytes([buf[*off], buf[*off + 1], buf[*off + 2], buf[*off + 3]]);
551 *off += 4;
552 Ok(v)
553}
554
555fn read_f32(buf: &[u8], off: &mut usize) -> Result<f32, TranscriptError> {
556 if *off + 4 > buf.len() {
557 return Err(TranscriptError::UnexpectedEof);
558 }
559 let v = f32::from_le_bytes([buf[*off], buf[*off + 1], buf[*off + 2], buf[*off + 3]]);
560 *off += 4;
561 Ok(v)
562}
563
564fn read_u64(buf: &[u8], off: &mut usize) -> Result<u64, TranscriptError> {
565 if *off + 8 > buf.len() {
566 return Err(TranscriptError::UnexpectedEof);
567 }
568 let v = u64::from_le_bytes([
569 buf[*off],
570 buf[*off + 1],
571 buf[*off + 2],
572 buf[*off + 3],
573 buf[*off + 4],
574 buf[*off + 5],
575 buf[*off + 6],
576 buf[*off + 7],
577 ]);
578 *off += 8;
579 Ok(v)
580}
581
582fn read_str(buf: &[u8], off: &mut usize) -> Result<String, TranscriptError> {
583 let len = read_u32(buf, off)? as usize;
584 if *off + len > buf.len() {
585 return Err(TranscriptError::UnexpectedEof);
586 }
587 let bytes = &buf[*off..*off + len];
588 *off += len;
589 String::from_utf8(bytes.to_vec()).map_err(|_| TranscriptError::InvalidUtf8)
590}
591
592fn read_def_id(buf: &[u8], off: &mut usize) -> Result<DefinitionId, TranscriptError> {
593 let raw = read_u64(buf, off)?;
594 DefinitionId::from_raw(raw).ok_or(TranscriptError::InvalidDefinitionId)
595}
596
597#[expect(clippy::cast_possible_truncation)]
600#[expect(
601 clippy::too_many_lines,
602 reason = "one match arm per value tag — T1e's VAL_PROJECTION arm pushed this past 100"
603)]
604fn encode_value(v: &Value, buf: &mut Vec<u8>) {
605 match v {
606 Value::Int(n) => {
607 write_u8(buf, VAL_INT);
608 write_i32(buf, *n);
609 }
610 Value::Float(n) => {
611 write_u8(buf, VAL_FLOAT);
612 buf.extend_from_slice(&n.to_le_bytes());
613 }
614 Value::Bool(b) => {
615 write_u8(buf, VAL_BOOL);
616 write_u8(buf, u8::from(*b));
617 }
618 Value::String(s) => {
619 write_u8(buf, VAL_STRING);
620 write_str(buf, s);
621 }
622 Value::List(lv) => {
623 write_u8(buf, VAL_LIST);
624 write_u32(buf, lv.items.len() as u32);
625 for item in &lv.items {
626 write_def_id(buf, *item);
627 }
628 write_u32(buf, lv.origins.len() as u32);
629 for origin in &lv.origins {
630 write_def_id(buf, *origin);
631 }
632 }
633 Value::DivertTarget(id) => {
634 write_u8(buf, VAL_DIVERT_TARGET);
635 write_def_id(buf, *id);
636 }
637 Value::VariablePointer(id) => {
638 write_u8(buf, VAL_VAR_POINTER);
639 write_def_id(buf, *id);
640 }
641 Value::FragmentRef(idx) => {
642 write_u8(buf, VAL_FRAGMENT_REF);
643 write_u32(buf, *idx);
644 }
645 Value::TempPointer { .. } | Value::Null => {
647 write_u8(buf, VAL_NULL);
648 }
649 Value::Array(items) => {
653 write_u8(buf, VAL_ARRAY);
654 write_u32(buf, items.len() as u32);
655 for item in items.iter() {
656 encode_value(item, buf);
657 }
658 }
659 Value::Map(map) => {
660 write_u8(buf, VAL_MAP);
661 write_u32(buf, map.len() as u32);
662 for (key, val) in map.iter() {
663 encode_map_key(key, buf);
664 encode_value(val, buf);
665 }
666 }
667 Value::Record { shape, fields } => {
668 write_u8(buf, VAL_RECORD);
669 write_u32(buf, shape.0);
670 write_u32(buf, fields.len() as u32);
671 for field in fields.iter() {
672 encode_value(field, buf);
673 }
674 }
675 Value::FnRef(target) => {
679 write_u8(buf, VAL_FN_REF);
680 write_def_id(buf, *target);
681 }
682 Value::Closure(c) => {
683 write_u8(buf, VAL_CLOSURE);
684 write_def_id(buf, c.target);
685 write_u32(buf, c.env.len() as u32);
686 for entry in &c.env {
687 write_u16(buf, entry.name.0);
688 write_u8(buf, u8::from(entry.is_ref));
689 encode_value(&entry.payload, buf);
690 }
691 }
692 Value::Handle { kind, id } => {
697 write_u8(buf, VAL_HANDLE);
698 write_u16(buf, kind.0);
699 write_u64(buf, *id);
700 }
701 Value::Projection(p) => {
705 write_u8(buf, VAL_PROJECTION);
706 write_def_id(buf, p.cell);
707 write_u8(buf, p.segments.len() as u8);
708 for seg in &p.segments {
709 match seg {
710 brink_format::ProjSegment::Index(n) => {
711 write_u8(buf, PROJ_SEG_INDEX);
712 write_i32(buf, *n);
713 }
714 brink_format::ProjSegment::Key(v) => {
715 write_u8(buf, PROJ_SEG_KEY);
716 encode_value(v, buf);
717 }
718 }
719 }
720 }
721 Value::OptionVal(inner) => {
725 write_u8(buf, VAL_OPTION);
726 match inner {
727 None => write_u8(buf, 0),
728 Some(v) => {
729 write_u8(buf, 1);
730 encode_value(v, buf);
731 }
732 }
733 }
734 Value::Range {
740 start,
741 end,
742 inclusive,
743 } => {
744 write_u8(buf, VAL_RANGE);
745 write_i32(buf, *start);
746 write_i32(buf, *end);
747 write_u8(buf, u8::from(*inclusive));
748 }
749 Value::Vec2(v) => {
753 write_u8(buf, VAL_VEC2);
754 write_f32_lanes(buf, &v.to_array());
755 }
756 Value::Vec3(v) => {
757 write_u8(buf, VAL_VEC3);
758 write_f32_lanes(buf, &v.to_array());
759 }
760 Value::Vec4(v) => {
761 write_u8(buf, VAL_VEC4);
762 write_f32_lanes(buf, &v.to_array());
763 }
764 Value::Quat(q) => {
765 write_u8(buf, VAL_QUAT);
766 write_f32_lanes(buf, &q.to_array());
767 }
768 Value::Mat2(m) => {
769 write_u8(buf, VAL_MAT2);
770 write_f32_lanes(buf, &m.to_cols_array());
771 }
772 Value::Mat3(m) => {
773 write_u8(buf, VAL_MAT3);
774 write_f32_lanes(buf, &m.to_cols_array());
775 }
776 Value::Mat4(m) => {
777 write_u8(buf, VAL_MAT4);
778 write_f32_lanes(buf, &m.to_cols_array());
779 }
780 Value::Weighted(w) => {
781 write_u8(buf, VAL_WEIGHTED);
782 write_u32(buf, w.entries.len() as u32);
783 for (weight, value) in &w.entries {
784 write_i32(buf, *weight);
785 encode_value(value, buf);
786 }
787 }
788 }
789}
790
791fn write_f32_lanes(buf: &mut Vec<u8>, lanes: &[f32]) {
795 for lane in lanes {
796 buf.extend_from_slice(&lane.to_le_bytes());
797 }
798}
799
800fn read_f32_lanes<const N: usize>(
804 buf: &[u8],
805 off: &mut usize,
806) -> Result<[f32; N], TranscriptError> {
807 let mut lanes = [0.0f32; N];
808 for lane in &mut lanes {
809 *lane = read_f32(buf, off)?;
810 }
811 Ok(lanes)
812}
813
814fn encode_map_key(key: &MapKey, buf: &mut Vec<u8>) {
818 match key {
819 MapKey::Int(n) => {
820 write_u8(buf, VAL_INT);
821 write_i32(buf, *n);
822 }
823 MapKey::Str(s) => {
824 write_u8(buf, VAL_STRING);
825 write_str(buf, s);
826 }
827 MapKey::Bool(b) => {
828 write_u8(buf, VAL_BOOL);
829 write_u8(buf, u8::from(*b));
830 }
831 }
832}
833
834#[expect(
835 clippy::too_many_lines,
836 reason = "one match arm per value tag — T1e's VAL_PROJECTION arm pushed this past 100"
837)]
838fn decode_value(buf: &[u8], off: &mut usize, depth: usize) -> Result<Value, TranscriptError> {
839 if depth > MAX_DECODE_DEPTH {
840 return Err(TranscriptError::MaxDepthExceeded(MAX_DECODE_DEPTH));
841 }
842 let tag = read_u8(buf, off)?;
843 match tag {
844 VAL_INT => Ok(Value::Int(read_i32(buf, off)?)),
845 VAL_FLOAT => Ok(Value::Float(read_f32(buf, off)?)),
846 VAL_BOOL => {
847 let b = read_u8(buf, off)?;
848 Ok(Value::Bool(b != 0))
849 }
850 VAL_STRING => {
851 let s = read_str(buf, off)?;
852 Ok(Value::String(Arc::from(s.as_str())))
853 }
854 VAL_LIST => {
855 let item_count = read_u32(buf, off)? as usize;
856 let mut items = Vec::with_capacity(item_count);
857 for _ in 0..item_count {
858 items.push(read_def_id(buf, off)?);
859 }
860 let origin_count = read_u32(buf, off)? as usize;
861 let mut origins = Vec::with_capacity(origin_count);
862 for _ in 0..origin_count {
863 origins.push(read_def_id(buf, off)?);
864 }
865 Ok(Value::List(Arc::new(brink_format::ListValue {
866 items,
867 origins,
868 })))
869 }
870 VAL_DIVERT_TARGET => {
871 let id = read_def_id(buf, off)?;
872 Ok(Value::DivertTarget(id))
873 }
874 VAL_VAR_POINTER => {
875 let id = read_def_id(buf, off)?;
876 Ok(Value::VariablePointer(id))
877 }
878 VAL_FRAGMENT_REF => Ok(Value::FragmentRef(read_u32(buf, off)?)),
879 VAL_NULL => Ok(Value::Null),
880 VAL_ARRAY => {
881 let len = read_u32(buf, off)? as usize;
882 let mut items = Vec::with_capacity(len.min(buf.len().saturating_sub(*off)));
883 for _ in 0..len {
884 items.push(decode_value(buf, off, depth + 1)?);
885 }
886 Ok(Value::array(items))
887 }
888 VAL_MAP => {
889 let len = read_u32(buf, off)? as usize;
890 let mut map = OrderedMap::with_capacity(len.min(buf.len().saturating_sub(*off)));
891 for _ in 0..len {
892 let key = decode_map_key(buf, off)?;
893 let val = decode_value(buf, off, depth + 1)?;
894 if map.contains_key(&key) {
898 return Err(TranscriptError::DuplicateMapKey);
899 }
900 map.insert(key, val);
901 }
902 Ok(Value::map(map))
903 }
904 VAL_RECORD => {
905 let shape = brink_format::ShapeId(read_u32(buf, off)?);
906 let len = read_u32(buf, off)? as usize;
907 let mut fields = Vec::with_capacity(len.min(buf.len().saturating_sub(*off)));
908 for _ in 0..len {
909 fields.push(decode_value(buf, off, depth + 1)?);
910 }
911 Ok(Value::record(shape, fields))
912 }
913 VAL_FN_REF => Ok(Value::FnRef(read_def_id(buf, off)?)),
914 VAL_CLOSURE => {
915 let target = read_def_id(buf, off)?;
916 let count = read_u32(buf, off)? as usize;
917 let mut env = Vec::with_capacity(count.min(buf.len().saturating_sub(*off)));
918 for _ in 0..count {
919 let name = brink_format::NameId(read_u16(buf, off)?);
920 let is_ref = read_u8(buf, off)? != 0;
921 let payload = decode_value(buf, off, depth + 1)?;
922 env.push(brink_format::ClosureEnvEntry {
923 name,
924 is_ref,
925 payload,
926 });
927 }
928 Ok(Value::closure(target, env))
929 }
930 VAL_HANDLE => {
932 let kind = NameId(read_u16(buf, off)?);
933 let id = read_u64(buf, off)?;
934 Ok(Value::handle(kind, id))
935 }
936 VAL_PROJECTION => {
938 let cell = read_def_id(buf, off)?;
939 let count = read_u8(buf, off)? as usize;
940 let mut segments = Vec::with_capacity(count.min(buf.len().saturating_sub(*off)));
941 for _ in 0..count {
942 let kind = read_u8(buf, off)?;
943 let seg = match kind {
944 PROJ_SEG_INDEX => brink_format::ProjSegment::Index(read_i32(buf, off)?),
945 PROJ_SEG_KEY => {
946 brink_format::ProjSegment::Key(decode_value(buf, off, depth + 1)?)
947 }
948 other => return Err(TranscriptError::InvalidValueTag(other)),
949 };
950 segments.push(seg);
951 }
952 Ok(Value::projection(cell, segments))
953 }
954 VAL_OPTION => match read_u8(buf, off)? {
957 0 => Ok(Value::none()),
958 1 => Ok(Value::some(decode_value(buf, off, depth + 1)?)),
959 other => Err(TranscriptError::InvalidValueTag(other)),
960 },
961 VAL_RANGE => {
964 let start = read_i32(buf, off)?;
965 let end = read_i32(buf, off)?;
966 let inclusive = match read_u8(buf, off)? {
967 0 => false,
968 1 => true,
969 other => return Err(TranscriptError::InvalidValueTag(other)),
970 };
971 Ok(Value::range(start, end, inclusive))
972 }
973 VAL_VEC2 => Ok(Value::Vec2(glam::Vec2::from_array(read_f32_lanes::<2>(
977 buf, off,
978 )?))),
979 VAL_VEC3 => Ok(Value::Vec3(glam::Vec3::from_array(read_f32_lanes::<3>(
980 buf, off,
981 )?))),
982 VAL_VEC4 => Ok(Value::Vec4(glam::Vec4::from_array(read_f32_lanes::<4>(
983 buf, off,
984 )?))),
985 VAL_QUAT => Ok(Value::Quat(glam::Quat::from_array(read_f32_lanes::<4>(
986 buf, off,
987 )?))),
988 VAL_MAT2 => Ok(Value::Mat2(glam::Mat2::from_cols_array(&read_f32_lanes::<
989 4,
990 >(
991 buf, off
992 )?))),
993 VAL_MAT3 => Ok(Value::Mat3(glam::Mat3::from_cols_array(&read_f32_lanes::<
994 9,
995 >(
996 buf, off
997 )?))),
998 VAL_MAT4 => Ok(Value::Mat4(glam::Mat4::from_cols_array(&read_f32_lanes::<
999 16,
1000 >(
1001 buf, off
1002 )?))),
1003 VAL_WEIGHTED => {
1006 let count = read_u32(buf, off)? as usize;
1007 if count == 0 {
1008 return Err(TranscriptError::InvalidValueTag(VAL_WEIGHTED));
1009 }
1010 let mut entries = Vec::with_capacity(count.min(1024));
1011 for _ in 0..count {
1012 let weight = read_i32(buf, off)?;
1013 if weight < 1 {
1014 return Err(TranscriptError::InvalidValueTag(VAL_WEIGHTED));
1015 }
1016 let value = decode_value(buf, off, depth + 1)?;
1017 entries.push((weight, value));
1018 }
1019 Ok(Value::weighted(entries))
1020 }
1021 _ => Err(TranscriptError::InvalidValueTag(tag)),
1022 }
1023}
1024
1025fn decode_map_key(buf: &[u8], off: &mut usize) -> Result<MapKey, TranscriptError> {
1029 let tag = read_u8(buf, off)?;
1030 match tag {
1031 VAL_INT => Ok(MapKey::Int(read_i32(buf, off)?)),
1032 VAL_STRING => Ok(MapKey::Str(Arc::from(read_str(buf, off)?.as_str()))),
1033 VAL_BOOL => Ok(MapKey::Bool(read_u8(buf, off)? != 0)),
1034 _ => Err(TranscriptError::InvalidValueTag(tag)),
1035 }
1036}
1037
1038fn crc32(data: &[u8]) -> u32 {
1041 static TABLE: [u32; 256] = {
1042 let mut table = [0u32; 256];
1043 let mut i = 0u32;
1044 while i < 256 {
1045 let mut crc = i;
1046 let mut j = 0;
1047 while j < 8 {
1048 if crc & 1 != 0 {
1049 crc = (crc >> 1) ^ 0xEDB8_8320;
1050 } else {
1051 crc >>= 1;
1052 }
1053 j += 1;
1054 }
1055 table[i as usize] = crc;
1056 i += 1;
1057 }
1058 table
1059 };
1060
1061 let mut crc = 0xFFFF_FFFFu32;
1062 for &byte in data {
1063 let idx = ((crc ^ u32::from(byte)) & 0xFF) as usize;
1064 crc = (crc >> 8) ^ TABLE[idx];
1065 }
1066 crc ^ 0xFFFF_FFFF
1067}
1068
1069#[cfg(test)]
1070mod tests {
1071 use super::*;
1072 use brink_format::LineFlags;
1073
1074 #[test]
1075 fn round_trip_simple_parts() {
1076 let parts = vec![
1077 OutputPart::Text("Hello".to_string()),
1078 OutputPart::Spring,
1079 OutputPart::Newline,
1080 OutputPart::Tag("tag1".to_string()),
1081 OutputPart::Glue,
1082 ];
1083 let bytes = write_transcript(&parts, 0xDEAD_BEEF, &[]);
1084 let data = read_transcript(&bytes).unwrap();
1085 assert_eq!(data.source_checksum, 0xDEAD_BEEF);
1086 assert_eq!(data.parts.len(), 5);
1087 assert!(matches!(&data.parts[0], OutputPart::Text(s) if s == "Hello"));
1088 assert!(matches!(&data.parts[1], OutputPart::Spring));
1089 assert!(matches!(&data.parts[2], OutputPart::Newline));
1090 assert!(matches!(&data.parts[3], OutputPart::Tag(s) if s == "tag1"));
1091 assert!(matches!(&data.parts[4], OutputPart::Glue));
1092 }
1093
1094 #[test]
1102 fn is_persisted_filters_transient_markers_only() {
1103 assert!(!is_persisted(&OutputPart::Checkpoint));
1104 assert!(!is_persisted(&OutputPart::ElementAttach(
1105 "speaker".to_string(),
1106 "VENDOR".to_string()
1107 )));
1108 assert!(!is_persisted(&OutputPart::ElementAttachEnd));
1109 assert!(is_persisted(&OutputPart::Text("hi".to_string())));
1110 assert!(is_persisted(&OutputPart::LineRef {
1111 container_idx: 0,
1112 line_idx: 0,
1113 slots: Vec::new(),
1114 flags: LineFlags::empty(),
1115 }));
1116 assert!(is_persisted(&OutputPart::ValueRef(Value::Bool(true))));
1117 assert!(is_persisted(&OutputPart::Newline));
1118 assert!(is_persisted(&OutputPart::Spring));
1119 assert!(is_persisted(&OutputPart::Glue));
1120 assert!(is_persisted(&OutputPart::Tag("t".to_string())));
1121 }
1122
1123 #[test]
1134 fn top_level_and_fragment_part_codec_are_byte_identical() {
1135 let parts = vec![
1136 OutputPart::Text("Hello".to_string()),
1137 OutputPart::LineRef {
1138 container_idx: 3,
1139 line_idx: 9,
1140 slots: vec![Value::Int(1), Value::String(Arc::from("hi"))],
1141 flags: LineFlags::ALL_WS,
1142 },
1143 OutputPart::ValueRef(Value::Bool(true)),
1144 OutputPart::Spring,
1145 OutputPart::Newline,
1146 OutputPart::Glue,
1147 OutputPart::Tag("tag1".to_string()),
1148 OutputPart::Checkpoint, ];
1150
1151 let mut expected = Vec::new();
1156 for part in &parts {
1157 if !matches!(part, OutputPart::Checkpoint) {
1158 encode_part(part, &mut expected);
1159 }
1160 }
1161
1162 let top_level_bytes = write_transcript(&parts, 0, &[]);
1164 let top_level_part_bytes =
1165 &top_level_bytes[HEADER_SIZE + 4..HEADER_SIZE + 4 + expected.len()];
1166 assert_eq!(
1167 top_level_part_bytes,
1168 expected.as_slice(),
1169 "top-level part encoding must match the shared codec exactly"
1170 );
1171
1172 let fragment = crate::output::Fragment {
1175 parts: parts.clone(),
1176 tags: Vec::new(),
1177 };
1178 let fragment_bytes = write_transcript(&[], 0, &[fragment]);
1179 let frag_start = HEADER_SIZE + 4 + 4 + 4;
1180 let fragment_part_bytes = &fragment_bytes[frag_start..frag_start + expected.len()];
1181 assert_eq!(
1182 fragment_part_bytes,
1183 expected.as_slice(),
1184 "fragment part encoding must match the shared codec exactly"
1185 );
1186
1187 let top_level_data = read_transcript(&top_level_bytes).unwrap();
1189 let fragment_data = read_transcript(&fragment_bytes).unwrap();
1190 assert_eq!(top_level_data.parts.len(), 7); assert_eq!(fragment_data.fragments.len(), 1);
1192 assert_eq!(fragment_data.fragments[0].parts.len(), 7);
1193 assert_eq!(top_level_data.parts, fragment_data.fragments[0].parts);
1194 }
1195
1196 #[test]
1201 fn round_trip_value_ref_tower() {
1202 let parts = vec![
1203 OutputPart::ValueRef(Value::Vec3(glam::Vec3::new(1.5, -0.0, 3.0))),
1204 OutputPart::ValueRef(Value::Quat(glam::Quat::from_xyzw(0.5, -0.5, 0.5, 0.5))),
1205 OutputPart::ValueRef(Value::Mat2(glam::Mat2::from_cols_array(&[
1206 1.0, 2.0, 3.0, 4.0,
1207 ]))),
1208 OutputPart::ValueRef(Value::Vec2(glam::Vec2::new(f32::NAN, 7.0))),
1209 ];
1210 let bytes = write_transcript(&parts, 0, &[]);
1211 let data = read_transcript(&bytes).unwrap();
1212 assert_eq!(data.parts.len(), 4);
1213 assert!(
1214 matches!(&data.parts[0], OutputPart::ValueRef(v) if *v == Value::Vec3(glam::Vec3::new(1.5, -0.0, 3.0)))
1215 );
1216 assert!(
1217 matches!(&data.parts[1], OutputPart::ValueRef(v) if *v == Value::Quat(glam::Quat::from_xyzw(0.5, -0.5, 0.5, 0.5)))
1218 );
1219 assert!(
1220 matches!(&data.parts[2], OutputPart::ValueRef(v) if *v == Value::Mat2(glam::Mat2::from_cols_array(&[1.0, 2.0, 3.0, 4.0])))
1221 );
1222 let OutputPart::ValueRef(Value::Vec2(v)) = &data.parts[3] else {
1223 unreachable!("expected vec2 part, got {:?}", data.parts[3]);
1224 };
1225 assert_eq!(v.x.to_bits(), f32::NAN.to_bits(), "NaN lane bits drifted");
1226 assert_eq!(v.y.to_bits(), 7.0f32.to_bits());
1227 }
1228
1229 #[test]
1235 fn round_trip_value_ref_collections() {
1236 use brink_format::{MapKey, OrderedMap};
1237
1238 let map: OrderedMap = [
1239 (MapKey::from("name"), Value::String(Arc::from("goblin"))),
1240 (
1241 MapKey::from(1),
1242 Value::array(vec![Value::Int(10), Value::Int(20)]),
1243 ),
1244 (MapKey::from(true), Value::Bool(false)),
1245 ]
1246 .into_iter()
1247 .collect();
1248 let array = Value::array(vec![
1249 Value::Int(1),
1250 Value::String(Arc::from("two")),
1251 Value::map(map.clone()),
1252 Value::Null,
1253 ]);
1254
1255 let parts = vec![
1256 OutputPart::ValueRef(array.clone()),
1257 OutputPart::ValueRef(Value::map(map.clone())),
1258 ];
1259 let bytes = write_transcript(&parts, 42, &[]);
1260 let data = read_transcript(&bytes).unwrap();
1261
1262 assert_eq!(data.parts.len(), 2);
1263 match &data.parts[0] {
1264 OutputPart::ValueRef(v) => assert_eq!(*v, array),
1265 other => unreachable!("expected ValueRef(array), got {other:?}"),
1266 }
1267 match &data.parts[1] {
1268 OutputPart::ValueRef(v) => assert_eq!(*v, Value::map(map)),
1269 other => unreachable!("expected ValueRef(map), got {other:?}"),
1270 }
1271 }
1272
1273 #[test]
1278 fn round_trip_value_ref_function_values() {
1279 use brink_format::{ClosureEnvEntry, DefinitionId, DefinitionTag, NameId};
1280
1281 let target = DefinitionId::new(DefinitionTag::Address, 7);
1282 let cell = DefinitionId::new(DefinitionTag::Address, 3);
1283 let fn_ref = Value::FnRef(target);
1284 let closure = Value::closure(
1285 target,
1286 vec![
1287 ClosureEnvEntry {
1288 name: NameId(2),
1289 is_ref: true,
1290 payload: Value::VariablePointer(cell),
1291 },
1292 ClosureEnvEntry {
1293 name: NameId(5),
1294 is_ref: false,
1295 payload: Value::Int(41),
1296 },
1297 ],
1298 );
1299
1300 let parts = vec![
1301 OutputPart::ValueRef(fn_ref.clone()),
1302 OutputPart::ValueRef(closure.clone()),
1303 ];
1304 let bytes = write_transcript(&parts, 7, &[]);
1305 let data = read_transcript(&bytes).unwrap();
1306
1307 assert_eq!(data.parts.len(), 2);
1308 match &data.parts[0] {
1309 OutputPart::ValueRef(v) => assert_eq!(*v, fn_ref),
1310 other => unreachable!("expected ValueRef(fn_ref), got {other:?}"),
1311 }
1312 match &data.parts[1] {
1313 OutputPart::ValueRef(v) => assert_eq!(*v, closure),
1314 other => unreachable!("expected ValueRef(closure), got {other:?}"),
1315 }
1316 }
1317
1318 #[test]
1326 fn round_trip_value_ref_handle() {
1327 let handle = Value::handle(NameId(9), u64::MAX);
1328 let nested = Value::array(vec![
1329 Value::handle(NameId(3), 0),
1330 Value::String(Arc::from("goblin")),
1331 ]);
1332
1333 let parts = vec![
1334 OutputPart::ValueRef(handle.clone()),
1335 OutputPart::ValueRef(nested.clone()),
1336 ];
1337 let bytes = write_transcript(&parts, 13, &[]);
1338 let data = read_transcript(&bytes).unwrap();
1339
1340 assert_eq!(data.parts.len(), 2);
1341 match &data.parts[0] {
1342 OutputPart::ValueRef(v) => assert_eq!(*v, handle),
1343 other => unreachable!("expected ValueRef(handle), got {other:?}"),
1344 }
1345 match &data.parts[1] {
1346 OutputPart::ValueRef(v) => assert_eq!(*v, nested),
1347 other => unreachable!("expected ValueRef(nested handle), got {other:?}"),
1348 }
1349 }
1350
1351 #[test]
1357 fn round_trip_value_ref_projection() {
1358 use brink_format::ProjSegment;
1359
1360 let cell = DefinitionId::new(brink_format::DefinitionTag::GlobalVar, 42);
1361 let proj = Value::projection(
1362 cell,
1363 vec![
1364 ProjSegment::Key(Value::String("hp".into())),
1365 ProjSegment::Index(3),
1366 ],
1367 );
1368 let nested = Value::array(vec![Value::projection(cell, vec![]), Value::Bool(true)]);
1369
1370 let parts = vec![
1371 OutputPart::ValueRef(proj.clone()),
1372 OutputPart::ValueRef(nested.clone()),
1373 ];
1374 let bytes = write_transcript(&parts, 13, &[]);
1375 let data = read_transcript(&bytes).unwrap();
1376
1377 assert_eq!(data.parts.len(), 2);
1378 match &data.parts[0] {
1379 OutputPart::ValueRef(v) => assert_eq!(*v, proj),
1380 other => unreachable!("expected ValueRef(projection), got {other:?}"),
1381 }
1382 match &data.parts[1] {
1383 OutputPart::ValueRef(v) => assert_eq!(*v, nested),
1384 other => unreachable!("expected ValueRef(nested projection), got {other:?}"),
1385 }
1386 }
1387
1388 #[test]
1389 fn round_trip_line_ref_with_slots() {
1390 let parts = vec![OutputPart::LineRef {
1391 container_idx: 42,
1392 line_idx: 7,
1393 slots: vec![Value::Int(123), Value::String(Arc::from("hello"))],
1394 flags: LineFlags::ALL_WS | LineFlags::EMPTY,
1395 }];
1396 let bytes = write_transcript(&parts, 1234, &[]);
1397 let data = read_transcript(&bytes).unwrap();
1398 assert_eq!(data.parts.len(), 1);
1399 match &data.parts[0] {
1400 OutputPart::LineRef {
1401 container_idx,
1402 line_idx,
1403 slots,
1404 flags,
1405 } => {
1406 assert_eq!(*container_idx, 42);
1407 assert_eq!(*line_idx, 7);
1408 assert_eq!(slots.len(), 2);
1409 assert!(matches!(&slots[0], Value::Int(123)));
1410 assert!(flags.contains(LineFlags::ALL_WS));
1411 assert!(flags.contains(LineFlags::EMPTY));
1412 }
1413 other => unreachable!("expected LineRef, got {other:?}"),
1414 }
1415 }
1416
1417 #[test]
1418 fn checkpoint_filtered_on_write() {
1419 let parts = vec![
1420 OutputPart::Text("hello".to_string()),
1421 OutputPart::Checkpoint,
1422 OutputPart::Newline,
1423 ];
1424 let bytes = write_transcript(&parts, 0, &[]);
1425 let data = read_transcript(&bytes).unwrap();
1426 assert_eq!(data.parts.len(), 2); assert!(matches!(&data.parts[0], OutputPart::Text(_)));
1428 assert!(matches!(&data.parts[1], OutputPart::Newline));
1429 }
1430
1431 #[test]
1439 fn round_trip_fragment_tags() {
1440 let fragments = vec![
1441 crate::output::Fragment {
1442 parts: vec![OutputPart::Text("hp: 10".to_string())],
1443 tags: vec!["a_tag".to_string(), "b_tag".to_string()],
1444 },
1445 crate::output::Fragment {
1446 parts: vec![OutputPart::Newline],
1447 tags: Vec::new(),
1448 },
1449 ];
1450 let bytes = write_transcript(&[], 0, &fragments);
1451 let data = read_transcript(&bytes).unwrap();
1452
1453 assert_eq!(data.fragments.len(), 2);
1454 assert_eq!(
1455 data.fragments[0].tags,
1456 vec!["a_tag".to_string(), "b_tag".to_string()]
1457 );
1458 assert_eq!(data.fragments[0].parts, fragments[0].parts);
1459 assert!(data.fragments[1].tags.is_empty());
1460 }
1461
1462 #[test]
1470 fn legacy_transcript_without_tag_section_reads_as_empty_tags() {
1471 let mut body = Vec::new();
1472 write_u32(&mut body, 0); write_u32(&mut body, 1); write_u32(&mut body, 1); write_u8(&mut body, TAG_TEXT);
1476 write_str(&mut body, "legacy");
1477 let content_crc = crc32(&body);
1480 let mut bytes = Vec::with_capacity(HEADER_SIZE + body.len());
1481 bytes.extend_from_slice(MAGIC);
1482 write_u16(&mut bytes, VERSION);
1483 write_u16(&mut bytes, 0);
1484 write_u32(&mut bytes, 0xCAFE_BABE);
1485 write_u32(&mut bytes, content_crc);
1486 bytes.extend(body);
1487
1488 let data = read_transcript(&bytes).expect("legacy transcript must still decode");
1489 assert_eq!(data.fragments.len(), 1);
1490 assert!(matches!(&data.fragments[0].parts[0], OutputPart::Text(s) if s == "legacy"));
1491 assert!(data.fragments[0].tags.is_empty());
1492 }
1493
1494 #[test]
1508 fn legacy_transcript_without_fragment_section_reads_as_no_fragments() {
1509 let mut body = Vec::new();
1510 write_u32(&mut body, 1); write_u8(&mut body, TAG_TEXT);
1512 write_str(&mut body, "legacy");
1513 let content_crc = crc32(&body);
1517 let mut bytes = Vec::with_capacity(HEADER_SIZE + body.len());
1518 bytes.extend_from_slice(MAGIC);
1519 write_u16(&mut bytes, VERSION);
1520 write_u16(&mut bytes, 0);
1521 write_u32(&mut bytes, 0xCAFE_BABE);
1522 write_u32(&mut bytes, content_crc);
1523 bytes.extend(body);
1524
1525 let data = read_transcript(&bytes).expect("legacy transcript must still decode");
1526 assert_eq!(data.parts.len(), 1);
1527 assert!(matches!(&data.parts[0], OutputPart::Text(s) if s == "legacy"));
1528 assert!(
1529 data.fragments.is_empty(),
1530 "a pre-fragments `.brkt` must decode with zero fragments, not error: {:?}",
1531 data.fragments
1532 );
1533 }
1534
1535 #[test]
1536 fn invalid_magic_errors() {
1537 let mut bytes = write_transcript(&[], 0, &[]);
1538 bytes[0] = b'X';
1539 assert!(matches!(
1540 read_transcript(&bytes),
1541 Err(TranscriptError::InvalidMagic)
1542 ));
1543 }
1544
1545 #[test]
1546 fn integrity_check_errors() {
1547 let mut bytes = write_transcript(&[OutputPart::Newline], 0, &[]);
1548 if let Some(last) = bytes.last_mut() {
1550 *last ^= 0xFF;
1551 }
1552 assert!(matches!(
1553 read_transcript(&bytes),
1554 Err(TranscriptError::IntegrityCheckFailed)
1555 ));
1556 }
1557
1558 fn nested_array(depth: usize) -> Value {
1574 let mut v = Value::Int(42);
1575 for _ in 0..depth {
1576 v = Value::array(vec![v]);
1577 }
1578 v
1579 }
1580
1581 fn nested_map(depth: usize) -> Value {
1585 use brink_format::{MapKey, OrderedMap};
1586
1587 let mut v = Value::Int(42);
1588 for _ in 0..depth {
1589 let mut map = OrderedMap::with_capacity(1);
1590 map.insert(MapKey::Int(0), v);
1591 v = Value::map(map);
1592 }
1593 v
1594 }
1595
1596 #[test]
1597 fn decode_value_accepts_max_depth_nesting() {
1598 let value = nested_array(MAX_DECODE_DEPTH);
1601 let parts = vec![OutputPart::ValueRef(value.clone())];
1602 let bytes = write_transcript(&parts, 0, &[]);
1603
1604 let data = read_transcript(&bytes).expect("depth exactly at cap must decode");
1605 match &data.parts[0] {
1606 OutputPart::ValueRef(v) => assert_eq!(*v, value),
1607 other => unreachable!("expected ValueRef, got {other:?}"),
1608 }
1609 }
1610
1611 #[test]
1612 fn decode_value_rejects_beyond_max_depth() {
1613 let value = nested_array(MAX_DECODE_DEPTH + 1);
1616 let parts = vec![OutputPart::ValueRef(value)];
1617 let bytes = write_transcript(&parts, 0, &[]);
1618
1619 assert!(matches!(
1620 read_transcript(&bytes),
1621 Err(TranscriptError::MaxDepthExceeded(MAX_DECODE_DEPTH))
1622 ));
1623 }
1624
1625 #[test]
1626 fn decode_value_rejects_deeply_crafted_nesting() {
1627 let value = nested_array(8 * MAX_DECODE_DEPTH);
1635 let parts = vec![OutputPart::ValueRef(value)];
1636 let bytes = write_transcript(&parts, 0, &[]);
1637
1638 assert!(matches!(
1639 read_transcript(&bytes),
1640 Err(TranscriptError::MaxDepthExceeded(MAX_DECODE_DEPTH))
1641 ));
1642 }
1643
1644 #[test]
1647 fn decode_value_accepts_max_depth_map_nesting() {
1648 let value = nested_map(MAX_DECODE_DEPTH);
1651 let parts = vec![OutputPart::ValueRef(value.clone())];
1652 let bytes = write_transcript(&parts, 0, &[]);
1653
1654 let data = read_transcript(&bytes).expect("map depth exactly at cap must decode");
1655 match &data.parts[0] {
1656 OutputPart::ValueRef(v) => assert_eq!(*v, value),
1657 other => unreachable!("expected ValueRef, got {other:?}"),
1658 }
1659 }
1660
1661 #[test]
1662 fn decode_value_rejects_beyond_max_depth_map_nesting() {
1663 let value = nested_map(MAX_DECODE_DEPTH + 1);
1666 let parts = vec![OutputPart::ValueRef(value)];
1667 let bytes = write_transcript(&parts, 0, &[]);
1668
1669 assert!(matches!(
1670 read_transcript(&bytes),
1671 Err(TranscriptError::MaxDepthExceeded(MAX_DECODE_DEPTH))
1672 ));
1673 }
1674
1675 fn duplicate_int_key_map_body() -> Vec<u8> {
1685 let mut body = Vec::new();
1686 write_u32(&mut body, 1); write_u8(&mut body, TAG_VALUE_REF);
1688 write_u8(&mut body, VAL_MAP);
1689 write_u32(&mut body, 2); write_u8(&mut body, VAL_INT);
1691 write_i32(&mut body, 0);
1692 write_u8(&mut body, VAL_INT);
1693 write_i32(&mut body, 1);
1694 write_u8(&mut body, VAL_INT);
1695 write_i32(&mut body, 0);
1696 write_u8(&mut body, VAL_INT);
1697 write_i32(&mut body, 2);
1698 write_u32(&mut body, 0); body
1700 }
1701
1702 fn wrap_body_as_transcript(body: &[u8]) -> Vec<u8> {
1703 let content_crc = crc32(body);
1704 let mut bytes = Vec::with_capacity(HEADER_SIZE + body.len());
1705 bytes.extend_from_slice(MAGIC);
1706 write_u16(&mut bytes, VERSION);
1707 write_u16(&mut bytes, 0);
1708 write_u32(&mut bytes, 0);
1709 write_u32(&mut bytes, content_crc);
1710 bytes.extend_from_slice(body);
1711 bytes
1712 }
1713
1714 #[test]
1715 fn decode_value_rejects_duplicate_map_key() {
1716 let bytes = wrap_body_as_transcript(&duplicate_int_key_map_body());
1717 assert!(matches!(
1718 read_transcript(&bytes),
1719 Err(TranscriptError::DuplicateMapKey)
1720 ));
1721 }
1722
1723 #[test]
1724 fn decode_value_accepts_distinct_map_keys() {
1725 let mut body = Vec::new();
1726 write_u32(&mut body, 1); write_u8(&mut body, TAG_VALUE_REF);
1728 write_u8(&mut body, VAL_MAP);
1729 write_u32(&mut body, 2); write_u8(&mut body, VAL_INT);
1731 write_i32(&mut body, 0);
1732 write_u8(&mut body, VAL_INT);
1733 write_i32(&mut body, 1);
1734 write_u8(&mut body, VAL_INT);
1735 write_i32(&mut body, 5);
1736 write_u8(&mut body, VAL_INT);
1737 write_i32(&mut body, 2);
1738 write_u32(&mut body, 0); let bytes = wrap_body_as_transcript(&body);
1741 let data = read_transcript(&bytes).expect("distinct keys must decode cleanly");
1742 match &data.parts[0] {
1743 OutputPart::ValueRef(Value::Map(map)) => assert_eq!(map.len(), 2),
1744 other => unreachable!("expected ValueRef(map), got {other:?}"),
1745 }
1746 }
1747}