use super::types::{
DnpAppFunctionKind, DnpApplication, DnpInternalIndications, DnpLinkDirection,
DnpLinkFunctionKind, DnpLinkRole, DnpMessage,
};
const START_BYTES: [u8; 2] = [0x05, 0x64];
const LINK_HEADER_LEN: usize = 10;
const FIRST_BLOCK_DATA: usize = 16;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseError {
Truncated { need: usize, have: usize },
BadStartBytes,
InvalidLength(u8),
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Truncated { need, have } => {
write!(f, "truncated DNP3 link header: need {need}, have {have}")
}
Self::BadStartBytes => f.write_str("bad start bytes (expected 0x0564)"),
Self::InvalidLength(n) => write!(f, "invalid link-layer length: {n} (min 5)"),
}
}
}
impl std::error::Error for ParseError {}
pub fn parse(payload: &[u8]) -> Result<DnpMessage, ParseError> {
if payload.len() < LINK_HEADER_LEN {
return Err(ParseError::Truncated {
need: LINK_HEADER_LEN,
have: payload.len(),
});
}
if payload[0..2] != START_BYTES {
return Err(ParseError::BadStartBytes);
}
let length = payload[2] as usize;
if length < 5 {
return Err(ParseError::InvalidLength(payload[2]));
}
let control = payload[3];
let dst_addr = u16::from_le_bytes([payload[4], payload[5]]);
let src_addr = u16::from_le_bytes([payload[6], payload[7]]);
let link_function = DnpLinkFunctionKind::from_raw(control);
let link_dir = DnpLinkDirection::from_bit((control & 0x80) != 0);
let link_prm = DnpLinkRole::from_bit((control & 0x40) != 0);
let user_data_len = length - 5;
let mut application = None;
if user_data_len > 0 {
let first_block_data = user_data_len.min(FIRST_BLOCK_DATA);
let data_end = LINK_HEADER_LEN + first_block_data;
if payload.len() >= data_end {
application = decode_application(&payload[LINK_HEADER_LEN..data_end]);
}
}
Ok(DnpMessage {
src_addr,
dst_addr,
link_function,
link_dir,
link_prm,
application,
})
}
fn decode_application(buf: &[u8]) -> Option<DnpApplication> {
if buf.len() < 3 {
return None;
}
let _transport = buf[0];
let ac = buf[1]; let raw_function_code = buf[2];
let fir = (ac & 0x80) != 0;
let fin = (ac & 0x40) != 0;
let con = (ac & 0x20) != 0;
let uns = (ac & 0x10) != 0;
let sequence = ac & 0x0F;
let function = DnpAppFunctionKind::from_raw(raw_function_code);
let iin = if function.is_response() && buf.len() >= 5 {
let bits = u16::from_le_bytes([buf[3], buf[4]]);
Some(DnpInternalIndications::from_bits_truncate(bits))
} else {
None
};
Some(DnpApplication {
sequence,
fir,
fin,
con,
uns,
function,
raw_function_code,
iin,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn build_frame(control: u8, dst: u16, src: u16, user_data: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&START_BYTES);
out.push((5 + user_data.len()) as u8);
out.push(control);
out.extend_from_slice(&dst.to_le_bytes());
out.extend_from_slice(&src.to_le_bytes());
out.extend_from_slice(&[0xAA, 0xBB]);
out.extend_from_slice(user_data);
out
}
#[test]
fn parses_minimum_link_only_frame() {
let buf = build_frame(0xC0, 1, 2, &[]);
let msg = parse(&buf).expect("parse");
assert_eq!(msg.src_addr, 2);
assert_eq!(msg.dst_addr, 1);
assert_eq!(msg.link_function, DnpLinkFunctionKind::ResetLinkStates);
assert_eq!(msg.link_dir, DnpLinkDirection::ToOutstation);
assert_eq!(msg.link_prm, DnpLinkRole::Primary);
assert!(msg.application.is_none());
}
#[test]
fn parses_user_data_with_app_header() {
let user_data = &[0xC1, 0xC1, 0x01];
let buf = build_frame(0xC3, 0x000A, 0x0001, user_data); let msg = parse(&buf).expect("parse");
assert_eq!(msg.link_function, DnpLinkFunctionKind::UserData);
let app = msg.application.expect("app");
assert!(app.fir);
assert!(app.fin);
assert!(!app.uns);
assert_eq!(app.sequence, 1);
assert_eq!(app.function, DnpAppFunctionKind::Read);
assert_eq!(app.raw_function_code, 1);
assert!(app.iin.is_none());
}
#[test]
fn response_carries_iin_bits() {
let user_data = &[0xC0, 0xC0, 0x81, 0x82, 0x00];
let buf = build_frame(0xC3, 1, 2, user_data);
let msg = parse(&buf).expect("parse");
let app = msg.application.expect("app");
assert_eq!(app.function, DnpAppFunctionKind::Response);
let iin = app.iin.expect("iin");
assert!(iin.contains(DnpInternalIndications::DEVICE_RESTART));
assert!(iin.contains(DnpInternalIndications::CLASS_1_EVENTS));
}
#[test]
fn rejects_wrong_start_bytes() {
let mut buf = build_frame(0xC0, 1, 2, &[]);
buf[0] = 0x99;
assert_eq!(parse(&buf).unwrap_err(), ParseError::BadStartBytes);
}
#[test]
fn rejects_too_short() {
assert_eq!(
parse(&[0u8; 9]).unwrap_err(),
ParseError::Truncated { need: 10, have: 9 }
);
}
#[test]
fn rejects_invalid_length() {
let mut buf = build_frame(0xC0, 1, 2, &[]);
buf[2] = 0x02; assert_eq!(parse(&buf).unwrap_err(), ParseError::InvalidLength(0x02));
}
#[test]
fn unknown_function_code_preserved_as_other() {
let user_data = &[0xC0, 0xC0, 0x99];
let buf = build_frame(0xC3, 1, 2, user_data);
let msg = parse(&buf).expect("parse");
let app = msg.application.expect("app");
assert_eq!(app.function, DnpAppFunctionKind::Other(0x99));
assert_eq!(app.raw_function_code, 0x99);
}
#[test]
fn link_function_secondary_decodes_to_ack() {
let buf = build_frame(0x00, 1, 2, &[]);
let msg = parse(&buf).expect("parse");
assert_eq!(msg.link_function, DnpLinkFunctionKind::Ack);
assert_eq!(msg.link_prm, DnpLinkRole::Secondary);
}
#[test]
fn link_function_slugs() {
assert_eq!(DnpLinkFunctionKind::UserData.as_str(), "user_data");
assert_eq!(DnpLinkFunctionKind::Ack.as_str(), "ack");
assert_eq!(DnpLinkFunctionKind::NotSupported.as_str(), "not_supported");
}
#[test]
fn app_function_response_predicate() {
assert!(DnpAppFunctionKind::Response.is_response());
assert!(DnpAppFunctionKind::UnsolicitedResponse.is_response());
assert!(DnpAppFunctionKind::AuthenticateResponse.is_response());
assert!(!DnpAppFunctionKind::Read.is_response());
}
}