use core::fmt;
use super::opcode::Opcode;
use super::uid::{DeviceUid, UID_LEN};
pub(crate) const HEADER_LEN: usize = 24;
const MAGIC: [u8; 2] = [0x50, 0x38];
pub(crate) const DEVICE_NAME_LEN: usize = 16;
pub(crate) const FW_VERSION_LEN: usize = 128;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum FrameError {
TooShort(usize),
BadMagic(u8, u8),
}
impl fmt::Display for FrameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooShort(n) => write!(f, "invalid mx_remote frame (length = {n})"),
Self::BadMagic(a, b) => write!(f, "invalid mx_remote frame (header = {a}:{b})"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Frame<'a> {
data: &'a [u8],
}
impl<'a> Frame<'a> {
pub(crate) fn parse(data: &'a [u8]) -> Result<Self, FrameError> {
match (data.first(), data.get(1)) {
_ if data.len() < HEADER_LEN => Err(FrameError::TooShort(data.len())),
(Some(&a), Some(&b)) if [a, b] != MAGIC => Err(FrameError::BadMagic(a, b)),
_ => Ok(Self { data }),
}
}
pub(crate) fn protocol(&self) -> u16 {
self.header_u16(2)
}
pub(crate) fn remote_id(&self) -> DeviceUid {
self.data
.get(4..4 + UID_LEN)
.and_then(|b| <[u8; UID_LEN]>::try_from(b).ok())
.map(DeviceUid::from_array)
.unwrap_or_default()
}
pub(crate) fn opcode(&self) -> Opcode {
Opcode(self.header_u16(20))
}
pub(crate) fn payload_len(&self) -> u16 {
self.header_u16(22)
}
pub(crate) fn payload(&self) -> &'a [u8] {
let declared = HEADER_LEN.saturating_add(self.payload_len() as usize);
let end = declared.min(self.data.len());
self.data.get(HEADER_LEN..end).unwrap_or_default()
}
fn header_u16(&self, idx: usize) -> u16 {
self.data
.get(idx..idx + 2)
.and_then(|b| <[u8; 2]>::try_from(b).ok())
.map(u16::from_le_bytes)
.unwrap_or(0)
}
fn slice(&self, idx: usize, len: usize) -> Option<&'a [u8]> {
let start = HEADER_LEN.checked_add(idx)?;
let end = start.checked_add(len)?;
self.data.get(start..end)
}
pub(crate) fn u8(&self, idx: usize) -> Option<u8> {
self.slice(idx, 1).and_then(|b| b.first().copied())
}
pub(crate) fn boolean(&self, idx: usize) -> bool {
self.u8(idx) == Some(1)
}
pub(crate) fn u16(&self, idx: usize) -> Option<u16> {
self.slice(idx, 2)
.and_then(|b| <[u8; 2]>::try_from(b).ok())
.map(u16::from_le_bytes)
}
pub(crate) fn u32(&self, idx: usize) -> Option<u32> {
self.slice(idx, 4)
.and_then(|b| <[u8; 4]>::try_from(b).ok())
.map(u32::from_le_bytes)
}
pub(crate) fn str(&self, idx: usize, len: usize) -> Option<String> {
self.slice(idx, len).map(cstr)
}
pub(crate) fn str_to_end(&self, idx: usize) -> Option<String> {
self.bytes_from(idx).map(cstr)
}
pub(crate) fn uid(&self, idx: usize) -> Option<DeviceUid> {
self.slice(idx, UID_LEN)
.and_then(|b| <[u8; UID_LEN]>::try_from(b).ok())
.map(DeviceUid::from_array)
}
pub(crate) fn bytes_from(&self, idx: usize) -> Option<&'a [u8]> {
let start = HEADER_LEN.checked_add(idx)?;
self.data.get(start..)
}
}
pub(crate) fn cstr(b: &[u8]) -> String {
let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
b.get(..end)
.unwrap_or_default()
.iter()
.map(|&c| {
if c > 0x7F {
char::REPLACEMENT_CHARACTER
} else {
c as char
}
})
.collect()
}
pub(in crate::wire) fn build_frame(
uid: DeviceUid,
opcode: Opcode,
protocol: u16,
payload: &[u8],
) -> Vec<u8> {
let declared = u16::try_from(payload.len()).unwrap_or(u16::MAX);
let mut out = Vec::with_capacity(HEADER_LEN + payload.len());
out.extend_from_slice(&MAGIC);
out.extend_from_slice(&protocol.to_le_bytes());
out.extend_from_slice(uid.as_bytes());
out.extend_from_slice(&opcode.0.to_le_bytes());
out.extend_from_slice(&declared.to_le_bytes());
out.extend_from_slice(payload);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wire::opcode::op;
const UID: DeviceUid =
DeviceUid::from_array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
const PAYLOAD: [u8; 8] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
fn built() -> Vec<u8> {
build_frame(UID, op::BAY_STATUS, 0x0F, &PAYLOAD)
}
#[test]
fn a_built_frame_parses_back_to_its_header_fields() {
let raw = built();
let f = Frame::parse(&raw).expect("a frame this library built must parse");
assert_eq!(f.protocol(), 0x0F);
assert_eq!(f.remote_id(), UID);
assert_eq!(f.opcode(), op::BAY_STATUS);
assert_eq!(f.payload_len(), PAYLOAD.len() as u16);
assert_eq!(f.payload(), PAYLOAD);
}
#[test]
fn a_datagram_shorter_than_the_header_is_dropped() {
let raw = built();
for len in 0..HEADER_LEN {
assert_eq!(
Frame::parse(&raw[..len]),
Err(FrameError::TooShort(len)),
"a {len}-byte datagram was accepted"
);
}
}
#[test]
fn a_datagram_that_is_not_p8_is_dropped() {
let mut raw = built();
raw[1] = b'9';
assert_eq!(Frame::parse(&raw), Err(FrameError::BadMagic(0x50, b'9')));
}
#[test]
fn a_truncated_payload_is_bounded_by_what_arrived() {
let raw = built();
let cut = &raw[..raw.len() - 3];
let f = Frame::parse(cut).expect("the header still arrived in full");
assert_eq!(f.payload_len(), 8, "the header still claims eight bytes");
assert_eq!(f.payload(), &PAYLOAD[..5]);
assert_eq!(f.u32(1), Some(0x55443322));
assert_eq!(f.u32(2), None, "a read running past the datagram must fail");
}
#[test]
fn a_padded_payload_is_bounded_by_the_declared_length() {
let mut raw = built();
raw.extend_from_slice(&[0xFF; 4]);
let f = Frame::parse(&raw).expect("padding does not stop a frame parsing");
assert_eq!(f.payload(), PAYLOAD);
}
#[test]
fn accessors_refuse_to_read_past_the_datagram() {
let raw = built();
let f = Frame::parse(&raw).expect("a frame this library built must parse");
assert_eq!(f.u8(7), Some(0x88));
assert_eq!(f.u8(8), None);
assert_eq!(f.u16(6), Some(0x8877));
assert_eq!(f.u16(7), None);
assert_eq!(f.u32(4), Some(0x88776655));
assert_eq!(f.u32(5), None);
assert_eq!(f.uid(0), None, "eight payload bytes cannot hold a uid");
assert!(!f.boolean(8), "a byte that did not arrive is not true");
}
#[test]
fn a_string_stops_at_its_field_width() {
let raw = build_frame(UID, op::CHANGE_BAY_NAME, 0x06, b"ABCDEFGH");
let f = Frame::parse(&raw).expect("a frame this library built must parse");
assert_eq!(
f.str(0, 4).as_deref(),
Some("ABCD"),
"a value filling its field ran on into the next one"
);
assert_eq!(
f.str(0, 9),
None,
"a field wider than the payload must fail"
);
assert_eq!(f.str_to_end(4).as_deref(), Some("EFGH"));
}
#[test]
fn a_string_stops_at_a_nul_inside_its_field() {
let raw = build_frame(UID, op::CHANGE_BAY_NAME, 0x06, b"AB\0DEFGH");
let f = Frame::parse(&raw).expect("a frame this library built must parse");
assert_eq!(f.str(0, 8).as_deref(), Some("AB"));
}
#[test]
fn a_non_ascii_byte_costs_one_character_not_the_whole_field() {
assert_eq!(cstr(b"Ki\xFFchen"), "Ki\u{FFFD}chen");
}
}