//! Human-readable dumps for debugging and capture review. //! //! Diagnostics only: nothing here is part of the wire contract, and no output //! format should ever be parsed back. use crate::fire2::{Frame, Header}; use crate::heat2::{Struct, Value}; use std::fmt::Write as _; /// Render a TDF struct as an indented tree, mirroring `heat2.py::dump`. pub fn dump_struct(s: &Struct) -> String { let mut out = String::new(); write_struct(s, 0, &mut out); out } fn write_struct(s: &Struct, depth: usize, out: &mut String) { for (tag, value) in s.iter() { let pad = " ".repeat(depth); let _ = write!(out, "{pad}{tag} ({})", value.type_id().name()); match value { Value::Struct(inner) => { let _ = writeln!(out, " {{"); write_struct(inner, depth + 1, out); let _ = writeln!(out, "{pad}}}"); } Value::List { elem, items } => { let _ = writeln!(out, " [{} x {}]", elem.name(), items.len()); for item in items { write_value(item, depth + 1, out); } } Value::Map { key, val, entries } => { let _ = writeln!( out, " {{{} -> {} x {}}}", key.name(), val.name(), entries.len() ); for (k, v) in entries { write_value(k, depth + 1, out); write_value(v, depth + 2, out); } } other => { let _ = writeln!(out, " = {}", scalar(other)); } } } } fn write_value(value: &Value, depth: usize, out: &mut String) { let pad = " ".repeat(depth); match value { Value::Struct(inner) => { let _ = writeln!(out, "{pad}{{"); write_struct(inner, depth + 1, out); let _ = writeln!(out, "{pad}}}"); } other => { let _ = writeln!(out, "{pad}{}", scalar(other)); } } } fn scalar(value: &Value) -> String { match value { Value::Int(v) => format!("{v}"), Value::String(s) => format!("{s:?}"), Value::Blob(b) => format!("<{} bytes> {}", b.len(), hex(&b[..b.len().min(32)])), Value::VarList(v) => format!("{v:?}"), Value::ObjType { component, ty } => format!("({component}, {ty})"), Value::ObjId { component, ty, id } => format!("({component}, {ty}, {id})"), Value::Float(f) => format!("{f}"), Value::Union { key, member } => match member { Some(m) => format!("union[{key}] {} = {}", m.0, scalar(&m.1)), None => format!("union[{key}] unset"), }, Value::Struct(_) | Value::List { .. } | Value::Map { .. } => String::from("..."), } } /// One-line summary of a Fire2 header. pub fn describe_header(h: &Header) -> String { format!( "component=0x{:04x} command=0x{:04x} {} msgNum={} userIdx={} opts=0x{:02x} \ payload={}B metadata={}B", h.component, h.command, h.msg_type.name(), h.msg_num, h.user_index, h.options, h.payload_len, h.metadata_len ) } /// Header summary plus a decoded body, falling back to hex when it will not /// parse. A capture is most valuable exactly when the body is malformed, so /// this must never fail. pub fn describe_frame(frame: &Frame) -> String { let mut out = describe_header(&frame.header); out.push('\n'); if frame.payload.is_empty() { out.push_str("(empty payload)\n"); return out; } match crate::heat2::decode(&frame.payload) { Ok(body) => out.push_str(&dump_struct(&body)), Err(e) => { let _ = writeln!(out, "(TDF decode failed: {e})"); out.push_str(&hexdump(&frame.payload, 256)); } } out } pub fn hex(bytes: &[u8]) -> String { let mut s = String::with_capacity(bytes.len() * 2); for b in bytes { let _ = write!(s, "{b:02x}"); } s } /// Classic offset / hex / ASCII dump, truncated to `limit` bytes. pub fn hexdump(bytes: &[u8], limit: usize) -> String { let shown = &bytes[..bytes.len().min(limit)]; let mut out = String::new(); for (row, chunk) in shown.chunks(16).enumerate() { let _ = write!(out, "{:08x} ", row * 16); for i in 0..16 { match chunk.get(i) { Some(b) => { let _ = write!(out, "{b:02x} "); } None => out.push_str(" "), } if i == 7 { out.push(' '); } } out.push_str(" |"); for b in chunk { out.push(if (0x20..0x7F).contains(b) { *b as char } else { '.' }); } out.push_str("|\n"); } if bytes.len() > limit { let _ = writeln!(out, "... {} more bytes", bytes.len() - limit); } out } #[cfg(test)] mod tests { use super::*; use crate::fire2::MsgType; use crate::heat2; #[test] fn dumps_nested_structures() { let s = Struct::new().with("PID", Value::Int(33068179)).with( "CINF", Value::Struct(Struct::new().with("ENV", Value::String("prod".into()))), ); let text = dump_struct(&s); assert!(text.contains("PID (int) = 33068179"), "{text}"); assert!(text.contains("ENV (string) = \"prod\""), "{text}"); } #[test] fn describes_a_frame_with_an_undecodable_body() { // 0xFF is not a TDF type byte, so this must fall back to hex. let frame = Frame::new(9, 7, 1, MsgType::Reply, vec![0xFF; 8]); let text = describe_frame(&frame); assert!(text.contains("decode failed"), "{text}"); assert!(text.contains("ff ff ff"), "{text}"); } #[test] fn describes_a_frame_with_a_good_body() { let body = Struct::new().with("A", Value::Int(1)); let frame = Frame::new(9, 7, 1, MsgType::Reply, heat2::encode(&body)); let text = describe_frame(&frame); assert!(text.contains("REPLY"), "{text}"); assert!(text.contains("A (int) = 1"), "{text}"); } #[test] fn hexdump_truncates_and_says_so() { let text = hexdump(&[0x41; 100], 32); assert!(text.contains("68 more bytes"), "{text}"); assert!(text.contains("AAAA"), "{text}"); } }