use rand::Rng;
use super::config::{CrcCorruptionMode, FaultTypeConfig};
use super::stats::{FaultStats, FaultStatsSnapshot};
use super::targeting::FaultTarget;
use super::{FaultAction, ModbusFault, ModbusFaultContext, TransportKind};
pub struct CrcCorruptionFault {
mode: CrcCorruptionMode,
fixed_crc: u16,
target: FaultTarget,
stats: FaultStats,
}
impl CrcCorruptionFault {
pub fn new(mode: CrcCorruptionMode, target: FaultTarget) -> Self {
Self {
mode,
fixed_crc: 0xDEAD,
target,
stats: FaultStats::new(),
}
}
pub fn with_fixed_crc(mut self, crc: u16) -> Self {
self.fixed_crc = crc;
self
}
pub fn from_config(config: &FaultTypeConfig, target: FaultTarget) -> Self {
Self {
mode: config.crc_mode.unwrap_or(CrcCorruptionMode::Invert),
fixed_crc: 0xDEAD,
target,
stats: FaultStats::new(),
}
}
fn compute_crc(data: &[u8]) -> u16 {
let mut crc: u16 = 0xFFFF;
for &byte in data {
crc ^= byte as u16;
for _ in 0..8 {
if crc & 0x0001 != 0 {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
crc
}
fn corrupt_crc(&self, correct_crc: u16) -> u16 {
match self.mode {
CrcCorruptionMode::Zero => 0x0000,
CrcCorruptionMode::Invert => !correct_crc,
CrcCorruptionMode::RandomXor => {
let mut rng = rand::thread_rng();
let xor_val: u16 = rng.gen_range(1..=0xFFFF);
correct_crc ^ xor_val
}
CrcCorruptionMode::SetValue => self.fixed_crc,
CrcCorruptionMode::SwapBytes => {
let hi = (correct_crc >> 8) & 0xFF;
let lo = correct_crc & 0xFF;
(lo << 8) | hi
}
}
}
}
impl ModbusFault for CrcCorruptionFault {
fn fault_type(&self) -> &'static str {
"crc_corruption"
}
fn is_enabled(&self) -> bool {
self.stats.is_enabled()
}
fn set_enabled(&self, enabled: bool) {
self.stats.set_enabled(enabled);
}
fn should_activate(&self, ctx: &ModbusFaultContext) -> bool {
self.stats.record_check();
self.target.should_activate(ctx.unit_id, ctx.function_code)
}
fn apply(&self, ctx: &ModbusFaultContext) -> FaultAction {
self.stats.record_activation();
self.stats.record_affected();
let mut frame = Vec::with_capacity(1 + ctx.response_pdu.len() + 2);
frame.push(ctx.unit_id);
frame.extend_from_slice(&ctx.response_pdu);
let correct_crc = Self::compute_crc(&frame);
let bad_crc = self.corrupt_crc(correct_crc);
frame.push((bad_crc & 0xFF) as u8); frame.push((bad_crc >> 8) as u8);
FaultAction::SendRawBytes(frame)
}
fn stats(&self) -> FaultStatsSnapshot {
self.stats.snapshot()
}
fn reset_stats(&self) {
self.stats.reset();
}
fn compatible_transport(&self) -> Option<TransportKind> {
Some(TransportKind::Rtu)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rtu_ctx() -> ModbusFaultContext {
ModbusFaultContext::rtu(
1,
0x03,
&[0x03, 0x00, 0x00, 0x00, 0x01],
&[0x03, 0x02, 0x00, 0x64],
1,
)
}
#[test]
fn test_crc_zero() {
let fault = CrcCorruptionFault::new(CrcCorruptionMode::Zero, FaultTarget::new());
let ctx = rtu_ctx();
let action = fault.apply(&ctx);
match action {
FaultAction::SendRawBytes(bytes) => {
let len = bytes.len();
assert_eq!(bytes[len - 2], 0x00);
assert_eq!(bytes[len - 1], 0x00);
assert_eq!(bytes[0], 1);
assert_eq!(&bytes[1..len - 2], &[0x03, 0x02, 0x00, 0x64]);
}
_ => panic!("Expected SendRawBytes"),
}
}
#[test]
fn test_crc_invert() {
let fault = CrcCorruptionFault::new(CrcCorruptionMode::Invert, FaultTarget::new());
let ctx = rtu_ctx();
let action = fault.apply(&ctx);
match action {
FaultAction::SendRawBytes(bytes) => {
let len = bytes.len();
let frame_data = &bytes[..len - 2];
let correct_crc = CrcCorruptionFault::compute_crc(frame_data);
let actual_crc = (bytes[len - 2] as u16) | ((bytes[len - 1] as u16) << 8);
assert_eq!(actual_crc, !correct_crc);
}
_ => panic!("Expected SendRawBytes"),
}
}
#[test]
fn test_crc_swap_bytes() {
let fault = CrcCorruptionFault::new(CrcCorruptionMode::SwapBytes, FaultTarget::new());
let ctx = rtu_ctx();
let action = fault.apply(&ctx);
match action {
FaultAction::SendRawBytes(bytes) => {
let len = bytes.len();
let frame_data = &bytes[..len - 2];
let correct_crc = CrcCorruptionFault::compute_crc(frame_data);
let expected = ((correct_crc & 0xFF) << 8) | ((correct_crc >> 8) & 0xFF);
let actual_crc = (bytes[len - 2] as u16) | ((bytes[len - 1] as u16) << 8);
assert_eq!(actual_crc, expected);
}
_ => panic!("Expected SendRawBytes"),
}
}
#[test]
fn test_crc_set_value() {
let fault = CrcCorruptionFault::new(CrcCorruptionMode::SetValue, FaultTarget::new())
.with_fixed_crc(0xBEEF);
let ctx = rtu_ctx();
let action = fault.apply(&ctx);
match action {
FaultAction::SendRawBytes(bytes) => {
let len = bytes.len();
let actual_crc = (bytes[len - 2] as u16) | ((bytes[len - 1] as u16) << 8);
assert_eq!(actual_crc, 0xBEEF);
}
_ => panic!("Expected SendRawBytes"),
}
}
#[test]
fn test_crc_random_xor() {
let fault = CrcCorruptionFault::new(CrcCorruptionMode::RandomXor, FaultTarget::new());
let ctx = rtu_ctx();
let action = fault.apply(&ctx);
match action {
FaultAction::SendRawBytes(bytes) => {
let len = bytes.len();
let frame_data = &bytes[..len - 2];
let correct_crc = CrcCorruptionFault::compute_crc(frame_data);
let actual_crc = (bytes[len - 2] as u16) | ((bytes[len - 1] as u16) << 8);
assert_ne!(actual_crc, correct_crc);
}
_ => panic!("Expected SendRawBytes"),
}
}
#[test]
fn test_rtu_only_transport() {
let fault = CrcCorruptionFault::new(CrcCorruptionMode::Zero, FaultTarget::new());
assert_eq!(fault.compatible_transport(), Some(TransportKind::Rtu));
}
#[test]
fn test_stats_tracking() {
let fault = CrcCorruptionFault::new(CrcCorruptionMode::Zero, FaultTarget::new());
let ctx = rtu_ctx();
assert!(fault.should_activate(&ctx));
fault.apply(&ctx);
let stats = fault.stats();
assert_eq!(stats.checks, 1);
assert_eq!(stats.activations, 1);
assert_eq!(stats.affected_requests, 1);
}
#[test]
fn test_crc_computation() {
let data = [0x01, 0x03, 0x00, 0x00, 0x00, 0x01];
let crc = CrcCorruptionFault::compute_crc(&data);
assert_eq!(crc, 0x0A84);
}
}