Skip to main content

mlt_core/dump/
render.rs

1//! Renders a [`DumpTree`] as an annotated hexdump.
2
3use std::io::{self, Write};
4
5use super::model::{BlobInfo, DecodeHint, DumpTree, Region, RegionKind};
6use crate::Decoder;
7use crate::decoder::RawStream;
8
9/// How data-payload blobs are shown.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum DataMode {
12    /// Raw hex plus best-effort decoded values (default).
13    #[default]
14    Both,
15    /// Raw hex only.
16    Blob,
17    /// Decoded values only.
18    Decoded,
19    /// A one-line summary, no payload bytes.
20    Hidden,
21}
22
23/// Rendering options for [`render`].
24#[derive(Debug, Clone, Copy)]
25pub struct RenderOpts {
26    /// Hex bytes per row.
27    pub width: usize,
28    /// Show the bit-level breakdown of packed bytes.
29    pub show_bits: bool,
30    /// Emit ANSI color escapes.
31    pub color: bool,
32    /// How to render data payloads.
33    pub data_mode: DataMode,
34    /// Truncate raw payload hex to this many bytes (`0` = unlimited).
35    pub max_blob: usize,
36}
37
38impl Default for RenderOpts {
39    fn default() -> Self {
40        Self {
41            width: 16,
42            show_bits: true,
43            color: false,
44            data_mode: DataMode::Both,
45            max_blob: 256,
46        }
47    }
48}
49
50const SEP: &str = " | ";
51
52/// Render `tree` as an annotated hexdump.
53/// `buf` must be the buffer that was passed to [`super::annotate_tile`].
54pub fn render(
55    tree: &DumpTree,
56    buf: &[u8],
57    opts: &RenderOpts,
58    w: &mut impl Write,
59) -> io::Result<()> {
60    if opts.width == 0 {
61        return Err(io::Error::new(
62            io::ErrorKind::InvalidInput,
63            "render width must be non-zero",
64        ));
65    }
66    let mut dec = Decoder::default();
67    let left_len = left_width(opts.width);
68    for region in &tree.regions {
69        render_region(w, buf, region, opts, left_len, &mut dec)?;
70    }
71    Ok(())
72}
73
74fn left_width(width: usize) -> usize {
75    // "{off:08x}  " + hex(width*3) + "  " + ascii(width)
76    8 + 2 + width * 3 + 2 + width
77}
78
79fn render_region(
80    w: &mut impl Write,
81    buf: &[u8],
82    region: &Region,
83    opts: &RenderOpts,
84    left_len: usize,
85    dec: &mut Decoder,
86) -> io::Result<()> {
87    let indent = "  ".repeat(region.depth);
88
89    if region.container {
90        let left = format!("{:08x}", region.offset);
91        let annot = format!(
92            "{indent}{} ({} B)",
93            paint(&region.label, opts, Paint::Label),
94            region.len
95        );
96        writeln!(w, "{left:<left_len$}{SEP}{annot}")?;
97        return Ok(());
98    }
99
100    let bytes = &buf[region.offset..region.offset + region.len];
101
102    match region.kind {
103        RegionKind::Meta => render_meta(w, region, bytes, opts, left_len, &indent),
104        RegionKind::DataBlob => render_blob(w, region, bytes, opts, left_len, &indent, dec),
105    }
106}
107
108fn render_meta(
109    w: &mut impl Write,
110    region: &Region,
111    bytes: &[u8],
112    opts: &RenderOpts,
113    left_len: usize,
114    indent: &str,
115) -> io::Result<()> {
116    let annot = match &region.value {
117        Some(v) => format!(
118            "{indent}{}: {}",
119            paint(&region.label, opts, Paint::Label),
120            paint(v, opts, Paint::Value)
121        ),
122        None => format!("{indent}{}", paint(&region.label, opts, Paint::Label)),
123    };
124    emit_bytes(w, region.offset, bytes, opts, left_len, &annot)?;
125
126    if opts.show_bits {
127        for bf in &region.bits {
128            let range = if bf.hi == bf.lo {
129                format!("bit {}", bf.hi)
130            } else {
131                format!("bits {}-{}", bf.hi, bf.lo)
132            };
133            let width = usize::from(bf.hi - bf.lo + 1);
134            let annot = format!(
135                "{indent}  └ {range} = {:0width$b} → {}",
136                bf.raw,
137                bf.meaning,
138                width = width
139            );
140            writeln!(
141                w,
142                "{:<left_len$}{SEP}{}",
143                "",
144                paint(&annot, opts, Paint::Dim)
145            )?;
146        }
147    }
148    Ok(())
149}
150
151fn render_blob(
152    w: &mut impl Write,
153    region: &Region,
154    bytes: &[u8],
155    opts: &RenderOpts,
156    left_len: usize,
157    indent: &str,
158    dec: &mut Decoder,
159) -> io::Result<()> {
160    let summary = blob_summary(region, bytes);
161
162    if opts.data_mode == DataMode::Hidden {
163        let annot = format!("{indent}{}", paint(&summary, opts, Paint::Dim));
164        let left = format!("{:08x}", region.offset);
165        writeln!(w, "{left:<left_len$}{SEP}{annot}")?;
166        return Ok(());
167    }
168
169    // Raw hex (unless Decoded-only).
170    if matches!(opts.data_mode, DataMode::Both | DataMode::Blob) {
171        let annot = format!("{indent}{}", paint(&summary, opts, Paint::Dim));
172        let shown = if opts.max_blob == 0 || bytes.len() <= opts.max_blob {
173            bytes
174        } else {
175            &bytes[..opts.max_blob]
176        };
177        emit_bytes(w, region.offset, shown, opts, left_len, &annot)?;
178        if shown.len() < bytes.len() {
179            let note = format!(
180                "{indent}  … {} more bytes omitted (--max-blob to change)",
181                bytes.len() - shown.len()
182            );
183            writeln!(
184                w,
185                "{:<left_len$}{SEP}{}",
186                "",
187                paint(&note, opts, Paint::Dim)
188            )?;
189        }
190    }
191
192    // Decoded values (unless Blob-only).
193    if matches!(opts.data_mode, DataMode::Both | DataMode::Decoded)
194        && let Some(info) = region.blob
195    {
196        let decoded = decode_blob(info, bytes, dec);
197        let annot = format!("{indent}  decoded: {}", paint(&decoded, opts, Paint::Value));
198        writeln!(w, "{:<left_len$}{SEP}{}", "", annot)?;
199    }
200    Ok(())
201}
202
203/// One-line description of a data blob for its annotation column.
204fn blob_summary(region: &Region, bytes: &[u8]) -> String {
205    match region.blob {
206        Some(info) => format!(
207            "{} [{:?} {:?}/{:?}, {} values, {} B]",
208            region.label,
209            info.meta.stream_type,
210            info.meta.encoding.logical,
211            info.meta.encoding.physical,
212            info.meta.num_values,
213            bytes.len()
214        ),
215        None => format!("{} [{} B]", region.label, bytes.len()),
216    }
217}
218
219/// Best-effort decode of a stream payload for display.
220/// Never panics; decode errors are rendered inline.
221fn decode_blob(info: BlobInfo, data: &[u8], dec: &mut Decoder) -> String {
222    // Bound memory/time per blob; the decoded values are dropped immediately.
223    dec.reset_budget();
224    let meta = info.meta;
225    match info.hint {
226        DecodeHint::Presence => match RawStream::new(meta, data).decode_bitvec(dec) {
227            Ok(bits) => {
228                let n = bits.len();
229                let shown: String = bits
230                    .iter()
231                    .take(96)
232                    .map(|b| if *b { '1' } else { '0' })
233                    .collect();
234                let more = if n > 96 { "…" } else { "" };
235                format!("{n} present-bits: {shown}{more}")
236            }
237            Err(e) => format!("<undecodable: {e}>"),
238        },
239        DecodeHint::Bool => fmt_res(RawStream::new(meta, data).decode_bools(dec)),
240        DecodeHint::I32 => fmt_res(RawStream::new(meta, data).decode_ints::<i32>(dec)),
241        DecodeHint::U32 => fmt_res(RawStream::new(meta, data).decode_ints::<u32>(dec)),
242        DecodeHint::I64 => fmt_res(RawStream::new(meta, data).decode_ints::<i64>(dec)),
243        DecodeHint::U64 => fmt_res(RawStream::new(meta, data).decode_ints::<u64>(dec)),
244        DecodeHint::F32 => fmt_res(RawStream::new(meta, data).decode_floats::<f32>(dec)),
245        DecodeHint::F64 => fmt_res(RawStream::new(meta, data).decode_floats::<f64>(dec)),
246        DecodeHint::Bytes => match std::str::from_utf8(data) {
247            Ok(s) => format!("utf-8 {:?}", truncate_str(s, 200)),
248            Err(_) => format!("<{} binary bytes>", data.len()),
249        },
250    }
251}
252
253fn fmt_res<T: std::fmt::Display>(res: crate::MltResult<Vec<T>>) -> String {
254    match res {
255        Ok(v) => fmt_list(&v),
256        Err(e) => format!("<undecodable: {e}>"),
257    }
258}
259
260fn fmt_list<T: std::fmt::Display>(v: &[T]) -> String {
261    const MAX: usize = 48;
262    let shown = v
263        .iter()
264        .take(MAX)
265        .map(ToString::to_string)
266        .collect::<Vec<_>>()
267        .join(", ");
268    if v.len() > MAX {
269        format!("[{shown}, … ] ({} total)", v.len())
270    } else {
271        format!("[{shown}]")
272    }
273}
274
275fn truncate_str(s: &str, max: usize) -> String {
276    if s.chars().count() <= max {
277        s.to_string()
278    } else {
279        let head: String = s.chars().take(max).collect();
280        format!("{head}…")
281    }
282}
283
284/// Emit one or more hexdump rows for `bytes` starting at `offset`.
285/// The `annotation` is printed on the first row only.
286fn emit_bytes(
287    w: &mut impl Write,
288    offset: usize,
289    bytes: &[u8],
290    opts: &RenderOpts,
291    left_len: usize,
292    annotation: &str,
293) -> io::Result<()> {
294    if bytes.is_empty() {
295        let left = format!("{offset:08x}");
296        writeln!(w, "{left:<left_len$}{SEP}{annotation}")?;
297        return Ok(());
298    }
299    for (row, chunk) in bytes.chunks(opts.width).enumerate() {
300        let row_off = offset + row * opts.width;
301        let hex = chunk
302            .iter()
303            .map(|b| format!("{b:02x}"))
304            .collect::<Vec<_>>()
305            .join(" ");
306        let ascii: String = chunk
307            .iter()
308            .map(|&b| {
309                if (0x20..=0x7e).contains(&b) {
310                    b as char
311                } else {
312                    '.'
313                }
314            })
315            .collect();
316        let hexw = opts.width * 3;
317        let left = format!("{row_off:08x}  {hex:<hexw$}  {ascii}");
318        if row == 0 {
319            writeln!(w, "{left:<left_len$}{SEP}{annotation}")?;
320        } else {
321            writeln!(w, "{left:<left_len$}{SEP}")?;
322        }
323    }
324    Ok(())
325}
326
327#[derive(Clone, Copy)]
328enum Paint {
329    Label,
330    Value,
331    Dim,
332}
333
334fn paint(s: &str, opts: &RenderOpts, kind: Paint) -> String {
335    if !opts.color {
336        return s.to_string();
337    }
338    let code = match kind {
339        Paint::Label => "1",  // bold
340        Paint::Value => "36", // cyan
341        Paint::Dim => "2",    // dim
342    };
343    format!("\x1b[{code}m{s}\x1b[0m")
344}