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}
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    /// Map `path`. A worker's job: the map is a system call against a file that may be
225    /// on a slow disk or a network share.
226    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
238/// The hex view's state.
239pub struct HexView {
240    pub path: PathBuf,
241    pub bytes: Arc<Bytes>,
242    pub origin: Origin,
243    /// Opened because no reader and no spec took the file.
244    pub fallback: bool,
245    /// Bumped per view, so a find's answer for another file is dropped.
246    pub serial: u64,
247    pub cursor: u64,
248    /// The offset of the first row on screen.
249    pub top: u64,
250    /// Bytes a row holds, when fixed (`--hex-width`, `r`, `R`).
251    pub record_size: Option<usize>,
252    pub decimal: bool,
253    /// The other end of a marked range, the cursor being one end.
254    pub mark: Option<u64>,
255    /// Whether the inspector is shown: beside the bytes when there is room, over them
256    /// when there is not.
257    pub inspector: bool,
258    pub found: Option<Found>,
259    /// The open prompt, and what it says went wrong.
260    pub prompt: Option<PromptKind>,
261    pub prompt_error: Option<String>,
262    /// A find's text as UTF-16 (little-endian) rather than UTF-8.
263    pub utf16: bool,
264    /// Where there is no room for the inspector beside the bytes, it opens over them.
265    pub inspector_open: bool,
266    /// The prompt's text field.
267    pub input: crate::widgets::text_input::TextInput,
268    /// The spec picker (`b`), when open.
269    pub picker: Option<crate::widgets::ui::PickerState>,
270    /// The layout of the last frame drawn, which the moves page by.
271    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    /// The file's name, for the header.
308    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    /// Whether the inspector fits beside the bytes at `width` columns.
316    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    /// The layout at `width` columns and `rows` rows of bytes, with the cursor kept on
322    /// screen. Kept for the moves that page.
323    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        // The top row, on a row boundary for this width, and the cursor on screen.
340        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    /// Put the cursor at `at`, inside the file.
378    pub fn go(&mut self, at: u64) {
379        self.cursor = at.min(self.last());
380    }
381
382    /// Move the cursor `delta` bytes.
383    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    /// Move the cursor `rows` rows, staying in its column while there is a row there,
393    /// and onto the last byte from a row above the last.
394    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    /// The start of the next group of four in the row, or of the next row.
413    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    /// The start of this group of four, or of the one before it.
429    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            // The last group of the row above.
438            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    /// The marked range, first and last byte, when a mark is set.
458    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    /// The bytes from `at`, at most `n`.
464    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    /// The matches of the last find that touch `[lo, hi)`, each as its first byte and
471    /// the byte past it.
472    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
502/// Parse a number: decimal, or hexadecimal after `0x`, with `_` allowed between digits.
503fn 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
512/// Where a typed offset points: decimal, `0x` hex, `+N` and `-N` from the cursor, and
513/// `e-N` from the end (`e-1` is the last byte). Past either end is an error.
514pub 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
544/// Whether `token` is one byte in hex, or `??`.
545fn 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
555/// A find's pattern from what was typed:
556///
557/// - `0x` and hex digits, or two or more space-separated hex pairs: bytes, where `??`
558///   matches any byte (`de ad ?? ef`);
559/// - anything else, or text in double quotes: the text's bytes, UTF-8, or UTF-16
560///   little-endian when `utf16` is set.
561pub 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
614/// The longest run of known bytes in a pattern: what is searched for, the rest checked
615/// around each place it is found.
616struct 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    /// The first match of `pattern` in `hay` starting in `[lo, hi)`.
649    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    /// The last match of `pattern` in `hay` starting in `[lo, hi)`.
674    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/// The find was stopped.
708#[derive(Debug, Clone, Copy, PartialEq, Eq)]
709pub struct Stopped;
710
711/// Find `pattern` in `hay`: forward from `from` (a match there counts), round to the
712/// start; or backward from `from` (inclusive), round to the end. Reads a window at a
713/// time, looking at `stop` between, and tells `progress` how many bytes it has read.
714pub 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
778/// The distance between the matches after `hit`, when the next few are all the same
779/// distance apart: a sync word or a magic that starts every record.
780pub 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/// One line of the inspector: what the bytes read as, little-endian and, where the
808/// order matters, big-endian.
809#[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::formats::fixed_records::read_unsigned(bytes, big)
818}
819
820fn signed(bytes: &[u8], big: bool) -> i64 {
821    crate::formats::fixed_records::read_signed(bytes, big)
822}
823
824/// A LEB128 varint at the front of `bytes`: its value and the bytes it took.
825pub 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
841/// Seconds since 1970 in a year from 1980 to 2100, as text.
842fn plausible_time(ns: i128) -> Option<String> {
843    const LO: i128 = 315_532_800 * 1_000_000_000; // 1980-01-01
844    const HI: i128 = 4_133_980_800 * 1_000_000_000; // 2101-01-01
845    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    // 1900 to 2100: past that a count of days is more likely something else.
860    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
893/// A 32-bit float as its own shortest text, not a 64-bit one's.
894fn 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
909/// Bits of `bytes`, most significant first, a space between bytes.
910pub fn bits(bytes: &[u8]) -> String {
911    bytes
912        .iter()
913        .map(|b| format!("{b:08b}"))
914        .collect::<Vec<_>>()
915        .join(" ")
916}
917
918/// Everything the bytes at the cursor read as. `bytes` is the cursor's byte and what
919/// follows it, as far as the file goes (sixteen is enough).
920pub 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    // Times, little-endian then big-endian, only where they land in a plausible year.
989    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
1033/// The text at the front of `bytes`, up to the first NUL: printable ASCII and UTF-8,
1034/// anything else ending it.
1035pub 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
1048/// The null sentinels the bytes at the cursor hold: the smallest signed integer, the
1049/// largest unsigned one, a NaN.
1050pub 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    // An all-ones word is every width's max and a NaN; say it once.
1081    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        // Too narrow even for eight: as many as fit, without the gutter.
1120        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        // The screen follows the cursor.
1185        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        // One pair alone is text; quotes make hex-looking text text.
1225        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"); // across the row boundary at 16
1240        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        // A wildcard at the front.
1265        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        // One byte: no wider readings.
1320        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        // 2024-01-02 00:00:00 UTC.
1329        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}