#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
use copybook_error::{Error, ErrorCode};
#[inline]
#[must_use]
pub const fn rdw_is_suspect_ascii_corruption(rdw_header: [u8; crate::RDW_HEADER_LEN]) -> bool {
is_ascii_digit(rdw_header[0]) && is_ascii_digit(rdw_header[1])
}
#[inline]
#[must_use]
pub fn rdw_is_suspect_ascii_corruption_slice(rdw_bytes: &[u8]) -> bool {
rdw_bytes.len() >= crate::RDW_HEADER_LEN
&& rdw_is_suspect_ascii_corruption([rdw_bytes[0], rdw_bytes[1], rdw_bytes[2], rdw_bytes[3]])
}
#[inline]
#[must_use = "Handle the returned error when corruption is detected"]
pub fn detect_rdw_ascii_corruption(rdw_bytes: &[u8]) -> Option<Error> {
if rdw_bytes.len() < crate::RDW_HEADER_LEN {
return None;
}
let length = u16::from_be_bytes([rdw_bytes[0], rdw_bytes[1]]);
if rdw_is_suspect_ascii_corruption_slice(rdw_bytes) {
return Some(ascii_corruption_error(format!(
"RDW length field appears to contain ASCII digits: 0x{:02X}{:02X} ('{}{}')",
rdw_bytes[0],
rdw_bytes[1],
ascii_char_or_dot(rdw_bytes[0]),
ascii_char_or_dot(rdw_bytes[1])
)));
}
if (0x3030..=0x3939).contains(&length) {
return Some(ascii_corruption_error(format!(
"RDW length field suspiciously large ({length}), may be ASCII-corrupted: 0x{length:04X}"
)));
}
if is_ascii_printable(rdw_bytes[2])
&& is_ascii_printable(rdw_bytes[3])
&& rdw_bytes[2..4] != [0x00, 0x00]
{
return Some(ascii_corruption_error(format!(
"RDW reserved bytes contain ASCII-like data: 0x{:02X}{:02X} ('{}{}')",
rdw_bytes[2],
rdw_bytes[3],
ascii_char_or_dot(rdw_bytes[2]),
ascii_char_or_dot(rdw_bytes[3])
)));
}
None
}
#[inline]
fn ascii_corruption_error(detail: impl Into<String>) -> Error {
let detail = detail.into();
Error::new(
ErrorCode::CBKF104_RDW_SUSPECT_ASCII,
format!("{detail}; verify binary transfer mode and preserve the RDW header bytes"),
)
}
#[inline]
#[must_use]
const fn is_ascii_digit(byte: u8) -> bool {
byte >= b'0' && byte <= b'9'
}
#[inline]
#[must_use]
const fn is_ascii_printable(byte: u8) -> bool {
byte >= 0x20 && byte <= 0x7E
}
#[inline]
fn ascii_char_or_dot(byte: u8) -> char {
if is_ascii_printable(byte) {
byte as char
} else {
'.'
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn digit_length_bytes_are_suspect() {
assert!(rdw_is_suspect_ascii_corruption([b'1', b'2', 0, 0]));
assert!(!rdw_is_suspect_ascii_corruption([0, 0x50, 0, 0]));
}
#[test]
fn slice_requires_a_complete_header() {
assert!(!rdw_is_suspect_ascii_corruption_slice(b"12"));
assert!(rdw_is_suspect_ascii_corruption_slice(b"12\0\0"));
}
#[test]
fn detects_ascii_digit_length_with_remediation() {
let error = detect_rdw_ascii_corruption(b"12\0\0").expect("digits should be detected");
assert_eq!(error.code, ErrorCode::CBKF104_RDW_SUSPECT_ASCII);
assert!(error.message.contains("binary transfer mode"));
}
#[test]
fn detects_printable_reserved_bytes() {
let error = detect_rdw_ascii_corruption(&[0x00, 0x50, b'A', b'B'])
.expect("printable reserved bytes should be detected");
assert_eq!(error.code, ErrorCode::CBKF104_RDW_SUSPECT_ASCII);
assert!(error.message.contains("reserved bytes"));
}
#[test]
fn clean_binary_header_is_not_corrupt() {
assert!(detect_rdw_ascii_corruption(&[0x00, 0x50, 0x00, 0x00]).is_none());
}
#[test]
fn short_input_is_not_corrupt() {
assert!(detect_rdw_ascii_corruption(&[b'1', b'2', 0]).is_none());
}
}