Skip to main content

rusty_modbus_codec/
validate.rs

1//! Server-side request validation helpers.
2//!
3//! Implements the validation state diagrams from Spec V1.1b3 §6.1–6.18.
4//! These are pure functions — no I/O.
5//!
6//! **Validation order matters** (per §4.5 Figure 9 and per-FC state diagrams):
7//! 1. Data value: quantity in range, byte count matches → `IllegalDataValue` (0x03)
8//! 2. Data address: `address + quantity` must not overflow → `IllegalDataAddress` (0x02)
9
10use rusty_modbus_types::{
11    ExceptionCode, MAX_READ_COILS, MAX_READ_DISCRETE_INPUTS, MAX_READ_REGISTERS,
12    MAX_RW_READ_REGISTERS, MAX_RW_WRITE_REGISTERS, MAX_WRITE_COILS, MAX_WRITE_REGISTERS,
13};
14
15/// Validate a read coils (FC 01) request.
16///
17/// Per spec §6.1 Figure 11 state diagram.
18///
19/// # Errors
20///
21/// - `IllegalDataValue` if `quantity` is 0 or exceeds 2000
22/// - `IllegalDataAddress` if `address + quantity > 0xFFFF`
23pub fn validate_read_coils(address: u16, quantity: u16) -> Result<(), ExceptionCode> {
24    if quantity == 0 || quantity > MAX_READ_COILS {
25        return Err(ExceptionCode::IllegalDataValue);
26    }
27    validate_address_range(address, quantity)?;
28    Ok(())
29}
30
31/// Validate a read discrete inputs (FC 02) request.
32///
33/// Per spec §6.2 Figure 12 state diagram.
34///
35/// # Errors
36///
37/// - `IllegalDataValue` if `quantity` is 0 or exceeds 2000
38/// - `IllegalDataAddress` if `address + quantity > 0xFFFF`
39pub fn validate_read_discrete_inputs(address: u16, quantity: u16) -> Result<(), ExceptionCode> {
40    if quantity == 0 || quantity > MAX_READ_DISCRETE_INPUTS {
41        return Err(ExceptionCode::IllegalDataValue);
42    }
43    validate_address_range(address, quantity)?;
44    Ok(())
45}
46
47/// Validate a read holding/input registers (FC 03/04) request.
48///
49/// Per spec §6.3 Figure 13 / §6.4 Figure 14 state diagrams.
50///
51/// # Errors
52///
53/// - `IllegalDataValue` if `quantity` is 0 or exceeds 125
54/// - `IllegalDataAddress` if `address + quantity > 0xFFFF`
55pub fn validate_read_registers(address: u16, quantity: u16) -> Result<(), ExceptionCode> {
56    if quantity == 0 || quantity > MAX_READ_REGISTERS {
57        return Err(ExceptionCode::IllegalDataValue);
58    }
59    validate_address_range(address, quantity)?;
60    Ok(())
61}
62
63/// Validate a write single coil (FC 05) request.
64///
65/// Per spec §6.5 Figure 15 state diagram — value must be 0x0000 or 0xFF00.
66///
67/// # Errors
68///
69/// - `IllegalDataValue` if `coil_value` is not 0x0000 or 0xFF00
70pub fn validate_write_single_coil(coil_value: u16) -> Result<(), ExceptionCode> {
71    if coil_value != 0x0000 && coil_value != 0xFF00 {
72        return Err(ExceptionCode::IllegalDataValue);
73    }
74    Ok(())
75}
76
77/// Validate a write multiple coils (FC 0F) request.
78///
79/// Per spec §6.11 Figure 21 state diagram.
80///
81/// # Errors
82///
83/// - `IllegalDataValue` if quantity is 0 or exceeds 1968, or byte count doesn't match
84/// - `IllegalDataAddress` if `address + quantity > 0xFFFF`
85pub fn validate_write_coils(
86    address: u16,
87    quantity: u16,
88    byte_count: u8,
89) -> Result<(), ExceptionCode> {
90    if quantity == 0 || quantity > MAX_WRITE_COILS {
91        return Err(ExceptionCode::IllegalDataValue);
92    }
93    let expected_bytes = quantity.div_ceil(8);
94    if u16::from(byte_count) != expected_bytes {
95        return Err(ExceptionCode::IllegalDataValue);
96    }
97    validate_address_range(address, quantity)?;
98    Ok(())
99}
100
101/// Validate a write multiple registers (FC 10) request.
102///
103/// Per spec §6.12 Figure 22 state diagram.
104///
105/// # Errors
106///
107/// - `IllegalDataValue` if quantity is 0 or exceeds 123, or byte count doesn't match
108/// - `IllegalDataAddress` if `address + quantity > 0xFFFF`
109pub fn validate_write_registers(
110    address: u16,
111    quantity: u16,
112    byte_count: u8,
113) -> Result<(), ExceptionCode> {
114    if quantity == 0 || quantity > MAX_WRITE_REGISTERS {
115        return Err(ExceptionCode::IllegalDataValue);
116    }
117    if u16::from(byte_count) != quantity * 2 {
118        return Err(ExceptionCode::IllegalDataValue);
119    }
120    validate_address_range(address, quantity)?;
121    Ok(())
122}
123
124/// Validate a mask write register (FC 16) address.
125///
126/// Per spec §6.16 Figure 26 — both AND and OR mask values accept 0x0000–0xFFFF.
127/// Only the address is validated at the protocol level.
128///
129/// # Errors
130///
131/// - `IllegalDataAddress` if `address` is not valid (overflows with quantity 1)
132pub fn validate_mask_write_address(address: u16) -> Result<(), ExceptionCode> {
133    validate_address_range(address, 1)
134}
135
136/// Validate a read/write multiple registers (FC 17) request.
137///
138/// Per spec §6.17 Figure 27 state diagram.
139///
140/// # Errors
141///
142/// - `IllegalDataValue` if quantities out of range or byte count doesn't match
143/// - `IllegalDataAddress` if either address+quantity overflows
144pub fn validate_read_write_registers(
145    read_address: u16,
146    read_quantity: u16,
147    write_address: u16,
148    write_quantity: u16,
149    write_byte_count: u8,
150) -> Result<(), ExceptionCode> {
151    if read_quantity == 0 || read_quantity > MAX_RW_READ_REGISTERS {
152        return Err(ExceptionCode::IllegalDataValue);
153    }
154    if write_quantity == 0 || write_quantity > MAX_RW_WRITE_REGISTERS {
155        return Err(ExceptionCode::IllegalDataValue);
156    }
157    if u16::from(write_byte_count) != write_quantity * 2 {
158        return Err(ExceptionCode::IllegalDataValue);
159    }
160    validate_address_range(read_address, read_quantity)?;
161    validate_address_range(write_address, write_quantity)?;
162    Ok(())
163}
164
165/// Check that `address + quantity` doesn't overflow the 16-bit address space.
166fn validate_address_range(address: u16, quantity: u16) -> Result<(), ExceptionCode> {
167    if u32::from(address) + u32::from(quantity) > 0x10000 {
168        return Err(ExceptionCode::IllegalDataAddress);
169    }
170    Ok(())
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    // ── Read coils (FC 01) ─────────────────────────────────────────
178
179    #[test]
180    fn read_coils_valid() {
181        assert!(validate_read_coils(0, 1).is_ok());
182        assert!(validate_read_coils(0, 2000).is_ok());
183        assert!(validate_read_coils(100, 100).is_ok());
184    }
185
186    #[test]
187    fn read_coils_quantity_zero() {
188        assert_eq!(
189            validate_read_coils(0, 0),
190            Err(ExceptionCode::IllegalDataValue)
191        );
192    }
193
194    #[test]
195    fn read_coils_quantity_too_large() {
196        assert_eq!(
197            validate_read_coils(0, 2001),
198            Err(ExceptionCode::IllegalDataValue)
199        );
200    }
201
202    #[test]
203    fn read_coils_address_overflow() {
204        assert_eq!(
205            validate_read_coils(0xFFFF, 2),
206            Err(ExceptionCode::IllegalDataAddress)
207        );
208    }
209
210    #[test]
211    fn quantity_checked_before_address() {
212        // Per spec state diagrams (V1.1b3 Figures 11-28): quantity check (0x03)
213        // takes priority over address check (0x02) when both fail.
214        assert_eq!(
215            validate_read_coils(0xFFFF, 5000),
216            Err(ExceptionCode::IllegalDataValue)
217        );
218    }
219
220    // ── Read registers (FC 03/04) ──────────────────────────────────
221
222    #[test]
223    fn read_registers_valid() {
224        assert!(validate_read_registers(0, 1).is_ok());
225        assert!(validate_read_registers(0, 125).is_ok());
226    }
227
228    #[test]
229    fn read_registers_quantity_out_of_range() {
230        assert_eq!(
231            validate_read_registers(0, 126),
232            Err(ExceptionCode::IllegalDataValue)
233        );
234    }
235
236    // ── Write single coil (FC 05) ──────────────────────────────────
237
238    #[test]
239    fn write_single_coil_valid_on() {
240        assert!(validate_write_single_coil(0xFF00).is_ok());
241    }
242
243    #[test]
244    fn write_single_coil_valid_off() {
245        assert!(validate_write_single_coil(0x0000).is_ok());
246    }
247
248    #[test]
249    fn write_single_coil_invalid_value() {
250        assert_eq!(
251            validate_write_single_coil(0x0001),
252            Err(ExceptionCode::IllegalDataValue)
253        );
254        assert_eq!(
255            validate_write_single_coil(0xFF01),
256            Err(ExceptionCode::IllegalDataValue)
257        );
258    }
259
260    // ── Write multiple coils (FC 0F) ───────────────────────────────
261
262    #[test]
263    fn write_coils_valid() {
264        assert!(validate_write_coils(0, 8, 1).is_ok());
265        assert!(validate_write_coils(0, 9, 2).is_ok());
266        assert!(validate_write_coils(0, 1968, 246).is_ok());
267    }
268
269    #[test]
270    fn write_coils_byte_count_mismatch() {
271        assert_eq!(
272            validate_write_coils(0, 8, 2),
273            Err(ExceptionCode::IllegalDataValue)
274        );
275    }
276
277    // ── Write multiple registers (FC 10) ───────────────────────────
278
279    #[test]
280    fn write_registers_valid() {
281        assert!(validate_write_registers(0, 1, 2).is_ok());
282        assert!(validate_write_registers(0, 123, 246).is_ok());
283    }
284
285    #[test]
286    fn write_registers_byte_count_mismatch() {
287        assert_eq!(
288            validate_write_registers(0, 1, 3),
289            Err(ExceptionCode::IllegalDataValue)
290        );
291    }
292
293    // ── Mask write register (FC 16) ────────────────────────────────
294
295    #[test]
296    fn mask_write_address_valid() {
297        assert!(validate_mask_write_address(0).is_ok());
298        assert!(validate_mask_write_address(0xFFFE).is_ok());
299    }
300
301    #[test]
302    fn mask_write_address_max_valid() {
303        // 0xFFFF + 1 = 0x10000, which is exactly at the boundary (not overflow)
304        assert!(validate_mask_write_address(0xFFFF).is_ok());
305    }
306
307    // ── Read/write multiple registers (FC 17) ──────────────────────
308
309    #[test]
310    fn read_write_registers_valid() {
311        assert!(validate_read_write_registers(0, 1, 0, 1, 2).is_ok());
312        assert!(validate_read_write_registers(0, 125, 0, 121, 242).is_ok());
313    }
314
315    #[test]
316    fn read_write_registers_read_overflow() {
317        assert_eq!(
318            validate_read_write_registers(0xFFFF, 2, 0, 1, 2),
319            Err(ExceptionCode::IllegalDataAddress)
320        );
321    }
322}