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,
139 0x03,
140 &[0x03, 0x00, 0x00, 0x00, 0x01],
141 &[0x03, 0x02, 0x00, 0x64],
142 1,
143 1,
144 )
145 }
146
147 #[test]
148 fn test_fixed_mode() {
149 let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Fixed, FaultTarget::new())
150 .with_fixed_fc(0x10);
151 let action = fault.apply(&test_ctx());
152
153 match action {
154 FaultAction::SendResponse(pdu) => {
155 assert_eq!(pdu[0], 0x10);
156 assert_eq!(&pdu[1..], &[0x02, 0x00, 0x64]); }
158 _ => panic!("Expected SendResponse"),
159 }
160 }
161
162 #[test]
163 fn test_increment_mode() {
164 let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Increment, FaultTarget::new());
165 let action = fault.apply(&test_ctx());
166
167 match action {
168 FaultAction::SendResponse(pdu) => {
169 assert_eq!(pdu[0], 0x04); }
171 _ => panic!("Expected SendResponse"),
172 }
173 }
174
175 #[test]
176 fn test_random_mode() {
177 let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Random, FaultTarget::new());
178 let ctx = test_ctx();
179 let action = fault.apply(&ctx);
180
181 match action {
182 FaultAction::SendResponse(pdu) => {
183 assert_ne!(pdu[0], 0x03);
184 }
185 _ => panic!("Expected SendResponse"),
186 }
187 }
188
189 #[test]
190 fn test_swap_rw_read_to_write() {
191 let fault = WrongFunctionCodeFault::new(FcCorruptionMode::SwapRW, FaultTarget::new());
192
193 let ctx = test_ctx();
195 match fault.apply(&ctx) {
196 FaultAction::SendResponse(pdu) => assert_eq!(pdu[0], 0x10),
197 _ => panic!("Expected SendResponse"),
198 }
199 }
200
201 #[test]
202 fn test_swap_rw_write_to_read() {
203 let fault = WrongFunctionCodeFault::new(FcCorruptionMode::SwapRW, FaultTarget::new());
204
205 let ctx = ModbusFaultContext::tcp(1, 0x10, &[0x10], &[0x10, 0x00], 1, 1);
207 match fault.apply(&ctx) {
208 FaultAction::SendResponse(pdu) => assert_eq!(pdu[0], 0x03),
209 _ => panic!("Expected SendResponse"),
210 }
211 }
212
213 #[test]
214 fn test_swap_rw_all_mappings() {
215 let fault = WrongFunctionCodeFault::new(FcCorruptionMode::SwapRW, FaultTarget::new());
216
217 let mappings = vec![
218 (0x01, 0x05),
219 (0x02, 0x0F),
220 (0x03, 0x10),
221 (0x04, 0x06),
222 (0x05, 0x01),
223 (0x06, 0x04),
224 (0x0F, 0x02),
225 (0x10, 0x03),
226 ];
227
228 for (from, to) in mappings {
229 let ctx = ModbusFaultContext::tcp(1, from, &[from], &[from, 0x00], 1, 1);
230 match fault.apply(&ctx) {
231 FaultAction::SendResponse(pdu) => {
232 assert_eq!(pdu[0], to, "FC 0x{:02X} should map to 0x{:02X}", from, to)
233 }
234 _ => panic!("Expected SendResponse"),
235 }
236 }
237 }
238
239 #[test]
240 fn test_empty_response() {
241 let fault = WrongFunctionCodeFault::new(FcCorruptionMode::Fixed, FaultTarget::new())
242 .with_fixed_fc(0x10);
243 let ctx = ModbusFaultContext::tcp(1, 0x03, &[0x03], &[], 1, 1);
244 let action = fault.apply(&ctx);
245
246 match action {
247 FaultAction::SendResponse(pdu) => {
248 assert!(pdu.is_empty());
249 }
250 _ => panic!("Expected SendResponse"),
251 }
252 }
253
254 #[test]
255 fn test_from_config() {
256 let config = FaultTypeConfig {
257 fc_mode: Some(FcCorruptionMode::Fixed),
258 fixed_fc: Some(0x42),
259 ..Default::default()
260 };
261 let fault = WrongFunctionCodeFault::from_config(&config, FaultTarget::new());
262 let ctx = test_ctx();
263 match fault.apply(&ctx) {
264 FaultAction::SendResponse(pdu) => assert_eq!(pdu[0], 0x42),
265 _ => panic!("Expected SendResponse"),
266 }
267 }
268}