1use std::io::{self, Write};
4
5use super::model::{BlobInfo, DecodeHint, DumpTree, Region, RegionKind};
6use crate::Decoder;
7use crate::decoder::RawStream;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum DataMode {
12 #[default]
14 Both,
15 Blob,
17 Decoded,
19 Hidden,
21}
22
23#[derive(Debug, Clone, Copy)]
25pub struct RenderOpts {
26 pub width: usize,
28 pub show_bits: bool,
30 pub color: bool,
32 pub data_mode: DataMode,
34 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
52pub 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 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(®ion.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 ®ion.value {
117 Some(v) => format!(
118 "{indent}{}: {}",
119 paint(®ion.label, opts, Paint::Label),
120 paint(v, opts, Paint::Value)
121 ),
122 None => format!("{indent}{}", paint(®ion.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 ®ion.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 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(¬e, opts, Paint::Dim)
188 )?;
189 }
190 }
191
192 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
203fn 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
219fn decode_blob(info: BlobInfo, data: &[u8], dec: &mut Decoder) -> String {
222 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
284fn 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", Paint::Value => "36", Paint::Dim => "2", };
343 format!("\x1b[{code}m{s}\x1b[0m")
344}