1use std::sync::Arc;
24
25use brink_format::{DefinitionId, LineFlags, Value};
26
27use crate::output::{OutputPart, resolve_lines};
28use crate::program::Program;
29
30const MAGIC: &[u8; 4] = b"BRKT";
33const VERSION: u16 = 1;
34const HEADER_SIZE: usize = 16;
35
36const TAG_TEXT: u8 = 0x01;
38const TAG_LINE_REF: u8 = 0x02;
39const TAG_VALUE_REF: u8 = 0x03;
40const TAG_NEWLINE: u8 = 0x04;
41const TAG_SPRING: u8 = 0x05;
42const TAG_GLUE: u8 = 0x06;
43const TAG_TAG: u8 = 0x07;
44
45const VAL_INT: u8 = 0x00;
47const VAL_FLOAT: u8 = 0x01;
48const VAL_BOOL: u8 = 0x02;
49const VAL_STRING: u8 = 0x03;
50const VAL_LIST: u8 = 0x04;
51const VAL_DIVERT_TARGET: u8 = 0x05;
52const VAL_NULL: u8 = 0x06;
53const VAL_FRAGMENT_REF: u8 = 0x08;
54
55#[derive(Debug, thiserror::Error)]
59pub enum TranscriptError {
60 #[error("invalid magic: expected BRKT")]
61 InvalidMagic,
62 #[error("unsupported version: {0}")]
63 UnsupportedVersion(u16),
64 #[error("checksum mismatch: transcript {transcript:#010x} != program {program:#010x}")]
65 ChecksumMismatch { transcript: u32, program: u32 },
66 #[error("integrity check failed: content CRC-32 mismatch")]
67 IntegrityCheckFailed,
68 #[error("unexpected end of data")]
69 UnexpectedEof,
70 #[error("invalid part tag: {0:#04x}")]
71 InvalidPartTag(u8),
72 #[error("invalid value tag: {0:#04x}")]
73 InvalidValueTag(u8),
74 #[error("invalid UTF-8")]
75 InvalidUtf8,
76 #[error("invalid definition ID")]
77 InvalidDefinitionId,
78}
79
80#[expect(clippy::cast_possible_truncation)]
87pub fn write_transcript(
88 parts: &[OutputPart],
89 source_checksum: u32,
90 fragments: &[crate::output::Fragment],
91) -> Vec<u8> {
92 let mut body = Vec::new();
93
94 let count = parts
96 .iter()
97 .filter(|p| !matches!(p, OutputPart::Checkpoint))
98 .count() as u32;
99 write_u32(&mut body, count);
100
101 for part in parts {
102 match part {
103 OutputPart::Text(s) => {
104 write_u8(&mut body, TAG_TEXT);
105 write_str(&mut body, s);
106 }
107 OutputPart::LineRef {
108 container_idx,
109 line_idx,
110 slots,
111 flags,
112 } => {
113 write_u8(&mut body, TAG_LINE_REF);
114 write_u32(&mut body, *container_idx);
115 write_u16(&mut body, *line_idx);
116 write_u8(&mut body, flags.bits());
117 write_u16(&mut body, slots.len() as u16);
118 for val in slots {
119 encode_value(val, &mut body);
120 }
121 }
122 OutputPart::ValueRef(val) => {
123 write_u8(&mut body, TAG_VALUE_REF);
124 encode_value(val, &mut body);
125 }
126 OutputPart::Newline => write_u8(&mut body, TAG_NEWLINE),
127 OutputPart::Spring => write_u8(&mut body, TAG_SPRING),
128 OutputPart::Glue => write_u8(&mut body, TAG_GLUE),
129 OutputPart::Tag(s) => {
130 write_u8(&mut body, TAG_TAG);
131 write_str(&mut body, s);
132 }
133 OutputPart::Checkpoint => {} }
135 }
136
137 write_u32(&mut body, fragments.len() as u32);
139 for fragment in fragments {
140 let filtered_count = fragment
141 .parts
142 .iter()
143 .filter(|p| !matches!(p, OutputPart::Checkpoint))
144 .count() as u32;
145 write_u32(&mut body, filtered_count);
146 for part in &fragment.parts {
147 match part {
148 OutputPart::Text(s) => {
149 write_u8(&mut body, TAG_TEXT);
150 write_str(&mut body, s);
151 }
152 OutputPart::LineRef {
153 container_idx,
154 line_idx,
155 slots,
156 flags,
157 } => {
158 write_u8(&mut body, TAG_LINE_REF);
159 write_u32(&mut body, *container_idx);
160 write_u16(&mut body, *line_idx);
161 write_u8(&mut body, flags.bits());
162 write_u16(&mut body, slots.len() as u16);
163 for val in slots {
164 encode_value(val, &mut body);
165 }
166 }
167 OutputPart::ValueRef(val) => {
168 write_u8(&mut body, TAG_VALUE_REF);
169 encode_value(val, &mut body);
170 }
171 OutputPart::Newline => write_u8(&mut body, TAG_NEWLINE),
172 OutputPart::Spring => write_u8(&mut body, TAG_SPRING),
173 OutputPart::Glue => write_u8(&mut body, TAG_GLUE),
174 OutputPart::Tag(s) => {
175 write_u8(&mut body, TAG_TAG);
176 write_str(&mut body, s);
177 }
178 OutputPart::Checkpoint => {}
179 }
180 }
181 }
182
183 let content_crc = crc32(&body);
185 let mut buf = Vec::with_capacity(HEADER_SIZE + body.len());
186 buf.extend_from_slice(MAGIC);
187 write_u16(&mut buf, VERSION);
188 write_u16(&mut buf, 0); write_u32(&mut buf, source_checksum);
190 write_u32(&mut buf, content_crc);
191 buf.extend(body);
192 buf
193}
194
195#[derive(Debug, Clone)]
205pub struct TranscriptData {
206 pub parts: Vec<OutputPart>,
207 pub source_checksum: u32,
208 pub fragments: Vec<crate::output::Fragment>,
209}
210
211pub fn read_transcript(bytes: &[u8]) -> Result<TranscriptData, TranscriptError> {
213 if bytes.len() < HEADER_SIZE {
214 return Err(TranscriptError::UnexpectedEof);
215 }
216
217 if &bytes[0..4] != MAGIC {
219 return Err(TranscriptError::InvalidMagic);
220 }
221 let mut off = 4;
222 let version = read_u16(bytes, &mut off)?;
223 if version != VERSION {
224 return Err(TranscriptError::UnsupportedVersion(version));
225 }
226 let _reserved = read_u16(bytes, &mut off)?;
227 let source_checksum = read_u32(bytes, &mut off)?;
228 let expected_crc = read_u32(bytes, &mut off)?;
229
230 let body = &bytes[HEADER_SIZE..];
232 if crc32(body) != expected_crc {
233 return Err(TranscriptError::IntegrityCheckFailed);
234 }
235
236 let mut off = HEADER_SIZE;
238 let count = read_u32(bytes, &mut off)? as usize;
239 let mut parts = Vec::with_capacity(count);
240
241 for _ in 0..count {
242 let tag = read_u8(bytes, &mut off)?;
243 let part = match tag {
244 TAG_TEXT => OutputPart::Text(read_str(bytes, &mut off)?),
245 TAG_LINE_REF => {
246 let container_idx = read_u32(bytes, &mut off)?;
247 let line_idx = read_u16(bytes, &mut off)?;
248 let flags_bits = read_u8(bytes, &mut off)?;
249 let flags = LineFlags::from_bits_truncate(flags_bits);
250 let slot_count = read_u16(bytes, &mut off)? as usize;
251 let mut slots = Vec::with_capacity(slot_count);
252 for _ in 0..slot_count {
253 slots.push(decode_value(bytes, &mut off)?);
254 }
255 OutputPart::LineRef {
256 container_idx,
257 line_idx,
258 slots,
259 flags,
260 }
261 }
262 TAG_VALUE_REF => OutputPart::ValueRef(decode_value(bytes, &mut off)?),
263 TAG_NEWLINE => OutputPart::Newline,
264 TAG_SPRING => OutputPart::Spring,
265 TAG_GLUE => OutputPart::Glue,
266 TAG_TAG => OutputPart::Tag(read_str(bytes, &mut off)?),
267 _ => return Err(TranscriptError::InvalidPartTag(tag)),
268 };
269 parts.push(part);
270 }
271
272 let fragment_count = if off < bytes.len() {
274 read_u32(bytes, &mut off)? as usize
275 } else {
276 0 };
278 let mut fragments = Vec::with_capacity(fragment_count);
279 for _ in 0..fragment_count {
280 let frag_part_count = read_u32(bytes, &mut off)? as usize;
281 let mut frag_parts = Vec::with_capacity(frag_part_count);
282 for _ in 0..frag_part_count {
283 let tag = read_u8(bytes, &mut off)?;
284 let part = match tag {
285 TAG_TEXT => OutputPart::Text(read_str(bytes, &mut off)?),
286 TAG_LINE_REF => {
287 let container_idx = read_u32(bytes, &mut off)?;
288 let line_idx = read_u16(bytes, &mut off)?;
289 let flags_bits = read_u8(bytes, &mut off)?;
290 let flags = LineFlags::from_bits_truncate(flags_bits);
291 let slot_count = read_u16(bytes, &mut off)? as usize;
292 let mut slots = Vec::with_capacity(slot_count);
293 for _ in 0..slot_count {
294 slots.push(decode_value(bytes, &mut off)?);
295 }
296 OutputPart::LineRef {
297 container_idx,
298 line_idx,
299 slots,
300 flags,
301 }
302 }
303 TAG_VALUE_REF => OutputPart::ValueRef(decode_value(bytes, &mut off)?),
304 TAG_NEWLINE => OutputPart::Newline,
305 TAG_SPRING => OutputPart::Spring,
306 TAG_GLUE => OutputPart::Glue,
307 TAG_TAG => OutputPart::Tag(read_str(bytes, &mut off)?),
308 _ => return Err(TranscriptError::InvalidPartTag(tag)),
309 };
310 frag_parts.push(part);
311 }
312 fragments.push(crate::output::Fragment {
313 parts: frag_parts,
314 tags: Vec::new(),
315 });
316 }
317
318 Ok(TranscriptData {
319 parts,
320 source_checksum,
321 fragments,
322 })
323}
324
325pub fn render_transcript(
332 parts: &[OutputPart],
333 program: &Program,
334 line_tables: &[Vec<brink_format::LineEntry>],
335 resolver: Option<&dyn brink_format::PluralResolver>,
336 fragments: &[crate::output::Fragment],
337) -> Vec<(String, Vec<String>)> {
338 resolve_lines(parts, program, line_tables, resolver, fragments)
339}
340
341fn write_u8(buf: &mut Vec<u8>, v: u8) {
344 buf.push(v);
345}
346
347fn write_u16(buf: &mut Vec<u8>, v: u16) {
348 buf.extend_from_slice(&v.to_le_bytes());
349}
350
351fn write_u32(buf: &mut Vec<u8>, v: u32) {
352 buf.extend_from_slice(&v.to_le_bytes());
353}
354
355fn write_u64(buf: &mut Vec<u8>, v: u64) {
356 buf.extend_from_slice(&v.to_le_bytes());
357}
358
359fn write_i32(buf: &mut Vec<u8>, v: i32) {
360 buf.extend_from_slice(&v.to_le_bytes());
361}
362
363#[expect(clippy::cast_possible_truncation)]
364fn write_str(buf: &mut Vec<u8>, s: &str) {
365 write_u32(buf, s.len() as u32);
366 buf.extend_from_slice(s.as_bytes());
367}
368
369fn write_def_id(buf: &mut Vec<u8>, id: DefinitionId) {
370 write_u64(buf, id.to_raw());
371}
372
373fn read_u8(buf: &[u8], off: &mut usize) -> Result<u8, TranscriptError> {
374 if *off >= buf.len() {
375 return Err(TranscriptError::UnexpectedEof);
376 }
377 let v = buf[*off];
378 *off += 1;
379 Ok(v)
380}
381
382fn read_u16(buf: &[u8], off: &mut usize) -> Result<u16, TranscriptError> {
383 if *off + 2 > buf.len() {
384 return Err(TranscriptError::UnexpectedEof);
385 }
386 let v = u16::from_le_bytes([buf[*off], buf[*off + 1]]);
387 *off += 2;
388 Ok(v)
389}
390
391fn read_u32(buf: &[u8], off: &mut usize) -> Result<u32, TranscriptError> {
392 if *off + 4 > buf.len() {
393 return Err(TranscriptError::UnexpectedEof);
394 }
395 let v = u32::from_le_bytes([buf[*off], buf[*off + 1], buf[*off + 2], buf[*off + 3]]);
396 *off += 4;
397 Ok(v)
398}
399
400fn read_i32(buf: &[u8], off: &mut usize) -> Result<i32, TranscriptError> {
401 if *off + 4 > buf.len() {
402 return Err(TranscriptError::UnexpectedEof);
403 }
404 let v = i32::from_le_bytes([buf[*off], buf[*off + 1], buf[*off + 2], buf[*off + 3]]);
405 *off += 4;
406 Ok(v)
407}
408
409fn read_f32(buf: &[u8], off: &mut usize) -> Result<f32, TranscriptError> {
410 if *off + 4 > buf.len() {
411 return Err(TranscriptError::UnexpectedEof);
412 }
413 let v = f32::from_le_bytes([buf[*off], buf[*off + 1], buf[*off + 2], buf[*off + 3]]);
414 *off += 4;
415 Ok(v)
416}
417
418fn read_u64(buf: &[u8], off: &mut usize) -> Result<u64, TranscriptError> {
419 if *off + 8 > buf.len() {
420 return Err(TranscriptError::UnexpectedEof);
421 }
422 let v = u64::from_le_bytes([
423 buf[*off],
424 buf[*off + 1],
425 buf[*off + 2],
426 buf[*off + 3],
427 buf[*off + 4],
428 buf[*off + 5],
429 buf[*off + 6],
430 buf[*off + 7],
431 ]);
432 *off += 8;
433 Ok(v)
434}
435
436fn read_str(buf: &[u8], off: &mut usize) -> Result<String, TranscriptError> {
437 let len = read_u32(buf, off)? as usize;
438 if *off + len > buf.len() {
439 return Err(TranscriptError::UnexpectedEof);
440 }
441 let bytes = &buf[*off..*off + len];
442 *off += len;
443 String::from_utf8(bytes.to_vec()).map_err(|_| TranscriptError::InvalidUtf8)
444}
445
446fn read_def_id(buf: &[u8], off: &mut usize) -> Result<DefinitionId, TranscriptError> {
447 let raw = read_u64(buf, off)?;
448 DefinitionId::from_raw(raw).ok_or(TranscriptError::InvalidDefinitionId)
449}
450
451#[expect(clippy::cast_possible_truncation)]
454fn encode_value(v: &Value, buf: &mut Vec<u8>) {
455 match v {
456 Value::Int(n) => {
457 write_u8(buf, VAL_INT);
458 write_i32(buf, *n);
459 }
460 Value::Float(n) => {
461 write_u8(buf, VAL_FLOAT);
462 buf.extend_from_slice(&n.to_le_bytes());
463 }
464 Value::Bool(b) => {
465 write_u8(buf, VAL_BOOL);
466 write_u8(buf, u8::from(*b));
467 }
468 Value::String(s) => {
469 write_u8(buf, VAL_STRING);
470 write_str(buf, s);
471 }
472 Value::List(lv) => {
473 write_u8(buf, VAL_LIST);
474 write_u32(buf, lv.items.len() as u32);
475 for item in &lv.items {
476 write_def_id(buf, *item);
477 }
478 write_u32(buf, lv.origins.len() as u32);
479 for origin in &lv.origins {
480 write_def_id(buf, *origin);
481 }
482 }
483 Value::DivertTarget(id) => {
484 write_u8(buf, VAL_DIVERT_TARGET);
485 write_def_id(buf, *id);
486 }
487 Value::VariablePointer(id) => {
488 write_u8(buf, VAL_DIVERT_TARGET); write_def_id(buf, *id);
490 }
491 Value::FragmentRef(idx) => {
492 write_u8(buf, VAL_FRAGMENT_REF);
493 write_u32(buf, *idx);
494 }
495 Value::TempPointer { .. } | Value::Null => {
496 write_u8(buf, VAL_NULL);
497 }
498 }
499}
500
501fn decode_value(buf: &[u8], off: &mut usize) -> Result<Value, TranscriptError> {
502 let tag = read_u8(buf, off)?;
503 match tag {
504 VAL_INT => Ok(Value::Int(read_i32(buf, off)?)),
505 VAL_FLOAT => Ok(Value::Float(read_f32(buf, off)?)),
506 VAL_BOOL => {
507 let b = read_u8(buf, off)?;
508 Ok(Value::Bool(b != 0))
509 }
510 VAL_STRING => {
511 let s = read_str(buf, off)?;
512 Ok(Value::String(Arc::from(s.as_str())))
513 }
514 VAL_LIST => {
515 let item_count = read_u32(buf, off)? as usize;
516 let mut items = Vec::with_capacity(item_count);
517 for _ in 0..item_count {
518 items.push(read_def_id(buf, off)?);
519 }
520 let origin_count = read_u32(buf, off)? as usize;
521 let mut origins = Vec::with_capacity(origin_count);
522 for _ in 0..origin_count {
523 origins.push(read_def_id(buf, off)?);
524 }
525 Ok(Value::List(Arc::new(brink_format::ListValue {
526 items,
527 origins,
528 })))
529 }
530 VAL_DIVERT_TARGET => {
531 let id = read_def_id(buf, off)?;
532 Ok(Value::DivertTarget(id))
533 }
534 VAL_FRAGMENT_REF => Ok(Value::FragmentRef(read_u32(buf, off)?)),
535 VAL_NULL => Ok(Value::Null),
536 _ => Err(TranscriptError::InvalidValueTag(tag)),
537 }
538}
539
540fn crc32(data: &[u8]) -> u32 {
543 static TABLE: [u32; 256] = {
544 let mut table = [0u32; 256];
545 let mut i = 0u32;
546 while i < 256 {
547 let mut crc = i;
548 let mut j = 0;
549 while j < 8 {
550 if crc & 1 != 0 {
551 crc = (crc >> 1) ^ 0xEDB8_8320;
552 } else {
553 crc >>= 1;
554 }
555 j += 1;
556 }
557 table[i as usize] = crc;
558 i += 1;
559 }
560 table
561 };
562
563 let mut crc = 0xFFFF_FFFFu32;
564 for &byte in data {
565 let idx = ((crc ^ u32::from(byte)) & 0xFF) as usize;
566 crc = (crc >> 8) ^ TABLE[idx];
567 }
568 crc ^ 0xFFFF_FFFF
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574 use brink_format::LineFlags;
575
576 #[test]
577 fn round_trip_simple_parts() {
578 let parts = vec![
579 OutputPart::Text("Hello".to_string()),
580 OutputPart::Spring,
581 OutputPart::Newline,
582 OutputPart::Tag("tag1".to_string()),
583 OutputPart::Glue,
584 ];
585 let bytes = write_transcript(&parts, 0xDEAD_BEEF, &[]);
586 let data = read_transcript(&bytes).unwrap();
587 assert_eq!(data.source_checksum, 0xDEAD_BEEF);
588 assert_eq!(data.parts.len(), 5);
589 assert!(matches!(&data.parts[0], OutputPart::Text(s) if s == "Hello"));
590 assert!(matches!(&data.parts[1], OutputPart::Spring));
591 assert!(matches!(&data.parts[2], OutputPart::Newline));
592 assert!(matches!(&data.parts[3], OutputPart::Tag(s) if s == "tag1"));
593 assert!(matches!(&data.parts[4], OutputPart::Glue));
594 }
595
596 #[test]
597 fn round_trip_line_ref_with_slots() {
598 let parts = vec![OutputPart::LineRef {
599 container_idx: 42,
600 line_idx: 7,
601 slots: vec![Value::Int(123), Value::String(Arc::from("hello"))],
602 flags: LineFlags::STARTS_WITH_WS | LineFlags::ENDS_WITH_WS,
603 }];
604 let bytes = write_transcript(&parts, 1234, &[]);
605 let data = read_transcript(&bytes).unwrap();
606 assert_eq!(data.parts.len(), 1);
607 match &data.parts[0] {
608 OutputPart::LineRef {
609 container_idx,
610 line_idx,
611 slots,
612 flags,
613 } => {
614 assert_eq!(*container_idx, 42);
615 assert_eq!(*line_idx, 7);
616 assert_eq!(slots.len(), 2);
617 assert!(matches!(&slots[0], Value::Int(123)));
618 assert!(flags.contains(LineFlags::STARTS_WITH_WS));
619 assert!(flags.contains(LineFlags::ENDS_WITH_WS));
620 }
621 other => unreachable!("expected LineRef, got {other:?}"),
622 }
623 }
624
625 #[test]
626 fn checkpoint_filtered_on_write() {
627 let parts = vec![
628 OutputPart::Text("hello".to_string()),
629 OutputPart::Checkpoint,
630 OutputPart::Newline,
631 ];
632 let bytes = write_transcript(&parts, 0, &[]);
633 let data = read_transcript(&bytes).unwrap();
634 assert_eq!(data.parts.len(), 2); assert!(matches!(&data.parts[0], OutputPart::Text(_)));
636 assert!(matches!(&data.parts[1], OutputPart::Newline));
637 }
638
639 #[test]
640 fn invalid_magic_errors() {
641 let mut bytes = write_transcript(&[], 0, &[]);
642 bytes[0] = b'X';
643 assert!(matches!(
644 read_transcript(&bytes),
645 Err(TranscriptError::InvalidMagic)
646 ));
647 }
648
649 #[test]
650 fn integrity_check_errors() {
651 let mut bytes = write_transcript(&[OutputPart::Newline], 0, &[]);
652 if let Some(last) = bytes.last_mut() {
654 *last ^= 0xFF;
655 }
656 assert!(matches!(
657 read_transcript(&bytes),
658 Err(TranscriptError::IntegrityCheckFailed)
659 ));
660 }
661}