mabi_modbus/fault_injection/
wrong_function_code.rs1use rand::Rng;
7
8use super::config::{FaultTypeConfig, FcCorruptionMode};
9use super::stats::{FaultStats, FaultStatsSnapshot};
10use super::targeting::FaultTarget;
11use super::{FaultAction, ModbusFault, ModbusFaultContext};
12
13pub struct WrongFunctionCodeFault {
25 mode: FcCorruptionMode,
26 fixed_fc: u8,
27 target: FaultTarget,
28 stats: FaultStats,
29}
30
31impl WrongFunctionCodeFault {
32 pub fn new(mode: FcCorruptionMode, target: FaultTarget) -> Self {
34 Self {
35 mode,
36 fixed_fc: 0xFF,
37 target,
38 stats: FaultStats::new(),
39 }
40 }
41
42 pub fn with_fixed_fc(mut self, fc: u8) -> Self {
44 self.fixed_fc = fc;
45 self
46 }
47
48 pub fn from_config(config: &FaultTypeConfig, target: FaultTarget) -> Self {
50 Self {
51 mode: config.fc_mode.unwrap_or(FcCorruptionMode::Increment),
52 fixed_fc: config.fixed_fc.unwrap_or(0xFF),
53 target,
54 stats: FaultStats::new(),
55 }
56 }
57
58 fn corrupt_fc(&self, original: u8) -> u8 {
60 match self.mode {
61 FcCorruptionMode::Fixed => self.fixed_fc,
62 FcCorruptionMode::Increment => original.wrapping_add(1),
63 FcCorruptionMode::Random => {
64 let mut rng = rand::thread_rng();
65 loop {
66 let candidate: u8 = rng.gen_range(1..=0x7F);
67 if candidate != original {
68 return candidate;
69 }
70 }
71 }
72 FcCorruptionMode::SwapRW => {
73 match original {
76 0x01 => 0x05,
77 0x02 => 0x0F,
78 0x03 => 0x10,
79 0x04 => 0x06,
80 0x05 => 0x01,
81 0x06 => 0x04,
82 0x0F => 0x02,
83 0x10 => 0x03,
84 0x16 => 0x03,
85 0x17 => 0x03,
86 _ => original.wrapping_add(1),
87 }
88 }
89 }
90 }
91}
92
93impl ModbusFault for WrongFunctionCodeFault {
94 fn fault_type(&self) -> &'static str {
95 "wrong_function_code"
96 }
97
98 fn is_enabled(&self) -> bool {
99 self.stats.is_enabled()
100 }
101
102 fn set_enabled(&self, enabled: bool) {
103 self.stats.set_enabled(enabled);
104 }
105
106 fn should_activate(&self, ctx: &ModbusFaultContext) -> bool {
107 self.stats.record_check();
108 self.target.should_activate(ctx.unit_id, ctx.function_code)
109 }
110
111 fn apply(&self, ctx: &ModbusFaultContext) -> FaultAction {
112 self.stats.record_activation();
113 self.stats.record_affected();
114
115 let mut response = ctx.response_pdu.clone();
116 if !response.is_empty() {
117 response[0] = self.corrupt_fc(ctx.function_code);
118 }
119
120 FaultAction::SendResponse(response)
121 }
122
123 fn stats(&self) -> FaultStatsSnapshot {
124 self.stats.snapshot()
125 }
126
127 fn reset_stats(&self) {
128 self.stats.reset();
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 fn test_ctx() -> ModbusFaultContext {
137 ModbusFaultContext::tcp(
138 1, 0x03,
139 &[0x03, 0x00, 0x00, 0x00, 0x01],
140 &[0x03, 0x02, 0x00, 0x64],
141 1, 1,
142 )
143 }
144
145 #[test]
146 fn test_fixed_mode() {
147 let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Fixed, FaultTarget::new())
148 .with_fixed_fc(0x10);
149 let action = fault.apply(&test_ctx());
150
151 match action {
152 FaultAction::SendResponse(pdu) => {
153 assert_eq!(pdu[0], 0x10);
154 assert_eq!(&pdu[1..], &[0x02, 0x00, 0x64]); }
156 _ => panic!("Expected SendResponse"),
157 }
158 }
159
160 #[test]
161 fn test_increment_mode() {
162 let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Increment, FaultTarget::new());
163 let action = fault.apply(&test_ctx());
164
165 match action {
166 FaultAction::SendResponse(pdu) => {
167 assert_eq!(pdu[0], 0x04); }
169 _ => panic!("Expected SendResponse"),
170 }
171 }
172
173 #[test]
174 fn test_random_mode() {
175 let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Random, FaultTarget::new());
176 let ctx = test_ctx();
177 let action = fault.apply(&ctx);
178
179 match action {
180 FaultAction::SendResponse(pdu) => {
181 assert_ne!(pdu[0], 0x03);
182 }
183 _ => panic!("Expected SendResponse"),
184 }
185 }
186
187 #[test]
188 fn test_swap_rw_read_to_write() {
189 let fault = WrongFunctionCodeFault::new(FcCorruptionMode::SwapRW, FaultTarget::new());
190
191 let ctx = test_ctx();
193 match fault.apply(&ctx) {
194 FaultAction::SendResponse(pdu) => assert_eq!(pdu[0], 0x10),
195 _ => panic!("Expected SendResponse"),
196 }
197 }
198
199 #[test]
200 fn test_swap_rw_write_to_read() {
201 let fault = WrongFunctionCodeFault::new(FcCorruptionMode::SwapRW, FaultTarget::new());
202
203 let ctx = ModbusFaultContext::tcp(1, 0x10, &[0x10], &[0x10, 0x00], 1, 1);
205 match fault.apply(&ctx) {
206 FaultAction::SendResponse(pdu) => assert_eq!(pdu[0], 0x03),
207 _ => panic!("Expected SendResponse"),
208 }
209 }
210
211 #[test]
212 fn test_swap_rw_all_mappings() {
213 let fault = WrongFunctionCodeFault::new(FcCorruptionMode::SwapRW, FaultTarget::new());
214
215 let mappings = vec![
216 (0x01, 0x05), (0x02, 0x0F), (0x03, 0x10), (0x04, 0x06),
217 (0x05, 0x01), (0x06, 0x04), (0x0F, 0x02), (0x10, 0x03),
218 ];
219
220 for (from, to) in mappings {
221 let ctx = ModbusFaultContext::tcp(1, from, &[from], &[from, 0x00], 1, 1);
222 match fault.apply(&ctx) {
223 FaultAction::SendResponse(pdu) => assert_eq!(pdu[0], to, "FC 0x{:02X} should map to 0x{:02X}", from, to),
224 _ => panic!("Expected SendResponse"),
225 }
226 }
227 }
228
229 #[test]
230 fn test_empty_response() {
231 let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Fixed, FaultTarget::new())
232 .with_fixed_fc(0x10);
233 let ctx = ModbusFaultContext::tcp(1, 0x03, &[0x03], &[], 1, 1);
234 let action = fault.apply(&ctx);
235
236 match action {
237 FaultAction::SendResponse(pdu) => {
238 assert!(pdu.is_empty());
239 }
240 _ => panic!("Expected SendResponse"),
241 }
242 }
243
244 #[test]
245 fn test_from_config() {
246 let config = FaultTypeConfig {
247 fc_mode: Some(FcCorruptionMode::Fixed),
248 fixed_fc: Some(0x42),
249 ..Default::default()
250 };
251 let fault = WrongFunctionCodeFault::from_config(&config, FaultTarget::new());
252 let ctx = test_ctx();
253 match fault.apply(&ctx) {
254 FaultAction::SendResponse(pdu) => assert_eq!(pdu[0], 0x42),
255 _ => panic!("Expected SendResponse"),
256 }
257 }
258}