use bytes::{BufMut, BytesMut};
use rusty_modbus_types::{MAX_PDU_SIZE, MAX_RTU_ADU_SIZE};
use tokio_util::codec::{Decoder, Encoder};
use crate::crc::{crc16, crc16_update};
use crate::error::FrameError;
use crate::frame::{Frame, FrameHeader};
const MIN_RTU_FRAME: usize = 4;
const MIN_PDU_LENGTH: usize = 1;
#[derive(Debug, Default)]
pub struct RtuOverTcpCodec;
impl Decoder for RtuOverTcpCodec {
type Item = Frame;
type Error = FrameError;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
if src.len() < MIN_RTU_FRAME {
return Ok(None);
}
let max_len = src.len().min(MAX_RTU_ADU_SIZE);
let mut crc = 0xFFFF;
crc = crc16_update(crc, src[0]);
crc = crc16_update(crc, src[1]);
for candidate_len in MIN_RTU_FRAME..=max_len {
let data_end = candidate_len - 2;
let actual = u16::from_le_bytes([src[data_end], src[data_end + 1]]);
if crc == actual {
let unit_id = src[0];
let adu = src.split_to(candidate_len).freeze();
let pdu = adu.slice(1..adu.len() - 2);
return Ok(Some(Frame {
header: FrameHeader::Rtu { unit_id },
pdu,
}));
}
if candidate_len < max_len {
crc = crc16_update(crc, src[data_end]);
}
}
if src.len() > MAX_RTU_ADU_SIZE {
return Err(FrameError::Truncated);
}
Ok(None)
}
}
impl Encoder<Frame> for RtuOverTcpCodec {
type Error = FrameError;
fn encode(&mut self, item: Frame, dst: &mut BytesMut) -> Result<(), Self::Error> {
let unit_id = match item.header {
FrameHeader::Rtu { unit_id } => unit_id,
FrameHeader::Mbap(h) => h.unit_id,
};
validate_outgoing_pdu(item.pdu.len())?;
dst.reserve(1 + item.pdu.len() + 2);
dst.put_u8(unit_id);
dst.put_slice(&item.pdu);
let crc_start = dst.len() - 1 - item.pdu.len();
let crc = crc16(&dst[crc_start..]);
dst.put_u16_le(crc);
Ok(())
}
}
fn validate_outgoing_pdu(pdu_len: usize) -> Result<(), FrameError> {
if pdu_len < MIN_PDU_LENGTH {
return Err(FrameError::InvalidPduLength {
length: pdu_len,
minimum: MIN_PDU_LENGTH,
});
}
if pdu_len > MAX_PDU_SIZE {
return Err(FrameError::PduLengthOverflow {
length: pdu_len,
maximum: MAX_PDU_SIZE,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crc::verify_crc;
fn make_rtu_frame(unit_id: u8, pdu: &[u8]) -> Vec<u8> {
let mut buf = vec![unit_id];
buf.extend_from_slice(pdu);
let crc = crc16(&buf);
buf.extend_from_slice(&crc.to_le_bytes());
buf
}
#[test]
fn decode_single_frame() {
let raw = make_rtu_frame(0x01, &[0x03, 0x00, 0x00, 0x00, 0x0A]);
let mut buf = BytesMut::from(&raw[..]);
let mut codec = RtuOverTcpCodec;
let frame = codec.decode(&mut buf).unwrap().unwrap();
assert_eq!(frame.unit_id(), 0x01);
assert_eq!(&frame.pdu[..], &[0x03, 0x00, 0x00, 0x00, 0x0A]);
assert!(buf.is_empty());
}
#[test]
fn decode_two_back_to_back_frames() {
let frame1 = make_rtu_frame(0x01, &[0x03, 0x02, 0x00, 0x64]);
let frame2 = make_rtu_frame(0x02, &[0x06, 0x00, 0x01, 0x00, 0x03]);
let mut buf = BytesMut::new();
buf.extend_from_slice(&frame1);
buf.extend_from_slice(&frame2);
let mut codec = RtuOverTcpCodec;
let f1 = codec.decode(&mut buf).unwrap().unwrap();
assert_eq!(f1.unit_id(), 0x01);
let f2 = codec.decode(&mut buf).unwrap().unwrap();
assert_eq!(f2.unit_id(), 0x02);
assert!(buf.is_empty());
}
#[test]
fn decode_incomplete_returns_none() {
let raw = make_rtu_frame(0x01, &[0x03, 0x00]);
let mut buf = BytesMut::from(&raw[..3]);
let mut codec = RtuOverTcpCodec;
assert!(codec.decode(&mut buf).unwrap().is_none());
}
#[test]
fn decode_partial_then_complete() {
let raw = make_rtu_frame(0x01, &[0x03, 0x02, 0xAB, 0xCD]);
let mut buf = BytesMut::new();
let mut codec = RtuOverTcpCodec;
buf.extend_from_slice(&raw[..4]);
assert!(codec.decode(&mut buf).unwrap().is_none());
buf.extend_from_slice(&raw[4..]);
let frame = codec.decode(&mut buf).unwrap().unwrap();
assert_eq!(frame.unit_id(), 0x01);
assert_eq!(&frame.pdu[..], &[0x03, 0x02, 0xAB, 0xCD]);
}
#[test]
fn encode_roundtrip() {
let original_pdu = vec![0x03, 0x02, 0x00, 0x64];
let frame = Frame {
header: FrameHeader::Rtu { unit_id: 0x01 },
pdu: bytes::Bytes::from(original_pdu.clone()),
};
let mut dst = BytesMut::new();
let mut codec = RtuOverTcpCodec;
codec.encode(frame, &mut dst).unwrap();
let decoded = codec.decode(&mut dst).unwrap().unwrap();
assert_eq!(decoded.unit_id(), 0x01);
assert_eq!(&decoded.pdu[..], &original_pdu[..]);
}
#[test]
fn encode_rejects_empty_pdu() {
let frame = Frame {
header: FrameHeader::Rtu { unit_id: 0x01 },
pdu: bytes::Bytes::new(),
};
let mut dst = BytesMut::new();
let mut codec = RtuOverTcpCodec;
let err = codec.encode(frame, &mut dst).unwrap_err();
assert!(matches!(err, FrameError::InvalidPduLength { .. }));
}
#[test]
fn encode_rejects_oversized_pdu() {
let frame = Frame {
header: FrameHeader::Rtu { unit_id: 0x01 },
pdu: bytes::Bytes::from(vec![0x03; MAX_PDU_SIZE + 1]),
};
let mut dst = BytesMut::new();
let mut codec = RtuOverTcpCodec;
let err = codec.encode(frame, &mut dst).unwrap_err();
assert!(matches!(err, FrameError::PduLengthOverflow { .. }));
}
#[test]
fn decode_exception_response() {
let raw = make_rtu_frame(0x01, &[0x83, 0x02]);
let mut buf = BytesMut::from(&raw[..]);
let mut codec = RtuOverTcpCodec;
let frame = codec.decode(&mut buf).unwrap().unwrap();
assert_eq!(frame.unit_id(), 0x01);
assert_eq!(&frame.pdu[..], &[0x83, 0x02]);
}
#[test]
fn overflow_returns_error() {
let mut buf = BytesMut::new();
buf.extend_from_slice(&vec![0xAA; MAX_RTU_ADU_SIZE + 1]);
let mut codec = RtuOverTcpCodec;
let err = codec.decode(&mut buf).unwrap_err();
assert!(matches!(err, FrameError::Truncated));
}
#[test]
fn max_len_crc_miss_keeps_buffering() {
let raw = crc_miss_buffer(MAX_RTU_ADU_SIZE);
let mut buf = BytesMut::from(&raw[..]);
let mut codec = RtuOverTcpCodec;
assert!(codec.decode(&mut buf).unwrap().is_none());
assert_eq!(buf.len(), MAX_RTU_ADU_SIZE);
}
fn crc_miss_buffer(len: usize) -> Vec<u8> {
for salt in 0u8..=u8::MAX {
let candidate: Vec<u8> = (0..len)
.map(|i| {
let byte = u8::try_from(i % 251).expect("modulo 251 fits u8");
byte.wrapping_mul(37).wrapping_add(0xA5 ^ salt)
})
.collect();
if (MIN_RTU_FRAME..=len).all(|candidate_len| !verify_crc(&candidate[..candidate_len])) {
return candidate;
}
}
unreachable!("salted deterministic buffers should produce a CRC-miss case");
}
}