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::Fragments,
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.iter() {
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.iter() {
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: crate::output::Fragments,
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: crate::output::Fragments::from(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::Fragments,
366) -> Vec<(String, Vec<String>)> {
367 resolve_lines(parts, program, line_tables, resolver, fragments)
373 .into_iter()
374 .map(|(text, tags, _element, _source)| (text, tags))
375 .collect()
376}
377
378pub fn render_transcript_with_source(
384 parts: &[OutputPart],
385 program: &Program,
386 line_tables: &[Vec<brink_format::LineEntry>],
387 resolver: Option<&dyn brink_format::PluralResolver>,
388 fragments: &crate::output::Fragments,
389) -> Vec<(String, Vec<String>, Option<brink_format::SourceLocation>)> {
390 resolve_lines(parts, program, line_tables, resolver, fragments)
391 .into_iter()
392 .map(|(text, tags, _element, source)| (text, tags, source))
393 .collect()
394}
395
396fn is_persisted(part: &OutputPart) -> bool {
420 !matches!(
421 part,
422 OutputPart::Checkpoint | OutputPart::ElementAttach(..) | OutputPart::ElementAttachEnd
423 )
424}
425
426#[expect(clippy::cast_possible_truncation)]
431fn encode_part(part: &OutputPart, buf: &mut Vec<u8>) {
432 match part {
433 OutputPart::Text(s) => {
434 write_u8(buf, TAG_TEXT);
435 write_str(buf, s);
436 }
437 OutputPart::LineRef {
438 container_idx,
439 line_idx,
440 slots,
441 flags,
442 } => {
443 write_u8(buf, TAG_LINE_REF);
444 write_u32(buf, *container_idx);
445 write_u16(buf, *line_idx);
446 write_u8(buf, flags.bits());
447 write_u16(buf, slots.len() as u16);
448 for val in slots {
449 encode_value(val, buf);
450 }
451 }
452 OutputPart::ValueRef(val) => {
453 write_u8(buf, TAG_VALUE_REF);
454 encode_value(val, buf);
455 }
456 OutputPart::Newline => write_u8(buf, TAG_NEWLINE),
457 OutputPart::Spring => write_u8(buf, TAG_SPRING),
458 OutputPart::Glue => write_u8(buf, TAG_GLUE),
459 OutputPart::Tag(s) => {
460 write_u8(buf, TAG_TAG);
461 write_str(buf, s);
462 }
463 OutputPart::Checkpoint | OutputPart::ElementAttach(..) | OutputPart::ElementAttachEnd => {}
469 }
470}
471
472fn decode_part(bytes: &[u8], off: &mut usize) -> Result<OutputPart, TranscriptError> {
475 let tag = read_u8(bytes, off)?;
476 let part = match tag {
477 TAG_TEXT => OutputPart::Text(read_str(bytes, off)?),
478 TAG_LINE_REF => {
479 let container_idx = read_u32(bytes, off)?;
480 let line_idx = read_u16(bytes, off)?;
481 let flags_bits = read_u8(bytes, off)?;
482 let flags = LineFlags::from_bits_truncate(flags_bits);
483 let slot_count = read_u16(bytes, off)? as usize;
484 let mut slots = Vec::with_capacity(slot_count);
485 for _ in 0..slot_count {
486 slots.push(decode_value(bytes, off, 0)?);
487 }
488 OutputPart::LineRef {
489 container_idx,
490 line_idx,
491 slots,
492 flags,
493 }
494 }
495 TAG_VALUE_REF => OutputPart::ValueRef(decode_value(bytes, off, 0)?),
496 TAG_NEWLINE => OutputPart::Newline,
497 TAG_SPRING => OutputPart::Spring,
498 TAG_GLUE => OutputPart::Glue,
499 TAG_TAG => OutputPart::Tag(read_str(bytes, off)?),
500 _ => return Err(TranscriptError::InvalidPartTag(tag)),
501 };
502 Ok(part)
503}
504
505fn write_u8(buf: &mut Vec<u8>, v: u8) {
508 buf.push(v);
509}
510
511fn write_u16(buf: &mut Vec<u8>, v: u16) {
512 buf.extend_from_slice(&v.to_le_bytes());
513}
514
515fn write_u32(buf: &mut Vec<u8>, v: u32) {
516 buf.extend_from_slice(&v.to_le_bytes());
517}
518
519fn write_u64(buf: &mut Vec<u8>, v: u64) {
520 buf.extend_from_slice(&v.to_le_bytes());
521}
522
523fn write_i32(buf: &mut Vec<u8>, v: i32) {
524 buf.extend_from_slice(&v.to_le_bytes());
525}
526
527#[expect(clippy::cast_possible_truncation)]
528fn write_str(buf: &mut Vec<u8>, s: &str) {
529 write_u32(buf, s.len() as u32);
530 buf.extend_from_slice(s.as_bytes());
531}
532
533fn write_def_id(buf: &mut Vec<u8>, id: DefinitionId) {
534 write_u64(buf, id.to_raw());
535}
536
537fn read_u8(buf: &[u8], off: &mut usize) -> Result<u8, TranscriptError> {
538 if *off >= buf.len() {
539 return Err(TranscriptError::UnexpectedEof);
540 }
541 let v = buf[*off];
542 *off += 1;
543 Ok(v)
544}
545
546fn read_u16(buf: &[u8], off: &mut usize) -> Result<u16, TranscriptError> {
547 if *off + 2 > buf.len() {
548 return Err(TranscriptError::UnexpectedEof);
549 }
550 let v = u16::from_le_bytes([buf[*off], buf[*off + 1]]);
551 *off += 2;
552 Ok(v)
553}
554
555fn read_u32(buf: &[u8], off: &mut usize) -> Result<u32, TranscriptError> {
556 if *off + 4 > buf.len() {
557 return Err(TranscriptError::UnexpectedEof);
558 }
559 let v = u32::from_le_bytes([buf[*off], buf[*off + 1], buf[*off + 2], buf[*off + 3]]);
560 *off += 4;
561 Ok(v)
562}
563
564fn read_i32(buf: &[u8], off: &mut usize) -> Result<i32, TranscriptError> {
565 if *off + 4 > buf.len() {
566 return Err(TranscriptError::UnexpectedEof);
567 }
568 let v = i32::from_le_bytes([buf[*off], buf[*off + 1], buf[*off + 2], buf[*off + 3]]);
569 *off += 4;
570 Ok(v)
571}
572
573fn read_f32(buf: &[u8], off: &mut usize) -> Result<f32, TranscriptError> {
574 if *off + 4 > buf.len() {
575 return Err(TranscriptError::UnexpectedEof);
576 }
577 let v = f32::from_le_bytes([buf[*off], buf[*off + 1], buf[*off + 2], buf[*off + 3]]);
578 *off += 4;
579 Ok(v)
580}
581
582fn read_u64(buf: &[u8], off: &mut usize) -> Result<u64, TranscriptError> {
583 if *off + 8 > buf.len() {
584 return Err(TranscriptError::UnexpectedEof);
585 }
586 let v = u64::from_le_bytes([
587 buf[*off],
588 buf[*off + 1],
589 buf[*off + 2],
590 buf[*off + 3],
591 buf[*off + 4],
592 buf[*off + 5],
593 buf[*off + 6],
594 buf[*off + 7],
595 ]);
596 *off += 8;
597 Ok(v)
598}
599
600fn read_str(buf: &[u8], off: &mut usize) -> Result<String, TranscriptError> {
601 let len = read_u32(buf, off)? as usize;
602 if *off + len > buf.len() {
603 return Err(TranscriptError::UnexpectedEof);
604 }
605 let bytes = &buf[*off..*off + len];
606 *off += len;
607 String::from_utf8(bytes.to_vec()).map_err(|_| TranscriptError::InvalidUtf8)
608}
609
610fn read_def_id(buf: &[u8], off: &mut usize) -> Result<DefinitionId, TranscriptError> {
611 let raw = read_u64(buf, off)?;
612 DefinitionId::from_raw(raw).ok_or(TranscriptError::InvalidDefinitionId)
613}
614
615#[expect(clippy::cast_possible_truncation)]
618#[expect(
619 clippy::too_many_lines,
620 reason = "one match arm per value tag — T1e's VAL_PROJECTION arm pushed this past 100"
621)]
622fn encode_value(v: &Value, buf: &mut Vec<u8>) {
623 match v {
624 Value::Int(n) => {
625 write_u8(buf, VAL_INT);
626 write_i32(buf, *n);
627 }
628 Value::Float(n) => {
629 write_u8(buf, VAL_FLOAT);
630 buf.extend_from_slice(&n.to_le_bytes());
631 }
632 Value::Bool(b) => {
633 write_u8(buf, VAL_BOOL);
634 write_u8(buf, u8::from(*b));
635 }
636 Value::String(s) => {
637 write_u8(buf, VAL_STRING);
638 write_str(buf, s);
639 }
640 Value::List(lv) => {
641 write_u8(buf, VAL_LIST);
642 write_u32(buf, lv.items.len() as u32);
643 for item in &lv.items {
644 write_def_id(buf, *item);
645 }
646 write_u32(buf, lv.origins.len() as u32);
647 for origin in &lv.origins {
648 write_def_id(buf, *origin);
649 }
650 }
651 Value::DivertTarget(id) => {
652 write_u8(buf, VAL_DIVERT_TARGET);
653 write_def_id(buf, *id);
654 }
655 Value::VariablePointer(id) => {
656 write_u8(buf, VAL_VAR_POINTER);
657 write_def_id(buf, *id);
658 }
659 Value::FragmentRef(idx) => {
660 write_u8(buf, VAL_FRAGMENT_REF);
661 write_u32(buf, *idx);
662 }
663 Value::TempPointer { .. } | Value::Null => {
665 write_u8(buf, VAL_NULL);
666 }
667 Value::Array(items) => {
671 write_u8(buf, VAL_ARRAY);
672 write_u32(buf, items.len() as u32);
673 for item in items.iter() {
674 encode_value(item, buf);
675 }
676 }
677 Value::Map(map) => {
678 write_u8(buf, VAL_MAP);
679 write_u32(buf, map.len() as u32);
680 for (key, val) in map.iter() {
681 encode_map_key(key, buf);
682 encode_value(val, buf);
683 }
684 }
685 Value::Record { shape, fields } => {
686 write_u8(buf, VAL_RECORD);
687 write_u32(buf, shape.0);
688 write_u32(buf, fields.len() as u32);
689 for field in fields.iter() {
690 encode_value(field, buf);
691 }
692 }
693 Value::FnRef(target) => {
697 write_u8(buf, VAL_FN_REF);
698 write_def_id(buf, *target);
699 }
700 Value::Closure(c) => {
701 write_u8(buf, VAL_CLOSURE);
702 write_def_id(buf, c.target);
703 write_u32(buf, c.env.len() as u32);
704 for entry in &c.env {
705 write_u16(buf, entry.name.0);
706 write_u8(buf, u8::from(entry.is_ref));
707 encode_value(&entry.payload, buf);
708 }
709 }
710 Value::Handle { kind, id } => {
715 write_u8(buf, VAL_HANDLE);
716 write_u16(buf, kind.0);
717 write_u64(buf, *id);
718 }
719 Value::Projection(p) => {
723 write_u8(buf, VAL_PROJECTION);
724 write_def_id(buf, p.cell);
725 write_u8(buf, p.segments.len() as u8);
726 for seg in &p.segments {
727 match seg {
728 brink_format::ProjSegment::Index(n) => {
729 write_u8(buf, PROJ_SEG_INDEX);
730 write_i32(buf, *n);
731 }
732 brink_format::ProjSegment::Key(v) => {
733 write_u8(buf, PROJ_SEG_KEY);
734 encode_value(v, buf);
735 }
736 }
737 }
738 }
739 Value::OptionVal(inner) => {
743 write_u8(buf, VAL_OPTION);
744 match inner {
745 None => write_u8(buf, 0),
746 Some(v) => {
747 write_u8(buf, 1);
748 encode_value(v, buf);
749 }
750 }
751 }
752 Value::Range {
758 start,
759 end,
760 inclusive,
761 } => {
762 write_u8(buf, VAL_RANGE);
763 write_i32(buf, *start);
764 write_i32(buf, *end);
765 write_u8(buf, u8::from(*inclusive));
766 }
767 Value::Vec2(v) => {
771 write_u8(buf, VAL_VEC2);
772 write_f32_lanes(buf, &v.to_array());
773 }
774 Value::Vec3(v) => {
775 write_u8(buf, VAL_VEC3);
776 write_f32_lanes(buf, &v.to_array());
777 }
778 Value::Vec4(v) => {
779 write_u8(buf, VAL_VEC4);
780 write_f32_lanes(buf, &v.to_array());
781 }
782 Value::Quat(q) => {
783 write_u8(buf, VAL_QUAT);
784 write_f32_lanes(buf, &q.to_array());
785 }
786 Value::Mat2(m) => {
787 write_u8(buf, VAL_MAT2);
788 write_f32_lanes(buf, &m.to_cols_array());
789 }
790 Value::Mat3(m) => {
791 write_u8(buf, VAL_MAT3);
792 write_f32_lanes(buf, &m.to_cols_array());
793 }
794 Value::Mat4(m) => {
795 write_u8(buf, VAL_MAT4);
796 write_f32_lanes(buf, &m.to_cols_array());
797 }
798 Value::Weighted(w) => {
799 write_u8(buf, VAL_WEIGHTED);
800 write_u32(buf, w.entries.len() as u32);
801 for (weight, value) in &w.entries {
802 write_i32(buf, *weight);
803 encode_value(value, buf);
804 }
805 }
806 }
807}
808
809fn write_f32_lanes(buf: &mut Vec<u8>, lanes: &[f32]) {
813 for lane in lanes {
814 buf.extend_from_slice(&lane.to_le_bytes());
815 }
816}
817
818fn read_f32_lanes<const N: usize>(
822 buf: &[u8],
823 off: &mut usize,
824) -> Result<[f32; N], TranscriptError> {
825 let mut lanes = [0.0f32; N];
826 for lane in &mut lanes {
827 *lane = read_f32(buf, off)?;
828 }
829 Ok(lanes)
830}
831
832fn encode_map_key(key: &MapKey, buf: &mut Vec<u8>) {
836 match key {
837 MapKey::Int(n) => {
838 write_u8(buf, VAL_INT);
839 write_i32(buf, *n);
840 }
841 MapKey::Str(s) => {
842 write_u8(buf, VAL_STRING);
843 write_str(buf, s);
844 }
845 MapKey::Bool(b) => {
846 write_u8(buf, VAL_BOOL);
847 write_u8(buf, u8::from(*b));
848 }
849 }
850}
851
852#[expect(
853 clippy::too_many_lines,
854 reason = "one match arm per value tag — T1e's VAL_PROJECTION arm pushed this past 100"
855)]
856fn decode_value(buf: &[u8], off: &mut usize, depth: usize) -> Result<Value, TranscriptError> {
857 if depth > MAX_DECODE_DEPTH {
858 return Err(TranscriptError::MaxDepthExceeded(MAX_DECODE_DEPTH));
859 }
860 let tag = read_u8(buf, off)?;
861 match tag {
862 VAL_INT => Ok(Value::Int(read_i32(buf, off)?)),
863 VAL_FLOAT => Ok(Value::Float(read_f32(buf, off)?)),
864 VAL_BOOL => {
865 let b = read_u8(buf, off)?;
866 Ok(Value::Bool(b != 0))
867 }
868 VAL_STRING => {
869 let s = read_str(buf, off)?;
870 Ok(Value::String(Arc::from(s.as_str())))
871 }
872 VAL_LIST => {
873 let item_count = read_u32(buf, off)? as usize;
874 let mut items = Vec::with_capacity(item_count);
875 for _ in 0..item_count {
876 items.push(read_def_id(buf, off)?);
877 }
878 let origin_count = read_u32(buf, off)? as usize;
879 let mut origins = Vec::with_capacity(origin_count);
880 for _ in 0..origin_count {
881 origins.push(read_def_id(buf, off)?);
882 }
883 Ok(Value::List(Arc::new(brink_format::ListValue {
884 items,
885 origins,
886 })))
887 }
888 VAL_DIVERT_TARGET => {
889 let id = read_def_id(buf, off)?;
890 Ok(Value::DivertTarget(id))
891 }
892 VAL_VAR_POINTER => {
893 let id = read_def_id(buf, off)?;
894 Ok(Value::VariablePointer(id))
895 }
896 VAL_FRAGMENT_REF => Ok(Value::FragmentRef(read_u32(buf, off)?)),
897 VAL_NULL => Ok(Value::Null),
898 VAL_ARRAY => {
899 let len = read_u32(buf, off)? as usize;
900 let mut items = Vec::with_capacity(len.min(buf.len().saturating_sub(*off)));
901 for _ in 0..len {
902 items.push(decode_value(buf, off, depth + 1)?);
903 }
904 Ok(Value::array(items))
905 }
906 VAL_MAP => {
907 let len = read_u32(buf, off)? as usize;
908 let mut map = OrderedMap::with_capacity(len.min(buf.len().saturating_sub(*off)));
909 for _ in 0..len {
910 let key = decode_map_key(buf, off)?;
911 let val = decode_value(buf, off, depth + 1)?;
912 if map.contains_key(&key) {
916 return Err(TranscriptError::DuplicateMapKey);
917 }
918 map.insert(key, val);
919 }
920 Ok(Value::map(map))
921 }
922 VAL_RECORD => {
923 let shape = brink_format::ShapeId(read_u32(buf, off)?);
924 let len = read_u32(buf, off)? as usize;
925 let mut fields = Vec::with_capacity(len.min(buf.len().saturating_sub(*off)));
926 for _ in 0..len {
927 fields.push(decode_value(buf, off, depth + 1)?);
928 }
929 Ok(Value::record(shape, fields))
930 }
931 VAL_FN_REF => Ok(Value::FnRef(read_def_id(buf, off)?)),
932 VAL_CLOSURE => {
933 let target = read_def_id(buf, off)?;
934 let count = read_u32(buf, off)? as usize;
935 let mut env = Vec::with_capacity(count.min(buf.len().saturating_sub(*off)));
936 for _ in 0..count {
937 let name = brink_format::NameId(read_u16(buf, off)?);
938 let is_ref = read_u8(buf, off)? != 0;
939 let payload = decode_value(buf, off, depth + 1)?;
940 env.push(brink_format::ClosureEnvEntry {
941 name,
942 is_ref,
943 payload,
944 });
945 }
946 Ok(Value::closure(target, env))
947 }
948 VAL_HANDLE => {
950 let kind = NameId(read_u16(buf, off)?);
951 let id = read_u64(buf, off)?;
952 Ok(Value::handle(kind, id))
953 }
954 VAL_PROJECTION => {
956 let cell = read_def_id(buf, off)?;
957 let count = read_u8(buf, off)? as usize;
958 let mut segments = Vec::with_capacity(count.min(buf.len().saturating_sub(*off)));
959 for _ in 0..count {
960 let kind = read_u8(buf, off)?;
961 let seg = match kind {
962 PROJ_SEG_INDEX => brink_format::ProjSegment::Index(read_i32(buf, off)?),
963 PROJ_SEG_KEY => {
964 brink_format::ProjSegment::Key(decode_value(buf, off, depth + 1)?)
965 }
966 other => return Err(TranscriptError::InvalidValueTag(other)),
967 };
968 segments.push(seg);
969 }
970 Ok(Value::projection(cell, segments))
971 }
972 VAL_OPTION => match read_u8(buf, off)? {
975 0 => Ok(Value::none()),
976 1 => Ok(Value::some(decode_value(buf, off, depth + 1)?)),
977 other => Err(TranscriptError::InvalidValueTag(other)),
978 },
979 VAL_RANGE => {
982 let start = read_i32(buf, off)?;
983 let end = read_i32(buf, off)?;
984 let inclusive = match read_u8(buf, off)? {
985 0 => false,
986 1 => true,
987 other => return Err(TranscriptError::InvalidValueTag(other)),
988 };
989 Ok(Value::range(start, end, inclusive))
990 }
991 VAL_VEC2 => Ok(Value::Vec2(glam::Vec2::from_array(read_f32_lanes::<2>(
995 buf, off,
996 )?))),
997 VAL_VEC3 => Ok(Value::Vec3(glam::Vec3::from_array(read_f32_lanes::<3>(
998 buf, off,
999 )?))),
1000 VAL_VEC4 => Ok(Value::Vec4(glam::Vec4::from_array(read_f32_lanes::<4>(
1001 buf, off,
1002 )?))),
1003 VAL_QUAT => Ok(Value::Quat(glam::Quat::from_array(read_f32_lanes::<4>(
1004 buf, off,
1005 )?))),
1006 VAL_MAT2 => Ok(Value::Mat2(glam::Mat2::from_cols_array(&read_f32_lanes::<
1007 4,
1008 >(
1009 buf, off
1010 )?))),
1011 VAL_MAT3 => Ok(Value::Mat3(glam::Mat3::from_cols_array(&read_f32_lanes::<
1012 9,
1013 >(
1014 buf, off
1015 )?))),
1016 VAL_MAT4 => Ok(Value::Mat4(glam::Mat4::from_cols_array(&read_f32_lanes::<
1017 16,
1018 >(
1019 buf, off
1020 )?))),
1021 VAL_WEIGHTED => {
1024 let count = read_u32(buf, off)? as usize;
1025 if count == 0 {
1026 return Err(TranscriptError::InvalidValueTag(VAL_WEIGHTED));
1027 }
1028 let mut entries = Vec::with_capacity(count.min(1024));
1029 for _ in 0..count {
1030 let weight = read_i32(buf, off)?;
1031 if weight < 1 {
1032 return Err(TranscriptError::InvalidValueTag(VAL_WEIGHTED));
1033 }
1034 let value = decode_value(buf, off, depth + 1)?;
1035 entries.push((weight, value));
1036 }
1037 Ok(Value::weighted(entries))
1038 }
1039 _ => Err(TranscriptError::InvalidValueTag(tag)),
1040 }
1041}
1042
1043fn decode_map_key(buf: &[u8], off: &mut usize) -> Result<MapKey, TranscriptError> {
1047 let tag = read_u8(buf, off)?;
1048 match tag {
1049 VAL_INT => Ok(MapKey::Int(read_i32(buf, off)?)),
1050 VAL_STRING => Ok(MapKey::Str(Arc::from(read_str(buf, off)?.as_str()))),
1051 VAL_BOOL => Ok(MapKey::Bool(read_u8(buf, off)? != 0)),
1052 _ => Err(TranscriptError::InvalidValueTag(tag)),
1053 }
1054}
1055
1056fn crc32(data: &[u8]) -> u32 {
1059 static TABLE: [u32; 256] = {
1060 let mut table = [0u32; 256];
1061 let mut i = 0u32;
1062 while i < 256 {
1063 let mut crc = i;
1064 let mut j = 0;
1065 while j < 8 {
1066 if crc & 1 != 0 {
1067 crc = (crc >> 1) ^ 0xEDB8_8320;
1068 } else {
1069 crc >>= 1;
1070 }
1071 j += 1;
1072 }
1073 table[i as usize] = crc;
1074 i += 1;
1075 }
1076 table
1077 };
1078
1079 let mut crc = 0xFFFF_FFFFu32;
1080 for &byte in data {
1081 let idx = ((crc ^ u32::from(byte)) & 0xFF) as usize;
1082 crc = (crc >> 8) ^ TABLE[idx];
1083 }
1084 crc ^ 0xFFFF_FFFF
1085}
1086
1087#[cfg(test)]
1088mod tests {
1089 use super::*;
1090 use brink_format::LineFlags;
1091
1092 #[test]
1093 fn round_trip_simple_parts() {
1094 let parts = vec![
1095 OutputPart::Text("Hello".to_string()),
1096 OutputPart::Spring,
1097 OutputPart::Newline,
1098 OutputPart::Tag("tag1".to_string()),
1099 OutputPart::Glue,
1100 ];
1101 let bytes = write_transcript(&parts, 0xDEAD_BEEF, &crate::output::Fragments::default());
1102 let data = read_transcript(&bytes).unwrap();
1103 assert_eq!(data.source_checksum, 0xDEAD_BEEF);
1104 assert_eq!(data.parts.len(), 5);
1105 assert!(matches!(&data.parts[0], OutputPart::Text(s) if s == "Hello"));
1106 assert!(matches!(&data.parts[1], OutputPart::Spring));
1107 assert!(matches!(&data.parts[2], OutputPart::Newline));
1108 assert!(matches!(&data.parts[3], OutputPart::Tag(s) if s == "tag1"));
1109 assert!(matches!(&data.parts[4], OutputPart::Glue));
1110 }
1111
1112 #[test]
1120 fn is_persisted_filters_transient_markers_only() {
1121 assert!(!is_persisted(&OutputPart::Checkpoint));
1122 assert!(!is_persisted(&OutputPart::ElementAttach(
1123 "speaker".to_string(),
1124 "VENDOR".to_string()
1125 )));
1126 assert!(!is_persisted(&OutputPart::ElementAttachEnd));
1127 assert!(is_persisted(&OutputPart::Text("hi".to_string())));
1128 assert!(is_persisted(&OutputPart::LineRef {
1129 container_idx: 0,
1130 line_idx: 0,
1131 slots: Vec::new(),
1132 flags: LineFlags::empty(),
1133 }));
1134 assert!(is_persisted(&OutputPart::ValueRef(Value::Bool(true))));
1135 assert!(is_persisted(&OutputPart::Newline));
1136 assert!(is_persisted(&OutputPart::Spring));
1137 assert!(is_persisted(&OutputPart::Glue));
1138 assert!(is_persisted(&OutputPart::Tag("t".to_string())));
1139 }
1140
1141 #[test]
1152 fn top_level_and_fragment_part_codec_are_byte_identical() {
1153 let parts = vec![
1154 OutputPart::Text("Hello".to_string()),
1155 OutputPart::LineRef {
1156 container_idx: 3,
1157 line_idx: 9,
1158 slots: vec![Value::Int(1), Value::String(Arc::from("hi"))],
1159 flags: LineFlags::ALL_WS,
1160 },
1161 OutputPart::ValueRef(Value::Bool(true)),
1162 OutputPart::Spring,
1163 OutputPart::Newline,
1164 OutputPart::Glue,
1165 OutputPart::Tag("tag1".to_string()),
1166 OutputPart::Checkpoint, ];
1168
1169 let mut expected = Vec::new();
1174 for part in &parts {
1175 if !matches!(part, OutputPart::Checkpoint) {
1176 encode_part(part, &mut expected);
1177 }
1178 }
1179
1180 let top_level_bytes = write_transcript(&parts, 0, &crate::output::Fragments::default());
1182 let top_level_part_bytes =
1183 &top_level_bytes[HEADER_SIZE + 4..HEADER_SIZE + 4 + expected.len()];
1184 assert_eq!(
1185 top_level_part_bytes,
1186 expected.as_slice(),
1187 "top-level part encoding must match the shared codec exactly"
1188 );
1189
1190 let fragment = crate::output::Fragment {
1193 parts: parts.clone(),
1194 tags: Vec::new(),
1195 };
1196 let fragment_bytes =
1197 write_transcript(&[], 0, &crate::output::Fragments::from(vec![fragment]));
1198 let frag_start = HEADER_SIZE + 4 + 4 + 4;
1199 let fragment_part_bytes = &fragment_bytes[frag_start..frag_start + expected.len()];
1200 assert_eq!(
1201 fragment_part_bytes,
1202 expected.as_slice(),
1203 "fragment part encoding must match the shared codec exactly"
1204 );
1205
1206 let top_level_data = read_transcript(&top_level_bytes).unwrap();
1208 let fragment_data = read_transcript(&fragment_bytes).unwrap();
1209 assert_eq!(top_level_data.parts.len(), 7); assert_eq!(fragment_data.fragments.len(), 1);
1211 let fragment_parts = fragment_data.fragments.parts(0).unwrap();
1212 assert_eq!(fragment_parts.len(), 7);
1213 assert_eq!(top_level_data.parts, fragment_parts);
1214 }
1215
1216 #[test]
1221 fn round_trip_value_ref_tower() {
1222 let parts = vec![
1223 OutputPart::ValueRef(Value::Vec3(glam::Vec3::new(1.5, -0.0, 3.0))),
1224 OutputPart::ValueRef(Value::Quat(glam::Quat::from_xyzw(0.5, -0.5, 0.5, 0.5))),
1225 OutputPart::ValueRef(Value::Mat2(glam::Mat2::from_cols_array(&[
1226 1.0, 2.0, 3.0, 4.0,
1227 ]))),
1228 OutputPart::ValueRef(Value::Vec2(glam::Vec2::new(f32::NAN, 7.0))),
1229 ];
1230 let bytes = write_transcript(&parts, 0, &crate::output::Fragments::default());
1231 let data = read_transcript(&bytes).unwrap();
1232 assert_eq!(data.parts.len(), 4);
1233 assert!(
1234 matches!(&data.parts[0], OutputPart::ValueRef(v) if *v == Value::Vec3(glam::Vec3::new(1.5, -0.0, 3.0)))
1235 );
1236 assert!(
1237 matches!(&data.parts[1], OutputPart::ValueRef(v) if *v == Value::Quat(glam::Quat::from_xyzw(0.5, -0.5, 0.5, 0.5)))
1238 );
1239 assert!(
1240 matches!(&data.parts[2], OutputPart::ValueRef(v) if *v == Value::Mat2(glam::Mat2::from_cols_array(&[1.0, 2.0, 3.0, 4.0])))
1241 );
1242 let OutputPart::ValueRef(Value::Vec2(v)) = &data.parts[3] else {
1243 unreachable!("expected vec2 part, got {:?}", data.parts[3]);
1244 };
1245 assert_eq!(v.x.to_bits(), f32::NAN.to_bits(), "NaN lane bits drifted");
1246 assert_eq!(v.y.to_bits(), 7.0f32.to_bits());
1247 }
1248
1249 #[test]
1255 fn round_trip_value_ref_collections() {
1256 use brink_format::{MapKey, OrderedMap};
1257
1258 let map: OrderedMap = [
1259 (MapKey::from("name"), Value::String(Arc::from("goblin"))),
1260 (
1261 MapKey::from(1),
1262 Value::array(vec![Value::Int(10), Value::Int(20)]),
1263 ),
1264 (MapKey::from(true), Value::Bool(false)),
1265 ]
1266 .into_iter()
1267 .collect();
1268 let array = Value::array(vec![
1269 Value::Int(1),
1270 Value::String(Arc::from("two")),
1271 Value::map(map.clone()),
1272 Value::Null,
1273 ]);
1274
1275 let parts = vec![
1276 OutputPart::ValueRef(array.clone()),
1277 OutputPart::ValueRef(Value::map(map.clone())),
1278 ];
1279 let bytes = write_transcript(&parts, 42, &crate::output::Fragments::default());
1280 let data = read_transcript(&bytes).unwrap();
1281
1282 assert_eq!(data.parts.len(), 2);
1283 match &data.parts[0] {
1284 OutputPart::ValueRef(v) => assert_eq!(*v, array),
1285 other => unreachable!("expected ValueRef(array), got {other:?}"),
1286 }
1287 match &data.parts[1] {
1288 OutputPart::ValueRef(v) => assert_eq!(*v, Value::map(map)),
1289 other => unreachable!("expected ValueRef(map), got {other:?}"),
1290 }
1291 }
1292
1293 #[test]
1298 fn round_trip_value_ref_function_values() {
1299 use brink_format::{ClosureEnvEntry, DefinitionId, DefinitionTag, NameId};
1300
1301 let target = DefinitionId::new(DefinitionTag::Address, 7);
1302 let cell = DefinitionId::new(DefinitionTag::Address, 3);
1303 let fn_ref = Value::FnRef(target);
1304 let closure = Value::closure(
1305 target,
1306 vec![
1307 ClosureEnvEntry {
1308 name: NameId(2),
1309 is_ref: true,
1310 payload: Value::VariablePointer(cell),
1311 },
1312 ClosureEnvEntry {
1313 name: NameId(5),
1314 is_ref: false,
1315 payload: Value::Int(41),
1316 },
1317 ],
1318 );
1319
1320 let parts = vec![
1321 OutputPart::ValueRef(fn_ref.clone()),
1322 OutputPart::ValueRef(closure.clone()),
1323 ];
1324 let bytes = write_transcript(&parts, 7, &crate::output::Fragments::default());
1325 let data = read_transcript(&bytes).unwrap();
1326
1327 assert_eq!(data.parts.len(), 2);
1328 match &data.parts[0] {
1329 OutputPart::ValueRef(v) => assert_eq!(*v, fn_ref),
1330 other => unreachable!("expected ValueRef(fn_ref), got {other:?}"),
1331 }
1332 match &data.parts[1] {
1333 OutputPart::ValueRef(v) => assert_eq!(*v, closure),
1334 other => unreachable!("expected ValueRef(closure), got {other:?}"),
1335 }
1336 }
1337
1338 #[test]
1346 fn round_trip_value_ref_handle() {
1347 let handle = Value::handle(NameId(9), u64::MAX);
1348 let nested = Value::array(vec![
1349 Value::handle(NameId(3), 0),
1350 Value::String(Arc::from("goblin")),
1351 ]);
1352
1353 let parts = vec![
1354 OutputPart::ValueRef(handle.clone()),
1355 OutputPart::ValueRef(nested.clone()),
1356 ];
1357 let bytes = write_transcript(&parts, 13, &crate::output::Fragments::default());
1358 let data = read_transcript(&bytes).unwrap();
1359
1360 assert_eq!(data.parts.len(), 2);
1361 match &data.parts[0] {
1362 OutputPart::ValueRef(v) => assert_eq!(*v, handle),
1363 other => unreachable!("expected ValueRef(handle), got {other:?}"),
1364 }
1365 match &data.parts[1] {
1366 OutputPart::ValueRef(v) => assert_eq!(*v, nested),
1367 other => unreachable!("expected ValueRef(nested handle), got {other:?}"),
1368 }
1369 }
1370
1371 #[test]
1377 fn round_trip_value_ref_projection() {
1378 use brink_format::ProjSegment;
1379
1380 let cell = DefinitionId::new(brink_format::DefinitionTag::GlobalVar, 42);
1381 let proj = Value::projection(
1382 cell,
1383 vec![
1384 ProjSegment::Key(Value::String("hp".into())),
1385 ProjSegment::Index(3),
1386 ],
1387 );
1388 let nested = Value::array(vec![Value::projection(cell, vec![]), Value::Bool(true)]);
1389
1390 let parts = vec![
1391 OutputPart::ValueRef(proj.clone()),
1392 OutputPart::ValueRef(nested.clone()),
1393 ];
1394 let bytes = write_transcript(&parts, 13, &crate::output::Fragments::default());
1395 let data = read_transcript(&bytes).unwrap();
1396
1397 assert_eq!(data.parts.len(), 2);
1398 match &data.parts[0] {
1399 OutputPart::ValueRef(v) => assert_eq!(*v, proj),
1400 other => unreachable!("expected ValueRef(projection), got {other:?}"),
1401 }
1402 match &data.parts[1] {
1403 OutputPart::ValueRef(v) => assert_eq!(*v, nested),
1404 other => unreachable!("expected ValueRef(nested projection), got {other:?}"),
1405 }
1406 }
1407
1408 #[test]
1409 fn round_trip_line_ref_with_slots() {
1410 let parts = vec![OutputPart::LineRef {
1411 container_idx: 42,
1412 line_idx: 7,
1413 slots: vec![Value::Int(123), Value::String(Arc::from("hello"))],
1414 flags: LineFlags::ALL_WS | LineFlags::EMPTY,
1415 }];
1416 let bytes = write_transcript(&parts, 1234, &crate::output::Fragments::default());
1417 let data = read_transcript(&bytes).unwrap();
1418 assert_eq!(data.parts.len(), 1);
1419 match &data.parts[0] {
1420 OutputPart::LineRef {
1421 container_idx,
1422 line_idx,
1423 slots,
1424 flags,
1425 } => {
1426 assert_eq!(*container_idx, 42);
1427 assert_eq!(*line_idx, 7);
1428 assert_eq!(slots.len(), 2);
1429 assert!(matches!(&slots[0], Value::Int(123)));
1430 assert!(flags.contains(LineFlags::ALL_WS));
1431 assert!(flags.contains(LineFlags::EMPTY));
1432 }
1433 other => unreachable!("expected LineRef, got {other:?}"),
1434 }
1435 }
1436
1437 #[test]
1438 fn checkpoint_filtered_on_write() {
1439 let parts = vec![
1440 OutputPart::Text("hello".to_string()),
1441 OutputPart::Checkpoint,
1442 OutputPart::Newline,
1443 ];
1444 let bytes = write_transcript(&parts, 0, &crate::output::Fragments::default());
1445 let data = read_transcript(&bytes).unwrap();
1446 assert_eq!(data.parts.len(), 2); assert!(matches!(&data.parts[0], OutputPart::Text(_)));
1448 assert!(matches!(&data.parts[1], OutputPart::Newline));
1449 }
1450
1451 #[test]
1459 fn round_trip_fragment_tags() {
1460 let fragments = vec![
1461 crate::output::Fragment {
1462 parts: vec![OutputPart::Text("hp: 10".to_string())],
1463 tags: vec!["a_tag".to_string(), "b_tag".to_string()],
1464 },
1465 crate::output::Fragment {
1466 parts: vec![OutputPart::Newline],
1467 tags: Vec::new(),
1468 },
1469 ];
1470 let bytes = write_transcript(&[], 0, &crate::output::Fragments::from(fragments.clone()));
1471 let data = read_transcript(&bytes).unwrap();
1472
1473 assert_eq!(data.fragments.len(), 2);
1474 assert_eq!(
1475 data.fragments.tags(0).unwrap(),
1476 vec!["a_tag".to_string(), "b_tag".to_string()]
1477 );
1478 assert_eq!(data.fragments.parts(0).unwrap(), fragments[0].parts);
1479 assert!(data.fragments.tags(1).unwrap().is_empty());
1480 }
1481
1482 #[test]
1490 fn legacy_transcript_without_tag_section_reads_as_empty_tags() {
1491 let mut body = Vec::new();
1492 write_u32(&mut body, 0); write_u32(&mut body, 1); write_u32(&mut body, 1); write_u8(&mut body, TAG_TEXT);
1496 write_str(&mut body, "legacy");
1497 let content_crc = crc32(&body);
1500 let mut bytes = Vec::with_capacity(HEADER_SIZE + body.len());
1501 bytes.extend_from_slice(MAGIC);
1502 write_u16(&mut bytes, VERSION);
1503 write_u16(&mut bytes, 0);
1504 write_u32(&mut bytes, 0xCAFE_BABE);
1505 write_u32(&mut bytes, content_crc);
1506 bytes.extend(body);
1507
1508 let data = read_transcript(&bytes).expect("legacy transcript must still decode");
1509 assert_eq!(data.fragments.len(), 1);
1510 assert!(
1511 matches!(&data.fragments.parts(0).unwrap()[0], OutputPart::Text(s) if s == "legacy")
1512 );
1513 assert!(data.fragments.tags(0).unwrap().is_empty());
1514 }
1515
1516 #[test]
1530 fn legacy_transcript_without_fragment_section_reads_as_no_fragments() {
1531 let mut body = Vec::new();
1532 write_u32(&mut body, 1); write_u8(&mut body, TAG_TEXT);
1534 write_str(&mut body, "legacy");
1535 let content_crc = crc32(&body);
1539 let mut bytes = Vec::with_capacity(HEADER_SIZE + body.len());
1540 bytes.extend_from_slice(MAGIC);
1541 write_u16(&mut bytes, VERSION);
1542 write_u16(&mut bytes, 0);
1543 write_u32(&mut bytes, 0xCAFE_BABE);
1544 write_u32(&mut bytes, content_crc);
1545 bytes.extend(body);
1546
1547 let data = read_transcript(&bytes).expect("legacy transcript must still decode");
1548 assert_eq!(data.parts.len(), 1);
1549 assert!(matches!(&data.parts[0], OutputPart::Text(s) if s == "legacy"));
1550 assert!(
1551 data.fragments.is_empty(),
1552 "a pre-fragments `.brkt` must decode with zero fragments, not error: {:?}",
1553 data.fragments
1554 );
1555 }
1556
1557 #[test]
1558 fn invalid_magic_errors() {
1559 let mut bytes = write_transcript(&[], 0, &crate::output::Fragments::default());
1560 bytes[0] = b'X';
1561 assert!(matches!(
1562 read_transcript(&bytes),
1563 Err(TranscriptError::InvalidMagic)
1564 ));
1565 }
1566
1567 #[test]
1568 fn integrity_check_errors() {
1569 let mut bytes = write_transcript(
1570 &[OutputPart::Newline],
1571 0,
1572 &crate::output::Fragments::default(),
1573 );
1574 if let Some(last) = bytes.last_mut() {
1576 *last ^= 0xFF;
1577 }
1578 assert!(matches!(
1579 read_transcript(&bytes),
1580 Err(TranscriptError::IntegrityCheckFailed)
1581 ));
1582 }
1583
1584 fn nested_array(depth: usize) -> Value {
1600 let mut v = Value::Int(42);
1601 for _ in 0..depth {
1602 v = Value::array(vec![v]);
1603 }
1604 v
1605 }
1606
1607 fn nested_map(depth: usize) -> Value {
1611 use brink_format::{MapKey, OrderedMap};
1612
1613 let mut v = Value::Int(42);
1614 for _ in 0..depth {
1615 let mut map = OrderedMap::with_capacity(1);
1616 map.insert(MapKey::Int(0), v);
1617 v = Value::map(map);
1618 }
1619 v
1620 }
1621
1622 #[test]
1623 fn decode_value_accepts_max_depth_nesting() {
1624 let value = nested_array(MAX_DECODE_DEPTH);
1627 let parts = vec![OutputPart::ValueRef(value.clone())];
1628 let bytes = write_transcript(&parts, 0, &crate::output::Fragments::default());
1629
1630 let data = read_transcript(&bytes).expect("depth exactly at cap must decode");
1631 match &data.parts[0] {
1632 OutputPart::ValueRef(v) => assert_eq!(*v, value),
1633 other => unreachable!("expected ValueRef, got {other:?}"),
1634 }
1635 }
1636
1637 #[test]
1638 fn decode_value_rejects_beyond_max_depth() {
1639 let value = nested_array(MAX_DECODE_DEPTH + 1);
1642 let parts = vec![OutputPart::ValueRef(value)];
1643 let bytes = write_transcript(&parts, 0, &crate::output::Fragments::default());
1644
1645 assert!(matches!(
1646 read_transcript(&bytes),
1647 Err(TranscriptError::MaxDepthExceeded(MAX_DECODE_DEPTH))
1648 ));
1649 }
1650
1651 #[test]
1652 fn decode_value_rejects_deeply_crafted_nesting() {
1653 let value = nested_array(8 * MAX_DECODE_DEPTH);
1661 let parts = vec![OutputPart::ValueRef(value)];
1662 let bytes = write_transcript(&parts, 0, &crate::output::Fragments::default());
1663
1664 assert!(matches!(
1665 read_transcript(&bytes),
1666 Err(TranscriptError::MaxDepthExceeded(MAX_DECODE_DEPTH))
1667 ));
1668 }
1669
1670 #[test]
1673 fn decode_value_accepts_max_depth_map_nesting() {
1674 let value = nested_map(MAX_DECODE_DEPTH);
1677 let parts = vec![OutputPart::ValueRef(value.clone())];
1678 let bytes = write_transcript(&parts, 0, &crate::output::Fragments::default());
1679
1680 let data = read_transcript(&bytes).expect("map depth exactly at cap must decode");
1681 match &data.parts[0] {
1682 OutputPart::ValueRef(v) => assert_eq!(*v, value),
1683 other => unreachable!("expected ValueRef, got {other:?}"),
1684 }
1685 }
1686
1687 #[test]
1688 fn decode_value_rejects_beyond_max_depth_map_nesting() {
1689 let value = nested_map(MAX_DECODE_DEPTH + 1);
1692 let parts = vec![OutputPart::ValueRef(value)];
1693 let bytes = write_transcript(&parts, 0, &crate::output::Fragments::default());
1694
1695 assert!(matches!(
1696 read_transcript(&bytes),
1697 Err(TranscriptError::MaxDepthExceeded(MAX_DECODE_DEPTH))
1698 ));
1699 }
1700
1701 fn duplicate_int_key_map_body() -> Vec<u8> {
1711 let mut body = Vec::new();
1712 write_u32(&mut body, 1); write_u8(&mut body, TAG_VALUE_REF);
1714 write_u8(&mut body, VAL_MAP);
1715 write_u32(&mut body, 2); write_u8(&mut body, VAL_INT);
1717 write_i32(&mut body, 0);
1718 write_u8(&mut body, VAL_INT);
1719 write_i32(&mut body, 1);
1720 write_u8(&mut body, VAL_INT);
1721 write_i32(&mut body, 0);
1722 write_u8(&mut body, VAL_INT);
1723 write_i32(&mut body, 2);
1724 write_u32(&mut body, 0); body
1726 }
1727
1728 fn wrap_body_as_transcript(body: &[u8]) -> Vec<u8> {
1729 let content_crc = crc32(body);
1730 let mut bytes = Vec::with_capacity(HEADER_SIZE + body.len());
1731 bytes.extend_from_slice(MAGIC);
1732 write_u16(&mut bytes, VERSION);
1733 write_u16(&mut bytes, 0);
1734 write_u32(&mut bytes, 0);
1735 write_u32(&mut bytes, content_crc);
1736 bytes.extend_from_slice(body);
1737 bytes
1738 }
1739
1740 #[test]
1741 fn decode_value_rejects_duplicate_map_key() {
1742 let bytes = wrap_body_as_transcript(&duplicate_int_key_map_body());
1743 assert!(matches!(
1744 read_transcript(&bytes),
1745 Err(TranscriptError::DuplicateMapKey)
1746 ));
1747 }
1748
1749 #[test]
1750 fn decode_value_accepts_distinct_map_keys() {
1751 let mut body = Vec::new();
1752 write_u32(&mut body, 1); write_u8(&mut body, TAG_VALUE_REF);
1754 write_u8(&mut body, VAL_MAP);
1755 write_u32(&mut body, 2); write_u8(&mut body, VAL_INT);
1757 write_i32(&mut body, 0);
1758 write_u8(&mut body, VAL_INT);
1759 write_i32(&mut body, 1);
1760 write_u8(&mut body, VAL_INT);
1761 write_i32(&mut body, 5);
1762 write_u8(&mut body, VAL_INT);
1763 write_i32(&mut body, 2);
1764 write_u32(&mut body, 0); let bytes = wrap_body_as_transcript(&body);
1767 let data = read_transcript(&bytes).expect("distinct keys must decode cleanly");
1768 match &data.parts[0] {
1769 OutputPart::ValueRef(Value::Map(map)) => assert_eq!(map.len(), 2),
1770 other => unreachable!("expected ValueRef(map), got {other:?}"),
1771 }
1772 }
1773}