use std::io::{self, Read, Write};
use super::RunRecord;
pub const RUN_ARCHIVE_MAGIC: &[u8; 4] = b"LVR1";
pub const RUN_ARCHIVE_VERSION: u16 = 1;
pub fn write_archive_start(w: &mut dyn Write, version: u16) -> io::Result<()> {
w.write_all(RUN_ARCHIVE_MAGIC)?;
w.write_all(&version.to_be_bytes())?;
Ok(())
}
pub fn read_archive_start(r: &mut dyn Read) -> io::Result<u16> {
let mut magic = [0u8; 4];
r.read_exact(&mut magic)?;
if &magic != RUN_ARCHIVE_MAGIC {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"not a leviath run archive (bad magic)",
));
}
let mut version = [0u8; 2];
r.read_exact(&mut version)?;
let version = u16::from_be_bytes(version);
if version > RUN_ARCHIVE_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"run archive is format version {version}, but this build reads up to \
{RUN_ARCHIVE_VERSION} - upgrade leviath to read it"
),
));
}
Ok(version)
}
pub fn write_record(w: &mut dyn Write, record: &RunRecord) -> io::Result<()> {
let payload = serde_json::to_vec(record).expect("a RunRecord always serializes to JSON");
let len = payload.len() as u64;
w.write_all(&len.to_be_bytes())?;
w.write_all(&payload)?;
Ok(())
}
fn read_exact_or_eof(r: &mut dyn Read, buf: &mut [u8]) -> io::Result<bool> {
let mut filled = 0;
while filled < buf.len() {
match r.read(&mut buf[filled..])? {
0 => {
if filled == 0 {
return Ok(false); }
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"truncated run-archive frame",
));
}
n => filled += n,
}
}
Ok(true)
}
const MAX_RECORD_BYTES: u64 = 256 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq)]
pub enum Frame {
Record(Box<RunRecord>),
Unreadable {
bytes: usize,
},
}
pub fn read_frame(r: &mut dyn Read) -> io::Result<Option<Frame>> {
let Some(payload) = read_framed_payload(r)? else {
return Ok(None);
};
match serde_json::from_slice(&payload) {
Ok(record) => Ok(Some(Frame::Record(Box::new(record)))),
Err(_) => Ok(Some(Frame::Unreadable {
bytes: payload.len(),
})),
}
}
pub fn read_record(r: &mut dyn Read) -> io::Result<Option<RunRecord>> {
let Some(payload) = read_framed_payload(r)? else {
return Ok(None);
};
let record = serde_json::from_slice(&payload)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
Ok(Some(record))
}
fn read_framed_payload(r: &mut dyn Read) -> io::Result<Option<Vec<u8>>> {
let mut len_bytes = [0u8; 8];
if !read_exact_or_eof(r, &mut len_bytes)? {
return Ok(None);
}
let len = u64::from_be_bytes(len_bytes);
if len > MAX_RECORD_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("run-archive frame claims {len} bytes, over the {MAX_RECORD_BYTES} cap"),
));
}
let mut payload = vec![0u8; len as usize];
if !read_exact_or_eof(r, &mut payload)? {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"truncated run-archive frame",
));
}
Ok(Some(payload))
}
pub fn read_archive(r: &mut dyn Read) -> io::Result<(u16, Vec<RunRecord>)> {
let version = read_archive_start(r)?;
let mut records = Vec::new();
while let Some(record) = read_record(r)? {
records.push(record);
}
Ok((version, records))
}
pub fn read_archive_lenient(r: &mut dyn Read) -> io::Result<(u16, Vec<RunRecord>)> {
let version = read_archive_start(r)?;
let mut records = Vec::new();
let mut skipped = 0usize;
while let Ok(Some(frame)) = read_frame(r) {
match frame {
Frame::Record(record) => records.push(*record),
Frame::Unreadable { .. } => skipped += 1,
}
}
if skipped > 0 {
tracing::debug!(
skipped,
"run archive holds record kinds this build does not know; skipped them"
);
}
Ok((version, records))
}