1use std::io::{self, BufRead, Read, Write};
6
7use serde::Deserialize;
8use serde_json::Value;
9
10use crate::{EventKind, MAX_CAPTURE_BYTES, MAX_RECORD_BYTES, SCHEMA_VERSION};
11
12#[derive(Deserialize)]
14pub struct Row {
15 pub schema_version: u32,
16 pub seq: u64,
17 pub event: EventKind,
18 pub fields: Value,
19}
20
21const LINE_LIMIT: usize = MAX_RECORD_BYTES + 1024;
24const TOTAL_LIMIT: usize = MAX_CAPTURE_BYTES;
27
28pub fn scan(
39 mut input: impl BufRead,
40 mut visit: impl FnMut(Row) -> io::Result<()>,
41) -> io::Result<bool> {
42 let (mut total, mut seq, mut ended, mut complete) = (0usize, 1u64, false, false);
43 let mut version: Option<u32> = None;
44 let mut chunks = crate::chunk::Assembler::new();
45 loop {
46 let mut bytes = Vec::new();
47 let read = Read::by_ref(&mut input)
48 .take(LINE_LIMIT as u64 + 1)
49 .read_until(b'\n', &mut bytes)?;
50 if read == 0 {
51 break;
52 }
53 total = total
54 .checked_add(read)
55 .ok_or_else(|| io::Error::other("input limit"))?;
56 if read > LINE_LIMIT || total > TOTAL_LIMIT || bytes.last() != Some(&b'\n') {
57 return Err(io::Error::other("trace truncated or exceeds input limit"));
58 }
59 if ended {
60 return Err(io::Error::other("records after trace terminal"));
61 }
62 let row: Row =
63 serde_json::from_slice(&bytes).map_err(|_| io::Error::other("invalid trace record"))?;
64 if version.is_none() {
67 version = Some(row.schema_version);
68 }
69 let supported = row.schema_version == SCHEMA_VERSION || row.schema_version == 2;
70 if !supported || version != Some(row.schema_version) || row.seq != seq {
71 return Err(io::Error::other("unsupported schema or missing sequence"));
72 }
73 seq = seq
74 .checked_add(1)
75 .ok_or_else(|| io::Error::other("sequence overflow"))?;
76 if row.event == EventKind::ReplayChunk {
77 if row.schema_version < 3 {
78 return Err(io::Error::other("chunk record in pre-chunk schema"));
79 }
80 if let Some((first, event, fields)) = chunks.accept(row.seq, row.fields)? {
81 visit(Row {
82 schema_version: row.schema_version,
83 seq: first,
84 event,
85 fields,
86 })?;
87 }
88 continue;
89 }
90 if row.event == EventKind::TraceEnd {
91 ended = true;
92 complete = row.fields["complete"] == true;
93 if complete && chunks.is_open() {
94 return Err(io::Error::other(
95 "complete trace cannot abandon a chunk run",
96 ));
97 }
98 }
99 visit(row)?;
100 }
101 Ok(ended && complete)
102}
103
104pub fn metadata(input: impl BufRead, mut out: impl Write) -> io::Result<()> {
111 writeln!(
112 out,
113 "{{\"schema\":\"strop-metadata-export-v1\",\"replayable\":false}}"
114 )?;
115 let complete = scan(input, |row| {
116 serde_json::to_writer(
117 &mut out,
118 &serde_json::json!({"seq": row.seq, "category": row.event}),
119 )?;
120 out.write_all(b"\n")
121 })?;
122 serde_json::to_writer(
123 &mut out,
124 &serde_json::json!({"export_end":true,"source_complete":complete,"replayable":false}),
125 )?;
126 out.write_all(b"\n")
127}
128
129pub fn replay_nodes(input: impl BufRead) -> io::Result<Vec<crate::replay::Node>> {
133 let mut nodes = Vec::new();
134 let mut full = false;
135 let complete = scan(input, |row| {
136 if row.seq == 1 {
137 full = row.event == EventKind::SessionStart && row.fields["full_content"] == true;
138 }
139 if row.event == EventKind::Replay {
140 nodes.push(
141 serde_json::from_value(row.fields)
142 .map_err(|_| io::Error::other("invalid forensic record"))?,
143 );
144 }
145 Ok(())
146 })?;
147 if !full || !complete {
148 return Err(io::Error::other(
149 "full replay requires complete full-content capture",
150 ));
151 }
152 if !matches!(nodes.first(), Some(crate::replay::Node::Seed { .. }))
153 || !matches!(nodes.last(), Some(crate::replay::Node::End))
154 {
155 return Err(io::Error::other("missing forensic seed or end"));
156 }
157 Ok(nodes)
158}