use std::io::{self, BufRead, Read, Write};
use serde::Deserialize;
use serde_json::Value;
use crate::{EventKind, MAX_CAPTURE_BYTES, MAX_RECORD_BYTES, SCHEMA_VERSION};
#[derive(Deserialize)]
pub struct Row {
pub schema_version: u32,
pub seq: u64,
pub event: EventKind,
pub fields: Value,
}
const LINE_LIMIT: usize = MAX_RECORD_BYTES + 1024;
const TOTAL_LIMIT: usize = MAX_CAPTURE_BYTES;
pub fn scan(
mut input: impl BufRead,
mut visit: impl FnMut(Row) -> io::Result<()>,
) -> io::Result<bool> {
let (mut total, mut seq, mut ended, mut complete) = (0usize, 1u64, false, false);
loop {
let mut bytes = Vec::new();
let read = Read::by_ref(&mut input)
.take(LINE_LIMIT as u64 + 1)
.read_until(b'\n', &mut bytes)?;
if read == 0 {
break;
}
total = total
.checked_add(read)
.ok_or_else(|| io::Error::other("input limit"))?;
if read > LINE_LIMIT || total > TOTAL_LIMIT || bytes.last() != Some(&b'\n') {
return Err(io::Error::other("trace truncated or exceeds input limit"));
}
if ended {
return Err(io::Error::other("records after trace terminal"));
}
let row: Row =
serde_json::from_slice(&bytes).map_err(|_| io::Error::other("invalid trace record"))?;
if row.schema_version != SCHEMA_VERSION || row.seq != seq {
return Err(io::Error::other("unsupported schema or missing sequence"));
}
seq = seq
.checked_add(1)
.ok_or_else(|| io::Error::other("sequence overflow"))?;
if row.event == EventKind::TraceEnd {
ended = true;
complete = row.fields["complete"] == true;
}
visit(row)?;
}
Ok(ended && complete)
}
pub fn metadata(input: impl BufRead, mut out: impl Write) -> io::Result<()> {
writeln!(
out,
"{{\"schema\":\"strop-metadata-export-v1\",\"replayable\":false}}"
)?;
let complete = scan(input, |row| {
serde_json::to_writer(
&mut out,
&serde_json::json!({"seq": row.seq, "category": row.event}),
)?;
out.write_all(b"\n")
})?;
serde_json::to_writer(
&mut out,
&serde_json::json!({"export_end":true,"source_complete":complete,"replayable":false}),
)?;
out.write_all(b"\n")
}
pub fn replay_nodes(input: impl BufRead) -> io::Result<Vec<crate::replay::Node>> {
let mut nodes = Vec::new();
let mut full = false;
let complete = scan(input, |row| {
if row.seq == 1 {
full = row.event == EventKind::SessionStart && row.fields["full_content"] == true;
}
if row.event == EventKind::Replay {
nodes.push(
serde_json::from_value(row.fields)
.map_err(|_| io::Error::other("invalid forensic record"))?,
);
}
Ok(())
})?;
if !full || !complete {
return Err(io::Error::other(
"full replay requires complete full-content capture",
));
}
if !matches!(nodes.first(), Some(crate::replay::Node::Seed { .. }))
|| !matches!(nodes.last(), Some(crate::replay::Node::End))
{
return Err(io::Error::other("missing forensic seed or end"));
}
Ok(nodes)
}