Skip to main content

rusty_modbus_server/store/
memory.rs

1//! In-memory data store backed by flat arrays with `RwLock` protection.
2
3use std::collections::HashMap;
4
5use parking_lot::RwLock;
6use rusty_modbus_types::{DiagnosticSubFunction, ExceptionCode};
7
8use crate::file_record::{self, MAX_RECORD_NUMBER, MIN_FILE_NUMBER, RECORD_COUNT};
9
10use super::{
11    DataStore, MAX_DIAGNOSTIC_RESPONSE_DATA_LEN, MAX_FILE_RECORD_REGISTERS, MAX_SERVER_ID_BYTES,
12    bits::BitTable, pack_registers_be, validate_packed_coils, validate_register_values_be,
13};
14
15/// Maximum number of entries in any Modbus data table.
16pub const MAX_TABLE_SIZE: usize = 65_536;
17
18/// Configuration for the in-memory data store.
19#[derive(Debug, Clone)]
20pub struct StoreConfig {
21    /// Number of coils (address space size). Default: 65536.
22    pub coil_count: usize,
23    /// Number of discrete inputs. Default: 65536.
24    pub discrete_input_count: usize,
25    /// Number of holding registers. Default: 65536.
26    pub holding_register_count: usize,
27    /// Number of input registers. Default: 65536.
28    pub input_register_count: usize,
29}
30
31impl Default for StoreConfig {
32    fn default() -> Self {
33        Self {
34            coil_count: 65536,
35            discrete_input_count: 65536,
36            holding_register_count: 65536,
37            input_register_count: 65536,
38        }
39    }
40}
41
42impl StoreConfig {
43    /// Validate configured table sizes before allocating backing vectors.
44    ///
45    /// # Errors
46    ///
47    /// Returns [`StoreError::TableTooLarge`] if any table exceeds the 16-bit
48    /// Modbus address space.
49    pub fn validate(&self) -> Result<(), StoreError> {
50        validate_table_size("coils", self.coil_count)?;
51        validate_table_size("discrete_inputs", self.discrete_input_count)?;
52        validate_table_size("holding_registers", self.holding_register_count)?;
53        validate_table_size("input_registers", self.input_register_count)?;
54        Ok(())
55    }
56}
57
58/// Errors produced by in-memory store configuration and setup helpers.
59#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
60pub enum StoreError {
61    /// A configured table has more entries than Modbus can address.
62    #[error("{table} table size {count} exceeds Modbus address space ({max})")]
63    TableTooLarge {
64        /// Table name.
65        table: &'static str,
66        /// Requested item count.
67        count: usize,
68        /// Maximum supported item count.
69        max: usize,
70    },
71    /// A setup helper addressed outside the configured table.
72    #[error("{table} address {address} is outside configured table size {len}")]
73    AddressOutOfRange {
74        /// Table name.
75        table: &'static str,
76        /// Requested Modbus address.
77        address: u16,
78        /// Configured table length.
79        len: usize,
80    },
81    /// A setup helper used file number 0, which is outside the Modbus range.
82    #[error("file number {file_number} is outside Modbus file range ({minimum}..=65535)")]
83    FileNumberOutOfRange {
84        /// Requested file number.
85        file_number: u16,
86        /// Minimum valid file number.
87        minimum: u16,
88    },
89    /// A setup helper used a file record outside the 10,000-record file range.
90    #[error("file record {record_number} is outside Modbus file record range (0..={maximum})")]
91    FileRecordOutOfRange {
92        /// Requested record number.
93        record_number: u16,
94        /// Maximum valid record number.
95        maximum: u16,
96    },
97}
98
99/// In-memory data store using flat tables with `RwLock` protection.
100pub struct InMemoryStore {
101    coils: RwLock<BitTable>,
102    discrete_inputs: RwLock<BitTable>,
103    holding_registers: RwLock<Vec<u16>>,
104    input_registers: RwLock<Vec<u16>>,
105    /// File records keyed by file number; each `Vec<u16>` is indexed by record
106    /// number (FC 0x14 / 0x15). Files grow lazily on write.
107    files: RwLock<HashMap<u16, Vec<u16>>>,
108    /// FIFO queues keyed by pointer address (FC 0x18).
109    fifo_queues: RwLock<HashMap<u16, Vec<u16>>>,
110    /// Eight exception-status coils packed into one byte (FC 0x07).
111    exception_status: RwLock<u8>,
112    /// Device-specific server-identification blob (FC 0x11).
113    server_id: RwLock<Vec<u8>>,
114}
115
116impl InMemoryStore {
117    /// Create a new in-memory store with the given address space sizes.
118    ///
119    /// # Panics
120    ///
121    /// Panics if any table size exceeds the 16-bit Modbus address space. Use
122    /// [`Self::try_new`] to handle invalid configuration without panicking.
123    #[must_use]
124    pub fn new(config: StoreConfig) -> Self {
125        Self::try_new(config).expect("StoreConfig should fit the Modbus address space")
126    }
127
128    /// Create a new in-memory store with checked address-space sizes.
129    ///
130    /// # Errors
131    ///
132    /// Returns [`StoreError::TableTooLarge`] if any table size exceeds 65,536.
133    pub fn try_new(config: StoreConfig) -> Result<Self, StoreError> {
134        config.validate()?;
135        Ok(Self {
136            coils: RwLock::new(BitTable::new(config.coil_count)),
137            discrete_inputs: RwLock::new(BitTable::new(config.discrete_input_count)),
138            holding_registers: RwLock::new(vec![0u16; config.holding_register_count]),
139            input_registers: RwLock::new(vec![0u16; config.input_register_count]),
140            files: RwLock::new(HashMap::new()),
141            fifo_queues: RwLock::new(HashMap::new()),
142            exception_status: RwLock::new(0),
143            server_id: RwLock::new(b"rusty-modbus\xFF".to_vec()),
144        })
145    }
146
147    /// Direct write to an input register (for application-level updates).
148    ///
149    /// # Errors
150    ///
151    /// Returns [`StoreError::AddressOutOfRange`] if `address` is outside the
152    /// configured input-register table.
153    pub fn set_input_register(&self, address: u16, value: u16) -> Result<(), StoreError> {
154        let mut regs = self.input_registers.write();
155        let index = check_setup_address("input_registers", address, regs.len())?;
156        regs[index] = value;
157        Ok(())
158    }
159
160    /// Direct write to a discrete input (for application-level updates).
161    ///
162    /// # Errors
163    ///
164    /// Returns [`StoreError::AddressOutOfRange`] if `address` is outside the
165    /// configured discrete-input table.
166    pub fn set_discrete_input(&self, address: u16, value: bool) -> Result<(), StoreError> {
167        let mut inputs = self.discrete_inputs.write();
168        let index = check_setup_address("discrete_inputs", address, inputs.len())?;
169        inputs.set(index, value);
170        Ok(())
171    }
172
173    /// Direct write to a holding register (for test setup).
174    ///
175    /// # Errors
176    ///
177    /// Returns [`StoreError::AddressOutOfRange`] if `address` is outside the
178    /// configured holding-register table.
179    pub fn set_holding_register(&self, address: u16, value: u16) -> Result<(), StoreError> {
180        let mut regs = self.holding_registers.write();
181        let index = check_setup_address("holding_registers", address, regs.len())?;
182        regs[index] = value;
183        Ok(())
184    }
185
186    /// Direct write to a coil (for test setup).
187    ///
188    /// # Errors
189    ///
190    /// Returns [`StoreError::AddressOutOfRange`] if `address` is outside the
191    /// configured coil table.
192    pub fn set_coil(&self, address: u16, value: bool) -> Result<(), StoreError> {
193        let mut coils = self.coils.write();
194        let index = check_setup_address("coils", address, coils.len())?;
195        coils.set(index, value);
196        Ok(())
197    }
198
199    /// Seed a single file-record register (for test/app setup). The file and
200    /// record grow lazily so sparse records can be set in any order.
201    ///
202    /// # Errors
203    ///
204    /// Returns [`StoreError`] if the file or record number is outside the
205    /// Modbus file-record address space.
206    pub fn set_file_record(
207        &self,
208        file_number: u16,
209        record_number: u16,
210        value: u16,
211    ) -> Result<(), StoreError> {
212        check_setup_file_record(file_number, record_number)?;
213        let mut files = self.files.write();
214        let file = files.entry(file_number).or_default();
215        let idx = usize::from(record_number);
216        if idx >= file.len() {
217            file.resize(idx + 1, 0);
218        }
219        file[idx] = value;
220        Ok(())
221    }
222
223    /// Seed a FIFO queue at `address` with `values` (for test/app setup).
224    pub fn set_fifo_queue(&self, address: u16, values: Vec<u16>) {
225        self.fifo_queues.write().insert(address, values);
226    }
227
228    /// Set the eight exception-status coils (FC 0x07) as one packed byte.
229    pub fn set_exception_status(&self, status: u8) {
230        *self.exception_status.write() = status;
231    }
232
233    /// Set the device-specific server-identification blob (FC 0x11).
234    pub fn set_server_id(&self, data: Vec<u8>) {
235        *self.server_id.write() = data;
236    }
237}
238
239impl std::fmt::Debug for InMemoryStore {
240    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        f.debug_struct("InMemoryStore")
242            .field("coils", &self.coils.read().len())
243            .field("holding_registers", &self.holding_registers.read().len())
244            .finish_non_exhaustive()
245    }
246}
247
248fn check_range(address: u16, quantity: usize, max: usize) -> Result<(), ExceptionCode> {
249    let end = usize::from(address)
250        .checked_add(quantity)
251        .ok_or(ExceptionCode::IllegalDataAddress)?;
252    if end > max {
253        return Err(ExceptionCode::IllegalDataAddress);
254    }
255    Ok(())
256}
257
258fn validate_table_size(table: &'static str, count: usize) -> Result<(), StoreError> {
259    if count > MAX_TABLE_SIZE {
260        return Err(StoreError::TableTooLarge {
261            table,
262            count,
263            max: MAX_TABLE_SIZE,
264        });
265    }
266    Ok(())
267}
268
269fn check_setup_address(table: &'static str, address: u16, len: usize) -> Result<usize, StoreError> {
270    let index = usize::from(address);
271    if index >= len {
272        return Err(StoreError::AddressOutOfRange {
273            table,
274            address,
275            len,
276        });
277    }
278    Ok(index)
279}
280
281fn check_setup_file_record(file_number: u16, record_number: u16) -> Result<(), StoreError> {
282    if file_number < MIN_FILE_NUMBER {
283        return Err(StoreError::FileNumberOutOfRange {
284            file_number,
285            minimum: MIN_FILE_NUMBER,
286        });
287    }
288    if usize::from(record_number) >= RECORD_COUNT {
289        return Err(StoreError::FileRecordOutOfRange {
290            record_number,
291            maximum: MAX_RECORD_NUMBER,
292        });
293    }
294    Ok(())
295}
296
297impl DataStore for InMemoryStore {
298    async fn read_coils(
299        &self,
300        address: u16,
301        quantity: u16,
302        buf: &mut [bool],
303    ) -> Result<usize, ExceptionCode> {
304        let coils = self.coils.read();
305        coils.read_bits(address, quantity, buf)
306    }
307
308    async fn read_coils_packed(
309        &self,
310        address: u16,
311        quantity: u16,
312        out: &mut [u8],
313    ) -> Result<usize, ExceptionCode> {
314        let coils = self.coils.read();
315        coils.read_packed(address, quantity, out)
316    }
317
318    async fn write_coil(&self, address: u16, value: bool) -> Result<(), ExceptionCode> {
319        let mut coils = self.coils.write();
320        check_range(address, 1, coils.len())?;
321        coils.set(usize::from(address), value);
322        Ok(())
323    }
324
325    async fn write_coils(&self, address: u16, values: &[bool]) -> Result<(), ExceptionCode> {
326        let mut coils = self.coils.write();
327        coils.write_bits(address, values)
328    }
329
330    async fn write_coils_packed(
331        &self,
332        address: u16,
333        quantity: u16,
334        packed_values: &[u8],
335    ) -> Result<(), ExceptionCode> {
336        let quantity = validate_packed_coils(quantity, packed_values)?;
337        let mut coils = self.coils.write();
338        coils.write_packed(address, quantity, packed_values)
339    }
340
341    async fn read_discrete_inputs(
342        &self,
343        address: u16,
344        quantity: u16,
345        buf: &mut [bool],
346    ) -> Result<usize, ExceptionCode> {
347        let inputs = self.discrete_inputs.read();
348        inputs.read_bits(address, quantity, buf)
349    }
350
351    async fn read_discrete_inputs_packed(
352        &self,
353        address: u16,
354        quantity: u16,
355        out: &mut [u8],
356    ) -> Result<usize, ExceptionCode> {
357        let inputs = self.discrete_inputs.read();
358        inputs.read_packed(address, quantity, out)
359    }
360
361    async fn read_holding_registers(
362        &self,
363        address: u16,
364        quantity: u16,
365        buf: &mut [u16],
366    ) -> Result<usize, ExceptionCode> {
367        let regs = self.holding_registers.read();
368        check_range(address, usize::from(quantity), regs.len())?;
369        let start = address as usize;
370        let qty = quantity as usize;
371        buf[..qty].copy_from_slice(&regs[start..start + qty]);
372        Ok(qty)
373    }
374
375    async fn read_holding_registers_be(
376        &self,
377        address: u16,
378        quantity: u16,
379        out: &mut [u8],
380    ) -> Result<usize, ExceptionCode> {
381        let regs = self.holding_registers.read();
382        check_range(address, usize::from(quantity), regs.len())?;
383        let start = address as usize;
384        let qty = quantity as usize;
385        pack_registers_be(&regs[start..start + qty], out)?;
386        Ok(qty)
387    }
388
389    async fn write_register(&self, address: u16, value: u16) -> Result<(), ExceptionCode> {
390        let mut regs = self.holding_registers.write();
391        check_range(address, 1, regs.len())?;
392        regs[address as usize] = value;
393        Ok(())
394    }
395
396    async fn write_registers(&self, address: u16, values: &[u16]) -> Result<(), ExceptionCode> {
397        let mut regs = self.holding_registers.write();
398        check_range(address, values.len(), regs.len())?;
399        let start = address as usize;
400        regs[start..start + values.len()].copy_from_slice(values);
401        Ok(())
402    }
403
404    async fn write_registers_be(
405        &self,
406        address: u16,
407        quantity: u16,
408        value_bytes: &[u8],
409    ) -> Result<(), ExceptionCode> {
410        let quantity = validate_register_values_be(quantity, value_bytes)?;
411        let mut regs = self.holding_registers.write();
412        check_range(address, quantity, regs.len())?;
413        let start = address as usize;
414        for (slot, chunk) in regs[start..start + quantity]
415            .iter_mut()
416            .zip(value_bytes.chunks_exact(2))
417        {
418            *slot = u16::from_be_bytes([chunk[0], chunk[1]]);
419        }
420        Ok(())
421    }
422
423    async fn read_input_registers(
424        &self,
425        address: u16,
426        quantity: u16,
427        buf: &mut [u16],
428    ) -> Result<usize, ExceptionCode> {
429        let regs = self.input_registers.read();
430        check_range(address, usize::from(quantity), regs.len())?;
431        let start = address as usize;
432        let qty = quantity as usize;
433        buf[..qty].copy_from_slice(&regs[start..start + qty]);
434        Ok(qty)
435    }
436
437    async fn read_input_registers_be(
438        &self,
439        address: u16,
440        quantity: u16,
441        out: &mut [u8],
442    ) -> Result<usize, ExceptionCode> {
443        let regs = self.input_registers.read();
444        check_range(address, usize::from(quantity), regs.len())?;
445        let start = address as usize;
446        let qty = quantity as usize;
447        pack_registers_be(&regs[start..start + qty], out)?;
448        Ok(qty)
449    }
450
451    async fn read_file_record(
452        &self,
453        file_number: u16,
454        record_number: u16,
455        record_length: u16,
456        buf: &mut [u16],
457    ) -> Result<usize, ExceptionCode> {
458        file_record::validate_range(file_number, record_number, usize::from(record_length))?;
459        let files = self.files.read();
460        let file = files
461            .get(&file_number)
462            .ok_or(ExceptionCode::IllegalDataAddress)?;
463        let start = usize::from(record_number);
464        let len = usize::from(record_length);
465        let end = start
466            .checked_add(len)
467            .ok_or(ExceptionCode::IllegalDataAddress)?;
468        // The record range must exist in the file, and must fit the caller's
469        // scratch buffer (the handler caps a single sub-response at the PDU limit).
470        if end > file.len() || len > buf.len() {
471            return Err(ExceptionCode::IllegalDataAddress);
472        }
473        buf[..len].copy_from_slice(&file[start..end]);
474        Ok(len)
475    }
476
477    async fn read_file_record_be(
478        &self,
479        file_number: u16,
480        record_number: u16,
481        record_length: u16,
482        out: &mut [u8],
483    ) -> Result<usize, ExceptionCode> {
484        let len = usize::from(record_length);
485        file_record::validate_range(file_number, record_number, len)?;
486        if len > MAX_FILE_RECORD_REGISTERS {
487            return Err(ExceptionCode::IllegalDataAddress);
488        }
489        let files = self.files.read();
490        let file = files
491            .get(&file_number)
492            .ok_or(ExceptionCode::IllegalDataAddress)?;
493        let start = usize::from(record_number);
494        let end = start
495            .checked_add(len)
496            .ok_or(ExceptionCode::IllegalDataAddress)?;
497        if end > file.len() {
498            return Err(ExceptionCode::IllegalDataAddress);
499        }
500        pack_registers_be(&file[start..end], out)?;
501        Ok(len)
502    }
503
504    async fn write_file_record(
505        &self,
506        file_number: u16,
507        record_number: u16,
508        values: &[u16],
509    ) -> Result<(), ExceptionCode> {
510        file_record::validate_range(file_number, record_number, values.len())?;
511        let mut files = self.files.write();
512        let file = files.entry(file_number).or_default();
513        let start = usize::from(record_number);
514        let end = start
515            .checked_add(values.len())
516            .ok_or(ExceptionCode::IllegalDataAddress)?;
517        // Scratch-pad semantics: an in-memory store auto-creates and extends
518        // files on write.
519        if end > file.len() {
520            file.resize(end, 0);
521        }
522        file[start..end].copy_from_slice(values);
523        Ok(())
524    }
525
526    async fn write_file_record_be(
527        &self,
528        file_number: u16,
529        record_number: u16,
530        record_length: u16,
531        value_bytes: &[u8],
532    ) -> Result<(), ExceptionCode> {
533        let len = usize::from(record_length);
534        if value_bytes.len() != len * 2 {
535            return Err(ExceptionCode::IllegalDataValue);
536        }
537        file_record::validate_range(file_number, record_number, len)?;
538        let mut files = self.files.write();
539        let file = files.entry(file_number).or_default();
540        let start = usize::from(record_number);
541        let end = start
542            .checked_add(len)
543            .ok_or(ExceptionCode::IllegalDataAddress)?;
544        if end > file.len() {
545            file.resize(end, 0);
546        }
547        for (slot, chunk) in file[start..end].iter_mut().zip(value_bytes.chunks_exact(2)) {
548            *slot = u16::from_be_bytes([chunk[0], chunk[1]]);
549        }
550        Ok(())
551    }
552
553    async fn read_fifo_queue(&self, address: u16) -> Result<Vec<u16>, ExceptionCode> {
554        // `.cloned()` honors the non-destructive read contract (ยง6.18).
555        self.fifo_queues
556            .read()
557            .get(&address)
558            .cloned()
559            .ok_or(ExceptionCode::IllegalDataAddress)
560    }
561
562    async fn read_fifo_queue_be(
563        &self,
564        address: u16,
565        out: &mut [u8],
566    ) -> Result<usize, ExceptionCode> {
567        let queues = self.fifo_queues.read();
568        let values = queues
569            .get(&address)
570            .ok_or(ExceptionCode::IllegalDataAddress)?;
571        if values.len() > usize::from(rusty_modbus_types::MAX_FIFO_VALUES) {
572            return Err(ExceptionCode::IllegalDataValue);
573        }
574        pack_registers_be(values, out)?;
575        Ok(values.len())
576    }
577
578    async fn read_exception_status(&self) -> Result<u8, ExceptionCode> {
579        Ok(*self.exception_status.read())
580    }
581
582    async fn report_server_id(&self) -> Result<Vec<u8>, ExceptionCode> {
583        Ok(self.server_id.read().clone())
584    }
585
586    async fn append_server_id(&self, out: &mut Vec<u8>) -> Result<usize, ExceptionCode> {
587        let server_id = self.server_id.read();
588        if server_id.len() > MAX_SERVER_ID_BYTES {
589            return Err(ExceptionCode::ServerDeviceFailure);
590        }
591        out.extend_from_slice(&server_id);
592        Ok(server_id.len())
593    }
594
595    async fn append_diagnostic_response(
596        &self,
597        sub_function: DiagnosticSubFunction,
598        data: &[u8],
599        out: &mut Vec<u8>,
600    ) -> Result<Option<usize>, ExceptionCode> {
601        match sub_function {
602            DiagnosticSubFunction::ReturnQueryData => {
603                if data.len() > MAX_DIAGNOSTIC_RESPONSE_DATA_LEN {
604                    return Err(ExceptionCode::ServerDeviceFailure);
605                }
606                out.extend_from_slice(data);
607                Ok(Some(data.len()))
608            }
609            _ => Err(ExceptionCode::IllegalFunction),
610        }
611    }
612}