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