Skip to main content

copybook_rdw_predicates/
lib.rs

1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! RDW header ASCII-digit detection helper.
4//!
5//! This crate owns exactly one predicate: whether RDW length bytes look like ASCII
6//! digits and therefore likely represent ASCII-transfer corruption.
7
8/// RDW header size in bytes.
9pub const RDW_HEADER_LEN: usize = 4;
10
11/// Returns `true` if the first two RDW header bytes are ASCII digits, indicating likely ASCII-transfer corruption.
12#[inline]
13#[must_use]
14pub const fn rdw_is_suspect_ascii_corruption(rdw_header: [u8; RDW_HEADER_LEN]) -> bool {
15    let b0 = rdw_header[0];
16    let b1 = rdw_header[1];
17
18    is_ascii_digit(b0) && is_ascii_digit(b1)
19}
20
21/// Slice-based variant of [`rdw_is_suspect_ascii_corruption`] that checks at least 4 bytes.
22#[inline]
23#[must_use]
24pub fn rdw_is_suspect_ascii_corruption_slice(rdw_bytes: &[u8]) -> bool {
25    rdw_bytes.len() >= RDW_HEADER_LEN
26        && rdw_is_suspect_ascii_corruption([rdw_bytes[0], rdw_bytes[1], rdw_bytes[2], rdw_bytes[3]])
27}
28
29#[inline]
30#[must_use]
31const fn is_ascii_digit(byte: u8) -> bool {
32    byte >= b'0' && byte <= b'9'
33}
34
35#[cfg(test)]
36#[allow(clippy::expect_used, clippy::unwrap_used)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn ascii_digit_bytes_are_suspect() {
42        assert!(rdw_is_suspect_ascii_corruption([b'1', b'2', 0x00, 0x00]));
43        assert!(rdw_is_suspect_ascii_corruption([b'0', b'9', 0xFF, 0xEE]));
44    }
45
46    #[test]
47    fn non_ascii_digit_length_bytes_are_not_suspect() {
48        assert!(!rdw_is_suspect_ascii_corruption([b'1', b'G', 0x00, 0x00]));
49        assert!(!rdw_is_suspect_ascii_corruption([0x00, 0x01, 0x00, 0x00]));
50        assert!(!rdw_is_suspect_ascii_corruption([0x31, 0x00, 0x30, 0x30]));
51    }
52
53    #[test]
54    fn short_headers_are_not_suspect() {
55        assert!(!rdw_is_suspect_ascii_corruption_slice(b"12"));
56    }
57
58    #[test]
59    fn slice_uses_same_rule_as_array() {
60        let header = [b'7', b'8', 0x10, 0x20];
61        assert!(rdw_is_suspect_ascii_corruption_slice(&header));
62    }
63
64    // ── Additional tests ─────────────────────────────────────────────
65
66    #[test]
67    fn rdw_header_len_is_four() {
68        assert_eq!(RDW_HEADER_LEN, 4);
69    }
70
71    #[test]
72    fn all_ascii_digit_pairs_suspect() {
73        for b0 in b'0'..=b'9' {
74            for b1 in b'0'..=b'9' {
75                assert!(
76                    rdw_is_suspect_ascii_corruption([b0, b1, 0x00, 0x00]),
77                    "expected suspect for ({b0}, {b1})"
78                );
79            }
80        }
81    }
82
83    #[test]
84    fn first_byte_non_digit_not_suspect() {
85        // First byte is 0x2F (just below '0')
86        assert!(!rdw_is_suspect_ascii_corruption([0x2F, b'5', 0x00, 0x00]));
87        // First byte is 0x3A (just above '9')
88        assert!(!rdw_is_suspect_ascii_corruption([0x3A, b'5', 0x00, 0x00]));
89    }
90
91    #[test]
92    fn second_byte_non_digit_not_suspect() {
93        assert!(!rdw_is_suspect_ascii_corruption([b'5', 0x2F, 0x00, 0x00]));
94        assert!(!rdw_is_suspect_ascii_corruption([b'5', 0x3A, 0x00, 0x00]));
95    }
96
97    #[test]
98    fn reserved_bytes_do_not_affect_detection() {
99        // Detection only looks at first two bytes
100        assert!(rdw_is_suspect_ascii_corruption([b'0', b'0', 0xFF, 0xFF]));
101        assert!(rdw_is_suspect_ascii_corruption([b'9', b'9', b'A', b'B']));
102    }
103
104    #[test]
105    fn all_zeros_not_suspect() {
106        assert!(!rdw_is_suspect_ascii_corruption([0x00, 0x00, 0x00, 0x00]));
107    }
108
109    #[test]
110    fn all_0xff_not_suspect() {
111        assert!(!rdw_is_suspect_ascii_corruption([0xFF, 0xFF, 0xFF, 0xFF]));
112    }
113
114    #[test]
115    fn slice_empty_not_suspect() {
116        assert!(!rdw_is_suspect_ascii_corruption_slice(&[]));
117    }
118
119    #[test]
120    fn slice_exactly_4_bytes() {
121        assert!(rdw_is_suspect_ascii_corruption_slice(&[
122            b'3', b'4', 0x00, 0x00
123        ]));
124        assert!(!rdw_is_suspect_ascii_corruption_slice(&[
125            0x00, 0x50, 0x00, 0x00
126        ]));
127    }
128
129    #[test]
130    fn slice_longer_than_4_uses_first_4() {
131        let data = [b'1', b'2', 0x00, 0x00, 0xFF, 0xFF, 0xFF];
132        assert!(rdw_is_suspect_ascii_corruption_slice(&data));
133    }
134
135    #[test]
136    fn slice_3_bytes_not_suspect() {
137        assert!(!rdw_is_suspect_ascii_corruption_slice(&[b'1', b'2', 0x00]));
138    }
139}