Skip to main content

copybook_corruption_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//! Pure predicate helpers for transfer-corruption heuristics used by higher-level
4//! codec error reporting.
5
6/// Return `true` for ASCII-printable bytes.
7#[inline]
8#[must_use = "This predicate should be used intentionally in byte-level checks"]
9pub const fn is_ascii_printable(byte: u8) -> bool {
10    byte >= 0x20 && byte <= 0x7E
11}
12
13/// Predicate for control/control-like bytes commonly seen in EBCDIC text.
14///
15/// This intentionally flags:
16/// - C0 controls (`0x00..=0x1F`)
17/// - C1 controls and related (`0x7F..=0x9F`)
18#[inline]
19#[must_use = "This predicate should be used intentionally in byte-level checks"]
20pub const fn is_likely_corrupted_ebcdic_byte(byte: u8) -> bool {
21    matches!(byte, 0x00..=0x1F | 0x7F..=0x9F)
22}
23
24/// Detect whether a packed-decimal high nibble can never be part of COMP-3 payload.
25#[inline]
26#[must_use = "This predicate should be used intentionally in packed-decimal checks"]
27pub const fn is_invalid_comp3_high_nibble(byte: u8) -> bool {
28    matches!((byte >> 4) & 0x0F, 0xA | 0xB | 0xE)
29}
30
31/// Detect whether a packed-decimal non-terminal low nibble can never be part of
32/// COMP-3 payload.
33#[inline]
34#[must_use = "This predicate should be used intentionally in packed-decimal checks"]
35pub const fn is_invalid_comp3_low_nibble(byte: u8) -> bool {
36    matches!(byte & 0x0F, 0xA | 0xB | 0xE)
37}
38
39/// Detect whether a packed-decimal sign nibble (last low nibble) is invalid.
40#[inline]
41#[must_use = "This predicate should be used intentionally in packed-decimal checks"]
42pub const fn is_invalid_comp3_sign_nibble(byte: u8) -> bool {
43    matches!(byte & 0x0F, 0x0..=0x9)
44}
45
46#[cfg(test)]
47#[allow(clippy::expect_used, clippy::unwrap_used)]
48mod tests {
49    use super::*;
50
51    // ── is_ascii_printable ───────────────────────────────────────────
52
53    #[test]
54    fn ascii_printable_examples() {
55        assert!(is_ascii_printable(b'A'));
56        assert!(is_ascii_printable(b' '));
57        assert!(!is_ascii_printable(0x1F));
58    }
59
60    #[test]
61    fn ascii_printable_lower_boundary() {
62        assert!(!is_ascii_printable(0x1F)); // just below range
63        assert!(is_ascii_printable(0x20)); // space, first printable
64    }
65
66    #[test]
67    fn ascii_printable_upper_boundary() {
68        assert!(is_ascii_printable(0x7E)); // tilde, last printable
69        assert!(!is_ascii_printable(0x7F)); // DEL, not printable
70    }
71
72    #[test]
73    fn ascii_printable_all_digits() {
74        for b in b'0'..=b'9' {
75            assert!(is_ascii_printable(b));
76        }
77    }
78
79    #[test]
80    fn ascii_printable_all_uppercase() {
81        for b in b'A'..=b'Z' {
82            assert!(is_ascii_printable(b));
83        }
84    }
85
86    #[test]
87    fn ascii_printable_null_and_max() {
88        assert!(!is_ascii_printable(0x00));
89        assert!(!is_ascii_printable(0xFF));
90    }
91
92    // ── is_likely_corrupted_ebcdic_byte ──────────────────────────────
93
94    #[test]
95    fn likely_corrupted_ebcdic_examples() {
96        assert!(is_likely_corrupted_ebcdic_byte(0x00));
97        assert!(is_likely_corrupted_ebcdic_byte(0x7F));
98        assert!(!is_likely_corrupted_ebcdic_byte(b'A'));
99        assert!(!is_likely_corrupted_ebcdic_byte(0xFF));
100    }
101
102    #[test]
103    fn ebcdic_c0_range_all_corrupted() {
104        for b in 0x00..=0x1F {
105            assert!(
106                is_likely_corrupted_ebcdic_byte(b),
107                "byte 0x{b:02X} should be corrupted"
108            );
109        }
110    }
111
112    #[test]
113    fn ebcdic_c1_range_all_corrupted() {
114        for b in 0x7F..=0x9F {
115            assert!(
116                is_likely_corrupted_ebcdic_byte(b),
117                "byte 0x{b:02X} should be corrupted"
118            );
119        }
120    }
121
122    #[test]
123    fn ebcdic_boundary_0x20_not_corrupted() {
124        assert!(!is_likely_corrupted_ebcdic_byte(0x20));
125    }
126
127    #[test]
128    fn ebcdic_boundary_0xa0_not_corrupted() {
129        assert!(!is_likely_corrupted_ebcdic_byte(0xA0));
130    }
131
132    #[test]
133    fn ebcdic_high_bytes_not_corrupted() {
134        // 0xC1 is EBCDIC 'A', 0xF0 is EBCDIC '0'
135        assert!(!is_likely_corrupted_ebcdic_byte(0xC1));
136        assert!(!is_likely_corrupted_ebcdic_byte(0xF0));
137    }
138
139    // ── is_invalid_comp3_high_nibble ─────────────────────────────────
140
141    #[test]
142    fn comp3_invalid_high_nibble_detection() {
143        assert!(is_invalid_comp3_high_nibble(0xA2));
144        assert!(is_invalid_comp3_high_nibble(0xBE));
145        assert!(!is_invalid_comp3_high_nibble(0x12));
146    }
147
148    #[test]
149    fn comp3_high_nibble_valid_digits_0_to_9() {
150        for high in 0..=9u8 {
151            assert!(
152                !is_invalid_comp3_high_nibble(high << 4),
153                "high nibble {high} should be valid"
154            );
155        }
156    }
157
158    #[test]
159    fn comp3_high_nibble_a_b_e_invalid() {
160        assert!(is_invalid_comp3_high_nibble(0xA0));
161        assert!(is_invalid_comp3_high_nibble(0xB0));
162        assert!(is_invalid_comp3_high_nibble(0xE0));
163    }
164
165    #[test]
166    fn comp3_high_nibble_c_d_f_valid() {
167        // C, D, F are valid as sign nibbles but also valid high nibbles
168        assert!(!is_invalid_comp3_high_nibble(0xC0));
169        assert!(!is_invalid_comp3_high_nibble(0xD0));
170        assert!(!is_invalid_comp3_high_nibble(0xF0));
171    }
172
173    // ── is_invalid_comp3_low_nibble ──────────────────────────────────
174
175    #[test]
176    fn comp3_invalid_low_nibble_detection() {
177        assert!(is_invalid_comp3_low_nibble(0x0A));
178        assert!(is_invalid_comp3_low_nibble(0x5B));
179        assert!(!is_invalid_comp3_low_nibble(0x56));
180    }
181
182    #[test]
183    fn comp3_low_nibble_valid_digits_0_to_9() {
184        for low in 0..=9u8 {
185            assert!(
186                !is_invalid_comp3_low_nibble(low),
187                "low nibble {low} should be valid"
188            );
189        }
190    }
191
192    #[test]
193    fn comp3_low_nibble_a_b_e_invalid() {
194        assert!(is_invalid_comp3_low_nibble(0x0A));
195        assert!(is_invalid_comp3_low_nibble(0x0B));
196        assert!(is_invalid_comp3_low_nibble(0x0E));
197    }
198
199    #[test]
200    fn comp3_low_nibble_c_d_f_valid() {
201        // C, D, F are valid sign nibbles; not flagged by low nibble check
202        assert!(!is_invalid_comp3_low_nibble(0x0C));
203        assert!(!is_invalid_comp3_low_nibble(0x0D));
204        assert!(!is_invalid_comp3_low_nibble(0x0F));
205    }
206
207    // ── is_invalid_comp3_sign_nibble ─────────────────────────────────
208
209    #[test]
210    fn comp3_invalid_sign_nibble_detection() {
211        assert!(is_invalid_comp3_sign_nibble(0x12));
212        assert!(!is_invalid_comp3_sign_nibble(0x1C));
213        assert!(!is_invalid_comp3_sign_nibble(0x1D));
214    }
215
216    #[test]
217    fn comp3_sign_nibble_digits_0_to_9_all_invalid() {
218        for low in 0..=9u8 {
219            assert!(
220                is_invalid_comp3_sign_nibble(low),
221                "sign nibble {low} should be invalid"
222            );
223        }
224    }
225
226    #[test]
227    fn comp3_sign_nibble_c_d_f_all_valid() {
228        assert!(!is_invalid_comp3_sign_nibble(0x0C)); // C = positive
229        assert!(!is_invalid_comp3_sign_nibble(0x0D)); // D = negative
230        assert!(!is_invalid_comp3_sign_nibble(0x0F)); // F = unsigned
231    }
232
233    #[test]
234    fn comp3_sign_nibble_a_b_e_also_valid() {
235        // A, B, E are not in 0..=9 so they pass the sign check
236        assert!(!is_invalid_comp3_sign_nibble(0x0A));
237        assert!(!is_invalid_comp3_sign_nibble(0x0B));
238        assert!(!is_invalid_comp3_sign_nibble(0x0E));
239    }
240}