use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::process::ExitCode;
use ibapi::IncomingMessages;
const USAGE: &str = "usage: replay_raw_capture [--frames] <capture.bin>";
const MAX_FRAME_LENGTH: usize = 0x00FF_FFFF;
const MIN_FRAME_LENGTH: usize = 4;
const PROTOBUF_MSG_ID: i32 = 200;
fn main() -> ExitCode {
let mut path = None;
let mut list_frames = false;
for arg in env::args().skip(1) {
match arg.as_str() {
"--frames" => list_frames = true,
"-h" | "--help" => {
eprintln!("{USAGE}");
return ExitCode::SUCCESS;
}
other => path = Some(other.to_string()),
}
}
let Some(path) = path else {
eprintln!("{USAGE}");
eprintln!(" capture files are written by setting IBAPI_RAW_CAPTURE_DIR");
return ExitCode::FAILURE;
};
let capture = match fs::read(&path) {
Ok(bytes) => bytes,
Err(err) => {
eprintln!("cannot read {path}: {err}");
return ExitCode::FAILURE;
}
};
println!("{path}: {} bytes", capture.len());
let report = walk(&capture, list_frames);
report.print(capture.len());
if report.desync.is_some() {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct FrameKind {
raw_id: i32,
encoding: &'static str,
}
struct Report {
frames: usize,
by_kind: BTreeMap<FrameKind, usize>,
desync: Option<(usize, String)>,
}
impl Report {
fn print(&self, capture_len: usize) {
println!("{} frames read", self.frames);
for (kind, count) in &self.by_kind {
println!(" {count:>7} {}", label(kind));
}
match &self.desync {
Some((offset, reason)) => {
println!();
println!("DESYNC at byte {offset}: {reason}");
println!("{} bytes after this point were not walked.", capture_len - offset);
println!("Cross-reference the `.idx` beside this file for the wall-clock time of that frame.");
}
None => println!("no framing anomalies; the capture reads end to end"),
}
}
}
fn label(kind: &FrameKind) -> String {
let resolved = IncomingMessages::from(if kind.encoding == "proto" {
kind.raw_id - PROTOBUF_MSG_ID
} else {
kind.raw_id
});
if resolved == IncomingMessages::NotValid {
format!("NotValid (raw id {})", kind.raw_id)
} else {
format!("{resolved:?} [{}]", kind.encoding)
}
}
fn walk(capture: &[u8], list_frames: bool) -> Report {
let mut report = Report {
frames: 0,
by_kind: BTreeMap::new(),
desync: None,
};
let mut offset = 0usize;
while offset < capture.len() {
let Some(prefix) = capture.get(offset..offset + 4) else {
report.desync = Some((offset, format!("{} trailing bytes, too few for a length prefix", capture.len() - offset)));
break;
};
let declared = u32::from_be_bytes(prefix.try_into().expect("4 bytes")) as usize;
if !(MIN_FRAME_LENGTH..=MAX_FRAME_LENGTH).contains(&declared) {
report.desync = Some((
offset,
format!("length prefix {declared} is outside {MIN_FRAME_LENGTH}..={MAX_FRAME_LENGTH}"),
));
break;
}
let body_start = offset + 4;
let Some(body) = capture.get(body_start..body_start + declared) else {
let available = capture.len() - body_start;
report.desync = Some((offset, format!("length prefix {declared} but only {available} bytes remain")));
break;
};
let raw_id = i32::from_be_bytes(body[..4].try_into().expect("4 bytes"));
let encoding = if raw_id > PROTOBUF_MSG_ID { "proto" } else { "text" };
let kind = FrameKind { raw_id, encoding };
if list_frames {
println!(" #{:<6} offset {offset:<10} len {declared:<8} {}", report.frames, label(&kind));
}
*report.by_kind.entry(kind).or_insert(0) += 1;
report.frames += 1;
offset = body_start + declared;
}
report
}