1use core::fmt::Write;
7
8use hopper_schema::{
9 decode_account_fields, decode_header, decode_segments, DecodedHeader, DecodedSegment,
10 LayoutManifest, ProgramManifest,
11};
12
13const MAX_FIELDS: usize = 64;
14const MAX_SEGMENTS: usize = 32;
15
16#[derive(Debug, Clone, Copy)]
21pub enum IdentifyOutcome<'a> {
22 Match {
23 layout: &'a LayoutManifest,
24 header: DecodedHeader,
25 data_len: usize,
26 size_mismatch: bool,
27 },
28 NoMatch {
29 header: DecodedHeader,
30 data_len: usize,
31 },
32 HeaderTooShort {
33 data_len: usize,
34 },
35}
36
37#[inline]
42pub fn identify_account<'a>(manifest: &'a ProgramManifest, data: &[u8]) -> IdentifyOutcome<'a> {
43 let Some(header) = decode_header(data) else {
44 return IdentifyOutcome::HeaderTooShort {
45 data_len: data.len(),
46 };
47 };
48 match manifest.identify_from_data(data) {
49 Some(layout) => IdentifyOutcome::Match {
50 layout,
51 header,
52 data_len: data.len(),
53 size_mismatch: data.len() != layout.total_size,
54 },
55 None => IdentifyOutcome::NoMatch {
56 header,
57 data_len: data.len(),
58 },
59 }
60}
61
62pub fn identify_report(manifest: &ProgramManifest, data: &[u8]) -> String {
65 let mut out = String::new();
66 match identify_account(manifest, data) {
67 IdentifyOutcome::HeaderTooShort { data_len } => {
68 let _ = writeln!(
69 out,
70 "Data too short for Hopper header (need 16 bytes, got {})",
71 data_len
72 );
73 }
74 IdentifyOutcome::Match {
75 layout,
76 header,
77 data_len,
78 size_mismatch,
79 } => {
80 let _ = writeln!(out, "=== Account Identification ===");
81 let _ = writeln!(out, " Data size : {} bytes", data_len);
82 let _ = writeln!(out, " Header disc : {}", header.disc);
83 let _ = writeln!(out, " Header ver : {}", header.version);
84 let _ = writeln!(out, " Layout ID : {}", hex8(&header.layout_id));
85 let _ = writeln!(out);
86 let _ = writeln!(out, " MATCH: {} v{}", layout.name, layout.version);
87 let _ = writeln!(out, " Expected size: {} bytes", layout.total_size);
88 let _ = writeln!(out, " Fields : {}", layout.field_count);
89 if size_mismatch {
90 let _ = writeln!(
91 out,
92 " WARNING: data size ({}) != expected size ({})",
93 data_len, layout.total_size
94 );
95 }
96 }
97 IdentifyOutcome::NoMatch { header, data_len } => {
98 let _ = writeln!(out, "=== Account Identification ===");
99 let _ = writeln!(out, " Data size : {} bytes", data_len);
100 let _ = writeln!(out, " Header disc : {}", header.disc);
101 let _ = writeln!(out, " Header ver : {}", header.version);
102 let _ = writeln!(out, " Layout ID : {}", hex8(&header.layout_id));
103 let _ = writeln!(out);
104 let _ = writeln!(
105 out,
106 " NO MATCH: This account does not match any layout in the manifest."
107 );
108 let _ = writeln!(out);
109 let _ = writeln!(out, "Known layouts:");
110 for l in manifest.layouts.iter() {
111 let _ = writeln!(
112 out,
113 " {} v{} (disc={}, id={})",
114 l.name,
115 l.version,
116 l.disc,
117 hex8(&l.layout_id)
118 );
119 }
120 }
121 }
122 out
123}
124
125pub fn decode_account(
131 manifest: &ProgramManifest,
132 data: &[u8],
133 heading: &str,
134) -> Result<String, String> {
135 if data.len() < 16 {
136 return Err(format!(
137 "Data too short for Hopper header (need 16, got {})",
138 data.len()
139 ));
140 }
141 let header =
142 decode_header(data).ok_or_else(|| String::from("Failed to decode Hopper header"))?;
143 let layout = manifest.identify_from_data(data).ok_or_else(|| {
144 format!(
145 "Cannot identify account type (disc={}, layout_id={})",
146 header.disc,
147 hex8(&header.layout_id),
148 )
149 })?;
150
151 let mut out = String::new();
152 let _ = writeln!(
153 out,
154 "=== {}: {} v{} ===",
155 heading, layout.name, layout.version
156 );
157 let _ = writeln!(
158 out,
159 " Size: {} bytes (expected {})",
160 data.len(),
161 layout.total_size
162 );
163 let _ = writeln!(
164 out,
165 " Flags: {} (0x{:04x})",
166 format_flags(header.flags),
167 header.flags
168 );
169 let _ = writeln!(out, " Disc : {}", header.disc);
170 let _ = writeln!(out, " Wire : {}", hex8(&layout.layout_id));
171 let _ = writeln!(out);
172
173 if layout.field_count == 0 {
174 let _ = writeln!(out, " (no field descriptors in manifest)");
175 return Ok(out);
176 }
177
178 let (count, fields) = decode_account_fields::<MAX_FIELDS>(data, layout);
179 let mut val_buf = [0u8; 128];
180 let _ = writeln!(
181 out,
182 " {:>4} {:>20} {:>12} {:>6} {:>6} Value",
183 "#", "Field", "Type", "Offset", "Size"
184 );
185 let _ = writeln!(out, " {}", "-".repeat(76));
186 for (i, slot) in fields.iter().enumerate().take(count) {
187 if let Some(ref field) = slot {
188 let val_len = field.format_value(&mut val_buf);
189 let val_str = core::str::from_utf8(&val_buf[..val_len]).unwrap_or("???");
190 let _ = writeln!(
191 out,
192 " {:>4} {:>20} {:>12} {:>6} {:>6} {}",
193 i, field.name, field.canonical_type, field.offset, field.size, val_str
194 );
195 }
196 }
197 let _ = writeln!(out);
198 let _ = writeln!(out, " Decoded {}/{} fields.", count, layout.field_count);
199 Ok(out)
200}
201
202pub fn header_report(data: &[u8]) -> String {
207 let mut out = String::new();
208 match decode_header(data) {
209 Some(h) => {
210 let _ = writeln!(out, "=== Hopper Header ===");
211 let _ = writeln!(out, " Disc : {}", h.disc);
212 let _ = writeln!(out, " Version : {}", h.version);
213 let _ = writeln!(
214 out,
215 " Flags : 0x{:04x} ({})",
216 h.flags,
217 format_flags(h.flags)
218 );
219 let _ = writeln!(out, " Layout ID : {}", hex8(&h.layout_id));
220 let _ = writeln!(out, " Reserved : {}", hex4(&h.reserved));
221 }
222 None => {
223 let _ = writeln!(
224 out,
225 "Data too short to decode Hopper header (need 16 bytes, got {})",
226 data.len()
227 );
228 }
229 }
230 out
231}
232
233pub fn segment_map_report(data: &[u8]) -> String {
236 let mut out = String::new();
237 match decode_segments::<MAX_SEGMENTS>(data) {
238 Some((count, segs)) => {
239 let _ = writeln!(out, "=== Segment Map ({} entries) ===", count);
240 for (i, seg) in segs.iter().enumerate().take(count) {
241 render_segment_line(&mut out, i, seg);
242 }
243 }
244 None => {
245 let _ = writeln!(out, "No segment map present (or data too short).");
246 }
247 }
248 out
249}
250
251fn render_segment_line(out: &mut String, index: usize, seg: &DecodedSegment) {
252 let _ = writeln!(
253 out,
254 " [{}] id={} offset={} size={} flags=0x{:04x} ver={}",
255 index,
256 hex_any(&seg.id),
257 seg.offset,
258 seg.size,
259 seg.flags,
260 seg.version,
261 );
262}
263
264fn format_flags(flags: u16) -> String {
267 let mut parts = Vec::with_capacity(4);
268 if flags & 0x0001 != 0 {
269 parts.push("INITIALIZED");
270 }
271 if flags & 0x0002 != 0 {
272 parts.push("LOCKED");
273 }
274 if flags & 0x0004 != 0 {
275 parts.push("UPGRADED");
276 }
277 if flags & 0x0008 != 0 {
278 parts.push("CLOSED");
279 }
280 if parts.is_empty() {
281 String::from("none")
282 } else {
283 parts.join("|")
284 }
285}
286
287fn hex8(bytes: &[u8; 8]) -> String {
288 let mut out = String::with_capacity(16);
289 for b in bytes {
290 let _ = write!(out, "{:02x}", b);
291 }
292 out
293}
294
295fn hex4(bytes: &[u8; 4]) -> String {
296 let mut out = String::with_capacity(8);
297 for b in bytes {
298 let _ = write!(out, "{:02x}", b);
299 }
300 out
301}
302
303fn hex_any(bytes: &[u8]) -> String {
304 let mut out = String::with_capacity(bytes.len() * 2);
305 for b in bytes {
306 let _ = write!(out, "{:02x}", b);
307 }
308 out
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn header_report_handles_short_data() {
317 let out = header_report(&[0x01, 0x02]);
318 assert!(out.contains("Data too short"));
319 }
320
321 #[test]
322 fn format_flags_all_zero_is_none() {
323 assert_eq!(format_flags(0), "none");
324 }
325
326 #[test]
327 fn format_flags_combines_known_bits() {
328 let s = format_flags(0x0001 | 0x0004);
329 assert!(s.contains("INITIALIZED"));
330 assert!(s.contains("UPGRADED"));
331 }
332}