#![forbid(unsafe_code)]
use thiserror::Error;
const MAX_DATASET_LEN: usize = 254;
const EXT_PAN_ID_TYPE: u8 = 0x02;
const EXT_PAN_ID_LEN: usize = 8;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ThreadDatasetError {
#[error("Thread operational dataset is empty")]
Empty,
#[error("Thread operational dataset is too large: {0} bytes (max 254)")]
TooLarge(usize),
#[error("Thread operational dataset is malformed (truncated TLV)")]
Malformed,
#[error("Thread operational dataset has no Extended PAN ID TLV")]
NoExtPanId,
}
#[derive(Clone, PartialEq, Eq)]
pub struct ThreadDataset {
bytes: Vec<u8>,
ext_pan_id: [u8; 8],
}
impl core::fmt::Debug for ThreadDataset {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ThreadDataset")
.field("len", &self.bytes.len())
.field("ext_pan_id", &format_args!("{:02x?}", self.ext_pan_id))
.finish()
}
}
impl ThreadDataset {
pub fn new(bytes: Vec<u8>) -> Result<Self, ThreadDatasetError> {
if bytes.is_empty() {
return Err(ThreadDatasetError::Empty);
}
if bytes.len() > MAX_DATASET_LEN {
return Err(ThreadDatasetError::TooLarge(bytes.len()));
}
let mut ext_pan_id = None;
let mut offset = 0usize;
while offset < bytes.len() {
let Some(&tlv_type) = bytes.get(offset) else {
return Err(ThreadDatasetError::Malformed);
};
let Some(&tlv_len) = bytes.get(offset + 1) else {
return Err(ThreadDatasetError::Malformed);
};
let value_start = offset + 2;
let value_len = usize::from(tlv_len);
let value_end = value_start
.checked_add(value_len)
.ok_or(ThreadDatasetError::Malformed)?;
if value_end > bytes.len() {
return Err(ThreadDatasetError::Malformed);
}
if tlv_type == EXT_PAN_ID_TYPE && value_len == EXT_PAN_ID_LEN {
let mut id = [0u8; EXT_PAN_ID_LEN];
id.copy_from_slice(&bytes[value_start..value_end]);
ext_pan_id = Some(id);
}
offset = value_end;
}
match ext_pan_id {
Some(ext_pan_id) => Ok(Self { bytes, ext_pan_id }),
None => Err(ThreadDatasetError::NoExtPanId),
}
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
#[must_use]
pub fn ext_pan_id(&self) -> [u8; 8] {
self.ext_pan_id
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
fn hex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
const DS: &str = "0e08000000000001000000030000184a0300001235060004001fffe002087896217f787f6ebe0708fdec3f34f3cd2020051071dccee3f164f15da92254e0b9c8a3a5030f4f70656e5468726561642d38396437010289d70410dc4b544c7a58671a2ce4f876f5d6dcd90c0402a0f7f8";
#[test]
fn parses_and_extracts_ext_pan_id() {
let d = ThreadDataset::new(hex(DS)).unwrap();
assert_eq!(
d.ext_pan_id(),
[0x78, 0x96, 0x21, 0x7f, 0x78, 0x7f, 0x6e, 0xbe]
);
assert_eq!(d.as_bytes(), hex(DS).as_slice());
}
#[test]
fn rejects_empty() {
assert!(matches!(
ThreadDataset::new(vec![]),
Err(ThreadDatasetError::Empty)
));
}
#[test]
fn rejects_oversize() {
assert!(matches!(
ThreadDataset::new(vec![0u8; 300]),
Err(ThreadDatasetError::TooLarge(300))
));
}
#[test]
fn rejects_truncated_tlv() {
assert!(matches!(
ThreadDataset::new(vec![0x02, 0x08, 0x01, 0x02]),
Err(ThreadDatasetError::Malformed)
));
}
#[test]
fn rejects_no_ext_pan_id() {
assert!(matches!(
ThreadDataset::new(vec![0x03, 0x00]),
Err(ThreadDatasetError::NoExtPanId)
));
}
#[test]
fn debug_redacts_network_key() {
let d = ThreadDataset::new(hex(DS)).unwrap();
let rendered = format!("{d:?}");
assert!(
!rendered.contains("dc4b544c7a58671a2ce4f876f5d6dcd9"),
"Debug must not contain the raw dataset bytes (network key): {rendered}",
);
assert!(rendered.contains("ThreadDataset"), "got {rendered}");
assert!(
rendered.contains("len"),
"dataset length should appear: {rendered}"
);
assert!(
rendered.contains("78, 96, 21, 7f, 78, 7f, 6e, be"),
"ext_pan_id should appear in Debug output: {rendered}"
);
}
}