use std::error::Error;
use std::fmt::Write as _;
use clap::error::ErrorKind;
use clap::{CommandFactory, Parser};
use tcslog::{record_trailer, LogError, LogRead, Meta, SegmentHeader};
const MAX_MESSAGE_SIZE: usize = 256;
#[derive(Parser)]
#[command(version, about)]
struct Args {
dirname: String,
prefix: String,
suffix: String,
#[arg(short, long)]
text: bool,
#[arg(short, long)]
verbose: bool,
}
fn main() -> Result<(), Box<dyn Error>> {
let args = Args::try_parse().unwrap_or_else(|e| {
if matches!(e.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion) {
e.exit();
}
eprintln!("{e}");
eprintln!("{}", Args::command().render_help());
std::process::exit(2);
});
let mut log = LogRead::new(&args.dirname, &args.prefix, &args.suffix)?;
log.collect_opened_headers(args.verbose);
let mut printed_header = false;
let mut files_lost = 0u64;
let mut total = 0u64;
let mut buf = vec![0u8; MAX_MESSAGE_SIZE];
loop {
let read_result = log.read(&mut buf);
for h in log.take_opened_headers() {
if printed_header {
println!();
}
printed_header = true;
print_header(&args.prefix, &args.suffix, &h);
}
match read_result {
Ok(res) => {
total += 1;
print_record(args.text, res.meta, &buf[..res.n as usize]);
println!();
}
Err(LogError::Eof) => break,
Err(LogError::ReadTruncated { lost, n }) => {
files_lost += lost;
if n > 0 {
total += 1;
print_truncated_record(args.text, &buf[..n as usize]);
println!();
}
if args.verbose {
if lost > 0 {
println!(
" -- {lost} missing segment file(s); \
resynchronizing --"
);
} else {
println!(
" -- corrupted or truncated segment file; \
resynchronizing --"
);
}
}
}
Err(LogError::ReadOverflow(n)) => {
total += 1;
print_partial_record(args.text, &buf[..n as usize]);
println!();
if args.verbose {
println!(
" (payload larger than {MAX_MESSAGE_SIZE}-byte buffer; \
{n} bytes captured, remainder discarded)"
);
}
}
Err(LogError::SessionEnd) => {
if args.verbose {
println!();
println!("--- End of Session---");
}
}
Err(e) => {
return Err(e.into());
}
}
}
if args.verbose {
println!(
"\nread {total} message(s) across {} file(s)",
log.segments_opened()
);
if files_lost > 0 {
println!("{files_lost} segment file(s) lost");
}
}
Ok(())
}
fn print_record(text: bool, meta: Meta, buf: &[u8]) {
let msg = format_msg(text, buf);
print!(" {msg} {}", record_trailer(buf.len(), meta));
}
fn print_partial_record(text: bool, buf: &[u8]) {
let msg = format_msg(text, buf);
print!(" {msg} ({} bytes captured, record truncated)", buf.len());
}
fn print_truncated_record(text: bool, buf: &[u8]) {
let msg = format_msg(text, buf);
print!(
" {msg} ({} bytes recovered, rest of record lost)",
buf.len()
);
}
fn format_msg(as_ascii: bool, buf: &[u8]) -> String {
if !as_ascii {
let mut out = String::with_capacity(buf.len() * 3);
for (i, b) in buf.iter().enumerate() {
if i > 0 {
out.push(' ');
}
let _ = write!(out, "{b:02x}");
}
return out;
}
let mut out = String::with_capacity(buf.len());
for &b in buf {
match b {
b'\\' => out.push_str("\\\\"),
b'\n' => out.push_str("\\n"),
b'\r' => out.push_str("\\r"),
b'\t' => out.push_str("\\t"),
0x20..=0x7e => out.push(b as char),
_ => {
let _ = write!(out, "\\x{b:02x}");
}
}
}
out
}
fn print_header(prefix: &str, suffix: &str, h: &SegmentHeader) {
println!("=== segment file: {}{}{} ===", prefix, h.segment_id, suffix);
println!(" segment_id: {}", h.segment_id);
println!(" session_id: {}", h.session_id);
println!(" max_size: {}", h.max_size);
println!(" remaining: {}", h.remaining);
println!(" format: {:?}", h.format);
println!(" sequence: {}", h.sequence);
}
#[cfg(test)]
mod tests {
use super::format_msg;
#[test]
fn ascii_shows_printable_characters_as_themselves() {
assert_eq!(format_msg(true, b"attitude nominal"), "attitude nominal");
assert_eq!(format_msg(true, &[0x20, 0x7e]), " ~");
}
#[test]
fn ascii_escapes_what_would_disturb_the_line() {
assert_eq!(format_msg(true, b"a\nb"), "a\\nb");
assert_eq!(format_msg(true, b"a\rb"), "a\\rb");
assert_eq!(format_msg(true, b"a\tb"), "a\\tb");
assert_eq!(format_msg(true, &[b'a', 0x1b, b'b']), "a\\x1bb");
assert_eq!(format_msg(true, &[0x00, 0x7f]), "\\x00\\x7f");
}
#[test]
fn ascii_escapes_everything_above_the_ascii_range() {
assert_eq!(format_msg(true, &[0xe9, b'A', 0x80]), "\\xe9A\\x80");
assert_eq!(format_msg(true, &[0xff]), "\\xff");
}
#[test]
fn a_backslash_in_the_payload_is_escaped_too() {
assert_eq!(format_msg(true, br"a\b"), "a\\\\b");
assert_eq!(format_msg(true, br"\x41"), "\\\\x41");
}
#[test]
fn without_the_flag_each_byte_is_two_hexadecimal_digits() {
assert_eq!(format_msg(false, &[0xe9, b'A', 0x80]), "e9 41 80");
assert_eq!(format_msg(false, &[0x00, 0x0f, 0xff]), "00 0f ff");
assert_eq!(format_msg(false, b"ab"), "61 62");
}
#[test]
fn hexadecimal_separates_bytes_without_trailing_space() {
assert_eq!(format_msg(false, &[0x01]), "01");
assert_eq!(format_msg(false, &[]), "");
assert_eq!(format_msg(false, &[1, 2, 3]).matches(' ').count(), 2);
}
}