Skip to main content

datui_lib/app/
hex_view.rs

1//! The hex view: any local file as its bytes, a row of them at a time.
2//!
3//! The file is memory-mapped by a worker ([`crate::formats::fixed_records::Bytes::map`]) and
4//! never read whole: drawing slices the map for the rows on screen, and a find reads it
5//! on a worker, a window at a time, so a stop is seen between windows. How many bytes a
6//! row holds is decided at draw time from the width (8, 16, 32 or 64), unless a record
7//! size fixes it, so that records line up.
8//!
9//! Everything here is pure: the layout, the cursor's moves, the parsers for an offset
10//! and a pattern, the search, and the byte inspector's readings. The App drives it.
11
12use crate::formats::fixed_records::Bytes;
13use std::path::PathBuf;
14use std::sync::Arc;
15use std::sync::atomic::{AtomicBool, Ordering};
16
17/// The widths a row takes on its own, smallest first.
18pub const WIDTHS: [usize; 4] = [8, 16, 32, 64];
19
20/// The longest record size a row may be fixed to.
21pub const MAX_RECORD_SIZE: usize = 4096;
22
23/// The longest pattern a find takes, in bytes.
24pub const MAX_PATTERN: usize = 4096;
25
26/// Matches on screen are marked for patterns up to this long; a longer one marks only
27/// the match the cursor is on.
28const MAX_MARKED_PATTERN: usize = 256;
29
30/// Bytes from the cursor the inspector reads: enough for its text.
31pub const INSPECTED: usize = 64;
32
33/// Columns the inspector panel takes beside the bytes, not counting its rule.
34pub const PANEL_WIDTH: u16 = 46;
35
36/// Below this many columns the ASCII gutter is left out.
37pub const ASCII_MIN_WIDTH: u16 = 50;
38
39/// Start positions a find reads between looks at its stop flag.
40const WINDOW: usize = 8 << 20;
41
42/// How far past a match a find looks for the next ones, to guess a record size.
43const STRIDE_REACH: usize = 4 << 20;
44
45/// Matches a stride guess is made from.
46const STRIDE_MATCHES: usize = 8;
47
48/// Columns the hex bytes of a row of `n` take: a space between bytes, one more between
49/// groups of four, and another between groups of eight.
50pub fn hex_width(n: usize) -> usize {
51    if n == 0 { 0 } else { hex_x(n - 1) + 2 }
52}
53
54/// The column, from the start of the hex bytes, where byte `i` of a row starts.
55pub fn hex_x(i: usize) -> usize {
56    i * 3 + i / 4 + i / 8
57}
58
59/// Digits an offset into a file of `len` bytes takes: at least eight.
60pub 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
70/// Columns a row of `n` bytes takes: the offset, the hex bytes, and the ASCII gutter.
71pub fn row_width(n: usize, digits: usize, ascii: bool) -> usize {
72    digits + 2 + hex_width(n) + if ascii { 2 + n } else { 0 }
73}
74
75/// The most bytes a row of `width` columns shows, at least one.
76pub 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
84/// Bytes a row holds when nothing fixes it: the widest of [`WIDTHS`] that fits, or as
85/// many as fit when not even eight do.
86pub 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/// How the view is laid out at one width.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct Geometry {
98    /// Bytes a row holds.
99    pub per_row: usize,
100    /// Of those, the bytes that fit on screen: fewer than `per_row` only when a record
101    /// size is wider than the screen.
102    pub shown: usize,
103    /// The row's first byte on screen, when not all of it fits.
104    pub first_col: usize,
105    pub ascii: bool,
106    /// Whether the inspector sits beside the bytes.
107    pub panel: bool,
108    /// Whether there is room for it there.
109    pub room_for_panel: bool,
110    pub digits: usize,
111    /// Rows of bytes on screen.
112    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/// What a byte is, which picks its color.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum ByteClass {
133    Null,
134    Printable,
135    Whitespace,
136    Control,
137    /// 0x80 and above, but not 0xFF.
138    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/// Where a hex view was opened from, which decides where Esc and `q` go.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum Origin {
156    /// The home screen: Esc and `q` go back there.
157    Home,
158    /// The table: Esc goes back to it.
159    Table,
160    /// The Info panel over a table: Esc goes back to the panel, as it was.
161    Info,
162    /// The command line: `q` quits.
163    Launch,
164}
165
166/// The prompt open at the foot of the view.
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum PromptKind {
169    GoTo,
170    Find,
171    RecordSize,
172}
173
174/// A find's pattern: each byte, or `None` for `??`.
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct Pattern {
177    pub bytes: Vec<Option<u8>>,
178    /// What was typed, for the status line.
179    pub label: String,
180}
181
182/// The last find: its pattern, where it landed, and what it says about a record size.
183#[derive(Debug, Clone)]
184pub struct Found {
185    pub pattern: Pattern,
186    pub hit: Option<u64>,
187    /// The distance between matches when it is the same for every one read.
188    pub stride: Option<u64>,
189}
190
191/// A find under way on a worker.
192#[derive(Debug, Clone)]
193pub struct HexFindRun {
194    pub stop: Arc<AtomicBool>,
195    /// Which hex view it reads: the view's `serial`.
196    pub view: u64,
197    pub pattern: Pattern,
198}
199
200/// A find's answer.
201#[derive(Debug, Clone)]
202pub struct HexHit {
203    pub at: Option<u64>,
204    pub wrapped: bool,
205    pub stride: Option<u64>,
206}
207
208/// A local file, mapped.
209pub struct HexSource {
210    pub path: PathBuf,
211    pub bytes: Arc<Bytes>,
212    /// A download `path` is: the URL it was fetched from, and the hold that keeps the
213    /// file while it is shown.
214    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    /// Map `path`. A worker's job: the map is a system call against a file that may be
228    /// on a slow disk or a network share.
229    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
245/// The hex view's state.
246pub struct HexView {
247    /// The file shown, or for a download the URL it was fetched from.
248    pub path: PathBuf,
249    pub bytes: Arc<Bytes>,
250    pub origin: Origin,
251    /// Opened because no reader and no spec took the file.
252    pub fallback: bool,
253    /// Bumped per view, so a find's answer for another file is dropped.
254    pub serial: u64,
255    pub cursor: u64,
256    /// The offset of the first row on screen.
257    pub top: u64,
258    /// Bytes a row holds, when fixed (`--hex-width`, `r`, `R`).
259    pub record_size: Option<usize>,
260    pub decimal: bool,
261    /// The other end of a marked range, the cursor being one end.
262    pub mark: Option<u64>,
263    /// Whether the inspector is shown: beside the bytes when there is room, over them
264    /// when there is not.
265    pub inspector: bool,
266    pub found: Option<Found>,
267    /// The open prompt, and what it says went wrong.
268    pub prompt: Option<PromptKind>,
269    pub prompt_error: Option<String>,
270    /// A find's text as UTF-16 (little-endian) rather than UTF-8.
271    pub utf16: bool,
272    /// Where there is no room for the inspector beside the bytes, it opens over them.
273    pub inspector_open: bool,
274    /// The prompt's text field.
275    pub input: crate::widgets::text_input::TextInput,
276    /// The spec picker (`b`), when open.
277    pub picker: Option<crate::widgets::ui::PickerState>,
278    /// The layout of the last frame drawn, which the moves page by.
279    pub geometry: Geometry,
280    /// The download shown, held until the view goes: after `bytes`, which map it.
281    _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    /// The file's name, for the header.
323    pub fn name(&self) -> String {
324        // Piped bytes are named `stdin`, not `-`.
325        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    /// Whether the inspector fits beside the bytes at `width` columns.
333    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    /// The layout at `width` columns and `rows` rows of bytes, with the cursor kept on
339    /// screen. Kept for the moves that page.
340    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        // The top row, on a row boundary for this width, and the cursor on screen.
357        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    /// Put the cursor at `at`, inside the file.
395    pub fn go(&mut self, at: u64) {
396        self.cursor = at.min(self.last());
397    }
398
399    /// Move the cursor `delta` bytes.
400    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    /// Move the cursor `rows` rows, staying in its column while there is a row there,
410    /// and onto the last byte from a row above the last.
411    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    /// The start of the next group of four in the row, or of the next row.
430    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    /// The start of this group of four, or of the one before it.
446    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            // The last group of the row above.
455            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    /// The marked range, first and last byte, when a mark is set.
475    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    /// The bytes from `at`, at most `n`.
481    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    /// The matches of the last find that touch `[lo, hi)`, each as its first byte and
488    /// the byte past it.
489    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
519/// Parse a number: decimal, or hexadecimal after `0x`, with `_` allowed between digits.
520fn 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
529/// Where a typed offset points: decimal, `0x` hex, `+N` and `-N` from the cursor, and
530/// `e-N` from the end (`e-1` is the last byte). Past either end is an error.
531pub 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
561/// Whether `token` is one byte in hex, or `??`.
562fn 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
572/// A find's pattern from what was typed:
573///
574/// - `0x` and hex digits, or two or more space-separated hex pairs: bytes, where `??`
575///   matches any byte (`de ad ?? ef`);
576/// - anything else, or text in double quotes: the text's bytes, UTF-8, or UTF-16
577///   little-endian when `utf16` is set.
578pub 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
631/// The longest run of known bytes in a pattern: what is searched for, the rest checked
632/// around each place it is found.
633struct 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    /// The first match of `pattern` in `hay` starting in `[lo, hi)`.
666    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    /// The last match of `pattern` in `hay` starting in `[lo, hi)`.
691    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/// The find was stopped.
725#[derive(Debug, Clone, Copy, PartialEq, Eq)]
726pub struct Stopped;
727
728/// Find `pattern` in `hay`: forward from `from` (a match there counts), round to the
729/// start; or backward from `from` (inclusive), round to the end. Reads a window at a
730/// time, looking at `stop` between, and tells `progress` how many bytes it has read.
731pub 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
795/// The distance between the matches after `hit`, when the next few are all the same
796/// distance apart: a sync word or a magic that starts every record.
797pub 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/// One line of the inspector: what the bytes read as, little-endian and, where the
825/// order matters, big-endian.
826#[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
841/// A LEB128 varint at the front of `bytes`: its value and the bytes it took.
842pub 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
858/// Seconds since 1970 in a year from 1980 to 2100, as text.
859fn plausible_time(ns: i128) -> Option<String> {
860    const LO: i128 = 315_532_800 * 1_000_000_000; // 1980-01-01
861    const HI: i128 = 4_133_980_800 * 1_000_000_000; // 2101-01-01
862    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    // 1900 to 2100: past that a count of days is more likely something else.
877    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
910/// A 32-bit float as its own shortest text, not a 64-bit one's.
911fn 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
926/// Bits of `bytes`, most significant first, a space between bytes.
927pub fn bits(bytes: &[u8]) -> String {
928    bytes
929        .iter()
930        .map(|b| format!("{b:08b}"))
931        .collect::<Vec<_>>()
932        .join(" ")
933}
934
935/// Everything the bytes at the cursor read as. `bytes` is the cursor's byte and what
936/// follows it, as far as the file goes (sixteen is enough).
937pub 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    // Times, little-endian then big-endian, only where they land in a plausible year.
1006    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
1050/// The text at the front of `bytes`, up to the first NUL: printable ASCII and UTF-8,
1051/// anything else ending it.
1052pub 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
1065/// The null sentinels the bytes at the cursor hold: the smallest signed integer, the
1066/// largest unsigned one, a NaN.
1067pub 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    // An all-ones word is every width's max and a NaN; say it once.
1098    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        // Too narrow even for eight: as many as fit, without the gutter.
1138        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        // The screen follows the cursor.
1203        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        // One pair alone is text; quotes make hex-looking text text.
1243        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"); // across the row boundary at 16
1258        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        // A wildcard at the front.
1283        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        // One byte: no wider readings.
1338        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        // 2024-01-02 00:00:00 UTC.
1347        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}