1use std::{
22 env, fs,
23 io::{self, Read},
24 process,
25};
26
27use half::{bf16, f16};
28
29use hurray_core::{
30 descriptor::{MemberRole, TensorDescriptor},
31 layout::{
32 BlockTableIndexType, CombineOp, CompositionRule, KvRole, LayoutDescriptor, TiledLayout,
33 },
34 QuantizationDescriptor, DYNAMIC,
35};
36
37#[derive(Debug, thiserror::Error)]
40enum Error {
41 #[error("I/O error: {0}")]
42 Io(#[from] io::Error),
43 #[error("{0}")]
44 Parse(#[from] hurray_core::Error),
45 #[error("malformed HRRYFILE: {0}")]
46 Hrryfile(String),
47 #[error("usage: hurray-inspect [--data[=hex|numpy]] <file> (use '-' for stdin)")]
48 Usage,
49}
50
51type Result<T> = std::result::Result<T, Error>;
52
53struct Row {
57 offset: usize,
59 bytes: Vec<u8>,
61 field: String,
63}
64
65const FILE_MAGIC: &[u8; 8] = b"HRRYFILE";
68const FILE_HEADER_SIZE: usize = 64;
69const TRAILER_SIZE: usize = 40;
70
71const FLAG_HAS_KV_METADATA: u32 = 1 << 0;
72const FLAG_SORTED_INDEX: u32 = 1 << 1;
73const FLAG_HAS_INDEX_CRC32C: u32 = 1 << 2;
74
75fn file_flags_display(flags: u32) -> String {
76 let mut parts = Vec::new();
77 if flags & FLAG_HAS_KV_METADATA != 0 {
78 parts.push("HAS_KV_METADATA");
79 }
80 if flags & FLAG_SORTED_INDEX != 0 {
81 parts.push("SORTED_INDEX");
82 }
83 if flags & FLAG_HAS_INDEX_CRC32C != 0 {
84 parts.push("HAS_INDEX_CRC32C");
85 }
86 let reserved = flags & !0x07u32;
87 let base = format!("0x{flags:08X}");
88 let mut suffix = parts.join(" | ");
89 if reserved != 0 {
90 if !suffix.is_empty() {
91 suffix.push_str(&format!(" | reserved=0x{reserved:08X}"));
92 } else {
93 suffix = format!("reserved=0x{reserved:08X}");
94 }
95 }
96 if suffix.is_empty() {
97 base
98 } else {
99 format!("{base} ({suffix})")
100 }
101}
102
103fn kv_tag_name(tag: u8) -> &'static str {
104 match tag {
105 0x01 => "string",
106 0x02 => "int64",
107 0x03 => "uint64",
108 0x04 => "float64",
109 0x05 => "bool",
110 0x06 => "bytes",
111 0x07 => "array",
112 _ => "unknown",
113 }
114}
115
116fn le_u8(b: &[u8]) -> u8 {
119 b[0]
120}
121fn le_u16(b: &[u8]) -> u16 {
122 u16::from_le_bytes(b[..2].try_into().unwrap())
124}
125fn le_u32(b: &[u8]) -> u32 {
126 u32::from_le_bytes(b[..4].try_into().unwrap())
128}
129fn le_u64(b: &[u8]) -> u64 {
130 u64::from_le_bytes(b[..8].try_into().unwrap())
132}
133fn le_i64(b: &[u8]) -> i64 {
134 i64::from_le_bytes(b[..8].try_into().unwrap())
136}
137fn le_f64(b: &[u8]) -> f64 {
138 f64::from_le_bytes(b[..8].try_into().unwrap())
140}
141
142#[derive(Clone, Copy, Debug, PartialEq)]
145enum DataMode {
146 None,
147 Hex,
148 Numpy,
149}
150
151fn format_float(v: f64) -> String {
156 if v.is_nan() {
157 return "nan".into();
158 }
159 if v.is_infinite() {
160 return if v > 0.0 { "inf".into() } else { "-inf".into() };
161 }
162 let s = format!("{v}");
163 if s.contains('.') || s.contains('e') || s.contains('E') {
164 s
165 } else {
166 format!("{s}.")
167 }
168}
169
170macro_rules! read_le {
172 ($data:expr, $idx:expr, $n:expr, $ty:ty) => {{
173 let i = ($idx * $n) as usize;
174 if i + $n <= $data.len() {
175 <$ty>::from_le_bytes($data[i..i + $n].try_into().unwrap()).to_string()
176 } else {
177 "?".into()
178 }
179 }};
180}
181
182fn read_pow2_code(data: &[u8], idx: u64, bits: u8) -> Option<u32> {
184 let per_byte = 8 / bits as u64;
185 let byte_i = (idx / per_byte) as usize;
186 let shift = ((idx % per_byte) * bits as u64) as u32;
187 let mask = (1u32 << bits) - 1;
188 data.get(byte_i).map(|&b| (b as u32 >> shift) & mask)
189}
190
191fn read_f6_code(data: &[u8], idx: u64) -> Option<u32> {
194 let g = (idx / 4) as usize * 3;
195 let (b0, b1, b2) = (
196 *data.get(g)? as u32,
197 *data.get(g + 1).unwrap_or(&0) as u32,
198 *data.get(g + 2).unwrap_or(&0) as u32,
199 );
200 Some(match idx % 4 {
201 0 => b0 & 0x3F,
202 1 => (b0 >> 6) | ((b1 & 0x0F) << 2),
203 2 => (b1 >> 4) | ((b2 & 0x03) << 4),
204 _ => b2 >> 2,
205 })
206}
207
208fn sign_extend(code: u32, bits: u32) -> i64 {
210 let shift = 64 - bits;
211 ((code as i64) << shift) >> shift
212}
213
214fn micro_float(
220 code: u32,
221 exp_bits: u32,
222 man_bits: u32,
223 bias: i32,
224 has_inf: bool,
225 nan_at_max: bool,
226) -> f64 {
227 let sign = if (code >> (exp_bits + man_bits)) & 1 == 1 {
228 -1.0
229 } else {
230 1.0
231 };
232 let exp = (code >> man_bits) & ((1 << exp_bits) - 1);
233 let man_mask = (1u32 << man_bits) - 1;
234 let man = code & man_mask;
235 let max_exp = (1u32 << exp_bits) - 1;
236 let man_div = (1u64 << man_bits) as f64;
237 if exp == max_exp && has_inf {
238 return if man == 0 {
239 sign * f64::INFINITY
240 } else {
241 f64::NAN
242 };
243 }
244 if exp == max_exp && nan_at_max && man == man_mask {
245 return f64::NAN;
246 }
247 if exp == 0 {
248 sign * (man as f64 / man_div) * 2f64.powi(1 - bias) } else {
250 sign * (1.0 + man as f64 / man_div) * 2f64.powi(exp as i32 - bias) }
252}
253
254fn e8m0(byte: u8) -> f64 {
256 if byte == 0x00 || byte == 0xFF {
257 f64::NAN
258 } else {
259 2f64.powi(byte as i32 - 127)
260 }
261}
262
263fn format_scalar(et: hurray_core::ElementType, data: &[u8], idx: u64) -> String {
268 use hurray_core::ElementType::*;
269
270 match et {
271 Bool => {
272 let byte = (idx / 8) as usize;
273 let bit = (idx % 8) as u8;
274 if byte < data.len() {
275 if (data[byte] >> bit) & 1 == 1 {
276 "True".into()
277 } else {
278 "False".into()
279 }
280 } else {
281 "?".into()
282 }
283 }
284 Int8 => {
285 let i = idx as usize;
286 if i < data.len() {
287 (data[i] as i8).to_string()
288 } else {
289 "?".into()
290 }
291 }
292 Uint8 => {
293 let i = idx as usize;
294 if i < data.len() {
295 data[i].to_string()
296 } else {
297 "?".into()
298 }
299 }
300 Int16 => read_le!(data, idx, 2, i16),
301 Uint16 => read_le!(data, idx, 2, u16),
302 Int32 => read_le!(data, idx, 4, i32),
303 Uint32 => read_le!(data, idx, 4, u32),
304 Int64 => read_le!(data, idx, 8, i64),
305 Uint64 => read_le!(data, idx, 8, u64),
306 Float16 => {
307 let i = (idx * 2) as usize;
308 if i + 2 <= data.len() {
309 let bits = u16::from_le_bytes(data[i..i + 2].try_into().unwrap());
310 format_float(f16::from_bits(bits).to_f64())
311 } else {
312 "?".into()
313 }
314 }
315 BFloat16 => {
316 let i = (idx * 2) as usize;
317 if i + 2 <= data.len() {
318 let bits = u16::from_le_bytes(data[i..i + 2].try_into().unwrap());
319 format_float(bf16::from_bits(bits).to_f64())
320 } else {
321 "?".into()
322 }
323 }
324 Float32 => {
325 let i = (idx * 4) as usize;
326 if i + 4 <= data.len() {
327 format_float(f32::from_le_bytes(data[i..i + 4].try_into().unwrap()) as f64)
328 } else {
329 "?".into()
330 }
331 }
332 Float64 => {
333 let i = (idx * 8) as usize;
334 if i + 8 <= data.len() {
335 format_float(f64::from_le_bytes(data[i..i + 8].try_into().unwrap()))
336 } else {
337 "?".into()
338 }
339 }
340 Complex64 => {
341 let i = (idx * 8) as usize;
342 if i + 8 <= data.len() {
343 let re = f32::from_le_bytes(data[i..i + 4].try_into().unwrap());
344 let im = f32::from_le_bytes(data[i + 4..i + 8].try_into().unwrap());
345 format!("({}+{}j)", format_float(re as f64), format_float(im as f64))
346 } else {
347 "?".into()
348 }
349 }
350 Complex128 => {
351 let i = (idx * 16) as usize;
352 if i + 16 <= data.len() {
353 let re = f64::from_le_bytes(data[i..i + 8].try_into().unwrap());
354 let im = f64::from_le_bytes(data[i + 8..i + 16].try_into().unwrap());
355 format!("({}+{}j)", format_float(re), format_float(im))
356 } else {
357 "?".into()
358 }
359 }
360 Int4 => read_pow2_code(data, idx, 4).map_or("?".into(), |c| sign_extend(c, 4).to_string()),
362 Uint4 => read_pow2_code(data, idx, 4).map_or("?".into(), |c| c.to_string()),
363 Int2 => read_pow2_code(data, idx, 2).map_or("?".into(), |c| sign_extend(c, 2).to_string()),
364 Uint2 => read_pow2_code(data, idx, 2).map_or("?".into(), |c| c.to_string()),
365
366 Float8E4M3 => data.get(idx as usize).map_or("?".into(), |&b| {
368 format_float(micro_float(b as u32, 4, 3, 7, false, true))
369 }),
370 Float8E5M2 => data.get(idx as usize).map_or("?".into(), |&b| {
371 format_float(micro_float(b as u32, 5, 2, 15, true, false))
372 }),
373 Float8E8M0 => data
374 .get(idx as usize)
375 .map_or("?".into(), |&b| format_float(e8m0(b))),
376 Float4E2M1 => read_pow2_code(data, idx, 4).map_or("?".into(), |c| {
377 format_float(micro_float(c, 2, 1, 1, false, false))
378 }),
379 Float6E2M3 => read_f6_code(data, idx).map_or("?".into(), |c| {
380 format_float(micro_float(c, 2, 3, 1, false, false))
381 }),
382 Float6E3M2 => read_f6_code(data, idx).map_or("?".into(), |c| {
383 format_float(micro_float(c, 3, 2, 3, false, false))
384 }),
385
386 _ => {
388 let bw = et.bit_width();
389 if bw == 0 || bw >= 8 {
390 let n = bw.div_ceil(8).max(1) as usize;
391 let i = (idx * n as u64) as usize;
392 if i + n <= data.len() {
393 hex_str(&data[i..i + n])
394 } else {
395 "?".into()
396 }
397 } else {
398 let elements_per_byte = 8 / bw as u64;
400 let byte_i = (idx / elements_per_byte) as usize;
401 let bit_off = ((idx % elements_per_byte) * bw as u64) as u8;
402 let mask = (1u8 << bw) - 1;
403 if byte_i < data.len() {
404 let raw = (data[byte_i] >> bit_off) & mask;
405 format!("0x{raw:01X}")
406 } else {
407 "?".into()
408 }
409 }
410 }
411 }
412}
413
414const NUMPY_THRESHOLD: u64 = 1000;
416
417fn format_numpy(et: hurray_core::ElementType, shape: &[u64], data: &[u8]) -> String {
421 let total: u64 = if shape.is_empty() {
422 1
423 } else {
424 shape.iter().product()
425 };
426
427 if total == 0 {
428 return "[]".to_string();
429 }
430
431 if shape.is_empty() {
432 return format_scalar(et, data, 0);
433 }
434
435 if total > NUMPY_THRESHOLD {
436 let head = 3u64.min(total);
437 let tail = 3u64.min(total - head);
438 let mut parts: Vec<String> = (0..head).map(|i| format_scalar(et, data, i)).collect();
439 parts.push("...".into());
440 for i in (total - tail)..total {
441 parts.push(format_scalar(et, data, i));
442 }
443 return format!("[{}] # shape={shape:?}, {total} elements", parts.join(" "));
444 }
445
446 format_numpy_dim(et, shape, data, 0, 0)
447}
448
449fn format_numpy_dim(
451 et: hurray_core::ElementType,
452 shape: &[u64],
453 data: &[u8],
454 depth: usize,
455 offset: u64,
456) -> String {
457 let ndim = shape.len();
458
459 if depth == ndim - 1 {
460 let parts: Vec<String> = (0..shape[depth])
462 .map(|i| format_scalar(et, data, offset + i))
463 .collect();
464 return format!("[{}]", parts.join(" "));
465 }
466
467 let stride: u64 = shape[depth + 1..].iter().product();
468 let parts: Vec<String> = (0..shape[depth])
469 .map(|i| format_numpy_dim(et, shape, data, depth + 1, offset + i * stride))
470 .collect();
471
472 let indent = " ".repeat(depth + 1);
474 let sep = if ndim - depth > 2 {
475 format!("\n\n{indent}")
476 } else {
477 format!("\n{indent}")
478 };
479 format!("[{}]", parts.join(&sep))
480}
481
482struct FReader<'a> {
486 data: &'a [u8],
487 pos: usize,
488}
489
490impl<'a> FReader<'a> {
491 fn new(data: &'a [u8]) -> Self {
492 Self { data, pos: 0 }
493 }
494
495 fn seek_to(&mut self, pos: usize) {
496 self.pos = pos;
497 }
498
499 fn take(&mut self, n: usize) -> Result<(usize, Vec<u8>)> {
501 let offset = self.pos;
502 let end = offset + n;
503 if end > self.data.len() {
504 return Err(Error::Hrryfile(format!(
505 "unexpected end of file at offset {offset} \
506 (need {n} bytes, have {})",
507 self.data.len().saturating_sub(offset)
508 )));
509 }
510 let bytes = self.data[offset..end].to_vec();
511 self.pos = end;
512 Ok((offset, bytes))
513 }
514
515 fn row(&mut self, n: usize, field: String) -> Result<Row> {
516 let (offset, bytes) = self.take(n)?;
517 Ok(Row {
518 offset,
519 bytes,
520 field,
521 })
522 }
523}
524
525struct Reader<'a> {
532 data: &'a [u8],
533 pos: usize,
534 limit: usize,
535 base_offset: usize,
536}
537
538impl<'a> Reader<'a> {
539 fn with_base(data: &'a [u8], limit: usize, base_offset: usize) -> Self {
540 Self {
541 data,
542 pos: 0,
543 limit,
544 base_offset,
545 }
546 }
547
548 fn take_row(&mut self, n: usize, field: String) -> Row {
550 let offset = self.pos;
551 let end = (self.pos + n).min(self.limit).min(self.data.len());
552 let bytes = self.data[offset..end].to_vec();
553 self.pos = end;
554 Row {
555 offset: self.base_offset + offset,
556 bytes,
557 field,
558 }
559 }
560
561 fn skip_to(&mut self, target: usize) -> Option<Row> {
563 if target <= self.pos || target > self.limit || target > self.data.len() {
564 return None;
565 }
566 let offset = self.pos;
567 let bytes = self.data[self.pos..target].to_vec();
568 self.pos = target;
569 Some(Row {
570 offset: self.base_offset + offset,
571 bytes,
572 field: "(unknown / padding bytes)".to_string(),
573 })
574 }
575}
576
577fn layout_tag_name(tag: u8) -> &'static str {
580 match tag {
581 0x01 => "row-major",
582 0x02 => "column-major",
583 0x03 => "strided",
584 0x04 => "tiled",
585 0x05 => "morton",
586 0x06 => "COO",
587 0x07 => "CSR",
588 0x08 => "CSC",
589 0x09 => "CSF",
590 0x0A => "block-paged",
591 0x0B => "composite",
592 0x40 => "hilbert",
593 0xF0..=0xFE => "private-extension",
594 _ => "unknown",
595 }
596}
597
598fn quant_scheme_name(tag: u8) -> &'static str {
601 match tag {
602 0x01 => "per-tensor-affine",
603 0x02 => "per-channel-affine",
604 0x03 => "per-block-affine",
605 0x04 => "NF4",
606 0x05 => "MXFP",
607 _ => "unknown",
608 }
609}
610
611fn quant_rows(reader: &mut Reader<'_>, qd: &QuantizationDescriptor) -> Vec<Row> {
614 let tag = qd.scheme_tag().tag();
615 let mut rows = vec![
616 reader.take_row(
617 1,
618 format!("scheme_tag = 0x{tag:02X} ({})", quant_scheme_name(tag)),
619 ),
620 reader.take_row(1, "scheme_version".to_string()),
621 reader.take_row(2, "quant.flags".to_string()),
622 ];
623 let zp = |z: Option<u32>| match z {
624 Some(x) => x.to_string(),
625 None => "none (symmetric)".to_string(),
626 };
627 match qd {
628 QuantizationDescriptor::PerTensorAffine(x) => {
629 rows.push(reader.take_row(4, format!("scale = {}", x.scale())));
630 rows.push(reader.take_row(4, format!("zero_point = {}", x.zero_point())));
631 rows.push(reader.take_row(4, "quant._reserved".to_string()));
632 }
633 QuantizationDescriptor::PerChannelAffine(x) => {
634 rows.push(reader.take_row(4, format!("axis = {}", x.axis())));
635 rows.push(reader.take_row(
636 4,
637 format!("scale_buffer_index = {}", x.scale_buffer_index()),
638 ));
639 rows.push(reader.take_row(
640 4,
641 format!(
642 "zero_point_buffer_index = {}",
643 zp(x.zero_point_buffer_index())
644 ),
645 ));
646 rows.push(reader.take_row(1, format!("scale_type = {}", x.scale_type())));
647 rows.push(reader.take_row(3, "quant._reserved".to_string()));
648 }
649 QuantizationDescriptor::PerBlockAffine(x) => {
650 rows.push(reader.take_row(4, format!("axis = {}", x.axis())));
651 rows.push(reader.take_row(4, format!("block_size = {}", x.block_size())));
652 rows.push(reader.take_row(
653 4,
654 format!("scale_buffer_index = {}", x.scale_buffer_index()),
655 ));
656 rows.push(reader.take_row(
657 4,
658 format!(
659 "zero_point_buffer_index = {}",
660 zp(x.zero_point_buffer_index())
661 ),
662 ));
663 rows.push(reader.take_row(1, "scale_type_tag".to_string()));
664 rows.push(reader.take_row(3, "quant._reserved".to_string()));
665 }
666 QuantizationDescriptor::Nf4(x) => {
667 rows.push(reader.take_row(4, format!("axis = {}", x.axis())));
668 rows.push(reader.take_row(4, format!("block_size = {}", x.block_size())));
669 rows.push(reader.take_row(
670 4,
671 format!("scale_buffer_index = {}", x.scale_buffer_index()),
672 ));
673 }
674 QuantizationDescriptor::Mxfp(x) => {
675 rows.push(reader.take_row(4, format!("axis = {}", x.axis())));
676 rows.push(reader.take_row(4, format!("block_size = {}", x.block_size())));
677 rows.push(reader.take_row(
678 4,
679 format!("scale_buffer_index = {}", x.scale_buffer_index()),
680 ));
681 }
682 }
683 rows
684}
685
686fn layout_rows(reader: &mut Reader<'_>, layout: &LayoutDescriptor) -> Vec<Row> {
689 match layout {
690 LayoutDescriptor::RowMajor | LayoutDescriptor::ColMajor => vec![],
691
692 LayoutDescriptor::Strided(s) => s
693 .strides
694 .iter()
695 .enumerate()
696 .map(|(i, &v)| reader.take_row(8, format!("strides[{i}] = {v}")))
697 .collect(),
698
699 LayoutDescriptor::Tiled(t) => tiled_rows(reader, t),
700
701 LayoutDescriptor::Morton(m) => m
702 .morton_bits
703 .iter()
704 .enumerate()
705 .map(|(i, &v)| reader.take_row(4, format!("morton_bits[{i}] = {v}")))
706 .collect(),
707
708 LayoutDescriptor::Coo(c) => vec![
709 reader.take_row(8, format!("nnz = {}", c.nnz)),
710 reader.take_row(1, format!("is_sorted = 0x{:02X}", u8::from(c.is_sorted))),
711 reader.take_row(7, "COO._reserved".to_string()),
712 ],
713
714 LayoutDescriptor::Csr(c) => vec![
715 reader.take_row(8, format!("nnz = {}", c.nnz)),
716 reader.take_row(8, "CSR._reserved".to_string()),
717 ],
718
719 LayoutDescriptor::Csc(c) => vec![
720 reader.take_row(8, format!("nnz = {}", c.nnz)),
721 reader.take_row(8, "CSC._reserved".to_string()),
722 ],
723
724 LayoutDescriptor::Hilbert(h) => vec![
725 reader.take_row(4, format!("hilbert_order = {}", h.hilbert_order)),
726 reader.take_row(4, format!("hilbert_rank = {}", h.hilbert_rank)),
727 ],
728
729 LayoutDescriptor::PrivateExtension(p) => {
730 let mut rows = vec![
731 reader.take_row(
732 8,
733 format!("extension_layout_id = 0x{:016X}", p.extension_layout_id),
734 ),
735 reader.take_row(
736 4,
737 format!("extension_data_length = {}", p.extension_data.len()),
738 ),
739 ];
740 if !p.extension_data.is_empty() {
741 rows.push(reader.take_row(p.extension_data.len(), "extension_data".to_string()));
742 }
743 rows
744 }
745
746 LayoutDescriptor::Csf(c) => {
747 let mut rows = vec![reader.take_row(8, format!("nnz = {}", c.nnz))];
748 for (i, &d) in c.mode_order.iter().enumerate() {
749 rows.push(reader.take_row(4, format!("mode_order[{i}] = {d}")));
750 }
751 rows.push(reader.take_row(8, "CSF._reserved".to_string()));
752 rows
753 }
754
755 LayoutDescriptor::BlockPaged(bp) => {
756 let kv_n = match bp.kv_role {
757 KvRole::Key => "key",
758 KvRole::Value => "value",
759 KvRole::Fused => "fused",
760 _ => "unknown",
761 };
762 let bt_n = match bp.block_table_index_type {
763 BlockTableIndexType::U32 => "uint32",
764 BlockTableIndexType::U64 => "uint64",
765 _ => "unknown",
766 };
767 let layer = match bp.layer_index {
768 Some(x) => x.to_string(),
769 None => "none".to_string(),
770 };
771 vec![
772 reader.take_row(4, format!("page_size = {}", bp.page_size)),
773 reader.take_row(8, format!("num_pages = {}", bp.num_pages)),
774 reader.take_row(4, format!("paged_axis = {}", bp.paged_axis)),
775 reader.take_row(4, format!("num_seqs = {}", bp.num_seqs)),
776 reader.take_row(1, format!("kv_role = {kv_n}")),
777 reader.take_row(4, format!("layer_index = {layer}")),
778 reader.take_row(1, format!("block_table_index_type = {bt_n}")),
779 reader.take_row(6, "block-paged._reserved".to_string()),
780 ]
781 }
782
783 LayoutDescriptor::Composite(c) => {
784 let rule_n = match &c.rule {
785 CompositionRule::Partition => "partition",
786 CompositionRule::Overlay(_) => "overlay",
787 CompositionRule::Group => "group",
788 _ => "unknown",
789 };
790 let op_n = match &c.rule {
791 CompositionRule::Overlay(CombineOp::Replace) => "replace",
792 CompositionRule::Overlay(CombineOp::Add) => "add",
793 _ => "n/a",
794 };
795 vec![
796 reader.take_row(1, format!("composition_rule = {rule_n}")),
797 reader.take_row(1, format!("combine_op = {op_n}")),
798 reader.take_row(2, "composite._reserved".to_string()),
799 reader.take_row(4, format!("member_count = {}", c.member_count)),
800 ]
801 }
802
803 LayoutDescriptor::Unknown(u) => {
804 if u.raw_bytes.is_empty() {
805 vec![]
806 } else {
807 vec![reader.take_row(
808 u.raw_bytes.len(),
809 format!("(unknown layout 0x{:02X} — raw bytes)", u.tag),
810 )]
811 }
812 }
813
814 _ => vec![],
816 }
817}
818
819fn tiled_rows(reader: &mut Reader<'_>, tiled: &TiledLayout) -> Vec<Row> {
820 let mut rows = Vec::new();
821
822 for (i, &v) in tiled.tile_shape.iter().enumerate() {
823 rows.push(reader.take_row(8, format!("tile_shape[{i}] = {v}")));
824 }
825 rows.push(reader.take_row(
826 1,
827 format!(
828 "outer_layout = 0x{:02X} ({})",
829 tiled.outer_layout,
830 layout_tag_name(tiled.outer_layout)
831 ),
832 ));
833 rows.push(reader.take_row(
834 1,
835 format!(
836 "inner_layout = 0x{:02X} ({})",
837 tiled.inner_layout,
838 layout_tag_name(tiled.inner_layout)
839 ),
840 ));
841 rows.push(reader.take_row(2, "tiled._reserved".to_string()));
842
843 if let Some(os) = &tiled.outer_strides {
844 for (i, &v) in os.strides.iter().enumerate() {
845 rows.push(reader.take_row(8, format!("outer_strides[{i}] = {v}")));
846 }
847 }
848 if let Some(is) = &tiled.inner_strides {
849 for (i, &v) in is.strides.iter().enumerate() {
850 rows.push(reader.take_row(8, format!("inner_strides[{i}] = {v}")));
851 }
852 } else if let Some(inner) = &tiled.inner_tiled {
853 rows.extend(tiled_rows(reader, inner));
854 }
855
856 rows
857}
858
859fn rows_from_descriptor(data: &[u8], desc: &TensorDescriptor, base_offset: usize) -> Vec<Row> {
866 let desc_len = u32::from_le_bytes(data[6..10].try_into().unwrap_or([0u8; 4])) as usize;
868 let mut reader = Reader::with_base(data, desc_len, base_offset);
869 let mut rows = Vec::new();
870
871 rows.push(reader.take_row(4, r#"magic = "HRRY""#.to_string()));
873 rows.push(reader.take_row(1, format!("version_major = {}", desc.version_major)));
874 rows.push(reader.take_row(1, format!("version_minor = {}", desc.version_minor)));
875 rows.push(reader.take_row(4, format!("descriptor_length = {desc_len}")));
876 rows.push(reader.take_row(4, format!("flags = 0x{:08X}", desc.flags().0)));
877 rows.push(reader.take_row(
878 1,
879 format!(
880 "type_tag = 0x{:02X} ({})",
881 desc.element_type.tag(),
882 desc.element_type
883 ),
884 ));
885 let ltag = desc.layout.tag();
886 rows.push(reader.take_row(
887 1,
888 format!("layout_tag = 0x{ltag:02X} ({})", layout_tag_name(ltag)),
889 ));
890 let rank = desc.shape.rank() as u32;
891 rows.push(reader.take_row(4, format!("rank = {rank}")));
892
893 for (i, &dim) in desc.shape.dims().iter().enumerate() {
895 let display = if dim == DYNAMIC {
896 "(dynamic)".to_string()
897 } else {
898 dim.to_string()
899 };
900 rows.push(reader.take_row(8, format!("shape[{i}] = {display}")));
901 }
902
903 rows.push(reader.take_row(8, format!("byte_offset = {}", desc.byte_offset)));
905
906 rows.extend(layout_rows(&mut reader, &desc.layout));
908
909 rows.push(reader.take_row(1, format!("buffer_count = {}", desc.buffers.len())));
911
912 for (b, buf) in desc.buffers.iter().enumerate() {
915 rows.push(reader.take_row(8, format!("buffer[{b}].byte_size = {}", buf.byte_size())));
916 rows.push(reader.take_row(4, format!("buffer[{b}].alignment = {}", buf.alignment())));
917 rows.push(reader.take_row(
918 1,
919 format!(
920 "buffer[{b}].device_tag = 0x{:02X} ({})",
921 buf.device_tag().to_byte(),
922 buf.device_tag()
923 ),
924 ));
925 rows.push(reader.take_row(
926 1,
927 format!(
928 "buffer[{b}].sync_mode = 0x{:02X} ({})",
929 buf.sync_mode().to_byte(),
930 buf.sync_mode()
931 ),
932 ));
933 rows.push(reader.take_row(2, format!("buffer[{b}]._reserved")));
934 }
935
936 if let Some(q) = &desc.quantization {
942 rows.push(reader.take_row(4, format!("quantization_length = {}", q.len())));
943 match QuantizationDescriptor::decode(q) {
944 Ok((qd, _)) => rows.extend(quant_rows(&mut reader, &qd)),
945 Err(_) if !q.is_empty() => {
946 rows.push(
947 reader.take_row(q.len(), "quantization_descriptor (undecodable)".to_string()),
948 );
949 }
950 Err(_) => {}
951 }
952 }
953
954 if let Some(s) = &desc.shard {
956 for (i, &v) in s.parent_shape.iter().enumerate() {
957 rows.push(reader.take_row(8, format!("parent_shape[{i}] = {v}")));
958 }
959 for (i, &v) in s.shard_offset.iter().enumerate() {
960 rows.push(reader.take_row(8, format!("shard_offset[{i}] = {v}")));
961 }
962 }
963
964 if let Some(st) = &desc.statistics {
966 rows.push(reader.take_row(
967 4,
968 format!("stats.computed_mask = 0x{:08X}", st.computed_mask.0),
969 ));
970 rows.push(reader.take_row(4, "stats._reserved".to_string()));
971 rows.push(reader.take_row(8, format!("stats.nnz = {}", st.nnz)));
972 rows.push(reader.take_row(8, format!("stats.sparsity_ratio = {}", st.sparsity_ratio)));
973 rows.push(reader.take_row(8, format!("stats.value_min = {}", st.value_min)));
974 rows.push(reader.take_row(8, format!("stats.value_max = {}", st.value_max)));
975 rows.push(reader.take_row(8, format!("stats.value_abs_max = {}", st.value_abs_max)));
976 rows.push(reader.take_row(8, format!("stats.value_mean = {}", st.value_mean)));
977 rows.push(reader.take_row(8, format!("stats.value_stddev = {}", st.value_stddev)));
978 rows.push(reader.take_row(1, format!("stats.nm_n = {}", st.nm_n)));
979 rows.push(reader.take_row(1, format!("stats.nm_m = {}", st.nm_m)));
980 rows.push(reader.take_row(1, format!("stats.has_nan = {}", u8::from(st.has_nan))));
981 rows.push(reader.take_row(1, format!("stats.has_inf = {}", u8::from(st.has_inf))));
982 rows.push(reader.take_row(4, "stats._reserved2".to_string()));
983 }
984
985 if let Some(ext) = &desc.extension_type {
987 rows.push(reader.take_row(4, format!("ext_type.bit_width = {}", ext.bit_width)));
988 rows.push(reader.take_row(
989 1,
990 format!("ext_type.packing_factor = {}", ext.packing_factor),
991 ));
992 rows.push(reader.take_row(1, format!("ext_type.is_float = {}", u8::from(ext.is_float))));
993 rows.push(reader.take_row(
994 1,
995 format!("ext_type.is_signed = {}", u8::from(ext.is_signed)),
996 ));
997 rows.push(reader.take_row(1, format!("ext_type.sign_bits = {}", ext.sign_bits)));
998 rows.push(reader.take_row(1, format!("ext_type.exponent_bits = {}", ext.exponent_bits)));
999 rows.push(reader.take_row(1, format!("ext_type.mantissa_bits = {}", ext.mantissa_bits)));
1000 rows.push(reader.take_row(2, "ext_type._reserved".to_string()));
1001 rows.push(reader.take_row(4, format!("ext_type.exponent_bias = {}", ext.exponent_bias)));
1002 rows.push(reader.take_row(1, format!("ext_type.has_nan = {}", u8::from(ext.has_nan))));
1003 rows.push(reader.take_row(1, format!("ext_type.has_inf = {}", u8::from(ext.has_inf))));
1004 rows.push(reader.take_row(2, "ext_type._reserved2".to_string()));
1005 }
1006
1007 if let Some(cm) = &desc.composite_member {
1010 let role_n = match cm.member_role {
1011 MemberRole::Base => "base",
1012 MemberRole::Correction => "correction",
1013 _ => "unknown",
1014 };
1015 rows.push(reader.take_row(1, format!("member_role = {role_n}")));
1016 rows.push(reader.take_row(15, "composite_member._reserved".to_string()));
1017 }
1018
1019 if let Some(row) = reader.skip_to(desc_len) {
1021 rows.push(row);
1022 }
1023
1024 rows
1025}
1026
1027fn section_row(title: &str) -> Row {
1030 Row {
1031 offset: 0,
1032 bytes: vec![],
1033 field: format!("── {title} ──"),
1034 }
1035}
1036
1037fn parse_kv_scalar(r: &mut FReader<'_>, rows: &mut Vec<Row>, label: &str, tag: u8) -> Result<()> {
1038 match tag {
1039 0x01 => {
1040 let (sl_off, sl) = r.take(4)?;
1041 let str_len = le_u32(&sl) as usize;
1042 rows.push(Row {
1043 offset: sl_off,
1044 bytes: sl,
1045 field: format!("{label}.str_len = {str_len}"),
1046 });
1047 let (sv_off, sv) = r.take(str_len)?;
1048 let s = String::from_utf8(sv.clone()).unwrap_or_else(|_| "(invalid UTF-8)".into());
1049 rows.push(Row {
1050 offset: sv_off,
1051 bytes: sv,
1052 field: format!("{label} = {s:?}"),
1053 });
1054 }
1055 0x02 => {
1056 let (vo, vb) = r.take(8)?;
1057 rows.push(Row {
1058 offset: vo,
1059 bytes: vb.clone(),
1060 field: format!("{label} = {}i64", le_i64(&vb)),
1061 });
1062 }
1063 0x03 => {
1064 let (vo, vb) = r.take(8)?;
1065 rows.push(Row {
1066 offset: vo,
1067 bytes: vb.clone(),
1068 field: format!("{label} = {}u64", le_u64(&vb)),
1069 });
1070 }
1071 0x04 => {
1072 let (vo, vb) = r.take(8)?;
1073 rows.push(Row {
1074 offset: vo,
1075 bytes: vb.clone(),
1076 field: format!("{label} = {}f64", le_f64(&vb)),
1077 });
1078 }
1079 0x05 => {
1080 let (vo, vb) = r.take(1)?;
1081 rows.push(Row {
1082 offset: vo,
1083 bytes: vb.clone(),
1084 field: format!("{label} = {}", vb[0] != 0),
1085 });
1086 }
1087 0x06 => {
1088 let (bl_off, bl) = r.take(4)?;
1089 let byte_len = le_u32(&bl) as usize;
1090 rows.push(Row {
1091 offset: bl_off,
1092 bytes: bl,
1093 field: format!("{label}.byte_len = {byte_len}"),
1094 });
1095 let (bv_off, bv) = r.take(byte_len)?;
1096 rows.push(Row {
1097 offset: bv_off,
1098 bytes: bv,
1099 field: format!("{label} = ({byte_len} bytes)"),
1100 });
1101 }
1102 _ => {
1103 rows.push(Row {
1104 offset: r.pos,
1105 bytes: vec![],
1106 field: format!("{label} = (unknown type 0x{tag:02X})"),
1107 });
1108 }
1109 }
1110 Ok(())
1111}
1112
1113fn parse_kv_section(
1114 data: &[u8],
1115 r: &mut FReader<'_>,
1116 rows: &mut Vec<Row>,
1117 kv_offset: usize,
1118 kv_length: usize,
1119) -> Result<()> {
1120 r.seek_to(kv_offset);
1121
1122 let (kc_off, kc) = r.take(4)?;
1123 let kv_count = le_u32(&kc) as usize;
1124 rows.push(section_row(&format!("KV METADATA ({kv_count} entries)")));
1125 rows.push(Row {
1126 offset: kc_off,
1127 bytes: kc,
1128 field: format!("kv_count = {kv_count}"),
1129 });
1130
1131 for i in 0..kv_count {
1132 let (kl_off, kl) = r.take(2)?;
1133 let key_len = le_u16(&kl) as usize;
1134 rows.push(Row {
1135 offset: kl_off,
1136 bytes: kl,
1137 field: format!("kv[{i}].key_len = {key_len}"),
1138 });
1139
1140 let (k_off, k) = r.take(key_len)?;
1141 let key = String::from_utf8(k.clone())
1142 .map_err(|_| Error::Hrryfile(format!("kv[{i}] key is not valid UTF-8")))?;
1143 rows.push(Row {
1144 offset: k_off,
1145 bytes: k,
1146 field: format!("kv[{i}].key = {key:?}"),
1147 });
1148
1149 let (tag_off, tag_b) = r.take(1)?;
1150 let tag = le_u8(&tag_b);
1151 rows.push(Row {
1152 offset: tag_off,
1153 bytes: tag_b,
1154 field: format!("kv[{i}].type = 0x{tag:02X} ({})", kv_tag_name(tag)),
1155 });
1156
1157 if tag == 0x07 {
1158 let (et_off, et) = r.take(1)?;
1160 let elem_tag = le_u8(&et);
1161 rows.push(Row {
1162 offset: et_off,
1163 bytes: et,
1164 field: format!(
1165 "kv[{i}].elem_type = 0x{elem_tag:02X} ({})",
1166 kv_tag_name(elem_tag)
1167 ),
1168 });
1169 let (ac_off, ac) = r.take(4)?;
1170 let count = le_u32(&ac) as usize;
1171 rows.push(Row {
1172 offset: ac_off,
1173 bytes: ac,
1174 field: format!("kv[{i}].elem_count = {count}"),
1175 });
1176 for j in 0..count {
1177 parse_kv_scalar(r, rows, &format!("kv[{i}][{j}]"), elem_tag)?;
1178 }
1179 } else {
1180 parse_kv_scalar(r, rows, &format!("kv[{i}].value"), tag)?;
1181 }
1182 }
1183
1184 let _ = (data, kv_length); Ok(())
1186}
1187
1188fn append_data_rows(
1195 rows: &mut Vec<Row>,
1196 desc: &TensorDescriptor,
1197 buf: &[u8],
1198 buf_file_offset: usize,
1199 mode: DataMode,
1200) {
1201 let et = desc.element_type;
1202 let shape = desc.shape.dims();
1203 rows.push(section_row(&format!(
1204 "TENSOR DATA ({}, shape={shape:?}, {} bytes)",
1205 et,
1206 buf.len()
1207 )));
1208 match mode {
1209 DataMode::Hex => {
1210 for (i, chunk) in buf.chunks(16).enumerate() {
1211 rows.push(Row {
1212 offset: buf_file_offset + i * 16,
1213 bytes: chunk.to_vec(),
1214 field: String::new(),
1215 });
1216 }
1217 }
1218 DataMode::Numpy => {
1219 let s = format_numpy(et, shape, buf);
1220 for line in s.lines() {
1221 rows.push(Row {
1222 offset: 0,
1223 bytes: vec![],
1224 field: line.to_string(),
1225 });
1226 }
1227 }
1228 DataMode::None => {}
1229 }
1230}
1231
1232fn inspect_hrryfile_inner(data: &[u8], rows: &mut Vec<Row>, data_mode: DataMode) -> Result<()> {
1233 let file_size = data.len();
1234
1235 if file_size < FILE_HEADER_SIZE + TRAILER_SIZE {
1236 return Err(Error::Hrryfile(format!(
1237 "file too small: {file_size} bytes (minimum {})",
1238 FILE_HEADER_SIZE + TRAILER_SIZE
1239 )));
1240 }
1241
1242 let mut r = FReader::new(data);
1243
1244 rows.push(section_row("FILE HEADER (64 bytes)"));
1246
1247 let (magic_off, magic) = r.take(8)?;
1248 rows.push(Row {
1249 offset: magic_off,
1250 bytes: magic,
1251 field: r#"file_magic = "HRRYFILE""#.to_string(),
1252 });
1253
1254 let (vmaj_off, vmaj) = r.take(1)?;
1255 let version_major = le_u8(&vmaj);
1256 rows.push(Row {
1257 offset: vmaj_off,
1258 bytes: vmaj,
1259 field: format!("container_version_major = {version_major}"),
1260 });
1261
1262 let (vmin_off, vmin) = r.take(1)?;
1263 let version_minor = le_u8(&vmin);
1264 rows.push(Row {
1265 offset: vmin_off,
1266 bytes: vmin,
1267 field: format!("container_version_minor = {version_minor}"),
1268 });
1269
1270 rows.push(r.row(2, "_reserved".to_string())?);
1271
1272 let (fl_off, fl) = r.take(4)?;
1273 let flags = le_u32(&fl);
1274 rows.push(Row {
1275 offset: fl_off,
1276 bytes: fl,
1277 field: format!("flags = {}", file_flags_display(flags)),
1278 });
1279
1280 let (al_off, al) = r.take(4)?;
1281 let alignment = le_u32(&al);
1282 rows.push(Row {
1283 offset: al_off,
1284 bytes: al,
1285 field: format!("data_buffer_alignment = {alignment}"),
1286 });
1287
1288 let (fdo_off, fdo) = r.take(8)?;
1289 let first_desc_offset = le_u64(&fdo);
1290 rows.push(Row {
1291 offset: fdo_off,
1292 bytes: fdo,
1293 field: format!("first_descriptor_offset = {first_desc_offset}"),
1294 });
1295
1296 let (tch_off, tch) = r.take(8)?;
1297 let tensor_count_hint = le_u64(&tch);
1298 rows.push(Row {
1299 offset: tch_off,
1300 bytes: tch,
1301 field: if tensor_count_hint == u64::MAX {
1302 "tensor_count_hint = (unknown)".to_string()
1303 } else {
1304 format!("tensor_count_hint = {tensor_count_hint}")
1305 },
1306 });
1307
1308 rows.push(r.row(28, "_reserved_header".to_string())?);
1309
1310 rows.push(section_row("TRAILER (40 bytes)"));
1312
1313 let trailer_start = file_size - TRAILER_SIZE;
1314 r.seek_to(trailer_start);
1315
1316 let (io_off, io) = r.take(8)?;
1317 let index_offset = le_u64(&io) as usize;
1318 rows.push(Row {
1319 offset: io_off,
1320 bytes: io,
1321 field: format!("index_offset = {index_offset}"),
1322 });
1323
1324 let (il_off, il) = r.take(8)?;
1325 let index_length = le_u64(&il) as usize;
1326 rows.push(Row {
1327 offset: il_off,
1328 bytes: il,
1329 field: format!("index_length = {index_length}"),
1330 });
1331
1332 let (ko_off, ko) = r.take(8)?;
1333 let kv_offset = le_u64(&ko) as usize;
1334 rows.push(Row {
1335 offset: ko_off,
1336 bytes: ko,
1337 field: format!("kv_offset = {kv_offset}"),
1338 });
1339
1340 let (kl_off, kl_b) = r.take(4)?;
1341 let kv_length = le_u32(&kl_b) as usize;
1342 rows.push(Row {
1343 offset: kl_off,
1344 bytes: kl_b,
1345 field: format!("kv_length = {kv_length}"),
1346 });
1347
1348 let (crc_off, crc_b) = r.take(4)?;
1349 let stored_crc = le_u32(&crc_b);
1350 rows.push(Row {
1351 offset: crc_off,
1352 bytes: crc_b,
1353 field: format!("index_crc32c = 0x{stored_crc:08X}"),
1354 });
1355
1356 rows.push(r.row(4, "_reserved".to_string())?);
1357
1358 let (tm_off, tm) = r.take(4)?;
1359 rows.push(Row {
1360 offset: tm_off,
1361 bytes: tm,
1362 field: r#"trailer_magic = "HRRY""#.to_string(),
1363 });
1364
1365 let sorted_label = if flags & FLAG_SORTED_INDEX != 0 {
1367 ", sorted"
1368 } else {
1369 ""
1370 };
1371
1372 r.seek_to(index_offset);
1373
1374 let (ec_off, ec) = r.take(8)?;
1375 let entry_count = le_u64(&ec) as usize;
1376
1377 rows.push(section_row(&format!(
1378 "INDEX ({entry_count} entr{}{sorted_label})",
1379 if entry_count == 1 { "y" } else { "ies" }
1380 )));
1381 rows.push(Row {
1382 offset: ec_off,
1383 bytes: ec,
1384 field: format!("entry_count = {entry_count}"),
1385 });
1386
1387 struct IndexEntry {
1388 name: String,
1389 descriptor_offset: usize,
1390 descriptor_length: usize,
1391 data_offset: u64,
1392 data_length: u64,
1393 }
1394 let mut entries: Vec<IndexEntry> = Vec::with_capacity(entry_count);
1395
1396 for i in 0..entry_count {
1397 let (nl_off, nl) = r.take(2)?;
1398 let name_len = le_u16(&nl) as usize;
1399 rows.push(Row {
1400 offset: nl_off,
1401 bytes: nl,
1402 field: format!("entry[{i}].name_len = {name_len}"),
1403 });
1404
1405 let (n_off, n_b) = r.take(name_len)?;
1406 let name = String::from_utf8(n_b.clone())
1407 .map_err(|_| Error::Hrryfile(format!("entry[{i}] name is not valid UTF-8")))?;
1408 rows.push(Row {
1409 offset: n_off,
1410 bytes: n_b,
1411 field: format!("entry[{i}].name = {name:?}"),
1412 });
1413
1414 let (do_off, do_b) = r.take(8)?;
1415 let descriptor_offset = le_u64(&do_b) as usize;
1416 rows.push(Row {
1417 offset: do_off,
1418 bytes: do_b,
1419 field: format!("entry[{i}].descriptor_offset = {descriptor_offset}"),
1420 });
1421
1422 let (dl_off, dl_b) = r.take(4)?;
1423 let descriptor_length = le_u32(&dl_b) as usize;
1424 rows.push(Row {
1425 offset: dl_off,
1426 bytes: dl_b,
1427 field: format!("entry[{i}].descriptor_length = {descriptor_length}"),
1428 });
1429
1430 let (dto_off, dto) = r.take(8)?;
1431 let data_offset = le_u64(&dto);
1432 rows.push(Row {
1433 offset: dto_off,
1434 bytes: dto,
1435 field: format!("entry[{i}].data_offset = {data_offset}"),
1436 });
1437
1438 let (dtl_off, dtl) = r.take(8)?;
1439 let data_length = le_u64(&dtl);
1440 rows.push(Row {
1441 offset: dtl_off,
1442 bytes: dtl,
1443 field: format!("entry[{i}].data_length = {data_length}"),
1444 });
1445
1446 let (ef_off, ef) = r.take(4)?;
1447 let entry_flags = le_u32(&ef);
1448 rows.push(Row {
1449 offset: ef_off,
1450 bytes: ef,
1451 field: format!("entry[{i}].flags = 0x{entry_flags:08X}"),
1452 });
1453
1454 entries.push(IndexEntry {
1455 name,
1456 descriptor_offset,
1457 descriptor_length,
1458 data_offset,
1459 data_length,
1460 });
1461 }
1462
1463 let has_kv = (flags & FLAG_HAS_KV_METADATA != 0) || (kv_offset != 0 && kv_length != 0);
1465 if has_kv && kv_offset != 0 && kv_length != 0 {
1466 parse_kv_section(data, &mut r, rows, kv_offset, kv_length)?;
1467 }
1468
1469 for entry in &entries {
1471 rows.push(section_row(&format!(
1472 "TENSOR DESCRIPTOR: {:?} (data at {}, {} bytes)",
1473 entry.name, entry.data_offset, entry.data_length
1474 )));
1475
1476 let end = entry.descriptor_offset + entry.descriptor_length;
1477 let desc_data = data.get(entry.descriptor_offset..end).ok_or_else(|| {
1478 Error::Hrryfile(format!(
1479 "descriptor for {:?} is out of file bounds (offset={}, length={})",
1480 entry.name, entry.descriptor_offset, entry.descriptor_length
1481 ))
1482 })?;
1483
1484 match TensorDescriptor::decode(desc_data) {
1485 Ok(desc) => {
1486 rows.extend(rows_from_descriptor(
1487 desc_data,
1488 &desc,
1489 entry.descriptor_offset,
1490 ));
1491 if data_mode != DataMode::None {
1492 let data_start = entry.data_offset as usize;
1493 let data_end = data_start.saturating_add(entry.data_length as usize);
1494 if data_end <= data.len() && entry.data_length > 0 {
1495 append_data_rows(
1496 rows,
1497 &desc,
1498 &data[data_start..data_end],
1499 data_start,
1500 data_mode,
1501 );
1502 } else {
1503 rows.push(section_row(&format!(
1504 "TENSOR DATA {:?}: not available (data outside file bounds)",
1505 entry.name
1506 )));
1507 }
1508 }
1509 }
1510 Err(e) => rows.push(Row {
1511 offset: entry.descriptor_offset,
1512 bytes: vec![],
1513 field: format!("ERROR decoding descriptor: {e}"),
1514 }),
1515 }
1516 }
1517
1518 Ok(())
1519}
1520
1521fn inspect_hrryfile(data: &[u8], data_mode: DataMode) -> (Vec<Row>, Option<Error>) {
1522 let mut rows = Vec::new();
1523 match inspect_hrryfile_inner(data, &mut rows, data_mode) {
1524 Ok(()) => (rows, None),
1525 Err(e) => (rows, Some(e)),
1526 }
1527}
1528
1529fn inspect(data: &[u8], data_mode: DataMode) -> (Vec<Row>, Option<Error>) {
1532 if data.len() >= 8 && &data[..8] == FILE_MAGIC {
1534 return inspect_hrryfile(data, data_mode);
1535 }
1536
1537 match TensorDescriptor::decode(data) {
1538 Ok(desc) => {
1539 let desc_len = u32::from_le_bytes(data[6..10].try_into().unwrap_or([0u8; 4])) as usize;
1540 let mut rows = rows_from_descriptor(data, &desc, 0);
1541 if data_mode != DataMode::None {
1542 let buf = data.get(desc_len..).unwrap_or(&[]);
1543 if buf.is_empty() {
1544 rows.push(section_row(
1545 "TENSOR DATA: not present in this file (descriptor only)",
1546 ));
1547 } else {
1548 append_data_rows(&mut rows, &desc, buf, desc_len, data_mode);
1549 }
1550 }
1551 (rows, None)
1552 }
1553 Err(e) => {
1554 let partial = if data.len() >= 4 {
1557 vec![Row {
1558 offset: 0,
1559 bytes: data[..4].to_vec(),
1560 field: format!(
1561 "magic = {:?}",
1562 std::str::from_utf8(&data[..4]).unwrap_or("(non-UTF-8)")
1563 ),
1564 }]
1565 } else {
1566 vec![]
1567 };
1568 (partial, Some(Error::Parse(e)))
1569 }
1570 }
1571}
1572
1573fn hex_str(bytes: &[u8]) -> String {
1577 bytes
1578 .iter()
1579 .map(|b| format!("{b:02X}"))
1580 .collect::<Vec<_>>()
1581 .join(" ")
1582}
1583
1584fn hex_chunks(bytes: &[u8], per_line: usize) -> Vec<String> {
1587 if bytes.is_empty() {
1588 return vec![String::new()];
1589 }
1590 bytes.chunks(per_line).map(hex_str).collect()
1591}
1592
1593fn print_table(rows: &[Row]) {
1603 println!("{:>6} {:<30} Field", "Offset", "Value (hex)");
1604 println!("{:->6} {:-<30} {:-<5}", "", "", "");
1605
1606 for row in rows {
1607 if row.bytes.is_empty() {
1608 if row.field.starts_with("── ") {
1609 println!();
1611 println!(" {}", row.field);
1612 } else {
1613 println!("{:>6} {:<30} {}", "", "", row.field);
1614 }
1615 } else {
1616 let mut first = true;
1617 for chunk in hex_chunks(&row.bytes, 10) {
1618 if first {
1619 println!("{:>6} {:<30} {}", row.offset, chunk, row.field);
1620 first = false;
1621 } else {
1622 println!("{:>6} {:<30}", "", chunk);
1623 }
1624 }
1625 }
1626 }
1627}
1628
1629fn parse_args(args: &[String]) -> Result<(String, DataMode)> {
1632 let mut path: Option<String> = None;
1633 let mut data_mode = DataMode::None;
1634
1635 for arg in args.iter().skip(1) {
1636 if arg == "--data" || arg == "--data=numpy" {
1637 data_mode = DataMode::Numpy;
1638 } else if arg == "--data=hex" {
1639 data_mode = DataMode::Hex;
1640 } else if (arg.starts_with('-') && arg != "-") || path.is_some() {
1641 return Err(Error::Usage);
1642 } else {
1643 path = Some(arg.clone());
1644 }
1645 }
1646
1647 path.map(|p| (p, data_mode)).ok_or(Error::Usage)
1648}
1649
1650fn run() -> Result<()> {
1651 let args: Vec<String> = env::args().collect();
1652 let (path, data_mode) = parse_args(&args)?;
1653
1654 let data: Vec<u8> = if path == "-" {
1655 let mut buf = Vec::new();
1656 io::stdin().read_to_end(&mut buf)?;
1657 buf
1658 } else {
1659 fs::read(&path)?
1660 };
1661
1662 let (rows, err) = inspect(&data, data_mode);
1663 print_table(&rows);
1664
1665 if let Some(e) = err {
1666 let error_row = Row {
1667 offset: 0,
1668 bytes: vec![],
1669 field: format!("ERROR: {e}"),
1670 };
1671 print_table(&[error_row]);
1672 eprintln!("error: {e}");
1673 process::exit(1);
1674 }
1675
1676 Ok(())
1677}
1678
1679fn main() {
1680 if let Err(e) = run() {
1681 eprintln!("error: {e}");
1682 process::exit(1);
1683 }
1684}
1685
1686#[cfg(test)]
1687mod tests {
1688 use super::*;
1689 use hurray_core::ElementType;
1690
1691 fn make_raw_descriptor(et: ElementType, dims: &[u64]) -> Vec<u8> {
1693 use hurray_core::{
1694 BufferHandle, LayoutDescriptor, MemoryClass, Shape, SyncMode, TensorDescriptor,
1695 DESCRIPTOR_VERSION_MAJOR, DESCRIPTOR_VERSION_MINOR,
1696 };
1697 let shape = Shape::new(dims.to_vec()).unwrap();
1698 let n: u64 = dims.iter().product();
1699 let byte_size = hurray_core::buffer_size_bytes(et, n);
1700 let bh = BufferHandle::with_memory_class(
1701 byte_size,
1702 64,
1703 hurray_core::DeviceTag::Cpu,
1704 SyncMode::ProducerSynced,
1705 MemoryClass::Standard,
1706 )
1707 .unwrap();
1708 let desc = TensorDescriptor::new(
1709 DESCRIPTOR_VERSION_MAJOR,
1710 DESCRIPTOR_VERSION_MINOR,
1711 et,
1712 shape,
1713 0,
1714 LayoutDescriptor::RowMajor,
1715 vec![bh],
1716 None,
1717 None,
1718 None,
1719 None,
1720 )
1721 .unwrap();
1722 desc.encode().unwrap()
1723 }
1724
1725 #[test]
1726 fn format_float_adds_dot() {
1727 assert_eq!(format_float(1.0), "1.");
1728 assert_eq!(format_float(1.5), "1.5");
1729 assert_eq!(format_float(f64::NAN), "nan");
1730 assert_eq!(format_float(f64::INFINITY), "inf");
1731 assert_eq!(format_float(f64::NEG_INFINITY), "-inf");
1732 }
1733
1734 #[test]
1735 fn format_scalar_float32() {
1736 let data = 1.0f32.to_le_bytes();
1737 assert_eq!(format_scalar(ElementType::Float32, &data, 0), "1.");
1738 }
1739
1740 #[test]
1741 fn format_scalar_int32() {
1742 let data = 42i32.to_le_bytes();
1743 assert_eq!(format_scalar(ElementType::Int32, &data, 0), "42");
1744 }
1745
1746 #[test]
1747 fn format_scalar_bool_true() {
1748 let data = [0b0000_0001u8];
1749 assert_eq!(format_scalar(ElementType::Bool, &data, 0), "True");
1750 assert_eq!(format_scalar(ElementType::Bool, &data, 1), "False");
1751 }
1752
1753 #[test]
1754 fn format_scalar_uint4_int4_lsb_first() {
1755 let data = [0x83u8];
1757 assert_eq!(format_scalar(ElementType::Uint4, &data, 0), "3");
1758 assert_eq!(format_scalar(ElementType::Uint4, &data, 1), "8");
1759 assert_eq!(format_scalar(ElementType::Int4, &data, 0), "3");
1761 assert_eq!(format_scalar(ElementType::Int4, &data, 1), "-8");
1762 }
1763
1764 #[test]
1765 fn format_scalar_uint2_int2_lsb_first() {
1766 let data = [0b1110_0100u8];
1768 for (idx, want) in [(0, "0"), (1, "1"), (2, "2"), (3, "3")] {
1769 assert_eq!(format_scalar(ElementType::Uint2, &data, idx), want);
1770 }
1771 for (idx, want) in [(0, "0"), (1, "1"), (2, "-2"), (3, "-1")] {
1773 assert_eq!(format_scalar(ElementType::Int2, &data, idx), want);
1774 }
1775 }
1776
1777 #[test]
1778 fn format_scalar_float8_e4m3() {
1779 assert_eq!(format_scalar(ElementType::Float8E4M3, &[0x38], 0), "1.");
1781 assert_eq!(format_scalar(ElementType::Float8E4M3, &[0x40], 0), "2.");
1783 assert_eq!(format_scalar(ElementType::Float8E4M3, &[0xB8], 0), "-1.");
1785 assert_eq!(format_scalar(ElementType::Float8E4M3, &[0x7F], 0), "nan");
1787 assert_eq!(format_scalar(ElementType::Float8E4M3, &[0x00], 0), "0.");
1789 }
1790
1791 #[test]
1792 fn format_scalar_float8_e5m2() {
1793 assert_eq!(format_scalar(ElementType::Float8E5M2, &[0x3C], 0), "1.");
1795 assert_eq!(format_scalar(ElementType::Float8E5M2, &[0x7C], 0), "inf");
1797 assert_eq!(format_scalar(ElementType::Float8E5M2, &[0x7D], 0), "nan");
1799 }
1800
1801 #[test]
1802 fn format_scalar_float8_e8m0_scale() {
1803 assert_eq!(format_scalar(ElementType::Float8E8M0, &[127], 0), "1.");
1805 assert_eq!(format_scalar(ElementType::Float8E8M0, &[128], 0), "2.");
1806 assert_eq!(format_scalar(ElementType::Float8E8M0, &[0x00], 0), "nan");
1808 assert_eq!(format_scalar(ElementType::Float8E8M0, &[0xFF], 0), "nan");
1809 }
1810
1811 #[test]
1812 fn format_scalar_float4_e2m1() {
1813 assert_eq!(format_scalar(ElementType::Float4E2M1, &[0x02], 0), "1.");
1815 assert_eq!(format_scalar(ElementType::Float4E2M1, &[0x07], 0), "6.");
1817 assert_eq!(format_scalar(ElementType::Float4E2M1, &[0x20], 1), "1.");
1819 }
1820
1821 #[test]
1822 fn format_scalar_float6_packing() {
1823 assert_eq!(
1825 format_scalar(ElementType::Float6E2M3, &[0x08, 0, 0], 0),
1826 "1."
1827 );
1828 assert_eq!(
1830 format_scalar(ElementType::Float6E3M2, &[0x0C, 0, 0], 0),
1831 "1."
1832 );
1833 }
1834
1835 #[test]
1836 fn quant_rows_span_matches_encoded_len() {
1837 use hurray_core::quantization::PerTensorAffine;
1838 let desc = QuantizationDescriptor::PerTensorAffine(PerTensorAffine::new(0.5, 128).unwrap());
1841 let bytes = desc.encode_to_vec();
1842 let mut reader = Reader::with_base(&bytes, bytes.len(), 0);
1843 let rows = quant_rows(&mut reader, &desc);
1844 let span: usize = rows.iter().map(|r| r.bytes.len()).sum();
1845 assert_eq!(span, bytes.len());
1846 assert!(rows[0].field.contains("per-tensor-affine"));
1848 assert!(rows.iter().any(|r| r.field.contains("scale = 0.5")));
1849 }
1850
1851 #[test]
1852 fn format_numpy_1d_int32() {
1853 let data: Vec<u8> = [1i32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
1854 let s = format_numpy(ElementType::Int32, &[3], &data);
1855 assert_eq!(s, "[1 2 3]");
1856 }
1857
1858 #[test]
1859 fn format_numpy_2d_float32() {
1860 let data: Vec<u8> = [1.0f32, 2.0, 3.0, 4.0]
1861 .iter()
1862 .flat_map(|v| v.to_le_bytes())
1863 .collect();
1864 let s = format_numpy(ElementType::Float32, &[2, 2], &data);
1865 assert!(s.starts_with("[[1. 2.]"), "got: {s}");
1866 assert!(s.contains("[3. 4.]"), "got: {s}");
1867 }
1868
1869 #[test]
1870 fn format_numpy_truncates_large() {
1871 let n = 2000u64;
1872 let data: Vec<u8> = (0..n).flat_map(|i| (i as i32).to_le_bytes()).collect();
1873 let s = format_numpy(ElementType::Int32, &[n], &data);
1874 assert!(s.contains("..."), "expected truncation: {s}");
1875 }
1876
1877 #[test]
1878 fn parse_args_no_data() {
1879 let args = ["prog", "file.hrry"].map(String::from);
1880 let (path, mode) = parse_args(&args).unwrap();
1881 assert_eq!(path, "file.hrry");
1882 assert_eq!(mode, DataMode::None);
1883 }
1884
1885 #[test]
1886 fn parse_args_data_numpy() {
1887 let args = ["prog", "--data", "file.hrry"].map(String::from);
1888 let (path, mode) = parse_args(&args).unwrap();
1889 assert_eq!(path, "file.hrry");
1890 assert_eq!(mode, DataMode::Numpy);
1891 }
1892
1893 #[test]
1894 fn parse_args_data_hex() {
1895 let args = ["prog", "--data=hex", "file.hrry"].map(String::from);
1896 let (_, mode) = parse_args(&args).unwrap();
1897 assert_eq!(mode, DataMode::Hex);
1898 }
1899
1900 #[test]
1901 fn parse_args_unknown_flag_is_error() {
1902 let args = ["prog", "--foo", "file.hrry"].map(String::from);
1903 assert!(parse_args(&args).is_err());
1904 }
1905
1906 #[test]
1907 fn inspect_raw_descriptor_no_data() {
1908 let desc_bytes = make_raw_descriptor(ElementType::Float32, &[2, 3]);
1909 let (rows, err) = inspect(&desc_bytes, DataMode::None);
1910 assert!(err.is_none());
1911 assert!(rows.iter().any(|r| r.field.contains("float32")));
1912 }
1913
1914 #[test]
1915 fn inspect_raw_descriptor_numpy_mode_no_buffer() {
1916 let desc_bytes = make_raw_descriptor(ElementType::Float32, &[2, 3]);
1917 let (rows, err) = inspect(&desc_bytes, DataMode::Numpy);
1918 assert!(err.is_none());
1919 assert!(rows
1921 .iter()
1922 .any(|r| r.field.contains("TENSOR DATA") || r.field.contains("not present")));
1923 }
1924
1925 #[test]
1926 fn inspect_raw_descriptor_numpy_mode_with_buffer() {
1927 let mut file = make_raw_descriptor(ElementType::Float32, &[3]);
1928 for v in [1.0f32, 2.0, 3.0] {
1930 file.extend_from_slice(&v.to_le_bytes());
1931 }
1932 let (rows, err) = inspect(&file, DataMode::Numpy);
1933 assert!(err.is_none());
1934 let all = rows
1935 .iter()
1936 .map(|r| r.field.as_str())
1937 .collect::<Vec<_>>()
1938 .join("\n");
1939 assert!(all.contains("[1. 2. 3.]"), "expected numpy row in: {all}");
1940 }
1941}