use crate::action::{Action, ActionStream};
use crate::error::{Result, TraceError};
use crate::event::{ContextOptions, RawEvent, TraceEvent};
use crate::jsonl::JsonLines;
use crate::network::NetworkEntry;
use std::io::{BufRead, BufReader, Read, Seek};
use std::path::Path;
use zip::ZipArchive;
const TRACE_ENTRY: &str = "trace.trace";
const NETWORK_ENTRY: &str = "trace.network";
const SUPPORTED_VERSION: u32 = 8;
const RESOURCE_SNAPSHOT_KIND: &str = "resource-snapshot";
pub struct TraceReader<R: Read + Seek> {
zip: ZipArchive<R>,
context: ContextOptions,
}
impl<R: Read + Seek> TraceReader<R> {
pub fn open(reader: R) -> Result<Self> {
let mut zip = ZipArchive::new(reader)?;
let context = parse_context(&mut zip)?;
if context.version != SUPPORTED_VERSION {
return Err(TraceError::UnsupportedVersion {
found: context.version,
expected: SUPPORTED_VERSION,
});
}
Ok(Self { zip, context })
}
pub fn context(&self) -> &ContextOptions {
&self.context
}
pub fn raw_events(&mut self) -> Result<impl Iterator<Item = Result<RawEvent>>> {
let entry = self.zip.by_name(TRACE_ENTRY)?;
let lines = JsonLines::new(BufReader::new(entry));
Ok(lines.map(|res| res.map(RawEvent::new)))
}
pub fn events(&mut self) -> Result<impl Iterator<Item = Result<TraceEvent>>> {
Ok(self
.raw_events()?
.map(|res| res.map(|raw| raw.into_typed())))
}
pub fn actions(&mut self) -> Result<impl Iterator<Item = Result<Action>>> {
Ok(ActionStream::new(self.events()?))
}
pub fn network(&mut self) -> Result<impl Iterator<Item = Result<NetworkEntry>>> {
let entry = self.zip.by_name(NETWORK_ENTRY)?;
let lines = JsonLines::new(BufReader::new(entry));
Ok(lines.map(|res| {
let mut map = res?;
let kind = map
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if kind != RESOURCE_SNAPSHOT_KIND {
return Err(TraceError::MalformedAction {
call_id: String::new(),
reason: format!(
"trace.network: expected `{RESOURCE_SNAPSHOT_KIND}` event, got `{kind}`",
),
});
}
let snapshot = map
.remove("snapshot")
.ok_or_else(|| TraceError::MalformedAction {
call_id: String::new(),
reason: "trace.network: resource-snapshot missing `snapshot` payload".into(),
})?;
NetworkEntry::from_snapshot(snapshot)
.map_err(|source| TraceError::Json { line: 0, source })
}))
}
}
fn parse_context<R: Read + Seek>(zip: &mut ZipArchive<R>) -> Result<ContextOptions> {
let entry = zip
.by_name(TRACE_ENTRY)
.map_err(|_| TraceError::MissingEntry(TRACE_ENTRY))?;
let mut reader = BufReader::new(entry);
let mut line = String::new();
let mut line_no = 0;
loop {
line.clear();
line_no += 1;
let n = reader.read_line(&mut line)?;
if n == 0 {
return Err(TraceError::MissingEntry(TRACE_ENTRY));
}
let trimmed = line.trim_end_matches(['\n', '\r']);
if trimmed.trim().is_empty() {
continue;
}
let value: serde_json::Value =
serde_json::from_str(trimmed).map_err(|source| TraceError::Json {
line: line_no,
source,
})?;
let kind = value.get("type").and_then(|v| v.as_str()).unwrap_or("");
if kind != "context-options" {
return Err(TraceError::MalformedAction {
call_id: String::new(),
reason: format!("expected first event to be `context-options`, got `{kind}`"),
});
}
return serde_json::from_value::<ContextOptions>(value).map_err(|source| {
TraceError::Json {
line: line_no,
source,
}
});
}
}
pub fn open<P: AsRef<Path>>(path: P) -> Result<TraceReader<std::fs::File>> {
let file = std::fs::File::open(path)?;
TraceReader::open(file)
}