1use crate::fixed_records::Bytes;
13use std::path::PathBuf;
14use std::sync::Arc;
15use std::sync::atomic::{AtomicBool, Ordering};
16
17pub const WIDTHS: [usize; 4] = [8, 16, 32, 64];
19
20pub const MAX_RECORD_SIZE: usize = 4096;
22
23pub const MAX_PATTERN: usize = 4096;
25
26const MAX_MARKED_PATTERN: usize = 256;
29
30pub const INSPECTED: usize = 64;
32
33pub const PANEL_WIDTH: u16 = 46;
35
36pub const ASCII_MIN_WIDTH: u16 = 50;
38
39const WINDOW: usize = 8 << 20;
41
42const STRIDE_REACH: usize = 4 << 20;
44
45const STRIDE_MATCHES: usize = 8;
47
48pub fn hex_width(n: usize) -> usize {
51 if n == 0 { 0 } else { hex_x(n - 1) + 2 }
52}
53
54pub fn hex_x(i: usize) -> usize {
56 i * 3 + i / 4 + i / 8
57}
58
59pub fn offset_digits(len: u64, decimal: bool) -> usize {
61 let last = len.saturating_sub(1);
62 let digits = if decimal {
63 last.checked_ilog10().map_or(1, |d| d as usize + 1)
64 } else {
65 last.checked_ilog2().map_or(1, |b| b as usize / 4 + 1)
66 };
67 digits.max(8)
68}
69
70pub fn row_width(n: usize, digits: usize, ascii: bool) -> usize {
72 digits + 2 + hex_width(n) + if ascii { 2 + n } else { 0 }
73}
74
75pub fn fit(width: usize, digits: usize, ascii: bool) -> usize {
77 let mut n = 1;
78 while row_width(n + 1, digits, ascii) <= width {
79 n += 1;
80 }
81 n
82}
83
84pub fn auto_per_row(width: usize, digits: usize, ascii: bool) -> usize {
87 WIDTHS
88 .iter()
89 .rev()
90 .copied()
91 .find(|&n| row_width(n, digits, ascii) <= width)
92 .unwrap_or_else(|| fit(width, digits, ascii))
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct Geometry {
98 pub per_row: usize,
100 pub shown: usize,
103 pub first_col: usize,
105 pub ascii: bool,
106 pub panel: bool,
108 pub room_for_panel: bool,
110 pub digits: usize,
111 pub rows: usize,
113}
114
115impl Default for Geometry {
116 fn default() -> Self {
117 Self {
118 per_row: 16,
119 shown: 16,
120 first_col: 0,
121 ascii: true,
122 panel: false,
123 room_for_panel: false,
124 digits: 8,
125 rows: 16,
126 }
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum ByteClass {
133 Null,
134 Printable,
135 Whitespace,
136 Control,
137 High,
139 Ff,
140}
141
142pub fn class(b: u8) -> ByteClass {
143 match b {
144 0 => ByteClass::Null,
145 0xff => ByteClass::Ff,
146 b'\t' | b'\n' | 0x0b | 0x0c | b'\r' | b' ' => ByteClass::Whitespace,
147 0x21..=0x7e => ByteClass::Printable,
148 0x80..=0xfe => ByteClass::High,
149 _ => ByteClass::Control,
150 }
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum Origin {
156 Home,
158 Table,
160 Info,
162 Launch,
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum PromptKind {
169 GoTo,
170 Find,
171 RecordSize,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct Pattern {
177 pub bytes: Vec<Option<u8>>,
178 pub label: String,
180}
181
182#[derive(Debug, Clone)]
184pub struct Found {
185 pub pattern: Pattern,
186 pub hit: Option<u64>,
187 pub stride: Option<u64>,
189}
190
191#[derive(Debug, Clone)]
193pub struct HexFindRun {
194 pub stop: Arc<AtomicBool>,
195 pub view: u64,
197 pub pattern: Pattern,
198}
199
200#[derive(Debug, Clone)]
202pub struct HexHit {
203 pub at: Option<u64>,
204 pub wrapped: bool,
205 pub stride: Option<u64>,
206}
207
208pub struct HexSource {
210 pub path: PathBuf,
211 pub bytes: Arc<Bytes>,
212}
213
214impl std::fmt::Debug for HexSource {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 f.debug_struct("HexSource")
217 .field("path", &self.path)
218 .field("len", &self.bytes.len())
219 .finish()
220 }
221}
222
223impl HexSource {
224 pub fn open(path: PathBuf) -> std::io::Result<Self> {
227 if path.is_dir() {
228 return Err(std::io::Error::other(format!(
229 "{} is a directory",
230 path.display()
231 )));
232 }
233 let bytes = Arc::new(Bytes::map(&path)?);
234 Ok(Self { path, bytes })
235 }
236}
237
238pub struct HexView {
240 pub path: PathBuf,
241 pub bytes: Arc<Bytes>,
242 pub origin: Origin,
243 pub fallback: bool,
245 pub serial: u64,
247 pub cursor: u64,
248 pub top: u64,
250 pub record_size: Option<usize>,
252 pub decimal: bool,
253 pub mark: Option<u64>,
255 pub inspector: bool,
258 pub found: Option<Found>,
259 pub prompt: Option<PromptKind>,
261 pub prompt_error: Option<String>,
262 pub utf16: bool,
264 pub inspector_open: bool,
266 pub input: crate::widgets::text_input::TextInput,
268 pub picker: Option<crate::widgets::ui::PickerState>,
270 pub geometry: Geometry,
272}
273
274impl HexView {
275 pub fn new(source: HexSource, origin: Origin, fallback: bool, serial: u64) -> Self {
276 Self {
277 path: source.path,
278 bytes: source.bytes,
279 origin,
280 fallback,
281 serial,
282 cursor: 0,
283 top: 0,
284 record_size: None,
285 decimal: false,
286 mark: None,
287 inspector: true,
288 found: None,
289 prompt: None,
290 prompt_error: None,
291 utf16: false,
292 inspector_open: false,
293 input: crate::widgets::text_input::TextInput::new(),
294 picker: None,
295 geometry: Geometry::default(),
296 }
297 }
298
299 pub fn len(&self) -> u64 {
300 self.bytes.len() as u64
301 }
302
303 pub fn is_empty(&self) -> bool {
304 self.bytes.is_empty()
305 }
306
307 pub fn name(&self) -> String {
309 self.path.file_name().map_or_else(
310 || self.path.display().to_string(),
311 |n| n.to_string_lossy().into_owned(),
312 )
313 }
314
315 pub fn panel_fits(&self, width: u16) -> bool {
317 let digits = offset_digits(self.len(), self.decimal);
318 width as usize > row_width(16, digits, true) + PANEL_WIDTH as usize
319 }
320
321 pub fn lay_out(&mut self, width: u16, rows: usize) -> Geometry {
324 let digits = offset_digits(self.len(), self.decimal);
325 let ascii = width >= ASCII_MIN_WIDTH;
326 let room_for_panel = self.panel_fits(width);
327 let panel = self.inspector && room_for_panel;
328 let avail = if panel {
329 width as usize - PANEL_WIDTH as usize - 1
330 } else {
331 width as usize
332 };
333 let per_row = self
334 .record_size
335 .unwrap_or_else(|| auto_per_row(avail, digits, ascii))
336 .max(1);
337 let shown = per_row.min(fit(avail, digits, ascii));
338 let rows = rows.max(1);
339 let per = per_row as u64;
341 let cursor_row = self.cursor / per;
342 let mut top_row = self.top / per;
343 if cursor_row < top_row {
344 top_row = cursor_row;
345 } else if cursor_row >= top_row + rows as u64 {
346 top_row = cursor_row + 1 - rows as u64;
347 }
348 self.top = top_row * per;
349 let col = (self.cursor % per) as usize;
350 let mut first_col = self.geometry.first_col.min(per_row.saturating_sub(shown));
351 if col < first_col {
352 first_col = col;
353 } else if col >= first_col + shown {
354 first_col = col + 1 - shown;
355 }
356 self.geometry = Geometry {
357 per_row,
358 shown,
359 first_col,
360 ascii,
361 panel,
362 room_for_panel,
363 digits,
364 rows,
365 };
366 self.geometry
367 }
368
369 fn last(&self) -> u64 {
370 self.len().saturating_sub(1)
371 }
372
373 fn per(&self) -> u64 {
374 self.geometry.per_row.max(1) as u64
375 }
376
377 pub fn go(&mut self, at: u64) {
379 self.cursor = at.min(self.last());
380 }
381
382 pub fn step(&mut self, delta: i64) {
384 let at = if delta < 0 {
385 self.cursor.saturating_sub(delta.unsigned_abs())
386 } else {
387 self.cursor.saturating_add(delta as u64)
388 };
389 self.go(at);
390 }
391
392 pub fn step_rows(&mut self, rows: i64) {
395 let per = self.per();
396 let delta = per.saturating_mul(rows.unsigned_abs());
397 if rows < 0 {
398 if self.cursor >= delta {
399 self.cursor -= delta;
400 } else {
401 self.cursor %= per;
402 }
403 } else {
404 let row = self.cursor / per;
405 let last_row = self.last() / per;
406 if row < last_row {
407 self.go(self.cursor.saturating_add(delta));
408 }
409 }
410 }
411
412 pub fn next_group(&mut self) {
414 let per = self.per();
415 let row_start = self.cursor / per * per;
416 let col = self.cursor - row_start;
417 let next = (col / 4 + 1) * 4;
418 let at = if next >= per {
419 row_start + per
420 } else {
421 row_start + next
422 };
423 if at <= self.last() {
424 self.cursor = at;
425 }
426 }
427
428 pub fn previous_group(&mut self) {
430 if self.cursor == 0 {
431 return;
432 }
433 let per = self.per();
434 let row_start = self.cursor / per * per;
435 let col = self.cursor - row_start;
436 self.cursor = if col == 0 {
437 let above = row_start - per;
439 above + (per - 1) / 4 * 4
440 } else if !col.is_multiple_of(4) {
441 row_start + col / 4 * 4
442 } else {
443 row_start + col - 4
444 };
445 }
446
447 pub fn row_start(&mut self) {
448 let per = self.per();
449 self.cursor = self.cursor / per * per;
450 }
451
452 pub fn row_end(&mut self) {
453 let per = self.per();
454 self.go(self.cursor / per * per + per - 1);
455 }
456
457 pub fn selection(&self) -> Option<(u64, u64)> {
459 let mark = self.mark?.min(self.last());
460 Some((mark.min(self.cursor), mark.max(self.cursor)))
461 }
462
463 pub fn slice(&self, at: u64, n: usize) -> &[u8] {
465 let data = self.bytes.as_slice();
466 let start = (at as usize).min(data.len());
467 &data[start..(start + n).min(data.len())]
468 }
469
470 pub fn matches_on_screen(&self, lo: u64, hi: u64) -> Vec<(u64, u64)> {
473 let Some(found) = &self.found else {
474 return Vec::new();
475 };
476 let m = found.pattern.bytes.len() as u64;
477 if found.pattern.bytes.len() > MAX_MARKED_PATTERN {
478 return found
479 .hit
480 .filter(|&at| at < hi && at + m > lo)
481 .map(|at| vec![(at, at + m)])
482 .unwrap_or_default();
483 }
484 let start = lo.saturating_sub(m - 1);
485 let data = self.bytes.as_slice();
486 let end = (hi as usize).min(data.len());
487 if start as usize >= end {
488 return Vec::new();
489 }
490 let window = &data[start as usize..end];
491 let mut out = Vec::new();
492 let anchor = Anchor::of(&found.pattern);
493 let mut at = 0;
494 while let Some(p) = anchor.next_in(window, &found.pattern, at, window.len()) {
495 out.push((start + p as u64, start + p as u64 + m));
496 at = p + 1;
497 }
498 out
499 }
500}
501
502fn number(text: &str) -> Option<u64> {
504 let text = text.trim().replace('_', "");
505 if let Some(hex) = text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) {
506 u64::from_str_radix(hex, 16).ok()
507 } else {
508 text.parse().ok()
509 }
510}
511
512pub fn parse_offset(text: &str, cursor: u64, len: u64) -> Result<u64, String> {
515 let text = text.trim();
516 if len == 0 {
517 return Err("The file is empty".to_string());
518 }
519 let bad = || format!("{text} is not an offset: a number, 0x..., +N, -N or e-N");
520 let at = if let Some(n) = text.strip_prefix("e-").or_else(|| text.strip_prefix("E-")) {
521 let n = number(n).ok_or_else(bad)?;
522 len.checked_sub(n)
523 .ok_or_else(|| format!("{text} is before the start of the file"))?
524 } else if let Some(n) = text.strip_prefix('+') {
525 cursor
526 .checked_add(number(n).ok_or_else(bad)?)
527 .ok_or_else(bad)?
528 } else if let Some(n) = text.strip_prefix('-') {
529 cursor
530 .checked_sub(number(n).ok_or_else(bad)?)
531 .ok_or_else(|| format!("{text} is before the start of the file"))?
532 } else {
533 number(text).ok_or_else(bad)?
534 };
535 if at >= len {
536 return Err(format!(
537 "{text} is past the end of the file ({} bytes)",
538 crate::numfmt::group_chrome(len as usize)
539 ));
540 }
541 Ok(at)
542}
543
544fn hex_pair(token: &str) -> Option<Option<u8>> {
546 if token == "??" {
547 return Some(None);
548 }
549 if token.len() != 2 {
550 return None;
551 }
552 u8::from_str_radix(token, 16).ok().map(Some)
553}
554
555pub fn parse_pattern(text: &str, utf16: bool) -> Result<Pattern, String> {
562 let trimmed = text.trim();
563 if trimmed.is_empty() {
564 return Err("Type text, 0x... or hex pairs to find".to_string());
565 }
566 let label = trimmed.to_string();
567 let as_text = |s: &str| -> Vec<Option<u8>> {
568 if utf16 {
569 s.encode_utf16()
570 .flat_map(|u| u.to_le_bytes())
571 .map(Some)
572 .collect()
573 } else {
574 s.bytes().map(Some).collect()
575 }
576 };
577 let quoted = trimmed.len() >= 2 && trimmed.starts_with('"') && trimmed.ends_with('"');
578 let bytes = if quoted {
579 as_text(&trimmed[1..trimmed.len() - 1])
580 } else if let Some(hex) = trimmed
581 .strip_prefix("0x")
582 .or_else(|| trimmed.strip_prefix("0X"))
583 {
584 let digits: String = hex.chars().filter(|c| !c.is_whitespace()).collect();
585 if digits.is_empty() || !digits.len().is_multiple_of(2) || !digits.is_ascii() {
586 return Err(format!(
587 "{trimmed}: after 0x, pairs of hex digits (?? for any byte)"
588 ));
589 }
590 (0..digits.len() / 2)
591 .map(|i| hex_pair(&digits[i * 2..i * 2 + 2]))
592 .collect::<Option<Vec<_>>>()
593 .ok_or_else(|| format!("{trimmed}: after 0x, pairs of hex digits (?? for any byte)"))?
594 } else {
595 let tokens: Vec<&str> = trimmed.split_whitespace().collect();
596 let pairs: Option<Vec<Option<u8>>> = tokens.iter().map(|t| hex_pair(t)).collect();
597 match pairs {
598 Some(pairs) if tokens.len() >= 2 || pairs.contains(&None) => pairs,
599 _ => as_text(trimmed),
600 }
601 };
602 if bytes.is_empty() {
603 return Err("Type text, 0x... or hex pairs to find".to_string());
604 }
605 if bytes.len() > MAX_PATTERN {
606 return Err(format!("A pattern is at most {MAX_PATTERN} bytes"));
607 }
608 if bytes.iter().all(Option::is_none) {
609 return Err("A pattern needs a byte that is not ??".to_string());
610 }
611 Ok(Pattern { bytes, label })
612}
613
614struct Anchor {
617 offset: usize,
618 literal: Vec<u8>,
619}
620
621impl Anchor {
622 fn of(pattern: &Pattern) -> Self {
623 let (mut best, mut best_len) = (0, 0);
624 let mut i = 0;
625 while i < pattern.bytes.len() {
626 if pattern.bytes[i].is_none() {
627 i += 1;
628 continue;
629 }
630 let start = i;
631 while i < pattern.bytes.len() && pattern.bytes[i].is_some() {
632 i += 1;
633 }
634 if i - start > best_len {
635 best = start;
636 best_len = i - start;
637 }
638 }
639 Self {
640 offset: best,
641 literal: pattern.bytes[best..best + best_len]
642 .iter()
643 .map(|b| b.expect("a run of known bytes"))
644 .collect(),
645 }
646 }
647
648 fn next_in(&self, hay: &[u8], pattern: &Pattern, lo: usize, hi: usize) -> Option<usize> {
650 let m = pattern.bytes.len();
651 if m > hay.len() || lo >= hi {
652 return None;
653 }
654 let hi = hi.min(hay.len() - m + 1);
655 if lo >= hi {
656 return None;
657 }
658 let from = lo + self.offset;
659 let to = (hi - 1 + self.offset + self.literal.len()).min(hay.len());
660 let finder = memchr::memmem::Finder::new(&self.literal);
661 let mut at = from;
662 while at < to {
663 let q = at + finder.find(&hay[at..to])?;
664 let p = q - self.offset;
665 if verify(hay, pattern, p) {
666 return Some(p);
667 }
668 at = q + 1;
669 }
670 None
671 }
672
673 fn prev_in(&self, hay: &[u8], pattern: &Pattern, lo: usize, hi: usize) -> Option<usize> {
675 let m = pattern.bytes.len();
676 if m > hay.len() || lo >= hi {
677 return None;
678 }
679 let hi = hi.min(hay.len() - m + 1);
680 if lo >= hi {
681 return None;
682 }
683 let from = lo + self.offset;
684 let mut to = (hi - 1 + self.offset + self.literal.len()).min(hay.len());
685 let finder = memchr::memmem::FinderRev::new(&self.literal);
686 while from < to {
687 let q = from + finder.rfind(&hay[from..to])?;
688 let p = q - self.offset;
689 if verify(hay, pattern, p) {
690 return Some(p);
691 }
692 to = q + self.literal.len() - 1;
693 }
694 None
695 }
696}
697
698fn verify(hay: &[u8], pattern: &Pattern, at: usize) -> bool {
699 hay.get(at..at + pattern.bytes.len()).is_some_and(|window| {
700 window
701 .iter()
702 .zip(&pattern.bytes)
703 .all(|(b, p)| p.is_none_or(|p| p == *b))
704 })
705}
706
707#[derive(Debug, Clone, Copy, PartialEq, Eq)]
709pub struct Stopped;
710
711pub fn find(
715 hay: &[u8],
716 pattern: &Pattern,
717 from: u64,
718 forward: bool,
719 stop: &AtomicBool,
720 mut progress: impl FnMut(u64),
721) -> Result<HexHit, Stopped> {
722 let anchor = Anchor::of(pattern);
723 let len = hay.len();
724 let from = (from as usize).min(len);
725 let mut read = 0u64;
726 let mut scan = |lo: usize, hi: usize, read: &mut u64| -> Result<Option<usize>, Stopped> {
727 if forward {
728 let mut at = lo;
729 while at < hi {
730 if stop.load(Ordering::Relaxed) {
731 return Err(Stopped);
732 }
733 let end = (at + WINDOW).min(hi);
734 if let Some(p) = anchor.next_in(hay, pattern, at, end) {
735 return Ok(Some(p));
736 }
737 *read += (end - at) as u64;
738 progress(*read);
739 at = end;
740 }
741 } else {
742 let mut at = hi;
743 while at > lo {
744 if stop.load(Ordering::Relaxed) {
745 return Err(Stopped);
746 }
747 let start = at.saturating_sub(WINDOW).max(lo);
748 if let Some(p) = anchor.prev_in(hay, pattern, start, at) {
749 return Ok(Some(p));
750 }
751 *read += (at - start) as u64;
752 progress(*read);
753 at = start;
754 }
755 }
756 Ok(None)
757 };
758 let (first, second) = if forward {
759 ((from, len), (0, from))
760 } else {
761 ((0, from + 1), (from + 1, len))
762 };
763 if let Some(p) = scan(first.0, first.1, &mut read)? {
764 return Ok(HexHit {
765 at: Some(p as u64),
766 wrapped: false,
767 stride: None,
768 });
769 }
770 let at = scan(second.0, second.1, &mut read)?;
771 Ok(HexHit {
772 at: at.map(|p| p as u64),
773 wrapped: at.is_some(),
774 stride: None,
775 })
776}
777
778pub fn stride(hay: &[u8], pattern: &Pattern, hit: u64, stop: &AtomicBool) -> Option<u64> {
781 let anchor = Anchor::of(pattern);
782 let hit = hit as usize;
783 let reach = (hit + STRIDE_REACH).min(hay.len());
784 let mut at = hit;
785 let mut hits = vec![hit];
786 while hits.len() < STRIDE_MATCHES {
787 if stop.load(Ordering::Relaxed) {
788 return None;
789 }
790 match anchor.next_in(hay, pattern, at + 1, reach) {
791 Some(p) => {
792 hits.push(p);
793 at = p;
794 }
795 None => break,
796 }
797 }
798 if hits.len() < 3 {
799 return None;
800 }
801 let distance = hits[1] - hits[0];
802 hits.windows(2)
803 .all(|w| w[1] - w[0] == distance)
804 .then_some(distance as u64)
805}
806
807#[derive(Debug, Clone, PartialEq, Eq)]
810pub struct Reading {
811 pub label: &'static str,
812 pub le: String,
813 pub be: Option<String>,
814}
815
816fn unsigned(bytes: &[u8], big: bool) -> u64 {
817 crate::fixed_records::read_unsigned(bytes, big)
818}
819
820fn signed(bytes: &[u8], big: bool) -> i64 {
821 crate::fixed_records::read_signed(bytes, big)
822}
823
824pub fn varint(bytes: &[u8]) -> Option<(u64, usize)> {
826 let mut value = 0u64;
827 for (i, b) in bytes.iter().take(10).enumerate() {
828 let part = u64::from(b & 0x7f);
829 let shift = 7 * i as u32;
830 if shift >= 64 || (shift == 63 && part > 1) {
831 return None;
832 }
833 value |= part << shift;
834 if b & 0x80 == 0 {
835 return Some((value, i + 1));
836 }
837 }
838 None
839}
840
841fn plausible_time(ns: i128) -> Option<String> {
843 const LO: i128 = 315_532_800 * 1_000_000_000; const HI: i128 = 4_133_980_800 * 1_000_000_000; if !(LO..HI).contains(&ns) {
846 return None;
847 }
848 let secs = (ns / 1_000_000_000) as i64;
849 let nanos = (ns % 1_000_000_000) as u32;
850 let at = chrono::DateTime::from_timestamp(secs, nanos)?;
851 Some(if nanos == 0 {
852 at.format("%Y-%m-%d %H:%M:%S").to_string()
853 } else {
854 at.format("%Y-%m-%d %H:%M:%S%.f").to_string()
855 })
856}
857
858fn date_from_days(days: i64, epoch: chrono::NaiveDate) -> Option<String> {
859 let date = epoch.checked_add_signed(chrono::Duration::days(days))?;
861 let year = chrono::Datelike::year(&date);
862 (1900..=2100)
863 .contains(&year)
864 .then(|| date.format("%Y-%m-%d").to_string())
865}
866
867fn yyyymmdd(v: u64) -> Option<String> {
868 let (y, m, d) = (v / 10_000, (v / 100) % 100, v % 100);
869 if !(1900..=2100).contains(&y) {
870 return None;
871 }
872 chrono::NaiveDate::from_ymd_opt(y as i32, m as u32, d as u32)
873 .map(|date| date.format("%Y-%m-%d").to_string())
874}
875
876fn float_text(v: f64) -> String {
877 if v.is_nan() {
878 "NaN".to_string()
879 } else if v.is_infinite() {
880 if v > 0.0 { "inf" } else { "-inf" }.to_string()
881 } else if v != 0.0 && (v.abs() >= 1e15 || v.abs() < 1e-6) {
882 format!("{v:.6e}")
883 } else {
884 let text = format!("{v}");
885 if text.len() > 20 {
886 format!("{v:.6e}")
887 } else {
888 text
889 }
890 }
891}
892
893fn float32_text(v: f32) -> String {
895 if v.is_finite() && v != 0.0 && (v.abs() >= 1e15 || v.abs() < 1e-6) {
896 format!("{v:.6e}")
897 } else if v.is_finite() {
898 let text = format!("{v}");
899 if text.len() > 20 {
900 format!("{v:.6e}")
901 } else {
902 text
903 }
904 } else {
905 float_text(f64::from(v))
906 }
907}
908
909pub fn bits(bytes: &[u8]) -> String {
911 bytes
912 .iter()
913 .map(|b| format!("{b:08b}"))
914 .collect::<Vec<_>>()
915 .join(" ")
916}
917
918pub fn readings(bytes: &[u8]) -> Vec<Reading> {
921 let mut out = Vec::new();
922 let both = |label: &'static str, le: String, be: String| Reading {
923 label,
924 le,
925 be: Some(be),
926 };
927 let one = |label: &'static str, value: String| Reading {
928 label,
929 le: value,
930 be: None,
931 };
932 let Some(&b0) = bytes.first() else {
933 return out;
934 };
935 out.push(one("u8", b0.to_string()));
936 out.push(one("i8", (b0 as i8).to_string()));
937 out.push(one("bits", bits(&bytes[..1])));
938 for (width, u, s) in [
939 (2, "u16", "i16"),
940 (3, "u24", "i24"),
941 (4, "u32", "i32"),
942 (5, "u40", "i40"),
943 (6, "u48", "i48"),
944 (8, "u64", "i64"),
945 ] {
946 let Some(b) = bytes.get(..width) else {
947 break;
948 };
949 out.push(both(
950 u,
951 unsigned(b, false).to_string(),
952 unsigned(b, true).to_string(),
953 ));
954 out.push(both(
955 s,
956 signed(b, false).to_string(),
957 signed(b, true).to_string(),
958 ));
959 }
960 if let Some(b) = bytes.get(..2) {
961 out.push(both(
962 "f16",
963 float_text(f64::from(half::f16::from_le_bytes([b[0], b[1]]))),
964 float_text(f64::from(half::f16::from_be_bytes([b[0], b[1]]))),
965 ));
966 }
967 if let Some(b) = bytes.get(..4) {
968 let raw: [u8; 4] = b.try_into().expect("four bytes");
969 out.push(both(
970 "f32",
971 float32_text(f32::from_le_bytes(raw)),
972 float32_text(f32::from_be_bytes(raw)),
973 ));
974 }
975 if let Some(b) = bytes.get(..8) {
976 let raw: [u8; 8] = b.try_into().expect("eight bytes");
977 out.push(both(
978 "f64",
979 float_text(f64::from_le_bytes(raw)),
980 float_text(f64::from_be_bytes(raw)),
981 ));
982 }
983 if let Some((value, n)) = varint(bytes) {
984 out.push(one("varint", format!("{value} ({n} B)")));
985 let zigzag = (value >> 1) as i64 ^ -((value & 1) as i64);
986 out.push(one("zigzag", zigzag.to_string()));
987 }
988 let time = |label: &'static str, width: usize, per: i128| -> Option<Reading> {
990 let b = bytes.get(..width)?;
991 let at = |big: bool| plausible_time(i128::from(signed(b, big)) * per);
992 let (le, be) = (at(false), at(true));
993 (le.is_some() || be.is_some()).then(|| Reading {
994 label,
995 le: le.unwrap_or_default(),
996 be: Some(be.unwrap_or_default()),
997 })
998 };
999 out.extend(time("unix s", 4, 1_000_000_000));
1000 out.extend(time("unix ms", 8, 1_000_000));
1001 out.extend(time("unix us", 8, 1_000));
1002 out.extend(time("unix ns", 8, 1));
1003 if let Some(b) = bytes.get(..4) {
1004 let (le, be) = (yyyymmdd(unsigned(b, false)), yyyymmdd(unsigned(b, true)));
1005 if le.is_some() || be.is_some() {
1006 out.push(both(
1007 "yyyymmdd",
1008 le.unwrap_or_default(),
1009 be.unwrap_or_default(),
1010 ));
1011 }
1012 let epoch_1970 = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).expect("a date");
1013 let epoch_2000 = chrono::NaiveDate::from_ymd_opt(2000, 1, 1).expect("a date");
1014 for (label, epoch) in [("days 1970", epoch_1970), ("days 2000", epoch_2000)] {
1015 let le = date_from_days(signed(b, false), epoch);
1016 let be = date_from_days(signed(b, true), epoch);
1017 if le.is_some() || be.is_some() {
1018 out.push(both(label, le.unwrap_or_default(), be.unwrap_or_default()));
1019 }
1020 }
1021 }
1022 let text = text_at(bytes);
1023 if !text.is_empty() {
1024 out.push(one("text", text));
1025 }
1026 let sentinels = sentinels(bytes);
1027 if !sentinels.is_empty() {
1028 out.push(one("null?", sentinels.join(", ")));
1029 }
1030 out
1031}
1032
1033pub fn text_at(bytes: &[u8]) -> String {
1036 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
1037 let valid = match std::str::from_utf8(&bytes[..end]) {
1038 Ok(s) => s,
1039 Err(e) => std::str::from_utf8(&bytes[..e.valid_up_to()]).unwrap_or_default(),
1040 };
1041 valid
1042 .chars()
1043 .take_while(|c| !c.is_control())
1044 .take(48)
1045 .collect()
1046}
1047
1048pub fn sentinels(bytes: &[u8]) -> Vec<String> {
1051 let mut out = Vec::new();
1052 for width in [2usize, 4, 8] {
1053 let Some(b) = bytes.get(..width) else {
1054 break;
1055 };
1056 let bits = width as u32 * 8;
1057 if b.iter().all(|&x| x == 0xff) {
1058 out.push(format!("u{bits} max"));
1059 }
1060 let min = 1u64 << (bits - 1);
1061 if unsigned(b, false) == min {
1062 out.push(format!("i{bits} min LE"));
1063 }
1064 if unsigned(b, true) == min {
1065 out.push(format!("i{bits} min BE"));
1066 }
1067 }
1068 if let Some(b) = bytes.get(..4) {
1069 let raw: [u8; 4] = b.try_into().expect("four bytes");
1070 if f32::from_le_bytes(raw).is_nan() || f32::from_be_bytes(raw).is_nan() {
1071 out.push("f32 NaN".to_string());
1072 }
1073 }
1074 if let Some(b) = bytes.get(..8) {
1075 let raw: [u8; 8] = b.try_into().expect("eight bytes");
1076 if f64::from_le_bytes(raw).is_nan() || f64::from_be_bytes(raw).is_nan() {
1077 out.push("f64 NaN".to_string());
1078 }
1079 }
1080 if out.iter().any(|s| s == "u64 max") {
1082 out.retain(|s| !s.ends_with("NaN") && !s.ends_with(" max") || s == "u64 max");
1083 }
1084 out
1085}
1086
1087#[cfg(test)]
1088mod tests {
1089 use super::*;
1090
1091 fn pattern(text: &str) -> Pattern {
1092 parse_pattern(text, false).unwrap()
1093 }
1094
1095 fn never() -> AtomicBool {
1096 AtomicBool::new(false)
1097 }
1098
1099 fn view(bytes: Vec<u8>) -> HexView {
1100 HexView::new(
1101 HexSource {
1102 path: PathBuf::from("x.bin"),
1103 bytes: Arc::new(Bytes::Owned(bytes)),
1104 },
1105 Origin::Launch,
1106 false,
1107 1,
1108 )
1109 }
1110
1111 #[test]
1112 fn widths_step_from_eight_to_sixty_four() {
1113 let auto = |w| auto_per_row(w, 8, w >= ASCII_MIN_WIDTH as usize);
1114 assert_eq!(row_width(16, 8, true), 79);
1115 assert_eq!(auto(60), 8);
1116 assert_eq!(auto(80), 16);
1117 assert_eq!(auto(150), 32);
1118 assert_eq!(auto(300), 64);
1119 assert_eq!(auto(30), 6);
1121 assert_eq!(hex_x(4), 13);
1122 assert_eq!(hex_x(8), 27);
1123 }
1124
1125 #[test]
1126 fn offsets_take_at_least_eight_digits() {
1127 assert_eq!(offset_digits(0, false), 8);
1128 assert_eq!(offset_digits(1 << 40, false), 10);
1129 assert_eq!(offset_digits(1 << 32, false), 8);
1130 assert_eq!(offset_digits(1 << 32, true), 10);
1131 }
1132
1133 #[test]
1134 fn the_panel_needs_room_for_sixteen_bytes_beside_it() {
1135 let mut v = view(vec![0; 1000]);
1136 assert!(!v.lay_out(100, 10).panel);
1137 let g = v.lay_out(140, 10);
1138 assert!(g.panel);
1139 assert_eq!(g.per_row, 16);
1140 let g = v.lay_out(200, 10);
1141 assert_eq!(g.per_row, 32);
1142 v.inspector = false;
1143 let g = v.lay_out(200, 10);
1144 assert!(!g.panel);
1145 assert_eq!(g.per_row, 32);
1146 }
1147
1148 #[test]
1149 fn a_record_size_wider_than_the_screen_scrolls_to_the_cursor() {
1150 let mut v = view(vec![0; 10_000]);
1151 v.record_size = Some(100);
1152 let g = v.lay_out(80, 10);
1153 assert_eq!((g.per_row, g.first_col), (100, 0));
1154 assert!(g.shown < 100);
1155 v.row_end();
1156 let g = v.lay_out(80, 10);
1157 assert_eq!(g.first_col + g.shown, 100);
1158 assert_eq!(v.cursor, 99);
1159 }
1160
1161 #[test]
1162 fn moves_stay_in_the_file() {
1163 let mut v = view((0..100u8).collect());
1164 v.lay_out(80, 4);
1165 v.step_rows(1);
1166 assert_eq!(v.cursor, 16);
1167 v.next_group();
1168 assert_eq!(v.cursor, 20);
1169 v.previous_group();
1170 assert_eq!(v.cursor, 16);
1171 v.previous_group();
1172 assert_eq!(v.cursor, 12, "the last group of the row above");
1173 v.row_end();
1174 assert_eq!(v.cursor, 15);
1175 v.step_rows(100);
1176 assert_eq!(v.cursor, 99, "past the last row lands on the last byte");
1177 v.step_rows(-100);
1178 assert_eq!(
1179 v.cursor, 3,
1180 "past the first row lands in the cursor's column"
1181 );
1182 v.go(1_000);
1183 assert_eq!(v.cursor, 99);
1184 assert_eq!(v.lay_out(80, 4).per_row, 16);
1186 assert_eq!(v.top, 48);
1187 }
1188
1189 #[test]
1190 fn offsets_parse_every_way() {
1191 assert_eq!(parse_offset("100", 0, 1000), Ok(100));
1192 assert_eq!(parse_offset("0x1f", 0, 1000), Ok(31));
1193 assert_eq!(parse_offset("+10", 50, 1000), Ok(60));
1194 assert_eq!(parse_offset("-10", 50, 1000), Ok(40));
1195 assert_eq!(parse_offset("e-1", 0, 1000), Ok(999));
1196 assert_eq!(parse_offset("e-0x10", 0, 1000), Ok(984));
1197 assert_eq!(parse_offset("1_000", 0, 2000), Ok(1000));
1198 assert!(
1199 parse_offset("1000", 0, 1000)
1200 .unwrap_err()
1201 .contains("past the end")
1202 );
1203 assert!(
1204 parse_offset("-60", 50, 1000)
1205 .unwrap_err()
1206 .contains("before")
1207 );
1208 assert!(parse_offset("e-2000", 0, 1000).is_err());
1209 assert!(parse_offset("zz", 0, 1000).is_err());
1210 assert!(parse_offset("0", 0, 0).is_err());
1211 }
1212
1213 #[test]
1214 fn patterns_are_text_hex_or_wildcards() {
1215 assert_eq!(
1216 pattern("abc").bytes,
1217 vec![Some(b'a'), Some(b'b'), Some(b'c')]
1218 );
1219 assert_eq!(pattern("0xdead").bytes, vec![Some(0xde), Some(0xad)]);
1220 assert_eq!(
1221 pattern("de ?? ef").bytes,
1222 vec![Some(0xde), None, Some(0xef)]
1223 );
1224 assert_eq!(pattern("de").bytes, vec![Some(b'd'), Some(b'e')]);
1226 assert_eq!(pattern("\"de ad\"").bytes.len(), 5);
1227 assert_eq!(
1228 parse_pattern("hi", true).unwrap().bytes,
1229 vec![Some(b'h'), Some(0), Some(b'i'), Some(0)]
1230 );
1231 assert!(parse_pattern("?? ??", false).is_err());
1232 assert!(parse_pattern("0xabc", false).is_err());
1233 assert!(parse_pattern(" ", false).is_err());
1234 }
1235
1236 #[test]
1237 fn a_find_spans_rows_wraps_and_goes_back() {
1238 let mut hay = vec![0u8; 100];
1239 hay[14..18].copy_from_slice(b"WXYZ"); hay[70..74].copy_from_slice(b"WXYZ");
1241 let p = pattern("WXYZ");
1242 let hit = find(&hay, &p, 0, true, &never(), |_| {}).unwrap();
1243 assert_eq!((hit.at, hit.wrapped), (Some(14), false));
1244 let hit = find(&hay, &p, 15, true, &never(), |_| {}).unwrap();
1245 assert_eq!(hit.at, Some(70));
1246 let hit = find(&hay, &p, 71, true, &never(), |_| {}).unwrap();
1247 assert_eq!((hit.at, hit.wrapped), (Some(14), true));
1248 let hit = find(&hay, &p, 69, false, &never(), |_| {}).unwrap();
1249 assert_eq!(hit.at, Some(14));
1250 let hit = find(&hay, &p, 13, false, &never(), |_| {}).unwrap();
1251 assert_eq!((hit.at, hit.wrapped), (Some(70), true));
1252 let none = find(&hay, &pattern("nope"), 0, true, &never(), |_| {}).unwrap();
1253 assert_eq!(none.at, None);
1254 }
1255
1256 #[test]
1257 fn a_wildcard_matches_any_byte() {
1258 let hay = b"..\xde\x01\xef..\xde\x02\xee..\xde\x03\xef".to_vec();
1259 let p = pattern("de ?? ef");
1260 let hit = find(&hay, &p, 0, true, &never(), |_| {}).unwrap();
1261 assert_eq!(hit.at, Some(2));
1262 let hit = find(&hay, &p, 3, true, &never(), |_| {}).unwrap();
1263 assert_eq!(hit.at, Some(12));
1264 let p = pattern("?? ef");
1266 let hit = find(&hay, &p, 0, true, &never(), |_| {}).unwrap();
1267 assert_eq!(hit.at, Some(3));
1268 }
1269
1270 #[test]
1271 fn a_stopped_find_says_so() {
1272 let hay = vec![0u8; WINDOW * 3];
1273 let stop = AtomicBool::new(true);
1274 assert_eq!(
1275 find(&hay, &pattern("x"), 0, true, &stop, |_| {}).unwrap_err(),
1276 Stopped
1277 );
1278 }
1279
1280 #[test]
1281 fn evenly_spaced_matches_give_a_stride() {
1282 let mut hay = vec![0u8; 21 * 20];
1283 for i in 0..20 {
1284 hay[i * 21..i * 21 + 2].copy_from_slice(b"SY");
1285 }
1286 assert_eq!(stride(&hay, &pattern("SY"), 0, &never()), Some(21));
1287 hay[21 * 5 + 7] = b'S';
1288 hay[21 * 5 + 8] = b'Y';
1289 assert_eq!(stride(&hay, &pattern("SY"), 0, &never()), None);
1290 }
1291
1292 #[test]
1293 fn matches_on_screen_include_ones_that_start_above_it() {
1294 let mut bytes = vec![0u8; 64];
1295 bytes[14..18].copy_from_slice(b"WXYZ");
1296 bytes[40..44].copy_from_slice(b"WXYZ");
1297 let mut v = view(bytes);
1298 v.found = Some(Found {
1299 pattern: pattern("WXYZ"),
1300 hit: Some(14),
1301 stride: None,
1302 });
1303 assert_eq!(v.matches_on_screen(16, 48), vec![(14, 18), (40, 44)]);
1304 assert_eq!(v.matches_on_screen(18, 40), vec![]);
1305 }
1306
1307 #[test]
1308 fn the_inspector_reads_every_width_both_ways() {
1309 let bytes = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
1310 let r = readings(&bytes);
1311 let get = |label: &str| r.iter().find(|x| x.label == label).unwrap().clone();
1312 assert_eq!(get("u16").le, "513");
1313 assert_eq!(get("u16").be.unwrap(), "258");
1314 assert_eq!(get("u24").le, "197121");
1315 assert_eq!(get("u48").be.unwrap(), "1108152157446");
1316 assert_eq!(get("u64").le, "578437695752307201");
1317 assert_eq!(get("bits").le, "00000001");
1318 assert_eq!(get("varint").le, "1 (1 B)");
1319 let r = readings(&[0xff]);
1321 assert_eq!(r.iter().find(|x| x.label == "i8").unwrap().le, "-1");
1322 assert!(!r.iter().any(|x| x.label == "u16"));
1323 assert!(readings(&[]).is_empty());
1324 }
1325
1326 #[test]
1327 fn the_inspector_reads_times_dates_and_text() {
1328 let r = readings(&1_704_153_600u32.to_le_bytes());
1330 let get = |label: &str| r.iter().find(|x| x.label == label).cloned();
1331 assert_eq!(get("unix s").unwrap().le, "2024-01-02 00:00:00");
1332 let r = readings(&20240102u32.to_le_bytes());
1333 let get = |label: &str| r.iter().find(|x| x.label == label).cloned();
1334 assert_eq!(get("yyyymmdd").unwrap().le, "2024-01-02");
1335 let r = readings(&19_724i32.to_le_bytes());
1336 let get = |label: &str| r.iter().find(|x| x.label == label).cloned();
1337 assert_eq!(get("days 1970").unwrap().le, "2024-01-02");
1338 let r = readings(b"PAR1\0xyz");
1339 assert_eq!(r.iter().find(|x| x.label == "text").unwrap().le, "PAR1");
1340 assert_eq!(varint(&[0xac, 0x02]), Some((300, 2)));
1341 let r = readings(&[0xac, 0x02]);
1342 assert_eq!(r.iter().find(|x| x.label == "zigzag").unwrap().le, "150");
1343 assert_eq!(f64::from(half::f16::from_le_bytes([0x00, 0x3c])), 1.0);
1344 }
1345
1346 #[test]
1347 fn sentinels_are_flagged() {
1348 assert!(sentinels(&[0, 0, 0, 0x80]).contains(&"i32 min LE".to_string()));
1349 assert_eq!(sentinels(&[0xff; 8]), vec!["u64 max".to_string()]);
1350 let nan = f64::NAN.to_le_bytes();
1351 assert!(sentinels(&nan).contains(&"f64 NaN".to_string()));
1352 assert!(sentinels(&[1, 2, 3, 4]).is_empty());
1353 }
1354
1355 #[test]
1356 fn bytes_have_classes() {
1357 assert_eq!(class(0), ByteClass::Null);
1358 assert_eq!(class(b'A'), ByteClass::Printable);
1359 assert_eq!(class(b' '), ByteClass::Whitespace);
1360 assert_eq!(class(0x01), ByteClass::Control);
1361 assert_eq!(class(0x7f), ByteClass::Control);
1362 assert_eq!(class(0x80), ByteClass::High);
1363 assert_eq!(class(0xff), ByteClass::Ff);
1364 }
1365}