1use pcf::TYPE_RAW;
6
7use super::color::Palette;
8use super::{label_or, uid_hex, Report};
9use crate::model::algo_name;
10use crate::model::diag::Severity;
11use crate::model::{LayoutMap, RegionKind};
12use crate::plugin::{Decoded, FieldNode, FieldValue};
13
14const STRIP_WIDTH: usize = 64;
16
17fn type_str(t: u32) -> String {
19 match t {
20 TYPE_RAW => format!("{t:#010x} (RAW)"),
21 0 => format!("{t:#010x} (RESERVED)"),
22 _ => format!("{t:#010x}"),
23 }
24}
25
26fn verify_cell(ok: Option<bool>, pal: Palette) -> String {
27 match ok {
28 Some(true) => pal.green("OK"),
29 Some(false) => pal.red("FAIL"),
30 None => pal.dim("—"),
31 }
32}
33
34pub fn strip(layout: &LayoutMap, pal: Palette) -> String {
36 if layout.file_len == 0 {
37 return String::new();
38 }
39 let mut cells = vec!['.'; STRIP_WIDTH];
40 for r in &layout.regions {
41 if r.len == 0 {
42 continue;
43 }
44 let start_cell = (r.start * STRIP_WIDTH as u64 / layout.file_len) as usize;
45 let end_cell =
46 ((r.end().saturating_sub(1)) * STRIP_WIDTH as u64 / layout.file_len) as usize;
47 for cell in cells
48 .iter_mut()
49 .take(end_cell.min(STRIP_WIDTH - 1) + 1)
50 .skip(start_cell)
51 {
52 *cell = r.kind.glyph();
53 }
54 }
55 let bar: String = cells.into_iter().collect();
56 format!(
57 "{}\n[{}]\nlegend: H=header T=table-hdr E=entries D=data _=slack .=gap",
58 pal.bold("byte map"),
59 bar
60 )
61}
62
63pub fn layout(layout: &LayoutMap, pal: Palette) -> String {
65 let mut out = String::new();
66 out.push_str(&pal.bold("layout\n"));
67 out.push_str(&format!(" file length: {} byte(s)\n", layout.file_len));
68 for r in &layout.regions {
69 let kind = match &r.kind {
70 RegionKind::Gap => pal.yellow(r.kind.short()),
71 RegionKind::Slack { .. } => pal.dim(r.kind.short()),
72 _ => pal.cyan(r.kind.short()),
73 };
74 out.push_str(&format!(
75 " {:#010x}..{:#010x} {:>8} {:>11} {}\n",
76 r.start,
77 r.end(),
78 r.len,
79 kind,
80 r.label
81 ));
82 }
83 out
84}
85
86pub fn table(layout: &LayoutMap, pal: Palette) -> String {
88 let mut out = String::new();
89 out.push_str(&pal.bold("partitions\n"));
90 out.push_str(&pal.dim(
91 " type uid label start used/max (free) algo data\n",
92 ));
93 let mut any = false;
94 for b in &layout.blocks {
95 for ev in &b.entries {
96 any = true;
97 let e = &ev.entry;
98 out.push_str(&format!(
99 " {:<14} {:.8}… {:<16} {:>9} {:>6}/{:<6} ({:>5}) {:<7} {}\n",
100 type_str(e.partition_type),
101 uid_hex(&e.uid),
102 label_or(e),
103 e.start_offset,
104 e.used_bytes,
105 e.max_length,
106 e.free_bytes(),
107 algo_name(e.data_hash_algo),
108 verify_cell(ev.data_hash_ok, pal),
109 ));
110 }
111 }
112 if !any {
113 out.push_str(&pal.dim(" (no partitions)\n"));
114 }
115 out
116}
117
118pub fn chain(layout: &LayoutMap, pal: Palette) -> String {
120 let mut out = String::new();
121 out.push_str(&pal.bold("block chain\n"));
122 if let Some(h) = layout.header {
123 out.push_str(&format!(
124 " header: v{}.{} first block @ {:#x}\n",
125 h.version_major, h.version_minor, h.partition_table_offset
126 ));
127 }
128 for b in &layout.blocks {
129 let next = if b.next_offset == 0 {
130 "end".to_string()
131 } else {
132 format!("{:#x}", b.next_offset)
133 };
134 let hash = match b.table_hash_ok {
135 Some(true) => pal.green("hash OK"),
136 Some(false) => pal.red("hash FAIL"),
137 None => pal.dim("hash —"),
138 };
139 out.push_str(&format!(
140 " block {} @ {:#x} count={} next={} {} [{}]\n",
141 b.index,
142 b.offset,
143 b.header.partition_count,
144 next,
145 hash,
146 algo_name(b.header.table_hash_algo),
147 ));
148 for (i, ev) in b.entries.iter().enumerate() {
149 let last = i + 1 == b.entries.len();
150 let branch = if last { "└─" } else { "├─" };
151 let valid = match &ev.validate_ok {
152 Ok(()) => String::new(),
153 Err(reason) => format!(" {}", pal.red(&format!("invalid: {reason}"))),
154 };
155 out.push_str(&format!(
156 " {branch} [{}] {} @ {:#x}{}\n",
157 ev.slot,
158 label_or(&ev.entry),
159 ev.entry.start_offset,
160 valid,
161 ));
162 }
163 }
164 out
165}
166
167fn value_str(v: &FieldValue) -> String {
169 match v {
170 FieldValue::None => String::new(),
171 FieldValue::U64(n) => n.to_string(),
172 FieldValue::Bytes(b) => {
173 if b.is_empty() {
174 "(empty)".into()
175 } else {
176 b.iter()
177 .map(|x| format!("{x:02x}"))
178 .collect::<Vec<_>>()
179 .join("")
180 }
181 }
182 FieldValue::Text(s) => format!("\"{s}\""),
183 FieldValue::Uid(u) => uid_hex(u),
184 FieldValue::Enum { raw, name } => format!("{raw} ({name})"),
185 FieldValue::Flags { raw, set } => {
186 if set.is_empty() {
187 format!("{raw:#x} (none)")
188 } else {
189 format!("{raw:#x} ({})", set.join("|"))
190 }
191 }
192 }
193}
194
195fn field_tree(node: &FieldNode, prefix: &str, last: bool, out: &mut String, pal: Palette) {
196 let branch = if last { "└─" } else { "├─" };
197 let val = value_str(&node.value);
198 let val_part = if val.is_empty() {
199 String::new()
200 } else {
201 format!(" = {val}")
202 };
203 let range_part = match node.range {
204 Some((a, b)) => pal.dim(&format!(" [{a}..{b}]")),
205 None => String::new(),
206 };
207 let note_part = match &node.note {
208 Some(n) => pal.dim(&format!(" // {n}")),
209 None => String::new(),
210 };
211 out.push_str(&format!(
212 "{prefix}{branch} {}{val_part}{range_part}{note_part}\n",
213 pal.bold(&node.name)
214 ));
215 let child_prefix = format!("{prefix}{}", if last { " " } else { "│ " });
216 for (i, c) in node.children.iter().enumerate() {
217 field_tree(c, &child_prefix, i + 1 == node.children.len(), out, pal);
218 }
219}
220
221pub fn decode(report: &Report, pal: Palette) -> String {
223 let mut out = String::new();
224 out.push_str(&pal.bold("decoded partitions\n"));
225 if report.decoded.is_empty() {
226 out.push_str(&pal.dim(" (nothing to decode)\n"));
227 return out;
228 }
229 for (uid, dec) in &report.decoded {
230 out.push_str(&format!(
231 " {} [{}]\n",
232 pal.magenta(&format!("uid {}", &uid_hex(uid)[..16])),
233 dec.format_name
234 ));
235 render_decoded_body(dec, " ", &mut out, pal);
236 }
237 out
238}
239
240fn render_decoded_body(dec: &Decoded, prefix: &str, out: &mut String, pal: Palette) {
241 for (i, f) in dec.fields.iter().enumerate() {
242 field_tree(f, prefix, i + 1 == dec.fields.len(), out, pal);
243 }
244 for w in &dec.warnings {
245 out.push_str(&format!("{prefix}{}\n", pal.yellow(&format!("⚠ {w}"))));
246 }
247}
248
249pub fn diagnostics(layout: &LayoutMap, pal: Palette) -> String {
251 let mut out = String::new();
252 out.push_str(&pal.bold("diagnostics\n"));
253 if layout.diagnostics.is_empty() {
254 out.push_str(&pal.green(" no anomalies\n"));
255 return out;
256 }
257 for d in &layout.diagnostics {
258 let tag = match d.severity {
259 Severity::Info => pal.dim(d.severity.tag()),
260 Severity::Warning => pal.yellow(d.severity.tag()),
261 Severity::Error => pal.red(d.severity.tag()),
262 };
263 out.push_str(&format!(" [{tag}] {}\n", d.message));
264 }
265 out
266}
267
268pub fn inspect(report: &Report, pal: Palette) -> String {
270 let l = &report.layout;
271 let mut out = String::new();
272 out.push_str(&strip(l, pal));
273 out.push_str("\n\n");
274 out.push_str(&layout(l, pal));
275 out.push('\n');
276 out.push_str(&table(l, pal));
277 out.push('\n');
278 out.push_str(&chain(l, pal));
279 out.push('\n');
280 out.push_str(&diagnostics(l, pal));
281 out
282}