use core::any::Any;
use crate::error::{CrafterError, Result};
use crate::field::Field;
use crate::packet::{IntoPacket, Layer, LayerContext, Packet};
use super::consts::DOT15D4_CHANNEL_MIN;
const DOT15D4_RADIO_DEFAULT_CHANNEL: u8 = DOT15D4_CHANNEL_MIN;
const DOT15D4_RADIO_DEFAULT_FCS_VALID: bool = true;
const DOT15D4_TAP_VERSION: u8 = 0;
const DOT15D4_TAP_HEADER_LEN: usize = 4;
const DOT15D4_TAP_TLV_HEADER_LEN: usize = 4;
const DOT15D4_TAP_TLV_ALIGN: usize = 4;
const DOT15D4_TAP_TLV_FCS_TYPE: u16 = 0;
const DOT15D4_TAP_TLV_RSS: u16 = 1;
const DOT15D4_TAP_TLV_CHANNEL_ASSIGNMENT: u16 = 3;
const DOT15D4_TAP_FCS_TYPE_CRC16: u8 = 1;
const DOT15D4_TAP_FCS_TYPE_NONE: u8 = 0;
const DOT15D4_TAP_CHANNEL_PAGE_2P4_GHZ: u8 = 0;
#[derive(Debug)]
pub struct Dot15d4Radio {
channel: Field<u8>,
rssi: Field<i16>,
lqi: Field<u8>,
fcs_valid: Field<bool>,
fcs_type: Field<u8>,
}
impl Clone for Dot15d4Radio {
fn clone(&self) -> Self {
Self {
channel: self.channel.clone(),
rssi: self.rssi.clone(),
lqi: self.lqi.clone(),
fcs_valid: self.fcs_valid.clone(),
fcs_type: self.fcs_type.clone(),
}
}
}
impl Dot15d4Radio {
pub fn new() -> Self {
Self {
channel: Field::unset(),
rssi: Field::unset(),
lqi: Field::unset(),
fcs_valid: Field::unset(),
fcs_type: Field::unset(),
}
}
pub fn on_channel(channel: u8) -> Self {
Self::new().channel(channel)
}
pub fn channel(mut self, channel: u8) -> Self {
self.channel.set_user(channel);
self
}
pub fn rssi(mut self, rssi: i16) -> Self {
self.rssi.set_user(rssi);
self
}
pub fn lqi(mut self, lqi: u8) -> Self {
self.lqi.set_user(lqi);
self
}
pub fn fcs_valid(mut self, fcs_valid: bool) -> Self {
self.fcs_valid.set_user(fcs_valid);
self
}
pub fn fcs_type(mut self, fcs_type: u8) -> Self {
self.fcs_type.set_user(fcs_type);
self
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn effective_channel(&self) -> u8 {
self.channel
.value()
.copied()
.unwrap_or(DOT15D4_RADIO_DEFAULT_CHANNEL)
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn effective_rssi(&self) -> Option<i16> {
self.rssi.value().copied()
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn effective_lqi(&self) -> Option<u8> {
self.lqi.value().copied()
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn effective_fcs_valid(&self) -> bool {
self.fcs_valid
.value()
.copied()
.unwrap_or(DOT15D4_RADIO_DEFAULT_FCS_VALID)
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn effective_fcs_type(&self) -> Option<u8> {
self.fcs_type.value().copied()
}
#[cfg(feature = "whad")]
pub(crate) fn effective_channel_for_backend(&self) -> u8 {
self.effective_channel()
}
#[cfg(feature = "whad")]
pub(crate) fn effective_fcs_valid_for_backend(&self) -> bool {
self.effective_fcs_valid()
}
fn effective_tap_fcs_type(&self) -> u8 {
match self.effective_fcs_type() {
Some(value) => value,
None if self.effective_fcs_valid() => DOT15D4_TAP_FCS_TYPE_CRC16,
None => DOT15D4_TAP_FCS_TYPE_NONE,
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn encoded_len(&self) -> usize {
DOT15D4_TAP_HEADER_LEN
+ tap_tlv_record_len(1) + tap_tlv_record_len(4) + tap_tlv_record_len(3) }
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn encode(&self, out: &mut Vec<u8>) {
let start = out.len();
out.push(DOT15D4_TAP_VERSION);
out.push(0); out.extend_from_slice(&0u16.to_le_bytes());
push_tap_tlv(
out,
DOT15D4_TAP_TLV_FCS_TYPE,
&[self.effective_tap_fcs_type()],
);
let rss_dbm = self.effective_rssi().unwrap_or(0) as f32;
push_tap_tlv(out, DOT15D4_TAP_TLV_RSS, &rss_dbm.to_le_bytes());
let channel = self.effective_channel();
let mut channel_value = Vec::with_capacity(3);
channel_value.extend_from_slice(&(channel as u16).to_le_bytes());
channel_value.push(DOT15D4_TAP_CHANNEL_PAGE_2P4_GHZ);
push_tap_tlv(out, DOT15D4_TAP_TLV_CHANNEL_ASSIGNMENT, &channel_value);
let total_len = (out.len() - start) as u16;
out[start + 2..start + 4].copy_from_slice(&total_len.to_le_bytes());
}
}
fn tap_tlv_record_len(value_len: usize) -> usize {
DOT15D4_TAP_TLV_HEADER_LEN + value_len.div_ceil(DOT15D4_TAP_TLV_ALIGN) * DOT15D4_TAP_TLV_ALIGN
}
fn push_tap_tlv(out: &mut Vec<u8>, tlv_type: u16, value: &[u8]) {
out.extend_from_slice(&tlv_type.to_le_bytes());
out.extend_from_slice(&(value.len() as u16).to_le_bytes());
out.extend_from_slice(value);
let padding = value.len().next_multiple_of(DOT15D4_TAP_TLV_ALIGN) - value.len();
out.extend(std::iter::repeat(0u8).take(padding));
}
impl Default for Dot15d4Radio {
fn default() -> Self {
Self::new()
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn decode_dot15d4_radio(bytes: &[u8]) -> Result<(Dot15d4Radio, &[u8])> {
if bytes.len() < DOT15D4_TAP_HEADER_LEN {
return Err(CrafterError::buffer_too_short(
"dot15d4.tap.header",
DOT15D4_TAP_HEADER_LEN,
bytes.len(),
));
}
if bytes[0] != DOT15D4_TAP_VERSION {
return Err(CrafterError::invalid_field_value(
"dot15d4.tap.header",
"unsupported TAP version",
));
}
let total_len = u16::from_le_bytes([bytes[2], bytes[3]]) as usize;
if total_len < DOT15D4_TAP_HEADER_LEN || total_len % DOT15D4_TAP_TLV_ALIGN != 0 {
return Err(CrafterError::invalid_field_value(
"dot15d4.tap.header",
"invalid TAP total length",
));
}
if bytes.len() < total_len {
return Err(CrafterError::buffer_too_short(
"dot15d4.tap.header",
total_len,
bytes.len(),
));
}
let mut radio = Dot15d4Radio::new();
let mut offset = DOT15D4_TAP_HEADER_LEN;
while offset < total_len {
if total_len - offset < DOT15D4_TAP_TLV_HEADER_LEN {
return Err(CrafterError::buffer_too_short(
"dot15d4.tap.tlv",
DOT15D4_TAP_TLV_HEADER_LEN,
total_len - offset,
));
}
let tlv_type = u16::from_le_bytes([bytes[offset], bytes[offset + 1]]);
let value_len = u16::from_le_bytes([bytes[offset + 2], bytes[offset + 3]]) as usize;
let value_start = offset + DOT15D4_TAP_TLV_HEADER_LEN;
let padded_value_len = value_len.next_multiple_of(DOT15D4_TAP_TLV_ALIGN);
let record_len = DOT15D4_TAP_TLV_HEADER_LEN + padded_value_len;
if total_len - offset < record_len {
return Err(CrafterError::buffer_too_short(
"dot15d4.tap.tlv",
record_len,
total_len - offset,
));
}
let value = &bytes[value_start..value_start + value_len];
match tlv_type {
DOT15D4_TAP_TLV_FCS_TYPE => {
if value_len < 1 {
return Err(CrafterError::invalid_field_value(
"dot15d4.tap.tlv",
"FCS_TYPE TLV value too short",
));
}
let fcs_type = value[0];
radio.fcs_type.set_user(fcs_type);
radio
.fcs_valid
.set_user(fcs_type != DOT15D4_TAP_FCS_TYPE_NONE);
}
DOT15D4_TAP_TLV_RSS => {
if value_len < 4 {
return Err(CrafterError::invalid_field_value(
"dot15d4.tap.tlv",
"RSS TLV value too short",
));
}
let rss = f32::from_le_bytes([value[0], value[1], value[2], value[3]]);
radio.rssi.set_user(rss as i16);
}
DOT15D4_TAP_TLV_CHANNEL_ASSIGNMENT => {
if value_len < 2 {
return Err(CrafterError::invalid_field_value(
"dot15d4.tap.tlv",
"CHANNEL_ASSIGNMENT TLV value too short",
));
}
let channel = u16::from_le_bytes([value[0], value[1]]);
radio.channel.set_user(channel as u8);
}
_ => {}
}
offset += record_len;
}
Ok((radio, &bytes[total_len..]))
}
impl Layer for Dot15d4Radio {
fn name(&self) -> &'static str {
"Dot15d4Radio"
}
fn summary(&self) -> String {
let fcs = if self.effective_fcs_valid() {
"fcs_ok"
} else {
"fcs_bad"
};
match self.effective_rssi() {
Some(rssi) => format!(
"Dot15d4Radio(ch={}, rssi={}, {})",
self.effective_channel(),
rssi,
fcs
),
None => format!("Dot15d4Radio(ch={}, {})", self.effective_channel(), fcs),
}
}
fn inspection_fields(&self) -> Vec<(&'static str, String)> {
let mut fields = vec![("channel", self.effective_channel().to_string())];
if let Some(rssi) = self.effective_rssi() {
fields.push(("rssi", rssi.to_string()));
}
if let Some(lqi) = self.effective_lqi() {
fields.push(("lqi", lqi.to_string()));
}
fields.push(("fcs_valid", self.effective_fcs_valid().to_string()));
fields
}
fn encoded_len(&self) -> usize {
Dot15d4Radio::encoded_len(self)
}
fn compile(&self, _ctx: &LayerContext<'_>, out: &mut Vec<u8>) -> Result<()> {
self.encode(out);
Ok(())
}
fn clone_layer(&self) -> Box<dyn Layer> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn into_any(self: Box<Self>) -> Box<dyn Any> {
self
}
}
impl<R: IntoPacket> core::ops::Div<R> for Dot15d4Radio {
type Output = Packet;
fn div(self, rhs: R) -> Self::Output {
Packet::from_layer(self).concat(rhs)
}
}
#[cfg(test)]
mod tests {
use crate::field::FieldState;
use crate::packet::{Packet, Raw};
use super::*;
#[test]
fn dot15d4_radio_builder_defaults_resolve_when_unset() {
let radio = Dot15d4Radio::new();
assert_eq!(radio.channel.state(), FieldState::Unset);
assert_eq!(radio.rssi.state(), FieldState::Unset);
assert_eq!(radio.lqi.state(), FieldState::Unset);
assert_eq!(radio.fcs_valid.state(), FieldState::Unset);
assert_eq!(radio.fcs_type.state(), FieldState::Unset);
assert_eq!(radio.effective_channel(), DOT15D4_CHANNEL_MIN);
assert_eq!(radio.effective_rssi(), None);
assert_eq!(radio.effective_lqi(), None);
assert!(radio.effective_fcs_valid());
assert_eq!(radio.effective_fcs_type(), None);
}
#[test]
fn dot15d4_radio_builder_setters_mark_fields_user() {
let radio = Dot15d4Radio::new()
.channel(15)
.rssi(-58)
.lqi(200)
.fcs_valid(false)
.fcs_type(1);
assert_eq!(radio.channel.state(), FieldState::User);
assert_eq!(radio.rssi.state(), FieldState::User);
assert_eq!(radio.lqi.state(), FieldState::User);
assert_eq!(radio.fcs_valid.state(), FieldState::User);
assert_eq!(radio.fcs_type.state(), FieldState::User);
assert_eq!(radio.effective_channel(), 15);
assert_eq!(radio.effective_rssi(), Some(-58));
assert_eq!(radio.effective_lqi(), Some(200));
assert!(!radio.effective_fcs_valid());
assert_eq!(radio.effective_fcs_type(), Some(1));
}
#[test]
fn dot15d4_radio_builder_on_channel_sets_channel_user() {
let radio = Dot15d4Radio::on_channel(26);
assert_eq!(radio.channel.state(), FieldState::User);
assert_eq!(radio.effective_channel(), 26);
assert_eq!(radio.rssi.state(), FieldState::Unset);
assert!(radio.effective_fcs_valid());
}
#[test]
fn dot15d4_radio_encode() {
let radio = Dot15d4Radio::new().channel(15).rssi(-60).fcs_valid(true);
let mut out = Vec::new();
radio.encode(&mut out);
assert_eq!(
(-60.0f32).to_le_bytes(),
[0x00, 0x00, 0x70, 0xC2],
"RSS float reference must match IEEE-754 little-endian encoding"
);
#[rustfmt::skip]
let expected: &[u8] = &[
0x00, 0x00, 0x1C, 0x00,
0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00,
0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x70, 0xC2,
0x03, 0x00, 0x03, 0x00, 0x0F, 0x00, 0x00, 0x00,
];
assert_eq!(out.as_slice(), expected);
let length = u16::from_le_bytes([out[2], out[3]]);
assert_eq!(length as usize, out.len());
assert_eq!(out.len(), radio.encoded_len());
assert_eq!(length % 4, 0);
}
#[test]
fn dot15d4_radio_encode_defaults_resolve_when_unset() {
let radio = Dot15d4Radio::new();
let mut out = Vec::new();
radio.encode(&mut out);
#[rustfmt::skip]
let expected: &[u8] = &[
0x00, 0x00, 0x1C, 0x00,
0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00,
0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
0x03, 0x00, 0x03, 0x00, 0x0B, 0x00, 0x00, 0x00,
];
assert_eq!(out.as_slice(), expected);
assert_eq!(out.len(), radio.encoded_len());
}
#[test]
fn dot15d4_radio_encode_honors_out_of_range_channel_without_clamping() {
let radio = Dot15d4Radio::new().channel(200).fcs_valid(false);
let mut out = Vec::new();
radio.encode(&mut out);
assert_eq!(
&out[4..12],
&[0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]
);
assert_eq!(
&out[20..28],
&[0x03, 0x00, 0x03, 0x00, 0xC8, 0x00, 0x00, 0x00]
);
}
#[test]
fn dot15d4_radio_layer_compile_equals_encode() {
let radio = Dot15d4Radio::new().channel(15).rssi(-60).fcs_valid(true);
let mut encoded = Vec::new();
radio.encode(&mut encoded);
let compiled = Packet::from_layer(radio.clone())
.compile()
.expect("compile Dot15d4Radio TAP pseudo-header");
assert_eq!(compiled.as_bytes(), encoded.as_slice());
assert_eq!(compiled.len(), radio.encoded_len());
}
#[test]
fn dot15d4_radio_layer_summary_text() {
let with_rssi = Dot15d4Radio::new().channel(15).rssi(-60).fcs_valid(true);
assert_eq!(with_rssi.summary(), "Dot15d4Radio(ch=15, rssi=-60, fcs_ok)");
let invalid_fcs = Dot15d4Radio::new().channel(20).fcs_valid(false);
assert_eq!(invalid_fcs.summary(), "Dot15d4Radio(ch=20, fcs_bad)");
let summary = Packet::from_layer(with_rssi).summary();
assert!(summary.contains("ch=15"));
assert!(summary.contains("rssi=-60"));
assert!(summary.contains("fcs_ok"));
}
#[test]
fn dot15d4_radio_layer_inspection_fields_list_channel_rssi_lqi_fcs_valid() {
let radio = Dot15d4Radio::new()
.channel(15)
.rssi(-60)
.lqi(200)
.fcs_valid(true);
let fields = radio.inspection_fields();
assert!(fields.contains(&("channel", "15".to_string())));
assert!(fields.contains(&("rssi", "-60".to_string())));
assert!(fields.contains(&("lqi", "200".to_string())));
assert!(fields.contains(&("fcs_valid", "true".to_string())));
}
#[test]
fn dot15d4_radio_layer_round_trips_compiled_pseudo_header() {
let radio = Dot15d4Radio::new().channel(15).rssi(-60).fcs_valid(true);
let mut bytes = Vec::new();
radio.encode(&mut bytes);
bytes.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
let (decoded, tail) =
decode_dot15d4_radio(&bytes).expect("decode Dot15d4Radio TAP pseudo-header");
assert_eq!(tail, &[0xAA, 0xBB, 0xCC]);
assert_eq!(decoded.effective_channel(), 15);
assert_eq!(decoded.effective_rssi(), Some(-60));
assert_eq!(
decoded.effective_fcs_type(),
Some(DOT15D4_TAP_FCS_TYPE_CRC16)
);
assert!(decoded.effective_fcs_valid());
}
#[test]
fn dot15d4_radio_decode_reports_invalid_fcs_when_type_none() {
let radio = Dot15d4Radio::new().channel(11).fcs_valid(false);
let mut bytes = Vec::new();
radio.encode(&mut bytes);
let (decoded, tail) =
decode_dot15d4_radio(&bytes).expect("decode NOFCS-style TAP pseudo-header");
assert!(tail.is_empty());
assert!(!decoded.effective_fcs_valid());
assert_eq!(
decoded.effective_fcs_type(),
Some(DOT15D4_TAP_FCS_TYPE_NONE)
);
}
#[test]
fn dot15d4_radio_decode_truncated_header_is_structured_error() {
let err = decode_dot15d4_radio(&[0x00, 0x00, 0x1C])
.expect_err("must reject a truncated TAP fixed header");
assert_eq!(
err,
CrafterError::BufferTooShort {
context: "dot15d4.tap.header",
required: DOT15D4_TAP_HEADER_LEN,
available: 3,
}
);
}
#[test]
fn dot15d4_radio_decode_truncated_tlv_body_is_structured_error_not_panic() {
let mut bytes = Vec::new();
radio_encode_only_header(&mut bytes, 28);
bytes.extend_from_slice(&[0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00]);
let err = decode_dot15d4_radio(&bytes)
.expect_err("must reject a TAP header whose declared length exceeds the buffer");
assert_eq!(
err,
CrafterError::BufferTooShort {
context: "dot15d4.tap.header",
required: 28,
available: bytes.len(),
}
);
}
#[test]
fn dot15d4_radio_decode_rejects_unsupported_version() {
let err = decode_dot15d4_radio(&[0x01, 0x00, 0x04, 0x00])
.expect_err("must reject an unsupported TAP version");
assert_eq!(
err,
CrafterError::invalid_field_value("dot15d4.tap.header", "unsupported TAP version")
);
}
#[test]
fn dot15d4_radio_decode_skips_unknown_tlv() {
let mut bytes = Vec::new();
radio_encode_only_header(&mut bytes, 12);
bytes.extend_from_slice(&[0xFF, 0x00, 0x04, 0x00, 0xDE, 0xAD, 0xBE, 0xEF]);
let (decoded, tail) =
decode_dot15d4_radio(&bytes).expect("unknown TLVs must be skipped, not rejected");
assert!(tail.is_empty());
assert_eq!(decoded.effective_channel(), DOT15D4_CHANNEL_MIN);
assert_eq!(decoded.effective_rssi(), None);
}
#[test]
fn dot15d4_radio_roundtrip() {
let radio = Dot15d4Radio::on_channel(20)
.rssi(-55)
.fcs_valid(true)
.lqi(200);
let bytes = Packet::from_layer(radio.clone())
.compile()
.expect("compile Dot15d4Radio TAP pseudo-header");
assert_eq!(bytes.len(), radio.encoded_len());
assert_eq!(bytes.as_bytes().len(), DOT15D4_TAP_HEADER_LEN + 8 + 8 + 8);
let total_len = u16::from_le_bytes([bytes.as_bytes()[2], bytes.as_bytes()[3]]);
assert_eq!(total_len as usize, bytes.as_bytes().len());
assert_eq!(total_len % DOT15D4_TAP_TLV_ALIGN as u16, 0);
let (decoded, tail) =
decode_dot15d4_radio(bytes.as_bytes()).expect("decode Dot15d4Radio TAP pseudo-header");
assert!(tail.is_empty());
assert_eq!(decoded.effective_channel(), 20);
assert_eq!(decoded.effective_rssi(), Some(-55));
assert!(decoded.effective_fcs_valid());
assert_eq!(decoded.effective_lqi(), None);
let err = decode_dot15d4_radio(&bytes.as_bytes()[..DOT15D4_TAP_HEADER_LEN - 1])
.expect_err("a truncated TAP fixed header must be a structured error");
assert_eq!(
err,
CrafterError::buffer_too_short(
"dot15d4.tap.header",
DOT15D4_TAP_HEADER_LEN,
DOT15D4_TAP_HEADER_LEN - 1,
)
);
}
#[test]
fn dot15d4_radio_div_builds_two_layer_packet() {
let packet = Dot15d4Radio::on_channel(15) / Raw::from_bytes([0u8; 4]);
assert_eq!(packet.len(), 2);
assert!(packet.layer::<Dot15d4Radio>().is_some());
assert!(packet.layer::<Raw>().is_some());
}
fn radio_encode_only_header(out: &mut Vec<u8>, total_len: u16) {
out.push(DOT15D4_TAP_VERSION);
out.push(0);
out.extend_from_slice(&total_len.to_le_bytes());
}
}