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
53pub 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 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(®ion.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 ®ion.value {
118 Some(v) => format!(
119 "{indent}{}: {}",
120 paint(®ion.label, opts, Paint::Label),
121 paint(v, opts, Paint::Value)
122 ),
123 None => format!("{indent}{}", paint(®ion.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 ®ion.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 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(¬e, opts, Paint::Dim))?;
169 }
170 }
171
172 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
183fn 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
199fn decode_blob(info: BlobInfo, data: &[u8], dec: &mut Decoder) -> String {
202 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#[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
270fn 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
310fn 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
348fn 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", Paint::Value => "36", Paint::Dim => "2", };
373 format!("\x1b[{code}m{s}\x1b[0m")
374}