1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum DataMode {
15 #[default]
17 Both,
18 Blob,
20 Decoded,
22 Hidden,
24}
25
26#[derive(Debug, Clone, Copy)]
28pub struct RenderOpts {
29 pub width: usize,
31 pub show_bits: bool,
33 pub color: bool,
35 pub data_mode: DataMode,
37 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
55pub 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 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(®ion.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 ®ion.value {
120 Some(v) => format!(
121 "{indent}{}: {}",
122 paint(®ion.label, opts, Paint::Label),
123 paint(v, opts, Paint::Value)
124 ),
125 None => format!("{indent}{}", paint(®ion.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 ®ion.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 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(¬e, opts, Paint::Dim)
191 )?;
192 }
193 }
194
195 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
206fn 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
222fn decode_blob(info: BlobInfo, data: &[u8], dec: &mut Decoder) -> String {
225 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
256fn 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
296fn 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", Paint::Value => "36", Paint::Dim => "2", };
355 format!("\x1b[{code}m{s}\x1b[0m")
356}