Skip to main content

mlt_core/dump/
render.rs

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