Skip to main content

rusty_modbus_server/
handler.rs

1//! Request dispatch and response building.
2
3use rusty_modbus_codec::request::{ReadFileRecordRequest, WriteFileRecordRequest};
4use rusty_modbus_codec::response::{
5    GetCommEventCounterResponse, MaskWriteRegisterResponse, ReadExceptionStatusResponse,
6    WriteFileRecordResponse, WriteMultipleCoilsResponse, WriteMultipleRegistersResponse,
7    WriteSingleCoilResponse, WriteSingleRegisterResponse,
8};
9use rusty_modbus_codec::{DecodeError, RequestPdu, decode_request, validate};
10use rusty_modbus_types::{
11    Address, DiagnosticSubFunction, ExceptionCode, FunctionCode, MAX_FIFO_VALUES, MAX_PDU_SIZE,
12    MeiType, Quantity, UnitId,
13};
14use tracing::debug;
15
16use crate::config::DeviceIdentification;
17use crate::device_id::build_device_id_response;
18use crate::file_record;
19use crate::response_encode::encode_response;
20use crate::store::{
21    DataStore, MAX_COMM_EVENT_LOG_EVENTS, MAX_DIAGNOSTIC_RESPONSE_DATA_LEN,
22    MAX_FILE_RECORD_REGISTERS, MAX_SERVER_ID_BYTES,
23};
24
25/// Process a request PDU and return a response PDU (or `None` for broadcast writes).
26///
27/// The `pdu` slice starts at the function code byte.
28#[allow(clippy::too_many_lines)]
29#[tracing::instrument(
30    level = "trace",
31    skip(pdu, store, device_id),
32    fields(
33        unit_id = unit_id.0,
34        pdu_len = pdu.len(),
35        function_code = pdu.first().copied().unwrap_or_default()
36    )
37)]
38pub async fn process_request<S: DataStore>(
39    pdu: &[u8],
40    unit_id: UnitId,
41    store: &S,
42    device_id: &DeviceIdentification,
43) -> Option<Vec<u8>> {
44    let is_broadcast = unit_id.is_broadcast();
45
46    let request = match decode_request(pdu) {
47        Ok(req) => req,
48        Err(e) => {
49            if is_broadcast {
50                return None;
51            }
52            let fc = pdu.first().copied().unwrap_or(0);
53            // Per spec state diagrams (V1.1b3 Figures 11-28):
54            // - Unknown function code → IllegalFunction (0x01)
55            // - Known function code with bad data → IllegalDataValue (0x03)
56            let exc = match e {
57                // Unknown function code, and an unrecognized Diagnostics (FC 0x08)
58                // sub-function, are both illegal *functions* — not illegal data
59                // values (V1.1b3 §4.5 and §6.8, Figure 18).
60                DecodeError::UnknownFunctionCode(_)
61                | DecodeError::UnknownDiagnosticSubFunction(_) => ExceptionCode::IllegalFunction,
62                DecodeError::InvalidReferenceType(_) | DecodeError::FileRecordOutOfRange { .. } => {
63                    ExceptionCode::IllegalDataAddress
64                }
65                _ => {
66                    // FC is recognized (otherwise decode would return UnknownFunctionCode),
67                    // but the data is malformed (truncated, bad quantity, bad byte count,
68                    // invalid coil value, etc.) → IllegalDataValue per spec §4.5
69                    ExceptionCode::IllegalDataValue
70                }
71            };
72            debug!(
73                function_code = fc,
74                exception_code = exc.code(),
75                error = %e,
76                "request decode failed; returning Modbus exception"
77            );
78            return Some(encode_exception(fc | 0x80, exc));
79        }
80    };
81
82    dispatch_request(request, pdu, is_broadcast, store, device_id).await
83}
84
85#[allow(clippy::too_many_lines)]
86async fn dispatch_request<S: DataStore>(
87    request: RequestPdu<'_>,
88    pdu: &[u8],
89    is_broadcast: bool,
90    store: &S,
91    device_id: &DeviceIdentification,
92) -> Option<Vec<u8>> {
93    match request {
94        RequestPdu::ReadHoldingRegisters(req) => {
95            if is_broadcast {
96                return None;
97            }
98            Some(
99                handle_read_registers(
100                    FunctionCode::ReadHoldingRegisters,
101                    req.address,
102                    req.quantity,
103                    store,
104                    true,
105                )
106                .await,
107            )
108        }
109        RequestPdu::ReadInputRegisters(req) => {
110            if is_broadcast {
111                return None;
112            }
113            Some(
114                handle_read_registers(
115                    FunctionCode::ReadInputRegisters,
116                    req.address,
117                    req.quantity,
118                    store,
119                    false,
120                )
121                .await,
122            )
123        }
124        RequestPdu::ReadCoils(req) => {
125            if is_broadcast {
126                return None;
127            }
128            Some(
129                handle_read_bits(
130                    FunctionCode::ReadCoils,
131                    req.address,
132                    req.quantity,
133                    store,
134                    true,
135                )
136                .await,
137            )
138        }
139        RequestPdu::ReadDiscreteInputs(req) => {
140            if is_broadcast {
141                return None;
142            }
143            Some(
144                handle_read_bits(
145                    FunctionCode::ReadDiscreteInputs,
146                    req.address,
147                    req.quantity,
148                    store,
149                    false,
150                )
151                .await,
152            )
153        }
154        RequestPdu::WriteSingleRegister(req) => {
155            let result = store.write_register(req.address.0, req.value).await;
156            if is_broadcast {
157                return None;
158            }
159            Some(match result {
160                Ok(()) => encode_response(&WriteSingleRegisterResponse {
161                    address: req.address,
162                    value: req.value,
163                }),
164                Err(ec) => encode_exception(FunctionCode::WriteSingleRegister.exception_code(), ec),
165            })
166        }
167        RequestPdu::WriteMultipleRegisters(req) => {
168            if let Err(ec) =
169                validate::validate_write_registers(req.address.0, req.quantity.0, req.byte_count)
170            {
171                if is_broadcast {
172                    return None;
173                }
174                return Some(encode_exception(
175                    FunctionCode::WriteMultipleRegisters.exception_code(),
176                    ec,
177                ));
178            }
179            let result = store
180                .write_registers_be(req.address.0, req.quantity.0, req.register_values)
181                .await;
182            if is_broadcast {
183                return None;
184            }
185            Some(match result {
186                Ok(()) => encode_response(&WriteMultipleRegistersResponse {
187                    address: req.address,
188                    quantity: req.quantity,
189                }),
190                Err(ec) => {
191                    encode_exception(FunctionCode::WriteMultipleRegisters.exception_code(), ec)
192                }
193            })
194        }
195        RequestPdu::WriteSingleCoil(req) => {
196            let result = store.write_coil(req.address.0, req.value.as_bool()).await;
197            if is_broadcast {
198                return None;
199            }
200            Some(match result {
201                Ok(()) => encode_response(&WriteSingleCoilResponse {
202                    address: req.address,
203                    value: req.value,
204                }),
205                Err(ec) => encode_exception(FunctionCode::WriteSingleCoil.exception_code(), ec),
206            })
207        }
208        RequestPdu::WriteMultipleCoils(req) => {
209            if let Err(ec) =
210                validate::validate_write_coils(req.address.0, req.quantity.0, req.byte_count)
211            {
212                if is_broadcast {
213                    return None;
214                }
215                return Some(encode_exception(
216                    FunctionCode::WriteMultipleCoils.exception_code(),
217                    ec,
218                ));
219            }
220            let result = store
221                .write_coils_packed(req.address.0, req.quantity.0, req.coil_values)
222                .await;
223            if is_broadcast {
224                return None;
225            }
226            Some(match result {
227                Ok(()) => encode_response(&WriteMultipleCoilsResponse {
228                    address: req.address,
229                    quantity: req.quantity,
230                }),
231                Err(ec) => encode_exception(FunctionCode::WriteMultipleCoils.exception_code(), ec),
232            })
233        }
234        RequestPdu::MaskWriteRegister(req) => {
235            let result = handle_mask_write(req.address, req.and_mask, req.or_mask, store).await;
236            if is_broadcast {
237                return None;
238            }
239            Some(match result {
240                Ok(()) => encode_response(&MaskWriteRegisterResponse {
241                    address: req.address,
242                    and_mask: req.and_mask,
243                    or_mask: req.or_mask,
244                }),
245                Err(ec) => encode_exception(FunctionCode::MaskWriteRegister.exception_code(), ec),
246            })
247        }
248        RequestPdu::ReadWriteMultipleRegisters(req) => {
249            if is_broadcast {
250                return None;
251            }
252            Some(handle_read_write_multiple(req, store).await)
253        }
254        RequestPdu::ReadFileRecord(req) => {
255            if is_broadcast {
256                return None;
257            }
258            Some(handle_read_file_record(req, store).await)
259        }
260        RequestPdu::WriteFileRecord(req) => {
261            let result = apply_write_file_record(&req, store).await;
262            if is_broadcast {
263                return None;
264            }
265            Some(match result {
266                // Per §6.15 a successful write echoes the request sub-requests verbatim.
267                Ok(()) => encode_response(&WriteFileRecordResponse {
268                    byte_count: req.byte_count,
269                    data: req.sub_requests,
270                }),
271                Err(ec) => encode_exception(FunctionCode::WriteFileRecord.exception_code(), ec),
272            })
273        }
274        RequestPdu::ReadFifoQueue(req) => {
275            if is_broadcast {
276                return None;
277            }
278            Some(handle_read_fifo_queue(req.fifo_pointer_address, store).await)
279        }
280        RequestPdu::ReadExceptionStatus => {
281            if is_broadcast {
282                return None;
283            }
284            Some(match store.read_exception_status().await {
285                Ok(status) => encode_response(&ReadExceptionStatusResponse { status }),
286                Err(ec) => encode_exception(FunctionCode::ReadExceptionStatus.exception_code(), ec),
287            })
288        }
289        RequestPdu::Diagnostics(req) => {
290            // Run the sub-function first (it may mutate device state, e.g. clear
291            // counters), then suppress the reply on broadcast — mirroring the
292            // write arms. Ok(None) is the spec "no reply" path (Force Listen Only).
293            let result = handle_diagnostics(req.sub_function, req.data, store).await;
294            if is_broadcast {
295                return None;
296            }
297            result
298        }
299        RequestPdu::GetCommEventCounter => {
300            if is_broadcast {
301                return None;
302            }
303            Some(match store.get_comm_event_counter().await {
304                Ok((status, event_count)) => encode_response(&GetCommEventCounterResponse {
305                    status,
306                    event_count,
307                }),
308                Err(ec) => encode_exception(FunctionCode::GetCommEventCounter.exception_code(), ec),
309            })
310        }
311        RequestPdu::GetCommEventLog => {
312            if is_broadcast {
313                return None;
314            }
315            Some(handle_comm_event_log(store).await)
316        }
317        RequestPdu::ReportServerId => {
318            if is_broadcast {
319                return None;
320            }
321            Some(handle_report_server_id(store).await)
322        }
323        RequestPdu::EncapsulatedInterface(req) => {
324            if is_broadcast {
325                return None;
326            }
327            if req.mei_type == MeiType::ReadDeviceIdentification {
328                Some(build_device_id_response(req.data, device_id))
329            } else {
330                let fc = pdu.first().copied().unwrap_or(0);
331                Some(encode_exception(fc | 0x80, ExceptionCode::IllegalFunction))
332            }
333        }
334        RequestPdu::Custom(..) => {
335            if is_broadcast {
336                return None;
337            }
338            let fc = pdu.first().copied().unwrap_or(0);
339            Some(encode_exception(fc | 0x80, ExceptionCode::IllegalFunction))
340        }
341    }
342}
343
344async fn handle_comm_event_log<S: DataStore>(store: &S) -> Vec<u8> {
345    let mut response = Vec::with_capacity(8 + MAX_COMM_EVENT_LOG_EVENTS);
346    response.push(FunctionCode::GetCommEventLog.code());
347    response.push(0);
348    response.extend_from_slice(&[0; 6]);
349
350    let events_start = response.len();
351    let result = store.append_comm_event_log(&mut response).await;
352    match result {
353        Ok(meta) => {
354            let events_len = response.len().saturating_sub(events_start);
355            if events_len > MAX_COMM_EVENT_LOG_EVENTS {
356                return encode_exception(
357                    FunctionCode::GetCommEventLog.exception_code(),
358                    ExceptionCode::ServerDeviceFailure,
359                );
360            }
361            let byte_count =
362                match checked_response_u8(events_len + 6, FunctionCode::GetCommEventLog) {
363                    Ok(byte_count) => byte_count,
364                    Err(resp) => return resp,
365                };
366            response[1] = byte_count;
367            response[2..4].copy_from_slice(&meta.status.to_be_bytes());
368            response[4..6].copy_from_slice(&meta.event_count.to_be_bytes());
369            response[6..8].copy_from_slice(&meta.message_count.to_be_bytes());
370            response
371        }
372        Err(ec) => encode_exception(FunctionCode::GetCommEventLog.exception_code(), ec),
373    }
374}
375
376async fn handle_diagnostics<S: DataStore>(
377    sub_function: DiagnosticSubFunction,
378    data: &[u8],
379    store: &S,
380) -> Option<Vec<u8>> {
381    let mut response = Vec::with_capacity(3 + MAX_DIAGNOSTIC_RESPONSE_DATA_LEN);
382    response.push(FunctionCode::Diagnostics.code());
383    response.extend_from_slice(&sub_function.code().to_be_bytes());
384
385    let data_start = response.len();
386    let result = store
387        .append_diagnostic_response(sub_function, data, &mut response)
388        .await;
389    match result {
390        Ok(Some(count)) => {
391            let actual_count = response.len().saturating_sub(data_start);
392            if count != actual_count
393                || actual_count > MAX_DIAGNOSTIC_RESPONSE_DATA_LEN
394                || !actual_count.is_multiple_of(2)
395            {
396                return Some(encode_exception(
397                    FunctionCode::Diagnostics.exception_code(),
398                    ExceptionCode::ServerDeviceFailure,
399                ));
400            }
401            Some(response)
402        }
403        Ok(None) => None,
404        Err(ec) => Some(encode_exception(
405            FunctionCode::Diagnostics.exception_code(),
406            ec,
407        )),
408    }
409}
410
411async fn handle_report_server_id<S: DataStore>(store: &S) -> Vec<u8> {
412    let mut response = Vec::with_capacity(2 + MAX_SERVER_ID_BYTES);
413    response.push(FunctionCode::ReportServerId.code());
414    response.push(0);
415
416    let data_start = response.len();
417    let result = store.append_server_id(&mut response).await;
418    match result {
419        Ok(count) => {
420            let actual_count = response.len().saturating_sub(data_start);
421            if count != actual_count || actual_count > MAX_SERVER_ID_BYTES {
422                return encode_exception(
423                    FunctionCode::ReportServerId.exception_code(),
424                    ExceptionCode::ServerDeviceFailure,
425                );
426            }
427            let byte_count = match checked_response_u8(actual_count, FunctionCode::ReportServerId) {
428                Ok(byte_count) => byte_count,
429                Err(resp) => return resp,
430            };
431            response[1] = byte_count;
432            response
433        }
434        Err(ec) => encode_exception(FunctionCode::ReportServerId.exception_code(), ec),
435    }
436}
437
438async fn handle_read_registers<S: DataStore>(
439    fc: FunctionCode,
440    address: Address,
441    quantity: Quantity,
442    store: &S,
443    is_holding: bool,
444) -> Vec<u8> {
445    if let Err(ec) = validate::validate_read_registers(address.0, quantity.0) {
446        return encode_exception(fc.exception_code(), ec);
447    }
448
449    let byte_count = match checked_response_u8(usize::from(quantity.0) * 2, fc) {
450        Ok(byte_count) => byte_count,
451        Err(resp) => return resp,
452    };
453    let response_fc = if is_holding {
454        FunctionCode::ReadHoldingRegisters
455    } else {
456        FunctionCode::ReadInputRegisters
457    };
458    let mut response = vec![0u8; 2 + usize::from(byte_count)];
459    response[0] = response_fc.code();
460    response[1] = byte_count;
461
462    let result = if is_holding {
463        store
464            .read_holding_registers_be(address.0, quantity.0, &mut response[2..])
465            .await
466    } else {
467        store
468            .read_input_registers_be(address.0, quantity.0, &mut response[2..])
469            .await
470    };
471
472    match result {
473        Ok(count) => {
474            if let Err(ec) = validate_store_count(count, quantity.0, usize::from(quantity.0)) {
475                return encode_exception(fc.exception_code(), ec);
476            }
477            response
478        }
479        Err(ec) => encode_exception(fc.exception_code(), ec),
480    }
481}
482
483async fn handle_read_bits<S: DataStore>(
484    fc: FunctionCode,
485    address: Address,
486    quantity: Quantity,
487    store: &S,
488    is_coils: bool,
489) -> Vec<u8> {
490    let validation = if is_coils {
491        validate::validate_read_coils(address.0, quantity.0)
492    } else {
493        validate::validate_read_discrete_inputs(address.0, quantity.0)
494    };
495    if let Err(ec) = validation {
496        return encode_exception(fc.exception_code(), ec);
497    }
498
499    let byte_count = match checked_response_u8(usize::from(quantity.0).div_ceil(8), fc) {
500        Ok(byte_count) => byte_count,
501        Err(resp) => return resp,
502    };
503    let response_fc = if is_coils {
504        FunctionCode::ReadCoils
505    } else {
506        FunctionCode::ReadDiscreteInputs
507    };
508    let mut response = vec![0u8; 2 + usize::from(byte_count)];
509    response[0] = response_fc.code();
510    response[1] = byte_count;
511
512    let result = if is_coils {
513        store
514            .read_coils_packed(address.0, quantity.0, &mut response[2..])
515            .await
516    } else {
517        store
518            .read_discrete_inputs_packed(address.0, quantity.0, &mut response[2..])
519            .await
520    };
521
522    match result {
523        Ok(count) => {
524            if let Err(ec) = validate_store_count(count, quantity.0, usize::from(quantity.0)) {
525                return encode_exception(fc.exception_code(), ec);
526            }
527            response
528        }
529        Err(ec) => encode_exception(fc.exception_code(), ec),
530    }
531}
532
533async fn handle_mask_write<S: DataStore>(
534    address: Address,
535    and_mask: u16,
536    or_mask: u16,
537    store: &S,
538) -> Result<(), ExceptionCode> {
539    validate::validate_mask_write_address(address.0)?;
540
541    let mut buf = [0u16; 1];
542    store.read_holding_registers(address.0, 1, &mut buf).await?;
543    let result = (buf[0] & and_mask) | (or_mask & !and_mask);
544    store.write_register(address.0, result).await
545}
546
547async fn handle_read_write_multiple<S: DataStore>(
548    req: rusty_modbus_codec::request::ReadWriteMultipleRegistersRequest<'_>,
549    store: &S,
550) -> Vec<u8> {
551    if let Err(ec) = validate::validate_read_write_registers(
552        req.read_address.0,
553        req.read_quantity.0,
554        req.write_address.0,
555        req.write_quantity.0,
556        req.write_byte_count,
557    ) {
558        return encode_exception(
559            FunctionCode::ReadWriteMultipleRegisters.exception_code(),
560            ec,
561        );
562    }
563
564    // Write executes before read per spec §6.17.
565    if let Err(ec) = store
566        .write_registers_be(
567            req.write_address.0,
568            req.write_quantity.0,
569            req.write_register_values,
570        )
571        .await
572    {
573        return encode_exception(
574            FunctionCode::ReadWriteMultipleRegisters.exception_code(),
575            ec,
576        );
577    }
578
579    let byte_count = match checked_response_u8(
580        usize::from(req.read_quantity.0) * 2,
581        FunctionCode::ReadWriteMultipleRegisters,
582    ) {
583        Ok(byte_count) => byte_count,
584        Err(resp) => return resp,
585    };
586    let mut response = vec![0u8; 2 + usize::from(byte_count)];
587    response[0] = FunctionCode::ReadWriteMultipleRegisters.code();
588    response[1] = byte_count;
589
590    match store
591        .read_holding_registers_be(req.read_address.0, req.read_quantity.0, &mut response[2..])
592        .await
593    {
594        Ok(count) => {
595            if let Err(ec) =
596                validate_store_count(count, req.read_quantity.0, usize::from(req.read_quantity.0))
597            {
598                return encode_exception(
599                    FunctionCode::ReadWriteMultipleRegisters.exception_code(),
600                    ec,
601                );
602            }
603            response
604        }
605        Err(ec) => encode_exception(
606            FunctionCode::ReadWriteMultipleRegisters.exception_code(),
607            ec,
608        ),
609    }
610}
611
612async fn handle_read_file_record<S: DataStore>(
613    req: ReadFileRecordRequest<'_>,
614    store: &S,
615) -> Vec<u8> {
616    let subs = req.sub_requests;
617    // Each sub-request is exactly 7 bytes; the byte count must be a non-empty
618    // multiple of 7 within 0x07..=0xF5 (§6.14, Figure 24 → IllegalDataValue).
619    if subs.is_empty() || !subs.len().is_multiple_of(7) || subs.len() > 0xF5 {
620        return encode_exception(
621            FunctionCode::ReadFileRecord.exception_code(),
622            ExceptionCode::IllegalDataValue,
623        );
624    }
625    let mut response = Vec::with_capacity(MAX_PDU_SIZE);
626    response.push(FunctionCode::ReadFileRecord.code());
627    response.push(0);
628    for chunk in subs.chunks_exact(7) {
629        if chunk[0] != 6 {
630            // Reference type must be 6 (Figure 24 groups this under 0x02).
631            return encode_exception(
632                FunctionCode::ReadFileRecord.exception_code(),
633                ExceptionCode::IllegalDataAddress,
634            );
635        }
636        let file = u16::from_be_bytes([chunk[1], chunk[2]]);
637        let record = u16::from_be_bytes([chunk[3], chunk[4]]);
638        let length = u16::from_be_bytes([chunk[5], chunk[6]]);
639        if let Err(ec) = file_record::validate_range(file, record, usize::from(length)) {
640            return encode_exception(FunctionCode::ReadFileRecord.exception_code(), ec);
641        }
642        let requested_count = usize::from(length);
643        if requested_count > MAX_FILE_RECORD_REGISTERS {
644            return encode_exception(
645                FunctionCode::ReadFileRecord.exception_code(),
646                ExceptionCode::IllegalDataAddress,
647            );
648        }
649        let group_start = response.len();
650        let value_byte_count = requested_count * 2;
651        response.resize(group_start + 2 + value_byte_count, 0);
652        match store
653            .read_file_record_be(
654                file,
655                record,
656                length,
657                &mut response[group_start + 2..group_start + 2 + value_byte_count],
658            )
659            .await
660        {
661            Ok(n) => {
662                let n = match validate_store_count(n, length, requested_count) {
663                    Ok(n) => n,
664                    Err(ec) => {
665                        return encode_exception(FunctionCode::ReadFileRecord.exception_code(), ec);
666                    }
667                };
668                // Each sub-response is [resp_len][ref_type=6][2*N data]; resp_len
669                // counts the ref-type byte plus the data, excluding itself.
670                let resp_len = 1 + 2 * n;
671                let resp_len = match checked_response_u8(resp_len, FunctionCode::ReadFileRecord) {
672                    Ok(resp_len) => resp_len,
673                    Err(resp) => return resp,
674                };
675                response[group_start] = resp_len;
676                response[group_start + 1] = 0x06;
677            }
678            Err(ec) => return encode_exception(FunctionCode::ReadFileRecord.exception_code(), ec),
679        }
680        // Response PDU is FC + byte_count(1) + data, capped at 253 bytes.
681        // FC14 sub-response groups are even-sized, so the largest valid
682        // byte_count is 250.
683        if response.len() - 2 > 250 {
684            return encode_exception(
685                FunctionCode::ReadFileRecord.exception_code(),
686                ExceptionCode::IllegalDataValue,
687            );
688        }
689    }
690    let byte_count = match checked_response_u8(response.len() - 2, FunctionCode::ReadFileRecord) {
691        Ok(byte_count) => byte_count,
692        Err(resp) => return resp,
693    };
694    response[1] = byte_count;
695    response
696}
697
698async fn apply_write_file_record<S: DataStore>(
699    req: &WriteFileRecordRequest<'_>,
700    store: &S,
701) -> Result<(), ExceptionCode> {
702    const MAX_WRITE_FILE_RECORD_REQUEST_BYTES: usize = 0xFB;
703    const MIN_WRITE_FILE_RECORD_SUB_REQUEST_BYTES: usize = 9;
704    const MAX_WRITE_FILE_RECORD_GROUPS: usize =
705        MAX_WRITE_FILE_RECORD_REQUEST_BYTES / MIN_WRITE_FILE_RECORD_SUB_REQUEST_BYTES;
706
707    #[derive(Clone, Copy)]
708    struct WriteFileRecordGroup<'a> {
709        file: u16,
710        record: u16,
711        length: u16,
712        value_bytes: &'a [u8],
713    }
714
715    // §6.15 field table: request data length must be 0x09..=0xFB.
716    if !(0x09..=0xFB).contains(&req.byte_count) {
717        return Err(ExceptionCode::IllegalDataValue);
718    }
719    // Pass 1: validate framing and collect every sub-request *before* writing, so
720    // a malformed later sub-request rejects the whole request without committing
721    // the earlier ones (write is atomic with respect to framing errors).
722    let mut subs = req.sub_requests;
723    let mut groups = [WriteFileRecordGroup {
724        file: 0,
725        record: 0,
726        length: 0,
727        value_bytes: &[],
728    }; MAX_WRITE_FILE_RECORD_GROUPS];
729    let mut group_count = 0;
730    while !subs.is_empty() {
731        // Sub-request header [ref_type][file Hi/Lo][record Hi/Lo][len Hi/Lo]
732        // followed by len*2 data bytes (§6.15).
733        if subs.len() < 7 {
734            return Err(ExceptionCode::IllegalDataValue);
735        }
736        if subs[0] != 6 {
737            return Err(ExceptionCode::IllegalDataAddress);
738        }
739        let file = u16::from_be_bytes([subs[1], subs[2]]);
740        let record = u16::from_be_bytes([subs[3], subs[4]]);
741        let length = u16::from_be_bytes([subs[5], subs[6]]);
742        let data_end = 7 + 2 * usize::from(length);
743        if subs.len() < data_end {
744            return Err(ExceptionCode::IllegalDataValue);
745        }
746        file_record::validate_range(file, record, usize::from(length))?;
747        if group_count == groups.len() {
748            return Err(ExceptionCode::IllegalDataValue);
749        }
750        groups[group_count] = WriteFileRecordGroup {
751            file,
752            record,
753            length,
754            value_bytes: &subs[7..data_end],
755        };
756        group_count += 1;
757        subs = &subs[data_end..];
758    }
759    // Pass 2: apply the validated sub-requests.
760    for group in &groups[..group_count] {
761        store
762            .write_file_record_be(group.file, group.record, group.length, group.value_bytes)
763            .await?;
764    }
765    Ok(())
766}
767
768async fn handle_read_fifo_queue<S: DataStore>(address: Address, store: &S) -> Vec<u8> {
769    const MAX_FIFO_VALUE_BYTES: usize = MAX_FIFO_VALUES as usize * 2;
770
771    let mut response = vec![0u8; 5 + MAX_FIFO_VALUE_BYTES];
772    response[0] = FunctionCode::ReadFifoQueue.code();
773
774    match store
775        .read_fifo_queue_be(address.0, &mut response[5..])
776        .await
777    {
778        Ok(count) => {
779            // §6.18 / Figure 28: at most 31 values. The store call ran first, so
780            // an unknown address still wins as 0x02 over this 0x03.
781            if count > usize::from(MAX_FIFO_VALUES) {
782                return encode_exception(
783                    FunctionCode::ReadFifoQueue.exception_code(),
784                    ExceptionCode::IllegalDataValue,
785                );
786            }
787            let byte_count = match checked_response_u16(2 + count * 2, FunctionCode::ReadFifoQueue)
788            {
789                Ok(byte_count) => byte_count,
790                Err(resp) => return resp,
791            };
792            let fifo_count = match checked_response_u16(count, FunctionCode::ReadFifoQueue) {
793                Ok(fifo_count) => fifo_count,
794                Err(resp) => return resp,
795            };
796            response[1..3].copy_from_slice(&byte_count.to_be_bytes());
797            response[3..5].copy_from_slice(&fifo_count.to_be_bytes());
798            response.truncate(5 + count * 2);
799            response
800        }
801        Err(ec) => encode_exception(FunctionCode::ReadFifoQueue.exception_code(), ec),
802    }
803}
804
805fn checked_response_u8(value: usize, fc: FunctionCode) -> Result<u8, Vec<u8>> {
806    u8::try_from(value)
807        .map_err(|_| encode_exception(fc.exception_code(), ExceptionCode::ServerDeviceFailure))
808}
809
810fn checked_response_u16(value: usize, fc: FunctionCode) -> Result<u16, Vec<u8>> {
811    u16::try_from(value)
812        .map_err(|_| encode_exception(fc.exception_code(), ExceptionCode::ServerDeviceFailure))
813}
814
815fn validate_store_count(
816    count: usize,
817    requested: u16,
818    capacity: usize,
819) -> Result<usize, ExceptionCode> {
820    if count == usize::from(requested) && count <= capacity {
821        Ok(count)
822    } else {
823        Err(ExceptionCode::ServerDeviceFailure)
824    }
825}
826
827fn encode_exception(fc_with_flag: u8, ec: ExceptionCode) -> Vec<u8> {
828    debug!(
829        function_code = fc_with_flag & 0x7F,
830        exception_function_code = fc_with_flag,
831        exception_code = ec.code(),
832        exception = ?ec,
833        "encoding Modbus exception response"
834    );
835    vec![fc_with_flag, ec.code()]
836}