use std::{env, fs, path::PathBuf};
use nkscan::protocol::{
data::{self, Boundary, PerfInformation},
window::Window,
};
struct Record<'a> {
seq: u32,
cdb: &'a [u8],
data: &'a [u8],
}
const CDB_AT: usize = 24;
const LEN_AT: usize = 40;
fn main() -> anyhow::Result<()> {
let path = env::args()
.nth(1)
.map(PathBuf::from)
.unwrap_or_else(|| panic!("usage: cargo run --example trace <scsi_trace.bin>"));
let bytes = fs::read(&path)?;
let mut records = Vec::new();
let mut at = 0usize;
while at + LEN_AT + 4 <= bytes.len() {
if &bytes[at..at + 4] != b"SREC" {
anyhow::bail!("bad magic at {at}: {:02?}", &bytes[at..at + 4]);
}
let seq = u32::from_le_bytes(bytes[at + 4..at + 8].try_into().unwrap());
let cdb_len = bytes[at + 8] as usize;
let sense_len = bytes[at + 11] as usize;
let data_len =
u32::from_le_bytes(bytes[at + LEN_AT..at + LEN_AT + 4].try_into().unwrap()) as usize;
let data_end = at + LEN_AT + 4 + data_len;
if data_end + sense_len > bytes.len() {
anyhow::bail!(
"record at {at} claims {data_len} data and {sense_len} sense bytes, \
past the end of the capture"
);
}
let cdb = &bytes[at + CDB_AT..at + CDB_AT + cdb_len.min(16)];
let data = &bytes[at + LEN_AT + 4..data_end];
records.push(Record { seq, cdb, data });
at = data_end + sense_len;
}
let mut image_reads = 0u64;
let mut image_bytes = 0u64;
for (n, r) in records.iter().enumerate() {
let op = opcode(r.cdb);
if op == 0x28 && dtc(r.cdb) == Some(0) {
image_reads += 1;
image_bytes += r.data.len() as u64;
continue;
}
match op {
0x24 => println!(
"[{n}] seq={} SET WINDOW: {}",
r.seq,
window_row(&r.data[8..])
),
0x1B => println!(
"[{n}] seq={} SCAN windows = [{}]",
r.seq,
r.data
.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.join(" ")
),
0x28 if dtc(r.cdb) == Some(0x88) => {
match r.data.get(data::HEADER..).and_then(Boundary::from_bytes) {
Some(b) => {
println!(
"[{n}] seq={} FRAME BOUNDARY ({} frame(s))",
r.seq,
b.frames.len()
);
for f in &b.frames {
println!(
" rect top={} left={} bottom={} right={}",
f.top, f.left, f.bottom, f.right
);
}
}
None => println!("[{n}] seq={} FRAME BOUNDARY (unparseable)", r.seq),
}
}
0x28 if dtc(r.cdb) == Some(0x8E) => {
match r
.data
.get(data::HEADER..)
.and_then(PerfInformation::from_bytes)
{
Some(b) => {
println!("[{n}] seq={} PERF ({} line(s))", r.seq, b.perfs.len());
for (line, f) in b.perfs.iter().enumerate() {
println!(
" line={line} perf num={} flag={} decimal={} pulse={}",
f.perf_number,
f.count_switching_flag,
f.perf_decimal,
f.pulse_number
);
}
}
None => println!("[{n}] seq={} PERF (unparseable)", r.seq),
}
}
_ => println!("[{n}] seq={} {}", r.seq, describe(r)),
}
}
println!("\nimage data: {image_bytes} bytes across {image_reads} READs");
Ok(())
}
fn window_row(descriptor: &[u8]) -> String {
let Ok(w) = Window::try_from(descriptor) else {
return format!("{} bytes, not a descriptor", descriptor.len());
};
format!(
"id={} dpi=({},{}) origin=({},{}) size=({},{}) bpp={} reading={} color=0x{:02x} flags=0x{:02x} kind=0x{:02x} mode=0x{:02x} interleave=0x{:02x} ae={} exposure={}",
w.id,
w.resolution.0,
w.resolution.1,
w.origin.0,
w.origin.1,
w.size.0,
w.size.1,
w.bpp,
w.multiple_reading,
w.color_ordering,
w.flags.bits(),
w.scanning_kind.bits(),
w.scanning_mode.bits(),
w.color_interleaving.bits(),
w.ae_value,
w.exposure,
)
}
fn describe(r: &Record) -> String {
let op = opcode(r.cdb);
match op {
0x28 | 0x2A => format!(
"{} [{}] {} byte(s)",
opcode_name(op),
data_type(dtc(r.cdb)),
r.data.len()
),
0xE0 | 0xE1 => format!(
"{} op=0x{:02x}",
opcode_name(op),
r.cdb.get(2).copied().unwrap_or(0)
),
_ if r.data.is_empty() => opcode_name(op),
_ => format!("{} {} bytes", opcode_name(op), r.data.len()),
}
}
fn opcode(cdb: &[u8]) -> u8 {
cdb.first().copied().unwrap_or(0)
}
fn dtc(cdb: &[u8]) -> Option<u8> {
cdb.get(2).copied()
}
fn opcode_name(op: u8) -> String {
let name = match op {
0x00 => "TEST UNIT READY",
0x12 => "INQUIRY",
0x15 => "MODE SELECT",
0x1A => "MODE SENSE",
0x1B => "SCAN",
0x1D => "SEND DIAGNOSTIC",
0x24 => "SET WINDOW",
0x25 => "GET WINDOW",
0x28 => "READ",
0x2A => "SEND",
0xC0 => "ABORT",
0xC1 => "EXECUTE",
0xE0 => "SET PARAMETER",
0xE1 => "GET PARAMETER",
_ => return format!("OPCODE {op:02x}"),
};
name.to_string()
}
fn data_type(code: Option<u8>) -> String {
let label = match code {
Some(0) => "IMAGE",
Some(0x02) => "HALFTONE MASK",
Some(0x03) => "LUT",
Some(0x80) => "HISTOGRAM",
Some(0x81) => "MAX VALUE",
Some(0x82) => "MATRIX",
Some(0x83) => "FILTER",
Some(0x84) => "SHADING",
Some(0x85) => "DARK VOLTAGE",
Some(0x86) => "MAGNETIC",
Some(0x87) => "COOPERATION",
Some(0x88) => "BOUNDARY",
Some(0x89) => "ANALOG GAMMA",
Some(0x8A) => "ANALOG GAIN",
Some(0x8B) => "DIGITAL GAIN",
Some(0x8C) => "WB EXPOSURE",
Some(0x8D) => "SETUP",
Some(0x8E) => "PERFORATION",
Some(0x8F) => "BOUNDARY 2",
Some(0x90) => "SHIPMENT WB",
Some(0x91) => "CCD DATA",
Some(0x92) => "DRIVER VERSION",
Some(0x93) => "LEAK VOLUME",
Some(0xE0) => "RAM BUFFER",
Some(0xE1) => "EEPROM BUFFER",
Some(other) => return format!("DATA {other:02x}"),
None => return "RAW".to_string(),
};
label.to_string()
}