use bytes::BytesMut;
use super::types::{ModbusExceptionCode, ModbusFunction, ModbusMessage};
pub(crate) const MBAP_HEADER_LEN: usize = 7;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseError {
Incomplete,
BadProtocolId,
InvalidLength,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Incomplete => f.write_str("incomplete Modbus frame (need more bytes)"),
Self::BadProtocolId => f.write_str("bad Modbus/TCP protocol_id (expected 0)"),
Self::InvalidLength => f.write_str("invalid Modbus length field (min 2)"),
}
}
}
impl std::error::Error for ParseError {}
impl From<ParseError> for crate::Error {
fn from(e: ParseError) -> Self {
use crate::error::{ErrorCode, Module};
let code = match &e {
ParseError::Incomplete => ErrorCode::Truncated,
ParseError::BadProtocolId | ParseError::InvalidLength => ErrorCode::Parse,
};
crate::Error::with_code(Module::Modbus, code, e.to_string())
}
}
pub fn parse_one(buf: &[u8]) -> Result<(ModbusMessage, usize), ParseError> {
if buf.len() < MBAP_HEADER_LEN {
return Err(ParseError::Incomplete);
}
let transaction_id = u16::from_be_bytes([buf[0], buf[1]]);
let protocol_id = u16::from_be_bytes([buf[2], buf[3]]);
let length = u16::from_be_bytes([buf[4], buf[5]]) as usize;
if protocol_id != 0 {
return Err(ParseError::BadProtocolId);
}
let total = 6 + length;
if buf.len() < total {
return Err(ParseError::Incomplete);
}
if length < 2 {
return Err(ParseError::InvalidLength);
}
let unit_id = buf[6];
let raw_fn = buf[7];
let is_exception = (raw_fn & 0x80) != 0;
let function = ModbusFunction::from_raw(raw_fn);
let pdu_data = &buf[8..total];
let (address, quantity, exception_code) = if is_exception {
let code = pdu_data.first().map(|&b| ModbusExceptionCode::from_raw(b));
(None, None, code)
} else {
decode_request(&function, pdu_data)
};
Ok((
ModbusMessage {
transaction_id,
unit_id,
raw_function_code: raw_fn,
function,
is_exception,
exception_code,
address,
quantity,
},
total,
))
}
fn decode_request(
fc: &ModbusFunction,
pdu: &[u8],
) -> (Option<u16>, Option<u16>, Option<ModbusExceptionCode>) {
let two_u16 = |data: &[u8]| -> Option<(u16, u16)> {
if data.len() < 4 {
return None;
}
Some((
u16::from_be_bytes([data[0], data[1]]),
u16::from_be_bytes([data[2], data[3]]),
))
};
let address_and_value = match fc {
ModbusFunction::ReadCoils
| ModbusFunction::ReadDiscreteInputs
| ModbusFunction::ReadHoldingRegisters
| ModbusFunction::ReadInputRegisters => two_u16(pdu),
ModbusFunction::WriteSingleCoil | ModbusFunction::WriteSingleRegister => two_u16(pdu),
ModbusFunction::WriteMultipleCoils | ModbusFunction::WriteMultipleRegisters => two_u16(pdu),
ModbusFunction::MaskWriteRegister => two_u16(pdu),
_ => None,
};
match address_and_value {
Some((a, q)) => (Some(a), Some(q), None),
None => (None, None, None),
}
}
pub(crate) fn drain_frames(buf: &mut BytesMut, out: &mut Vec<ModbusMessage>) {
use bytes::Buf;
loop {
let (msg, consumed) = match parse_one(buf) {
Ok(v) => v,
Err(_) => return,
};
out.push(msg);
buf.advance(consumed);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_read_holding(txn: u16, unit: u8, start: u16, qty: u16) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&txn.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&6u16.to_be_bytes()); buf.push(unit);
buf.push(3); buf.extend_from_slice(&start.to_be_bytes());
buf.extend_from_slice(&qty.to_be_bytes());
buf
}
#[test]
fn parse_read_holding_request() {
let buf = build_read_holding(0x1234, 0x01, 0x0064, 0x000a);
let (msg, consumed) = parse_one(&buf).expect("parse");
assert_eq!(consumed, buf.len());
assert_eq!(msg.transaction_id, 0x1234);
assert_eq!(msg.unit_id, 0x01);
assert_eq!(msg.function, ModbusFunction::ReadHoldingRegisters);
assert!(!msg.is_exception);
assert_eq!(msg.address, Some(0x0064));
assert_eq!(msg.quantity, Some(10));
}
#[test]
fn parse_exception_response_decodes_code() {
let mut buf = build_read_holding(0x1234, 0x01, 0x0064, 0x000a);
let length = 3u16; buf[4..6].copy_from_slice(&length.to_be_bytes());
buf[7] = 0x83; buf.truncate(8);
buf.push(0x02); let (msg, _consumed) = parse_one(&buf).expect("parse");
assert!(msg.is_exception);
assert_eq!(msg.function, ModbusFunction::ReadHoldingRegisters);
assert_eq!(
msg.exception_code,
Some(ModbusExceptionCode::IllegalDataAddress)
);
}
#[test]
fn rejects_non_modbus_protocol_id() {
let mut buf = build_read_holding(0x1234, 0x01, 0x0064, 0x000a);
buf[2..4].copy_from_slice(&1u16.to_be_bytes());
assert!(parse_one(&buf).is_err());
}
#[test]
fn rejects_truncated_frame() {
let mut buf = build_read_holding(0x1234, 0x01, 0x0064, 0x000a);
buf.pop();
assert!(parse_one(&buf).is_err());
}
#[test]
fn rejects_too_short_for_header() {
assert!(parse_one(&[0u8; 5]).is_err());
}
#[test]
fn write_single_coil_decoded_as_address_value() {
let mut buf = Vec::new();
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&6u16.to_be_bytes());
buf.push(1);
buf.push(5); buf.extend_from_slice(&0x00abu16.to_be_bytes()); buf.extend_from_slice(&0xff00u16.to_be_bytes()); let (msg, _) = parse_one(&buf).expect("parse");
assert_eq!(msg.function, ModbusFunction::WriteSingleCoil);
assert_eq!(msg.address, Some(0x00ab));
assert_eq!(msg.quantity, Some(0xff00));
}
#[test]
fn unknown_function_is_other_with_value() {
let mut buf = Vec::new();
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&2u16.to_be_bytes());
buf.push(1);
buf.push(0x42);
let (msg, _) = parse_one(&buf).expect("parse");
assert_eq!(msg.function, ModbusFunction::Other(0x42));
assert_eq!(msg.address, None);
assert_eq!(msg.quantity, None);
}
#[test]
fn drain_handles_multiple_frames() {
let a = build_read_holding(0x0001, 1, 0, 2);
let b = build_read_holding(0x0002, 1, 10, 2);
let mut combined = BytesMut::new();
combined.extend_from_slice(&a);
combined.extend_from_slice(&b);
let mut out = Vec::new();
drain_frames(&mut combined, &mut out);
assert_eq!(out.len(), 2);
assert_eq!(out[0].transaction_id, 0x0001);
assert_eq!(out[1].transaction_id, 0x0002);
assert!(combined.is_empty());
}
#[test]
fn drain_stops_at_partial_frame() {
let a = build_read_holding(0x0001, 1, 0, 2);
let mut combined = BytesMut::new();
combined.extend_from_slice(&a);
combined.extend_from_slice(&a[..5]); let mut out = Vec::new();
drain_frames(&mut combined, &mut out);
assert_eq!(out.len(), 1);
assert_eq!(combined.len(), 5, "partial bytes preserved for next call");
}
#[test]
fn function_predicates() {
assert!(ModbusFunction::ReadCoils.is_read());
assert!(ModbusFunction::WriteSingleCoil.is_write());
assert!(!ModbusFunction::ReadCoils.is_write());
assert!(!ModbusFunction::Diagnostics.is_read());
}
}