use std::io::{self, Write};
#[cfg(feature = "unstable-v2")]
use usize_cast::IntoUsize as _;
use super::model::{BlobInfo, DecodeHint, DumpTree, Region, RegionKind};
use crate::Decoder;
use crate::decoder::RawStream;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DataMode {
#[default]
Both,
Blob,
Decoded,
Hidden,
}
#[derive(Debug, Clone, Copy)]
pub struct RenderOpts {
pub width: usize,
pub show_bits: bool,
pub color: bool,
pub data_mode: DataMode,
pub max_blob: usize,
}
impl Default for RenderOpts {
fn default() -> Self {
Self {
width: 16,
show_bits: true,
color: false,
data_mode: DataMode::Both,
max_blob: 256,
}
}
}
pub fn render(
tree: &DumpTree,
buf: &[u8],
opts: &RenderOpts,
w: &mut impl Write,
) -> io::Result<()> {
if opts.width == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"render width must be non-zero",
));
}
let mut dec = Decoder::default();
let left_len = left_width(opts.width);
for region in &tree.regions {
render_region(w, buf, region, opts, left_len, &mut dec)?;
}
Ok(())
}
fn left_width(width: usize) -> usize {
8 + 2 + width * 3 + 2 + width
}
fn render_region(
w: &mut impl Write,
buf: &[u8],
region: &Region,
opts: &RenderOpts,
left_len: usize,
dec: &mut Decoder,
) -> io::Result<()> {
let indent = " ".repeat(region.depth);
if region.container {
let left = format!("{:08x}", region.offset);
let annot = format!(
"{indent}{} ({} B)",
paint(®ion.label, opts, Paint::Label),
region.len
);
writeln!(w, "{left:<left_len$} | {annot}")?;
return Ok(());
}
let bytes = &buf[region.offset..region.offset + region.len];
match region.kind {
RegionKind::Meta => render_meta(w, region, bytes, opts, left_len, &indent),
RegionKind::DataBlob => render_blob(w, region, bytes, opts, left_len, &indent, dec),
}
}
fn render_meta(
w: &mut impl Write,
region: &Region,
bytes: &[u8],
opts: &RenderOpts,
left_len: usize,
indent: &str,
) -> io::Result<()> {
let annot = match ®ion.value {
Some(v) => format!(
"{indent}{}: {}",
paint(®ion.label, opts, Paint::Label),
paint(v, opts, Paint::Value)
),
None => format!("{indent}{}", paint(®ion.label, opts, Paint::Label)),
};
emit_bytes(w, region.offset, bytes, opts, left_len, &annot)?;
if opts.show_bits {
for bf in ®ion.bits {
let annot = format!("{indent} └ {bf}");
writeln!(w, "{:<left_len$} | {}", "", paint(&annot, opts, Paint::Dim))?;
}
}
Ok(())
}
fn render_blob(
w: &mut impl Write,
region: &Region,
bytes: &[u8],
opts: &RenderOpts,
left_len: usize,
indent: &str,
dec: &mut Decoder,
) -> io::Result<()> {
let summary = blob_summary(region, bytes);
if opts.data_mode == DataMode::Hidden {
let annot = format!("{indent}{}", paint(&summary, opts, Paint::Dim));
let left = format!("{:08x}", region.offset);
writeln!(w, "{left:<left_len$} | {annot}")?;
return Ok(());
}
if matches!(opts.data_mode, DataMode::Both | DataMode::Blob) {
let annot = format!("{indent}{}", paint(&summary, opts, Paint::Dim));
let shown = if opts.max_blob == 0 || bytes.len() <= opts.max_blob {
bytes
} else {
&bytes[..opts.max_blob]
};
emit_bytes(w, region.offset, shown, opts, left_len, &annot)?;
if shown.len() < bytes.len() {
let note = format!(
"{indent} ... {} more bytes omitted (--max-blob to change)",
bytes.len() - shown.len()
);
writeln!(w, "{:<left_len$} | {}", "", paint(¬e, opts, Paint::Dim))?;
}
}
if matches!(opts.data_mode, DataMode::Both | DataMode::Decoded)
&& let Some(info) = region.blob
{
let decoded = decode_blob(info, bytes, dec);
let annot = format!("{indent} decoded: {}", paint(&decoded, opts, Paint::Value));
writeln!(w, "{:<left_len$} | {annot}", "")?;
}
Ok(())
}
fn blob_summary(region: &Region, bytes: &[u8]) -> String {
match region.blob {
Some(info) => format!(
"{} [{:?} {:?}/{:?}, {} values, {} B]",
region.label,
info.meta.stream_type,
info.meta.encoding.logical,
info.meta.encoding.physical,
info.meta.num_values,
bytes.len()
),
None => format!("{} [{} B]", region.label, bytes.len()),
}
}
fn decode_blob(info: BlobInfo, data: &[u8], dec: &mut Decoder) -> String {
dec.reset_budget();
let meta = info.meta;
match info.hint {
DecodeHint::Presence => match RawStream::new(meta, data).decode_bitvec(dec) {
Ok(bits) => fmt_bits(bits.len(), |i| bits[i]),
Err(e) => format!("<undecodable: {e}>"),
},
#[cfg(feature = "unstable-v2")]
DecodeHint::PresenceCoded(coding) => {
use crate::codecs::presence_coding::{Unmetered, read};
match read(data, meta.num_values, coding, &mut Unmetered) {
Ok((_, bits)) => fmt_bits(bits.len(), |i| bits[i]),
Err(e) => format!("<undecodable: {e}>"),
}
}
#[cfg(feature = "unstable-v2")]
DecodeHint::PackedBits => {
let n = meta.num_values.into_usize();
let available = data.len() * 8;
if available < n {
return format!("<undecodable: needs {n} bits, got {available}>");
}
fmt_bits(n, |i| data[i / 8] >> (i % 8) & 1 == 1)
}
DecodeHint::Bool => fmt_res(RawStream::new(meta, data).decode_bools(dec)),
DecodeHint::I32 => fmt_res(RawStream::new(meta, data).decode_ints::<i32>(dec)),
DecodeHint::U32 | DecodeHint::GeometryType => {
fmt_res(RawStream::new(meta, data).decode_ints::<u32>(dec))
}
DecodeHint::I64 => fmt_res(RawStream::new(meta, data).decode_ints::<i64>(dec)),
#[cfg(feature = "unstable-v2")]
DecodeHint::Alp(params) => {
fmt_res(RawStream::new(meta, data).decode_alp_codes(params, dec))
}
DecodeHint::U64 => fmt_res(RawStream::new(meta, data).decode_ints::<u64>(dec)),
DecodeHint::F32 => fmt_res(RawStream::new(meta, data).decode_floats::<f32>(dec)),
DecodeHint::F64 => fmt_res(RawStream::new(meta, data).decode_floats::<f64>(dec)),
DecodeHint::Bytes => match std::str::from_utf8(data) {
Ok(s) => format!("utf-8 {:?}", truncate_str(s, 200)),
Err(_) => format!("<{} binary bytes>", data.len()),
},
}
}
fn fmt_bits(n: usize, bit: impl Fn(usize) -> bool) -> String {
let shown: String = (0..n.min(96))
.map(|i| if bit(i) { '1' } else { '0' })
.collect();
let more = if n > 96 { "…" } else { "" };
format!("{n} present-bits: {shown}{more}")
}
fn fmt_res<T: std::fmt::Display>(res: crate::MltResult<Vec<T>>) -> String {
match res {
Ok(v) => fmt_list(&v),
Err(e) => format!("<undecodable: {e}>"),
}
}
fn fmt_list<T: std::fmt::Display>(v: &[T]) -> String {
const MAX: usize = 48;
let shown = v
.iter()
.take(MAX)
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
if v.len() > MAX {
format!("[{shown}, … ] ({} total)", v.len())
} else {
format!("[{shown}]")
}
}
fn truncate_str(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
let head: String = s.chars().take(max).collect();
format!("{head}…")
}
}
fn emit_bytes(
w: &mut impl Write,
offset: usize,
bytes: &[u8],
opts: &RenderOpts,
left_len: usize,
annotation: &str,
) -> io::Result<()> {
if bytes.is_empty() {
let left = format!("{offset:08x}");
return emit_row(w, &left, left_len, annotation);
}
for (row, chunk) in bytes.chunks(opts.width).enumerate() {
let row_off = offset + row * opts.width;
let hex = chunk
.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.join(" ");
let ascii: String = chunk
.iter()
.map(|&b| {
if (0x20..=0x7e).contains(&b) {
b as char
} else {
'.'
}
})
.collect();
let hexw = opts.width * 3;
let left = format!("{row_off:08x} {hex:<hexw$} {ascii}");
emit_row(w, &left, left_len, if row == 0 { annotation } else { "" })?;
}
Ok(())
}
fn emit_row(w: &mut impl Write, left: &str, left_len: usize, annotation: &str) -> io::Result<()> {
if annotation.is_empty() {
writeln!(w, "{left:<left_len$} |")
} else {
writeln!(w, "{left:<left_len$} | {annotation}")
}
}
#[derive(Clone, Copy)]
enum Paint {
Label,
Value,
Dim,
}
fn paint(s: &str, opts: &RenderOpts, kind: Paint) -> String {
if !opts.color {
return s.to_string();
}
let code = match kind {
Paint::Label => "1", Paint::Value => "36", Paint::Dim => "2", };
format!("\x1b[{code}m{s}\x1b[0m")
}