Skip to main content

rusty_modbus_codec/request/
file.rs

1//! File record requests: FC 14 (Read File Record) and FC 15 (Write File Record).
2
3use rusty_modbus_types::FunctionCode;
4
5use crate::error::{DecodeError, EncodeError};
6use crate::request::Encode;
7
8const READ_FILE_RECORD_MIN_BYTE_COUNT: usize = 0x07;
9const READ_FILE_RECORD_MAX_BYTE_COUNT: usize = 0xF5;
10const WRITE_FILE_RECORD_MIN_BYTE_COUNT: usize = 0x09;
11const WRITE_FILE_RECORD_MAX_BYTE_COUNT: usize = 0xFB;
12const FILE_RECORD_REFERENCE_TYPE: u8 = 0x06;
13const MIN_FILE_NUMBER: u16 = 0x0001;
14const MAX_RECORD_NUMBER: u16 = 0x270F;
15const RECORD_COUNT: usize = 0x2710;
16
17fn check_file_record_byte_count(
18    byte_count: u8,
19    minimum: usize,
20    maximum: usize,
21) -> Result<(), DecodeError> {
22    let count = usize::from(byte_count);
23    if (minimum..=maximum).contains(&count) {
24        Ok(())
25    } else {
26        Err(DecodeError::ByteCountOutOfRange {
27            count,
28            minimum,
29            maximum,
30        })
31    }
32}
33
34fn check_file_record_range(
35    file_number: u16,
36    record_number: u16,
37    record_length: u16,
38) -> Result<(), DecodeError> {
39    let end = usize::from(record_number)
40        .checked_add(usize::from(record_length))
41        .ok_or(DecodeError::FileRecordOutOfRange {
42            file_number,
43            record_number,
44            record_length,
45        })?;
46    if file_number < MIN_FILE_NUMBER
47        || record_length == 0
48        || record_number > MAX_RECORD_NUMBER
49        || end > RECORD_COUNT
50    {
51        return Err(DecodeError::FileRecordOutOfRange {
52            file_number,
53            record_number,
54            record_length,
55        });
56    }
57    Ok(())
58}
59
60fn check_file_record_range_encode(
61    file_number: u16,
62    record_number: u16,
63    record_length: u16,
64) -> Result<(), EncodeError> {
65    let end = usize::from(record_number)
66        .checked_add(usize::from(record_length))
67        .ok_or(EncodeError::FileRecordOutOfRange {
68            file_number,
69            record_number,
70            record_length,
71        })?;
72    if file_number < MIN_FILE_NUMBER
73        || record_length == 0
74        || record_number > MAX_RECORD_NUMBER
75        || end > RECORD_COUNT
76    {
77        return Err(EncodeError::FileRecordOutOfRange {
78            file_number,
79            record_number,
80            record_length,
81        });
82    }
83    Ok(())
84}
85
86fn validate_read_file_sub_requests(sub_requests: &[u8]) -> Result<(), DecodeError> {
87    if !sub_requests.len().is_multiple_of(7) {
88        return Err(DecodeError::InvalidFileRecordLength {
89            length: sub_requests.len(),
90        });
91    }
92    for chunk in sub_requests.chunks_exact(7) {
93        FileSubRequest::decode(chunk)?;
94    }
95    Ok(())
96}
97
98fn validate_read_file_sub_requests_encode(sub_requests: &[u8]) -> Result<(), EncodeError> {
99    if !sub_requests.len().is_multiple_of(7) {
100        return Err(EncodeError::InvalidFileRecordLength {
101            length: sub_requests.len(),
102        });
103    }
104    for chunk in sub_requests.chunks_exact(7) {
105        validate_file_sub_request_encode(chunk)?;
106    }
107    Ok(())
108}
109
110fn validate_write_file_sub_requests(sub_requests: &[u8]) -> Result<(), DecodeError> {
111    let mut remaining = sub_requests;
112    while !remaining.is_empty() {
113        if remaining.len() < 7 {
114            return Err(DecodeError::InvalidFileRecordLength {
115                length: remaining.len(),
116            });
117        }
118        let sub = FileSubRequest::decode(&remaining[..7])?;
119        let value_bytes = usize::from(sub.record_length) * 2;
120        let group_len = 7 + value_bytes;
121        if remaining.len() < group_len {
122            return Err(DecodeError::ByteCountMismatch {
123                declared: group_len,
124                actual: remaining.len(),
125            });
126        }
127        remaining = &remaining[group_len..];
128    }
129    Ok(())
130}
131
132fn validate_write_file_sub_requests_encode(sub_requests: &[u8]) -> Result<(), EncodeError> {
133    let mut remaining = sub_requests;
134    while !remaining.is_empty() {
135        if remaining.len() < 7 {
136            return Err(EncodeError::InvalidFileRecordLength {
137                length: remaining.len(),
138            });
139        }
140        let sub = validate_file_sub_request_encode(&remaining[..7])?;
141        let value_bytes = usize::from(sub.record_length) * 2;
142        let group_len = 7 + value_bytes;
143        if remaining.len() < group_len {
144            return Err(EncodeError::ByteCountMismatch {
145                declared: group_len,
146                actual: remaining.len(),
147            });
148        }
149        remaining = &remaining[group_len..];
150    }
151    Ok(())
152}
153
154fn validate_file_sub_request_encode(data: &[u8]) -> Result<FileSubRequest, EncodeError> {
155    if data.len() < 7 {
156        return Err(EncodeError::InvalidFileRecordLength { length: data.len() });
157    }
158    let reference_type = data[0];
159    if reference_type != FILE_RECORD_REFERENCE_TYPE {
160        return Err(EncodeError::InvalidReferenceType(reference_type));
161    }
162    let file_number = u16::from_be_bytes([data[1], data[2]]);
163    let record_number = u16::from_be_bytes([data[3], data[4]]);
164    let record_length = u16::from_be_bytes([data[5], data[6]]);
165    check_file_record_range_encode(file_number, record_number, record_length)?;
166    Ok(FileSubRequest {
167        reference_type,
168        file_number,
169        record_number,
170        record_length,
171    })
172}
173
174/// A single file sub-request record (7 bytes on the wire).
175///
176/// Used inside both Read File Record and Write File Record requests.
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub struct FileSubRequest {
179    /// Reference type — must be 6.
180    pub reference_type: u8,
181    /// File number.
182    pub file_number: u16,
183    /// Starting record number within the file.
184    pub record_number: u16,
185    /// Number of records to read/write.
186    pub record_length: u16,
187}
188
189impl FileSubRequest {
190    /// Decode a single sub-request from a 7-byte slice.
191    ///
192    /// # Errors
193    ///
194    /// Returns [`DecodeError::Truncated`] if `data` is shorter than 7 bytes.
195    /// Returns [`DecodeError::LengthMismatch`] if `data` has extra bytes.
196    /// Returns [`DecodeError::InvalidReferenceType`] if the reference type is not 6.
197    /// Returns [`DecodeError::FileRecordOutOfRange`] if file or record fields are
198    /// outside the Modbus file-record model.
199    pub fn decode(data: &[u8]) -> Result<Self, DecodeError> {
200        DecodeError::check_exact_len(data, 7)?;
201        let reference_type = data[0];
202        if reference_type != FILE_RECORD_REFERENCE_TYPE {
203            return Err(DecodeError::InvalidReferenceType(reference_type));
204        }
205        let file_number = u16::from_be_bytes([data[1], data[2]]);
206        let record_number = u16::from_be_bytes([data[3], data[4]]);
207        let record_length = u16::from_be_bytes([data[5], data[6]]);
208        check_file_record_range(file_number, record_number, record_length)?;
209        Ok(Self {
210            reference_type,
211            file_number,
212            record_number,
213            record_length,
214        })
215    }
216}
217
218/// FC 0x14 — Read File Record request.
219///
220/// Contains one or more 7-byte sub-request records packed in `sub_requests`.
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub struct ReadFileRecordRequest<'buf> {
223    /// Total byte count of the sub-request data that follows.
224    pub byte_count: u8,
225    /// Raw sub-request data (each sub-request is 7 bytes).
226    pub sub_requests: &'buf [u8],
227}
228
229impl<'buf> ReadFileRecordRequest<'buf> {
230    /// Decode from PDU data after the function code byte.
231    ///
232    /// # Errors
233    ///
234    /// Returns [`DecodeError::Truncated`] if `data` is too short.
235    /// Returns [`DecodeError::ByteCountMismatch`] if the declared byte count does not
236    /// match the remaining data length.
237    pub fn decode(data: &'buf [u8]) -> Result<Self, DecodeError> {
238        if data.is_empty() {
239            return Err(DecodeError::Truncated {
240                expected: 1,
241                actual: 0,
242            });
243        }
244        let byte_count = data[0];
245        check_file_record_byte_count(
246            byte_count,
247            READ_FILE_RECORD_MIN_BYTE_COUNT,
248            READ_FILE_RECORD_MAX_BYTE_COUNT,
249        )?;
250        let remaining = data.len() - 1;
251        if byte_count as usize != remaining {
252            return Err(DecodeError::ByteCountMismatch {
253                declared: byte_count as usize,
254                actual: remaining,
255            });
256        }
257        let sub_requests = &data[1..];
258        validate_read_file_sub_requests(sub_requests)?;
259        Ok(Self {
260            byte_count,
261            sub_requests,
262        })
263    }
264}
265
266impl Encode for ReadFileRecordRequest<'_> {
267    fn encode_into(&self, buf: &mut [u8]) -> Result<usize, EncodeError> {
268        let len = self.encoded_len();
269        if buf.len() < len {
270            return Err(EncodeError::BufferTooSmall {
271                required: len,
272                available: buf.len(),
273            });
274        }
275        EncodeError::check_byte_count_range(
276            usize::from(self.byte_count),
277            READ_FILE_RECORD_MIN_BYTE_COUNT,
278            READ_FILE_RECORD_MAX_BYTE_COUNT,
279        )?;
280        EncodeError::check_byte_count(usize::from(self.byte_count), self.sub_requests.len())?;
281        validate_read_file_sub_requests_encode(self.sub_requests)?;
282        EncodeError::check_pdu_len(len)?;
283        buf[0] = FunctionCode::ReadFileRecord.code();
284        buf[1] = self.byte_count;
285        buf[2..2 + self.sub_requests.len()].copy_from_slice(self.sub_requests);
286        Ok(len)
287    }
288
289    fn encoded_len(&self) -> usize {
290        // FC(1) + byte_count(1) + sub_requests
291        2 + self.sub_requests.len()
292    }
293}
294
295/// FC 0x15 — Write File Record request.
296///
297/// Contains one or more sub-request records packed in `sub_requests`.
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299pub struct WriteFileRecordRequest<'buf> {
300    /// Total byte count of the sub-request data that follows.
301    pub byte_count: u8,
302    /// Raw sub-request data.
303    pub sub_requests: &'buf [u8],
304}
305
306impl<'buf> WriteFileRecordRequest<'buf> {
307    /// Decode from PDU data after the function code byte.
308    ///
309    /// # Errors
310    ///
311    /// Returns [`DecodeError::Truncated`] if `data` is too short.
312    /// Returns [`DecodeError::ByteCountMismatch`] if the declared byte count does not
313    /// match the remaining data length.
314    pub fn decode(data: &'buf [u8]) -> Result<Self, DecodeError> {
315        if data.is_empty() {
316            return Err(DecodeError::Truncated {
317                expected: 1,
318                actual: 0,
319            });
320        }
321        let byte_count = data[0];
322        check_file_record_byte_count(
323            byte_count,
324            WRITE_FILE_RECORD_MIN_BYTE_COUNT,
325            WRITE_FILE_RECORD_MAX_BYTE_COUNT,
326        )?;
327        let remaining = data.len() - 1;
328        if byte_count as usize != remaining {
329            return Err(DecodeError::ByteCountMismatch {
330                declared: byte_count as usize,
331                actual: remaining,
332            });
333        }
334        let sub_requests = &data[1..];
335        validate_write_file_sub_requests(sub_requests)?;
336        Ok(Self {
337            byte_count,
338            sub_requests,
339        })
340    }
341}
342
343impl Encode for WriteFileRecordRequest<'_> {
344    fn encode_into(&self, buf: &mut [u8]) -> Result<usize, EncodeError> {
345        let len = self.encoded_len();
346        if buf.len() < len {
347            return Err(EncodeError::BufferTooSmall {
348                required: len,
349                available: buf.len(),
350            });
351        }
352        EncodeError::check_byte_count_range(
353            usize::from(self.byte_count),
354            WRITE_FILE_RECORD_MIN_BYTE_COUNT,
355            WRITE_FILE_RECORD_MAX_BYTE_COUNT,
356        )?;
357        EncodeError::check_byte_count(usize::from(self.byte_count), self.sub_requests.len())?;
358        validate_write_file_sub_requests_encode(self.sub_requests)?;
359        EncodeError::check_pdu_len(len)?;
360        buf[0] = FunctionCode::WriteFileRecord.code();
361        buf[1] = self.byte_count;
362        buf[2..2 + self.sub_requests.len()].copy_from_slice(self.sub_requests);
363        Ok(len)
364    }
365
366    fn encoded_len(&self) -> usize {
367        // FC(1) + byte_count(1) + sub_requests
368        2 + self.sub_requests.len()
369    }
370}