use super::{pack_u16, pack_u32, unpack_u16, unpack_u32, EventCode, OperationCode, ResponseCode};
const HEADER_SIZE: usize = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum ContainerType {
Command = 1,
Data = 2,
Response = 3,
Event = 4,
}
impl ContainerType {
#[must_use]
pub fn from_code(code: u16) -> Option<Self> {
match code {
1 => Some(ContainerType::Command),
2 => Some(ContainerType::Data),
3 => Some(ContainerType::Response),
4 => Some(ContainerType::Event),
_ => None,
}
}
#[must_use]
pub fn to_code(self) -> u16 {
self as u16
}
}
pub fn container_type(buf: &[u8]) -> Result<ContainerType, crate::Error> {
if buf.len() < HEADER_SIZE {
return Err(crate::Error::invalid_data(format!(
"container too small: need at least {} bytes, have {}",
HEADER_SIZE,
buf.len()
)));
}
let type_code = unpack_u16(&buf[4..6])?;
ContainerType::from_code(type_code)
.ok_or_else(|| crate::Error::invalid_data(format!("invalid container type: {}", type_code)))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandContainer {
pub code: OperationCode,
pub transaction_id: u32,
pub params: Vec<u32>,
}
impl CommandContainer {
pub fn to_bytes(&self) -> Vec<u8> {
let param_bytes = self.params.len() * 4;
let total_len = HEADER_SIZE + param_bytes;
let mut buf = Vec::with_capacity(total_len);
buf.extend_from_slice(&pack_u32(total_len as u32));
buf.extend_from_slice(&pack_u16(ContainerType::Command.to_code()));
buf.extend_from_slice(&pack_u16(self.code.into()));
buf.extend_from_slice(&pack_u32(self.transaction_id));
for ¶m in &self.params {
buf.extend_from_slice(&pack_u32(param));
}
buf
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DataContainer {
pub code: OperationCode,
pub transaction_id: u32,
pub payload: Vec<u8>,
}
impl DataContainer {
pub fn to_bytes(&self) -> Vec<u8> {
let total_len = HEADER_SIZE + self.payload.len();
let mut buf = Vec::with_capacity(total_len);
buf.extend_from_slice(&pack_u32(total_len as u32));
buf.extend_from_slice(&pack_u16(ContainerType::Data.to_code()));
buf.extend_from_slice(&pack_u16(self.code.into()));
buf.extend_from_slice(&pack_u32(self.transaction_id));
buf.extend_from_slice(&self.payload);
buf
}
pub fn from_bytes(buf: &[u8]) -> Result<Self, crate::Error> {
if buf.len() < HEADER_SIZE {
return Err(crate::Error::invalid_data(format!(
"data container too small: need at least {} bytes, have {}",
HEADER_SIZE,
buf.len()
)));
}
let length = unpack_u32(&buf[0..4])? as usize;
let type_code = unpack_u16(&buf[4..6])?;
let code = unpack_u16(&buf[6..8])?;
let transaction_id = unpack_u32(&buf[8..12])?;
if type_code != ContainerType::Data.to_code() {
return Err(crate::Error::invalid_data(format!(
"expected Data container type ({}), got {}",
ContainerType::Data.to_code(),
type_code
)));
}
if length < HEADER_SIZE {
return Err(crate::Error::invalid_data(format!(
"data container length too small: {} < header size {}",
length, HEADER_SIZE
)));
}
if buf.len() < length {
return Err(crate::Error::invalid_data(format!(
"data container length mismatch: header says {}, have {}",
length,
buf.len()
)));
}
let payload = buf[HEADER_SIZE..length].to_vec();
Ok(DataContainer {
code: code.into(),
transaction_id,
payload,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResponseContainer {
pub code: ResponseCode,
pub transaction_id: u32,
pub params: Vec<u32>,
}
impl ResponseContainer {
pub fn from_bytes(buf: &[u8]) -> Result<Self, crate::Error> {
if buf.len() < HEADER_SIZE {
return Err(crate::Error::invalid_data(format!(
"response container too small: need at least {} bytes, have {}",
HEADER_SIZE,
buf.len()
)));
}
let length = unpack_u32(&buf[0..4])? as usize;
let type_code = unpack_u16(&buf[4..6])?;
let code = unpack_u16(&buf[6..8])?;
let transaction_id = unpack_u32(&buf[8..12])?;
if type_code != ContainerType::Response.to_code() {
return Err(crate::Error::invalid_data(format!(
"expected Response container type ({}), got {}",
ContainerType::Response.to_code(),
type_code
)));
}
if buf.len() < length {
return Err(crate::Error::invalid_data(format!(
"response container length mismatch: header says {}, have {}",
length,
buf.len()
)));
}
let param_bytes = length - HEADER_SIZE;
if param_bytes % 4 != 0 {
return Err(crate::Error::invalid_data(format!(
"response parameter bytes not aligned: {} bytes",
param_bytes
)));
}
let param_count = param_bytes / 4;
let mut params = Vec::with_capacity(param_count);
for i in 0..param_count {
let offset = HEADER_SIZE + i * 4;
params.push(unpack_u32(&buf[offset..])?);
}
Ok(ResponseContainer {
code: code.into(),
transaction_id,
params,
})
}
#[must_use]
pub fn is_ok(&self) -> bool {
self.code == ResponseCode::Ok
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EventContainer {
pub code: EventCode,
pub transaction_id: u32,
pub params: [u32; 3],
}
impl EventContainer {
pub fn to_bytes(&self) -> Vec<u8> {
const EVENT_SIZE: usize = HEADER_SIZE + 12; let mut buf = Vec::with_capacity(EVENT_SIZE);
buf.extend_from_slice(&pack_u32(EVENT_SIZE as u32));
buf.extend_from_slice(&pack_u16(ContainerType::Event.to_code()));
buf.extend_from_slice(&pack_u16(self.code.into()));
buf.extend_from_slice(&pack_u32(self.transaction_id));
for ¶m in &self.params {
buf.extend_from_slice(&pack_u32(param));
}
buf
}
pub fn from_bytes(buf: &[u8]) -> Result<Self, crate::Error> {
const MAX_EVENT_SIZE: usize = HEADER_SIZE + 12;
if buf.len() < HEADER_SIZE {
return Err(crate::Error::invalid_data(format!(
"event container too small: need at least {} bytes, have {}",
HEADER_SIZE,
buf.len()
)));
}
let length = unpack_u32(&buf[0..4])? as usize;
let type_code = unpack_u16(&buf[4..6])?;
let code = unpack_u16(&buf[6..8])?;
let transaction_id = unpack_u32(&buf[8..12])?;
if type_code != ContainerType::Event.to_code() {
return Err(crate::Error::invalid_data(format!(
"expected Event container type ({}), got {}",
ContainerType::Event.to_code(),
type_code
)));
}
if !(HEADER_SIZE..=MAX_EVENT_SIZE).contains(&length) {
return Err(crate::Error::invalid_data(format!(
"event container invalid size: expected 12-24, got {}",
length
)));
}
let param_bytes = length - HEADER_SIZE;
if param_bytes % 4 != 0 {
return Err(crate::Error::invalid_data(format!(
"event parameter bytes not aligned: {} bytes",
param_bytes
)));
}
if buf.len() < length {
return Err(crate::Error::invalid_data(format!(
"event container buffer too small: need {}, have {}",
length,
buf.len()
)));
}
let param_count = param_bytes / 4;
let param1 = if param_count >= 1 {
unpack_u32(&buf[12..16])?
} else {
0
};
let param2 = if param_count >= 2 {
unpack_u32(&buf[16..20])?
} else {
0
};
let param3 = if param_count >= 3 {
unpack_u32(&buf[20..24])?
} else {
0
};
Ok(EventContainer {
code: code.into(),
transaction_id,
params: [param1, param2, param3],
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn container_type_conversions() {
for (code, ct) in [
(1, ContainerType::Command),
(2, ContainerType::Data),
(3, ContainerType::Response),
(4, ContainerType::Event),
] {
assert_eq!(ContainerType::from_code(code), Some(ct));
assert_eq!(ct.to_code(), code);
}
for invalid in [0, 5, 0xFFFF] {
assert_eq!(ContainerType::from_code(invalid), None);
}
}
#[test]
fn container_type_detection() {
let containers: [(u16, ContainerType); 4] = [
(1, ContainerType::Command),
(2, ContainerType::Data),
(3, ContainerType::Response),
(4, ContainerType::Event),
];
for (type_code, expected) in containers {
let mut bytes = vec![0x0C, 0x00, 0x00, 0x00]; bytes.extend_from_slice(&type_code.to_le_bytes());
bytes.extend_from_slice(&[0x00; 6]); assert_eq!(container_type(&bytes).unwrap(), expected);
}
for invalid in [0u16, 5] {
let mut bytes = vec![0x0C, 0x00, 0x00, 0x00];
bytes.extend_from_slice(&invalid.to_le_bytes());
bytes.extend_from_slice(&[0x00; 6]);
assert!(container_type(&bytes).is_err());
}
assert!(container_type(&[]).is_err());
assert!(container_type(&[0x00; 11]).is_err());
}
#[test]
fn command_container_serialization() {
let cmd = CommandContainer {
code: OperationCode::GetObjectHandles,
transaction_id: 10,
params: vec![0x00010001, 0x00000000, 0xFFFFFFFF],
};
let bytes = cmd.to_bytes();
assert_eq!(bytes.len(), 24);
assert_eq!(&bytes[0..4], &[0x18, 0x00, 0x00, 0x00]); assert_eq!(&bytes[4..6], &[0x01, 0x00]); assert_eq!(&bytes[6..8], &[0x07, 0x10]); assert_eq!(&bytes[8..12], &[0x0A, 0x00, 0x00, 0x00]); assert_eq!(&bytes[12..16], &[0x01, 0x00, 0x01, 0x00]); }
#[test]
fn data_container_roundtrip() {
let original = DataContainer {
code: OperationCode::GetObject,
transaction_id: 100,
payload: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
};
let parsed = DataContainer::from_bytes(&original.to_bytes()).unwrap();
assert_eq!(parsed, original);
let empty = DataContainer {
code: OperationCode::SendObject,
transaction_id: 5,
payload: vec![],
};
assert_eq!(DataContainer::from_bytes(&empty.to_bytes()).unwrap(), empty);
}
#[test]
fn data_container_errors() {
assert!(DataContainer::from_bytes(&[0x00; 11]).is_err());
let mut bad_type = vec![0x0C, 0x00, 0x00, 0x00, 0x03, 0x00]; bad_type.extend_from_slice(&[0x00; 6]);
assert!(DataContainer::from_bytes(&bad_type).is_err());
let mut truncated = vec![0x20, 0x00, 0x00, 0x00, 0x02, 0x00]; truncated.extend_from_slice(&[0x00; 6]);
assert!(DataContainer::from_bytes(&truncated).is_err());
}
#[test]
fn response_container_parsing() {
let bytes = [
0x18, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x20, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, ];
let resp = ResponseContainer::from_bytes(&bytes).unwrap();
assert_eq!(resp.code, ResponseCode::Ok);
assert!(resp.is_ok());
assert_eq!(resp.params, vec![0x00010001, 0, 5]);
let err_bytes = [
0x0C, 0x00, 0x00, 0x00, 0x03, 0x00, 0x02, 0x20, 0x03, 0x00, 0x00, 0x00,
];
let err_resp = ResponseContainer::from_bytes(&err_bytes).unwrap();
assert_eq!(err_resp.code, ResponseCode::GeneralError);
assert!(!err_resp.is_ok());
}
#[test]
fn response_container_errors() {
assert!(ResponseContainer::from_bytes(&[0x00; 11]).is_err());
let unaligned = [
0x0D, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x20, 0x01, 0x00, 0x00, 0x00, 0xFF,
];
assert!(ResponseContainer::from_bytes(&unaligned).is_err());
}
#[test]
fn event_container_to_bytes_roundtrip() {
let original = EventContainer {
code: EventCode::CancelTransaction,
transaction_id: 42,
params: [42, 0, 0],
};
let bytes = original.to_bytes();
assert_eq!(bytes.len(), 24);
let parsed = EventContainer::from_bytes(&bytes).unwrap();
assert_eq!(parsed, original);
}
#[test]
fn event_container_variable_params() {
let zero = [
0x0C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x08, 0x40, 0x00, 0x00, 0x00, 0x00,
];
let e0 = EventContainer::from_bytes(&zero).unwrap();
assert_eq!(e0.code, EventCode::DeviceInfoChanged);
assert_eq!(e0.params, [0, 0, 0]);
let one = [
0x10, 0x00, 0x00, 0x00, 0x04, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x00,
0x00, 0x00,
];
let e1 = EventContainer::from_bytes(&one).unwrap();
assert_eq!(e1.params, [42, 0, 0]);
let three = [
0x18, 0x00, 0x00, 0x00, 0x04, 0x00, 0x02, 0x40, 0x0A, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
];
let e3 = EventContainer::from_bytes(&three).unwrap();
assert_eq!(e3.transaction_id, 10);
assert_eq!(e3.params, [1, 2, 3]);
}
#[test]
fn event_container_errors() {
assert!(EventContainer::from_bytes(&[0x00; 11]).is_err());
let too_long = [
0x1C, 0x00, 0x00, 0x00, 0x04, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
assert!(EventContainer::from_bytes(&too_long).is_err());
let unaligned = [
0x0E, 0x00, 0x00, 0x00, 0x04, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
assert!(EventContainer::from_bytes(&unaligned).is_err());
}
fn valid_response_bytes(param_count: usize) -> impl Strategy<Value = Vec<u8>> {
(
any::<u16>(),
any::<u32>(),
prop::collection::vec(any::<u32>(), param_count..=param_count),
)
.prop_map(move |(code, tx_id, params)| {
let len = HEADER_SIZE + params.len() * 4;
let mut bytes = Vec::with_capacity(len);
bytes.extend_from_slice(&pack_u32(len as u32));
bytes.extend_from_slice(&pack_u16(ContainerType::Response.to_code()));
bytes.extend_from_slice(&pack_u16(code));
bytes.extend_from_slice(&pack_u32(tx_id));
for p in ¶ms {
bytes.extend_from_slice(&pack_u32(*p));
}
bytes
})
}
proptest! {
#[test]
fn prop_container_type_roundtrip(code in 1u16..=4u16) {
let ct = ContainerType::from_code(code).unwrap();
prop_assert_eq!(ct.to_code(), code);
}
#[test]
fn prop_data_container_roundtrip(
code in any::<u16>(),
tx_id in any::<u32>(),
payload in prop::collection::vec(any::<u8>(), 0..500)
) {
let original = DataContainer {
code: code.into(),
transaction_id: tx_id,
payload: payload.clone(),
};
let parsed = DataContainer::from_bytes(&original.to_bytes()).unwrap();
prop_assert_eq!(parsed, original);
}
#[test]
fn prop_command_container_length(
code in any::<u16>(),
tx_id in any::<u32>(),
params in prop::collection::vec(any::<u32>(), 0..5)
) {
let cmd = CommandContainer {
code: code.into(),
transaction_id: tx_id,
params: params.clone(),
};
let bytes = cmd.to_bytes();
let length = unpack_u32(&bytes[0..4]).unwrap() as usize;
prop_assert_eq!(length, HEADER_SIZE + params.len() * 4);
prop_assert_eq!(length, bytes.len());
}
#[test]
fn prop_response_container_parse(param_count in 0usize..=5usize) {
let strategy = valid_response_bytes(param_count);
proptest!(|(bytes in strategy)| {
let resp = ResponseContainer::from_bytes(&bytes).unwrap();
prop_assert_eq!(resp.params.len(), param_count);
});
}
#[test]
fn prop_container_type_identification(
code in any::<u16>(),
tx_id in any::<u32>(),
payload in prop::collection::vec(any::<u8>(), 0..50)
) {
let data = DataContainer {
code: code.into(),
transaction_id: tx_id,
payload,
};
prop_assert_eq!(container_type(&data.to_bytes()).unwrap(), ContainerType::Data);
let cmd = CommandContainer {
code: code.into(),
transaction_id: tx_id,
params: vec![],
};
prop_assert_eq!(container_type(&cmd.to_bytes()).unwrap(), ContainerType::Command);
}
#[test]
fn fuzz_data_container_length_underflow(fake_length in 0u32..12u32, tx_id: u32) {
let mut buf = fake_length.to_le_bytes().to_vec();
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&0x1001u16.to_le_bytes());
buf.extend_from_slice(&tx_id.to_le_bytes());
prop_assert!(DataContainer::from_bytes(&buf).is_err());
}
#[test]
fn fuzz_event_container_invalid_length(
fake_length in prop::sample::select(vec![
0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, // Too small
13, 14, 15, 17, 18, 19, 21, 22, 23, // Unaligned
25, 26, 28, 32, 100, // Too large
]),
tx_id: u32,
) {
let mut buf = fake_length.to_le_bytes().to_vec();
buf.extend_from_slice(&4u16.to_le_bytes());
buf.extend_from_slice(&0x4002u16.to_le_bytes());
buf.extend_from_slice(&tx_id.to_le_bytes());
buf.extend_from_slice(&[0u8; 12]); prop_assert!(EventContainer::from_bytes(&buf).is_err());
}
#[test]
fn fuzz_wrong_container_type(
tx_id: u32,
payload in prop::collection::vec(any::<u8>(), 0..20),
) {
let len = 12 + payload.len();
for (parser_type, wrong_type) in [(2u16, 1u16), (2, 3), (2, 4), (3, 1), (3, 2), (4, 1)] {
let mut buf = (len as u32).to_le_bytes().to_vec();
buf.extend_from_slice(&wrong_type.to_le_bytes());
buf.extend_from_slice(&0x1001u16.to_le_bytes());
buf.extend_from_slice(&tx_id.to_le_bytes());
buf.extend_from_slice(&payload);
match parser_type {
2 => prop_assert!(DataContainer::from_bytes(&buf).is_err()),
3 => prop_assert!(ResponseContainer::from_bytes(&buf).is_err()),
4 => prop_assert!(EventContainer::from_bytes(&buf).is_err()),
_ => {}
}
}
}
}
crate::fuzz_bytes_fn!(fuzz_container_type, container_type, 100);
crate::fuzz_bytes!(fuzz_data_container, DataContainer, 100);
crate::fuzz_bytes!(fuzz_response_container, ResponseContainer, 100);
crate::fuzz_bytes!(fuzz_event_container, EventContainer, 100);
}