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