use crate::error::{Error, Result};
use crate::length::{self, LengthForm};
use alloc::vec::Vec;
use broadcast_common::len::{need_in, need_out};
use broadcast_common::{Parse, Serialize};
pub mod tags {
pub const SB: u8 = 0x80;
pub const RCV: u8 = 0x81;
pub const CREATE_T_C: u8 = 0x82;
pub const C_T_C_REPLY: u8 = 0x83;
pub const DELETE_T_C: u8 = 0x84;
pub const D_T_C_REPLY: u8 = 0x85;
pub const REQUEST_T_C: u8 = 0x86;
pub const NEW_T_C: u8 = 0x87;
pub const T_C_ERROR: u8 = 0x88;
pub const DATA_LAST: u8 = 0xA0;
pub const DATA_MORE: u8 = 0xA1;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct SbValue(pub u8);
impl SbValue {
#[must_use]
pub const fn data_available(self) -> bool {
self.0 & 0x80 != 0
}
#[must_use]
pub const fn new(data_available: bool) -> Self {
Self(if data_available { 0x80 } else { 0x00 })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum DataBlock {
Last,
More,
}
impl DataBlock {
fn from_tag(tag: u8) -> Option<Self> {
match tag {
tags::DATA_LAST => Some(Self::Last),
tags::DATA_MORE => Some(Self::More),
_ => None,
}
}
#[must_use]
pub fn to_tag(self) -> u8 {
match self {
Self::Last => tags::DATA_LAST,
Self::More => tags::DATA_MORE,
}
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Last => "data_last",
Self::More => "data_more",
}
}
}
broadcast_common::impl_spec_display!(DataBlock);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct TcObject {
pub tag: u8,
pub t_c_id: u8,
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: LengthForm,
}
impl TcObject {
#[must_use]
pub const fn is_tc_tag(tag: u8) -> bool {
matches!(
tag,
tags::CREATE_T_C
| tags::C_T_C_REPLY
| tags::DELETE_T_C
| tags::D_T_C_REPLY
| tags::REQUEST_T_C
)
}
}
impl<'a> Parse<'a> for TcObject {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
need_in(bytes, 1, "TcObject")?;
let tag = bytes[0];
if !Self::is_tc_tag(tag) {
return Err(Error::InvalidObject {
what: "TcObject",
reason: "tpdu_tag is not a single-t_c_id connection object",
});
}
let (len, hdr, length_form) = length::decode_form(&bytes[1..])?;
if len != 1 {
return Err(Error::InvalidObject {
what: "TcObject",
reason: "length_field must be 1",
});
}
let t_c_id = *bytes.get(1 + hdr).ok_or(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: 1 + hdr + 1,
have: bytes.len(),
what: "TcObject t_c_id",
},
))?;
Ok(Self {
tag,
t_c_id,
length_form,
})
}
}
impl Serialize for TcObject {
type Error = Error;
fn serialized_len(&self) -> usize {
1 + length::encoded_len_with(1, self.length_form) + 1
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let total = self.serialized_len();
need_out(buf, total)?;
if !Self::is_tc_tag(self.tag) {
return Err(Error::InvalidObject {
what: "TcObject",
reason: "tpdu_tag is not a single-t_c_id connection object",
});
}
buf[0] = self.tag;
let pos = 1 + length::encode_with(1, self.length_form, &mut buf[1..])?;
buf[pos] = self.t_c_id;
Ok(pos + 1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct NewTc {
pub t_c_id: u8,
pub new_t_c_id: u8,
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: LengthForm,
}
impl<'a> Parse<'a> for NewTc {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let (length_form, body) = parse_fixed(bytes, tags::NEW_T_C, 2, "New_T_C")?;
Ok(Self {
t_c_id: body[0],
new_t_c_id: body[1],
length_form,
})
}
}
impl Serialize for NewTc {
type Error = Error;
fn serialized_len(&self) -> usize {
1 + length::encoded_len_with(2, self.length_form) + 2
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
write_fixed(
tags::NEW_T_C,
self.length_form,
&[self.t_c_id, self.new_t_c_id],
buf,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct TcError {
pub t_c_id: u8,
pub error_code: u8,
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: LengthForm,
}
impl<'a> Parse<'a> for TcError {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let (length_form, body) = parse_fixed(bytes, tags::T_C_ERROR, 2, "T_C_Error")?;
Ok(Self {
t_c_id: body[0],
error_code: body[1],
length_form,
})
}
}
impl Serialize for TcError {
type Error = Error;
fn serialized_len(&self) -> usize {
1 + length::encoded_len_with(2, self.length_form) + 2
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
write_fixed(
tags::T_C_ERROR,
self.length_form,
&[self.t_c_id, self.error_code],
buf,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CommandTpdu<'a> {
pub tag: u8,
pub t_c_id: u8,
#[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
pub data: &'a [u8],
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: LengthForm,
}
impl<'a> Parse<'a> for CommandTpdu<'a> {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
need_in(bytes, 1, "C_TPDU")?;
let tag = bytes[0];
let (len, hdr, length_form) = length::decode_form(&bytes[1..])?;
if len == 0 {
return Err(Error::InvalidObject {
what: "C_TPDU",
reason: "length_field must include t_c_id (>=1)",
});
}
let start = 1 + hdr;
let end = start + len;
if bytes.len() < end {
return Err(Error::LengthMismatch(
broadcast_common::error::LengthMismatch {
what: "C_TPDU",
declared: len,
actual: bytes.len().saturating_sub(start),
},
));
}
Ok(Self {
tag,
t_c_id: bytes[start],
data: &bytes[start + 1..end],
length_form,
})
}
}
impl Serialize for CommandTpdu<'_> {
type Error = Error;
fn serialized_len(&self) -> usize {
let len_value = 1 + self.data.len();
1 + length::encoded_len_with(len_value, self.length_form) + len_value
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let len_value = 1 + self.data.len();
let total = self.serialized_len();
need_out(buf, total)?;
buf[0] = self.tag;
let mut pos = 1 + length::encode_with(len_value, self.length_form, &mut buf[1..])?;
buf[pos] = self.t_c_id;
pos += 1;
buf[pos..pos + self.data.len()].copy_from_slice(self.data);
Ok(pos + self.data.len())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ResponseTpdu<'a> {
pub tag: u8,
pub t_c_id: u8,
#[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
pub data: &'a [u8],
pub sb_value: SbValue,
pub block: Option<DataBlock>,
#[cfg_attr(feature = "serde", serde(skip))]
pub length_form: LengthForm,
#[cfg_attr(feature = "serde", serde(skip))]
pub status_length_form: LengthForm,
}
impl<'a> Parse<'a> for ResponseTpdu<'a> {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
need_in(bytes, 1, "R_TPDU")?;
let tag = bytes[0];
let (len, hdr, length_form) = length::decode_form(&bytes[1..])?;
if len == 0 {
return Err(Error::InvalidObject {
what: "R_TPDU",
reason: "length_field must include t_c_id (>=1)",
});
}
let start = 1 + hdr;
let data_end = start + len;
need_in(bytes, data_end + 2, "R_TPDU status")?;
if bytes[data_end] != tags::SB {
return Err(Error::UnexpectedTpduTag {
got: bytes[data_end],
expected: tags::SB,
what: "R_TPDU SB_tag",
});
}
let (sb_len, sb_hdr, status_length_form) = length::decode_form(&bytes[data_end + 1..])?;
if sb_len != 2 {
return Err(Error::InvalidObject {
what: "R_TPDU status",
reason: "SB length_field must be 2",
});
}
let sb_body = data_end + 1 + sb_hdr;
let status_end = sb_body + 2;
need_in(bytes, status_end, "R_TPDU status")?;
if bytes[sb_body] != bytes[start] {
return Err(Error::InvalidObject {
what: "R_TPDU status",
reason: "status t_c_id does not match the TPDU's t_c_id",
});
}
Ok(Self {
tag,
t_c_id: bytes[start],
data: &bytes[start + 1..data_end],
sb_value: SbValue(bytes[sb_body + 1]),
block: DataBlock::from_tag(tag),
length_form,
status_length_form,
})
}
}
impl Serialize for ResponseTpdu<'_> {
type Error = Error;
fn serialized_len(&self) -> usize {
let len_value = 1 + self.data.len();
1 + length::encoded_len_with(len_value, self.length_form)
+ len_value
+ 1
+ length::encoded_len_with(2, self.status_length_form)
+ 2
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let total = self.serialized_len();
need_out(buf, total)?;
let len_value = 1 + self.data.len();
buf[0] = self.tag;
let mut pos = 1 + length::encode_with(len_value, self.length_form, &mut buf[1..])?;
buf[pos] = self.t_c_id;
pos += 1;
buf[pos..pos + self.data.len()].copy_from_slice(self.data);
pos += self.data.len();
buf[pos] = tags::SB;
pos += 1;
pos += length::encode_with(2, self.status_length_form, &mut buf[pos..])?;
buf[pos] = self.t_c_id;
buf[pos + 1] = self.sb_value.0;
Ok(pos + 2)
}
}
fn parse_fixed<'a>(
bytes: &'a [u8],
expected: u8,
body_len: usize,
what: &'static str,
) -> Result<(LengthForm, &'a [u8])> {
if bytes.is_empty() {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: 1,
have: 0,
what,
},
));
}
if bytes[0] != expected {
return Err(Error::UnexpectedTpduTag {
got: bytes[0],
expected,
what,
});
}
let (len, hdr, form) = length::decode_form(&bytes[1..])?;
if len != body_len {
return Err(Error::InvalidObject {
what,
reason: "unexpected length_field",
});
}
let start = 1 + hdr;
let end = start + body_len;
if bytes.len() < end {
return Err(Error::BufferTooShort(
broadcast_common::error::BufferTooShort {
need: end,
have: bytes.len(),
what,
},
));
}
Ok((form, &bytes[start..end]))
}
fn write_fixed(tag: u8, form: LengthForm, body: &[u8], buf: &mut [u8]) -> Result<usize> {
let total = 1 + length::encoded_len_with(body.len(), form) + body.len();
need_out(buf, total)?;
buf[0] = tag;
let pos = 1 + length::encode_with(body.len(), form, &mut buf[1..])?;
buf[pos..pos + body.len()].copy_from_slice(body);
Ok(total)
}
#[must_use]
pub fn create_t_c(t_c_id: u8) -> TcObject {
TcObject {
tag: tags::CREATE_T_C,
t_c_id,
length_form: LengthForm::Minimal,
}
}
pub fn tc_object_bytes(o: &TcObject) -> Result<Vec<u8>> {
o.try_to_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tc_object_rejects_foreign_tags_both_ways() {
for tag in [
tags::SB,
tags::RCV,
tags::NEW_T_C,
tags::T_C_ERROR,
0x00,
0xFF,
] {
let wire = [tag, 0x01, 0x05];
assert!(
matches!(TcObject::parse(&wire), Err(Error::InvalidObject { .. })),
"tag {tag:#04X} must be rejected on parse"
);
let o = TcObject {
length_form: crate::length::LengthForm::Minimal,
tag,
t_c_id: 5,
};
assert!(matches!(
tc_object_bytes(&o),
Err(Error::InvalidObject { .. })
));
}
for tag in [
tags::CREATE_T_C,
tags::C_T_C_REPLY,
tags::DELETE_T_C,
tags::D_T_C_REPLY,
tags::REQUEST_T_C,
] {
let o = TcObject::parse(&[tag, 0x01, 0x05]).unwrap();
assert_eq!(
o,
TcObject {
length_form: crate::length::LengthForm::Minimal,
tag,
t_c_id: 5
}
);
assert_eq!(tc_object_bytes(&o).unwrap(), [tag, 0x01, 0x05]);
}
}
#[test]
fn tc_object_round_trip() {
let o = create_t_c(0x01);
let bytes = o.try_to_bytes().unwrap();
assert_eq!(bytes, [0x82, 0x01, 0x01]);
assert_eq!(TcObject::parse(&bytes).unwrap(), o);
}
#[test]
fn new_tc_round_trip() {
let n = NewTc {
length_form: crate::length::LengthForm::Minimal,
t_c_id: 1,
new_t_c_id: 2,
};
let bytes = n.try_to_bytes().unwrap();
assert_eq!(bytes, [0x87, 0x02, 0x01, 0x02]);
assert_eq!(NewTc::parse(&bytes).unwrap(), n);
}
#[test]
fn tc_error_round_trip() {
let e = TcError {
length_form: crate::length::LengthForm::Minimal,
t_c_id: 3,
error_code: 1,
};
let bytes = e.try_to_bytes().unwrap();
assert_eq!(bytes, [0x88, 0x02, 0x03, 0x01]);
assert_eq!(TcError::parse(&bytes).unwrap(), e);
}
#[test]
fn command_tpdu_round_trip() {
let c = CommandTpdu {
length_form: crate::length::LengthForm::Minimal,
tag: tags::DATA_LAST,
t_c_id: 1,
data: &[0xAA, 0xBB, 0xCC],
};
let bytes = c.try_to_bytes().unwrap();
assert_eq!(bytes, [0xA0, 0x04, 0x01, 0xAA, 0xBB, 0xCC]);
assert_eq!(CommandTpdu::parse(&bytes).unwrap(), c);
}
#[test]
fn receive_data_command_no_payload() {
let c = CommandTpdu {
length_form: crate::length::LengthForm::Minimal,
tag: tags::RCV,
t_c_id: 1,
data: &[],
};
let bytes = c.try_to_bytes().unwrap();
assert_eq!(bytes, [0x81, 0x01, 0x01]);
assert_eq!(CommandTpdu::parse(&bytes).unwrap(), c);
}
#[test]
fn response_tpdu_round_trip_with_status() {
let r = ResponseTpdu {
length_form: crate::length::LengthForm::Minimal,
status_length_form: crate::length::LengthForm::Minimal,
tag: tags::DATA_LAST,
t_c_id: 1,
data: &[0x9F, 0x80, 0x30, 0x00],
sb_value: SbValue::new(true),
block: Some(DataBlock::Last),
};
let bytes = r.try_to_bytes().unwrap();
assert_eq!(
bytes,
[
0xA0, 0x05, 0x01, 0x9F, 0x80, 0x30, 0x00, 0x80, 0x02, 0x01, 0x80
]
);
let parsed = ResponseTpdu::parse(&bytes).unwrap();
assert_eq!(parsed, r);
assert!(parsed.sb_value.data_available());
assert_eq!(parsed.block, Some(DataBlock::Last));
}
#[test]
fn response_tpdu_rejects_mismatched_status_t_c_id() {
let r = ResponseTpdu {
length_form: crate::length::LengthForm::Minimal,
status_length_form: crate::length::LengthForm::Minimal,
tag: tags::DATA_LAST,
t_c_id: 1,
data: &[0x9F, 0x80, 0x30, 0x00],
sb_value: SbValue::new(true),
block: Some(DataBlock::Last),
};
let mut bytes = r.try_to_bytes().unwrap();
let status_t_c_id_idx = bytes.len() - 2;
assert_eq!(bytes[status_t_c_id_idx], 1);
bytes[status_t_c_id_idx] = 2; assert!(matches!(
ResponseTpdu::parse(&bytes),
Err(Error::InvalidObject { .. })
));
}
#[test]
fn mutating_data_changes_bytes() {
let c = CommandTpdu {
length_form: crate::length::LengthForm::Minimal,
tag: tags::DATA_LAST,
t_c_id: 1,
data: &[0xAA],
};
let a = c.try_to_bytes().unwrap();
let b = CommandTpdu {
length_form: crate::length::LengthForm::Minimal,
tag: tags::DATA_LAST,
t_c_id: 1,
data: &[0xBB],
}
.try_to_bytes()
.unwrap();
assert_ne!(a, b);
}
#[test]
fn sb_value_da() {
assert!(SbValue::new(true).data_available());
assert!(!SbValue::new(false).data_available());
}
fn widen(wire: &[u8], octets: usize) -> alloc::vec::Vec<u8> {
let (value, hdr) = length::decode(&wire[1..]).unwrap();
let mut out = alloc::vec![wire[0], 0x80 | octets as u8];
for i in (0..octets).rev() {
out.push((value >> (8 * i)) as u8);
}
out.extend_from_slice(&wire[1 + hdr..]);
out
}
#[test]
fn captured_long_form_r_tpdu_round_trips_byte_exactly() {
let frame: [u8; 17] = [
0xA0, 0x82, 0x00, 0x09, 0x01, 0x90, 0x02, 0x00, 0x01, 0x9F, 0x80, 0x11, 0x00, 0x80,
0x02, 0x01, 0x00,
];
let r = ResponseTpdu::parse(&frame).unwrap();
assert_eq!(r.length_form, LengthForm::Long { octets: 2 });
assert_eq!(r.status_length_form, LengthForm::Minimal);
assert_eq!(r.t_c_id, 1);
assert_eq!(r.data, &frame[5..13]);
assert_eq!(r.try_to_bytes().unwrap(), frame);
}
#[test]
fn every_tpdu_round_trips_each_length_form_byte_exactly() {
macro_rules! forms {
($ty:ty, $wire:expr) => {{
let wire: &[u8] = &$wire;
assert_eq!(<$ty>::parse(wire).unwrap().try_to_bytes().unwrap(), wire);
for octets in 1..=4 {
let wide = widen(wire, octets);
let parsed = <$ty>::parse(&wide).unwrap();
assert_eq!(
parsed.try_to_bytes().unwrap(),
wide,
"{} {octets}",
stringify!($ty)
);
assert_ne!(parsed, <$ty>::parse(wire).unwrap());
}
}};
}
forms!(TcObject, [tags::CREATE_T_C, 0x01, 0x05]);
forms!(NewTc, [tags::NEW_T_C, 0x02, 0x01, 0x02]);
forms!(TcError, [tags::T_C_ERROR, 0x02, 0x01, 0x01]);
forms!(CommandTpdu, [tags::DATA_LAST, 0x03, 0x01, 0xAA, 0xBB]);
forms!(
ResponseTpdu,
[
tags::DATA_LAST,
0x03,
0x01,
0xAA,
0xBB,
0x80,
0x02,
0x01,
0x80
]
);
let wire = [
tags::DATA_LAST,
0x03,
0x01,
0xAA,
0xBB,
0x80,
0x82,
0x00,
0x02,
0x01,
0x80,
];
let r = ResponseTpdu::parse(&wire).unwrap();
assert_eq!(r.status_length_form, LengthForm::Long { octets: 2 });
assert_eq!(r.try_to_bytes().unwrap(), wire);
}
}