1use pcf::TYPE_RAW;
6
7use super::{label_or, uid_hex, Report};
8use crate::model::algo_name;
9use crate::model::diag::Severity;
10use crate::model::{LayoutMap, RegionKind};
11use crate::plugin::{Decoded, FieldNode, FieldValue};
12
13fn esc(s: &str) -> String {
15 let mut out = String::with_capacity(s.len());
16 for c in s.chars() {
17 match c {
18 '&' => out.push_str("&"),
19 '<' => out.push_str("<"),
20 '>' => out.push_str(">"),
21 '"' => out.push_str("""),
22 '\'' => out.push_str("'"),
23 _ => out.push(c),
24 }
25 }
26 out
27}
28
29const STYLE: &str = r#"
30:root { color-scheme: light dark; }
31body { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
32 margin: 1.5rem; line-height: 1.4; }
33h1 { font-size: 1.3rem; } h2 { font-size: 1.05rem; margin-top: 1.5rem; }
34.bytemap { display: flex; width: 100%; height: 34px; border: 1px solid #8888;
35 border-radius: 4px; overflow: hidden; margin: .5rem 0; }
36.bytemap > div { min-width: 1px; }
37.k-header { background: #4e79a7; } .k-tableheader { background: #59a14f; }
38.k-entries { background: #76b7b2; } .k-data { background: #e15759; }
39.k-slack { background: #bab0ac; } .k-gap {
40 background: repeating-linear-gradient(45deg,#bbb,#bbb 4px,#ddd 4px,#ddd 8px); }
41.legend span { display: inline-block; margin-right: .8rem; }
42.legend i { display: inline-block; width: .8rem; height: .8rem; vertical-align: middle;
43 margin-right: .25rem; border: 1px solid #8884; }
44table { border-collapse: collapse; width: 100%; margin: .5rem 0; }
45th, td { border: 1px solid #8884; padding: .2rem .5rem; text-align: left;
46 font-size: .85rem; }
47th { background: #8881; }
48.ok { color: #2a8; } .fail { color: #c33; font-weight: bold; } .muted { color: #999; }
49details { margin: .3rem 0; } summary { cursor: pointer; }
50ul.fields { list-style: none; padding-left: 1.1rem; border-left: 1px dotted #8886; }
51.fname { font-weight: bold; } .frange { color: #999; font-size: .8rem; }
52.fnote { color: #999; font-style: italic; }
53.warn { color: #b80; } .diag-info { color: #888; } .diag-warn { color: #b80; }
54.diag-error { color: #c33; }
55"#;
56
57fn kind_class(kind: &RegionKind) -> &'static str {
58 match kind {
59 RegionKind::FileHeader => "k-header",
60 RegionKind::TableBlockHeader { .. } => "k-tableheader",
61 RegionKind::EntryArray { .. } => "k-entries",
62 RegionKind::PartitionData { .. } => "k-data",
63 RegionKind::Slack { .. } => "k-slack",
64 RegionKind::Gap => "k-gap",
65 }
66}
67
68fn byte_map(l: &LayoutMap) -> String {
69 let mut out = String::from("<div class=\"bytemap\">");
70 if l.file_len > 0 {
71 for r in &l.regions {
72 if r.len == 0 {
73 continue;
74 }
75 let pct = r.len as f64 / l.file_len as f64 * 100.0;
76 out.push_str(&format!(
77 "<div class=\"{}\" style=\"flex-grow:{:.4}\" title=\"{}\"></div>",
78 kind_class(&r.kind),
79 pct,
80 esc(&format!(
81 "{:#x}..{:#x} ({} B) {}",
82 r.start,
83 r.end(),
84 r.len,
85 r.label
86 ))
87 ));
88 }
89 }
90 out.push_str("</div>");
91 out.push_str(
92 "<div class=\"legend\">\
93 <span><i class=\"k-header\"></i>header</span>\
94 <span><i class=\"k-tableheader\"></i>table header</span>\
95 <span><i class=\"k-entries\"></i>entries</span>\
96 <span><i class=\"k-data\"></i>data</span>\
97 <span><i class=\"k-slack\"></i>slack</span>\
98 <span><i class=\"k-gap\"></i>gap</span>\
99 </div>",
100 );
101 out
102}
103
104fn type_str(t: u32) -> String {
105 match t {
106 TYPE_RAW => format!("{t:#010x} (RAW)"),
107 0 => format!("{t:#010x} (RESERVED)"),
108 _ => format!("{t:#010x}"),
109 }
110}
111
112fn partition_table(l: &LayoutMap) -> String {
113 let mut out = String::from(
114 "<table><tr><th>type</th><th>uid</th><th>label</th><th>start</th>\
115 <th>used</th><th>max</th><th>free</th><th>algo</th><th>data</th></tr>",
116 );
117 for b in &l.blocks {
118 for ev in &b.entries {
119 let e = &ev.entry;
120 let verify = match ev.data_hash_ok {
121 Some(true) => "<span class=\"ok\">OK</span>",
122 Some(false) => "<span class=\"fail\">FAIL</span>",
123 None => "<span class=\"muted\">—</span>",
124 };
125 out.push_str(&format!(
126 "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td>\
127 <td>{}</td><td>{}</td><td>{}</td><td>{verify}</td></tr>",
128 esc(&type_str(e.partition_type)),
129 esc(&uid_hex(&e.uid)),
130 esc(&label_or(e)),
131 e.start_offset,
132 e.used_bytes,
133 e.max_length,
134 e.free_bytes(),
135 algo_name(e.data_hash_algo),
136 ));
137 }
138 }
139 out.push_str("</table>");
140 out
141}
142
143fn value_html(v: &FieldValue) -> String {
144 match v {
145 FieldValue::None => String::new(),
146 FieldValue::U64(n) => n.to_string(),
147 FieldValue::Bytes(b) => {
148 if b.is_empty() {
149 "(empty)".into()
150 } else {
151 b.iter().map(|x| format!("{x:02x}")).collect::<String>()
152 }
153 }
154 FieldValue::Text(s) => format!(""{}"", esc(s)),
155 FieldValue::Uid(u) => uid_hex(u),
156 FieldValue::Enum { raw, name } => format!("{raw} ({})", esc(name)),
157 FieldValue::Flags { raw, set } => {
158 if set.is_empty() {
159 format!("{raw:#x} (none)")
160 } else {
161 format!("{raw:#x} ({})", esc(&set.join("|")))
162 }
163 }
164 }
165}
166
167fn field_node(node: &FieldNode, out: &mut String) {
168 out.push_str("<li>");
169 out.push_str(&format!("<span class=\"fname\">{}</span>", esc(&node.name)));
170 let v = value_html(&node.value);
171 if !v.is_empty() {
172 out.push_str(&format!(" = {v}"));
173 }
174 if let Some((a, b)) = node.range {
175 out.push_str(&format!(" <span class=\"frange\">[{a}..{b}]</span>"));
176 }
177 if let Some(n) = &node.note {
178 out.push_str(&format!(" <span class=\"fnote\">// {}</span>", esc(n)));
179 }
180 if !node.children.is_empty() {
181 out.push_str("<ul class=\"fields\">");
182 for c in &node.children {
183 field_node(c, out);
184 }
185 out.push_str("</ul>");
186 }
187 out.push_str("</li>");
188}
189
190fn decoded_section(uid: &[u8; 16], dec: &Decoded) -> String {
191 let mut out = format!(
192 "<details><summary>uid {} — {}</summary>",
193 esc(&uid_hex(uid)),
194 esc(&dec.format_name)
195 );
196 out.push_str("<ul class=\"fields\">");
197 for f in &dec.fields {
198 field_node(f, &mut out);
199 }
200 out.push_str("</ul>");
201 for w in &dec.warnings {
202 out.push_str(&format!("<div class=\"warn\">⚠ {}</div>", esc(w)));
203 }
204 out.push_str("</details>");
205 out
206}
207
208fn diagnostics_section(l: &LayoutMap) -> String {
209 if l.diagnostics.is_empty() {
210 return "<p class=\"ok\">no anomalies</p>".into();
211 }
212 let mut out = String::from("<ul>");
213 for d in &l.diagnostics {
214 let cls = match d.severity {
215 Severity::Info => "diag-info",
216 Severity::Warning => "diag-warn",
217 Severity::Error => "diag-error",
218 };
219 out.push_str(&format!(
220 "<li class=\"{cls}\">[{}] {}</li>",
221 d.severity.tag(),
222 esc(&d.message)
223 ));
224 }
225 out.push_str("</ul>");
226 out
227}
228
229pub fn render(report: &Report, title: &str) -> String {
231 let l = &report.layout;
232 let mut body = String::new();
233 body.push_str(&format!("<h1>PCF report: {}</h1>", esc(title)));
234 if let Some(h) = l.header {
235 body.push_str(&format!(
236 "<p>version {}.{} · first block @ {:#x} · file length {} B</p>",
237 h.version_major, h.version_minor, h.partition_table_offset, l.file_len
238 ));
239 }
240
241 body.push_str("<h2>byte map</h2>");
242 body.push_str(&byte_map(l));
243
244 body.push_str("<h2>partitions</h2>");
245 body.push_str(&partition_table(l));
246
247 body.push_str("<h2>decoded partitions</h2>");
248 if report.decoded.is_empty() {
249 body.push_str("<p class=\"muted\">(nothing to decode)</p>");
250 } else {
251 for (uid, dec) in &report.decoded {
252 body.push_str(&decoded_section(uid, dec));
253 }
254 }
255
256 body.push_str("<h2>diagnostics</h2>");
257 body.push_str(&diagnostics_section(l));
258
259 format!(
260 "<!DOCTYPE html>\n<html lang=\"en\"><head><meta charset=\"utf-8\">\
261 <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\
262 <title>{}</title><style>{STYLE}</style></head><body>{body}</body></html>\n",
263 esc(title)
264 )
265}