use std::net::{Ipv4Addr, Ipv6Addr};
use std::time::Duration;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use ipnet::{Ipv4Net, Ipv6Net};
use crate::data_item::{ConnectionPointFlags, DataItem, PeerFlags, RawDataItem};
use crate::error::{CodecError, ExpectedLen};
use crate::ids::{DataItemType, ExtensionId, MessageType, SignalType};
use crate::mac::MacAddress;
use crate::message::Message;
use crate::signal::Signal;
use crate::status::StatusCode;
use crate::{MIN_HEARTBEAT_INTERVAL_MS, SIGNAL_PREFIX};
pub const SIGNAL_HEADER_LEN: usize = 8;
pub const MESSAGE_HEADER_LEN: usize = 4;
mod field {
pub const DATA_ITEM_FLAGS: &str = "data_item_flags";
pub const HEARTBEAT_INTERVAL_MS: &str = "heartbeat_interval_ms";
pub const LATENCY_US: &str = "latency_us";
pub const RESOURCES: &str = "resources";
pub const RLQ_RECEIVE: &str = "relative_link_quality_receive";
pub const RLQ_TRANSMIT: &str = "relative_link_quality_transmit";
pub const IPV4_ATTACHED_SUBNET_PREFIX: &str = "ipv4_attached_subnet_prefix";
pub const IPV6_ATTACHED_SUBNET_PREFIX: &str = "ipv6_attached_subnet_prefix";
pub const DATA_ITEM_VALUE_LENGTH: &str = "data_item_value_length";
pub const SIGNAL_BODY_LENGTH: &str = "signal_body_length";
pub const MESSAGE_BODY_LENGTH: &str = "message_body_length";
}
impl RawDataItem {
pub fn encode(&self, out: &mut BytesMut) -> Result<(), CodecError> {
let len = u16_length(field::DATA_ITEM_VALUE_LENGTH, self.value.len())?;
out.put_u16(self.type_id.0);
out.put_u16(len);
out.put_slice(&self.value);
Ok(())
}
pub fn decode(src: &mut Bytes) -> Result<Self, CodecError> {
ensure_len(src.remaining(), 4)?;
let type_id = DataItemType(src.get_u16());
let len = src.get_u16() as usize;
ensure_len(src.remaining(), len)?;
let value = src.split_to(len);
Ok(RawDataItem { type_id, value })
}
}
impl DataItem {
pub fn encode(&self, out: &mut BytesMut) -> Result<(), CodecError> {
let restore_to = out.len();
match self.encode_into(out) {
Ok(()) => Ok(()),
Err(e) => {
out.truncate(restore_to);
Err(e)
}
}
}
fn encode_into(&self, out: &mut BytesMut) -> Result<(), CodecError> {
if let DataItem::Unknown(raw) = self {
return raw.encode(out);
}
if let Some(projected) = self.projected_value_len() {
u16_length(field::DATA_ITEM_VALUE_LENGTH, projected)?;
}
out.put_u16(self.type_id().0);
let len_pos = out.len();
out.put_u16(0);
let value_start = out.len();
match self {
DataItem::Status { code, text } => {
out.put_u8(code.0);
out.put_slice(text.as_bytes());
}
DataItem::Ipv4ConnectionPoint { flags, addr, port } => {
out.put_u8(encode_cp_flags(*flags));
out.put_slice(&addr.octets());
if let Some(p) = port {
out.put_u16(*p);
}
}
DataItem::Ipv6ConnectionPoint { flags, addr, port } => {
out.put_u8(encode_cp_flags(*flags));
out.put_slice(&addr.octets());
if let Some(p) = port {
out.put_u16(*p);
}
}
DataItem::PeerType { flags, description } => {
out.put_u8(encode_peer_flags(*flags));
out.put_slice(description.as_bytes());
}
DataItem::HeartbeatInterval(d) => {
let ms = d.as_millis();
if ms < MIN_HEARTBEAT_INTERVAL_MS as u128 {
return Err(CodecError::OutOfRange {
field: field::HEARTBEAT_INTERVAL_MS,
value: u64::try_from(ms).unwrap_or(u64::MAX),
});
}
if ms > u32::MAX as u128 {
return Err(CodecError::OutOfRange {
field: field::HEARTBEAT_INTERVAL_MS,
value: u64::try_from(ms).unwrap_or(u64::MAX),
});
}
out.put_u32(ms as u32);
}
DataItem::ExtensionsSupported(ids) => {
for id in ids {
out.put_u16(id.0);
}
}
DataItem::MacAddress(mac) => {
out.put_slice(mac.as_bytes());
}
DataItem::Ipv4Address { add, addr } => {
out.put_u8(u8::from(*add));
out.put_slice(&addr.octets());
}
DataItem::Ipv6Address { add, addr } => {
out.put_u8(u8::from(*add));
out.put_slice(&addr.octets());
}
DataItem::Ipv4AttachedSubnet { add, subnet } => {
out.put_u8(u8::from(*add));
out.put_slice(&subnet.network().octets());
out.put_u8(subnet.prefix_len());
}
DataItem::Ipv6AttachedSubnet { add, subnet } => {
out.put_u8(u8::from(*add));
out.put_slice(&subnet.network().octets());
out.put_u8(subnet.prefix_len());
}
DataItem::MaxDataRateReceive(bps)
| DataItem::MaxDataRateTransmit(bps)
| DataItem::CurrentDataRateReceive(bps)
| DataItem::CurrentDataRateTransmit(bps) => {
out.put_u64(*bps);
}
DataItem::Latency(d) => {
let us = d.as_micros();
if us > u64::MAX as u128 {
return Err(CodecError::OutOfRange {
field: field::LATENCY_US,
value: u64::MAX,
});
}
out.put_u64(us as u64);
}
DataItem::Resources(pct) => {
check_percent(field::RESOURCES, *pct)?;
out.put_u8(*pct);
}
DataItem::RelativeLinkQualityReceive(pct) => {
check_percent(field::RLQ_RECEIVE, *pct)?;
out.put_u8(*pct);
}
DataItem::RelativeLinkQualityTransmit(pct) => {
check_percent(field::RLQ_TRANSMIT, *pct)?;
out.put_u8(*pct);
}
DataItem::Mtu(mtu) => {
out.put_u16(*mtu);
}
DataItem::Unknown(_) => unreachable!("Unknown handled by early return"),
}
let value_len = out.len() - value_start;
let len_u16 = u16_length(field::DATA_ITEM_VALUE_LENGTH, value_len)?;
out[len_pos..len_pos + 2].copy_from_slice(&len_u16.to_be_bytes());
Ok(())
}
fn projected_value_len(&self) -> Option<usize> {
match self {
DataItem::Status { text, .. } => Some(1usize.saturating_add(text.len())),
DataItem::PeerType { description, .. } => {
Some(1usize.saturating_add(description.len()))
}
DataItem::ExtensionsSupported(ids) => Some(ids.len().saturating_mul(2)),
_ => None,
}
}
pub fn decode(raw: RawDataItem) -> Result<Self, CodecError> {
let kind = raw.type_id;
let len = raw.value.len();
let v = &raw.value[..];
match kind {
DataItemType::STATUS => {
if len < 1 {
return Err(CodecError::InvalidDataItemLength {
kind,
expected: ExpectedLen::AtLeast(1),
got: len,
});
}
let code = StatusCode(v[0]);
let text = String::from_utf8(v[1..].to_vec())?;
Ok(DataItem::Status { code, text })
}
DataItemType::IPV4_CONNECTION_POINT => {
if len != 5 && len != 7 {
return Err(CodecError::InvalidDataItemLength {
kind,
expected: ExpectedLen::OneOf(&[5, 7]),
got: len,
});
}
validate_flags(v[0])?;
let flags = decode_cp_flags(v[0]);
let addr = Ipv4Addr::new(v[1], v[2], v[3], v[4]);
let port = (len == 7).then(|| u16::from_be_bytes([v[5], v[6]]));
Ok(DataItem::Ipv4ConnectionPoint { flags, addr, port })
}
DataItemType::IPV6_CONNECTION_POINT => {
if len != 17 && len != 19 {
return Err(CodecError::InvalidDataItemLength {
kind,
expected: ExpectedLen::OneOf(&[17, 19]),
got: len,
});
}
validate_flags(v[0])?;
let flags = decode_cp_flags(v[0]);
let mut octets = [0u8; 16];
octets.copy_from_slice(&v[1..17]);
let addr = Ipv6Addr::from(octets);
let port = (len == 19).then(|| u16::from_be_bytes([v[17], v[18]]));
Ok(DataItem::Ipv6ConnectionPoint { flags, addr, port })
}
DataItemType::PEER_TYPE => {
if len < 1 {
return Err(CodecError::InvalidDataItemLength {
kind,
expected: ExpectedLen::AtLeast(1),
got: len,
});
}
validate_flags(v[0])?;
let flags = decode_peer_flags(v[0]);
let description = String::from_utf8(v[1..].to_vec())?;
Ok(DataItem::PeerType { flags, description })
}
DataItemType::HEARTBEAT_INTERVAL => {
expect_exact(kind, len, 4)?;
let ms = u32::from_be_bytes([v[0], v[1], v[2], v[3]]);
if ms < MIN_HEARTBEAT_INTERVAL_MS {
return Err(CodecError::OutOfRange {
field: field::HEARTBEAT_INTERVAL_MS,
value: ms.into(),
});
}
Ok(DataItem::HeartbeatInterval(Duration::from_millis(
ms.into(),
)))
}
DataItemType::EXTENSIONS_SUPPORTED => {
if len % 2 != 0 {
return Err(CodecError::InvalidDataItemLength {
kind,
expected: ExpectedLen::Multiple(2),
got: len,
});
}
let mut ids = Vec::with_capacity(len / 2);
let mut i = 0;
while i < len {
ids.push(ExtensionId(u16::from_be_bytes([v[i], v[i + 1]])));
i += 2;
}
Ok(DataItem::ExtensionsSupported(ids))
}
DataItemType::MAC_ADDRESS => {
let mac = match len {
6 => {
let mut octets = [0u8; 6];
octets.copy_from_slice(&v[..6]);
MacAddress::Eui48(octets)
}
8 => {
let mut octets = [0u8; 8];
octets.copy_from_slice(&v[..8]);
MacAddress::Eui64(octets)
}
_ => {
return Err(CodecError::InvalidDataItemLength {
kind,
expected: ExpectedLen::OneOf(&[6, 8]),
got: len,
});
}
};
Ok(DataItem::MacAddress(mac))
}
DataItemType::IPV4_ADDRESS => {
expect_exact(kind, len, 5)?;
validate_flags(v[0])?;
let add = (v[0] & 0x01) != 0;
let addr = Ipv4Addr::new(v[1], v[2], v[3], v[4]);
Ok(DataItem::Ipv4Address { add, addr })
}
DataItemType::IPV6_ADDRESS => {
expect_exact(kind, len, 17)?;
validate_flags(v[0])?;
let add = (v[0] & 0x01) != 0;
let mut octets = [0u8; 16];
octets.copy_from_slice(&v[1..17]);
Ok(DataItem::Ipv6Address {
add,
addr: Ipv6Addr::from(octets),
})
}
DataItemType::IPV4_ATTACHED_SUBNET => {
expect_exact(kind, len, 6)?;
validate_flags(v[0])?;
let add = (v[0] & 0x01) != 0;
let addr = Ipv4Addr::new(v[1], v[2], v[3], v[4]);
let prefix = v[5];
let subnet = Ipv4Net::new(addr, prefix)
.map_err(|_| CodecError::OutOfRange {
field: field::IPV4_ATTACHED_SUBNET_PREFIX,
value: prefix as u64,
})?
.trunc();
Ok(DataItem::Ipv4AttachedSubnet { add, subnet })
}
DataItemType::IPV6_ATTACHED_SUBNET => {
expect_exact(kind, len, 18)?;
validate_flags(v[0])?;
let add = (v[0] & 0x01) != 0;
let mut octets = [0u8; 16];
octets.copy_from_slice(&v[1..17]);
let prefix = v[17];
let subnet = Ipv6Net::new(Ipv6Addr::from(octets), prefix)
.map_err(|_| CodecError::OutOfRange {
field: field::IPV6_ATTACHED_SUBNET_PREFIX,
value: prefix as u64,
})?
.trunc();
Ok(DataItem::Ipv6AttachedSubnet { add, subnet })
}
DataItemType::MAXIMUM_DATA_RATE_RECEIVE => {
expect_exact(kind, len, 8)?;
Ok(DataItem::MaxDataRateReceive(read_u64_be(v)))
}
DataItemType::MAXIMUM_DATA_RATE_TRANSMIT => {
expect_exact(kind, len, 8)?;
Ok(DataItem::MaxDataRateTransmit(read_u64_be(v)))
}
DataItemType::CURRENT_DATA_RATE_RECEIVE => {
expect_exact(kind, len, 8)?;
Ok(DataItem::CurrentDataRateReceive(read_u64_be(v)))
}
DataItemType::CURRENT_DATA_RATE_TRANSMIT => {
expect_exact(kind, len, 8)?;
Ok(DataItem::CurrentDataRateTransmit(read_u64_be(v)))
}
DataItemType::LATENCY => {
expect_exact(kind, len, 8)?;
Ok(DataItem::Latency(Duration::from_micros(read_u64_be(v))))
}
DataItemType::RESOURCES => Ok(DataItem::Resources(decode_percent(
kind,
v,
field::RESOURCES,
)?)),
DataItemType::RELATIVE_LINK_QUALITY_RECEIVE => Ok(
DataItem::RelativeLinkQualityReceive(decode_percent(kind, v, field::RLQ_RECEIVE)?),
),
DataItemType::RELATIVE_LINK_QUALITY_TRANSMIT => {
Ok(DataItem::RelativeLinkQualityTransmit(decode_percent(
kind,
v,
field::RLQ_TRANSMIT,
)?))
}
DataItemType::MTU => {
expect_exact(kind, len, 2)?;
Ok(DataItem::Mtu(u16::from_be_bytes([v[0], v[1]])))
}
_ => Ok(DataItem::Unknown(raw)),
}
}
}
impl Signal {
pub fn encode(&self) -> Result<BytesMut, CodecError> {
let mut body = BytesMut::new();
for item in &self.data_items {
item.encode(&mut body)?;
}
let body_len = u16_length(field::SIGNAL_BODY_LENGTH, body.len())?;
let mut out = BytesMut::with_capacity(SIGNAL_HEADER_LEN + body.len());
out.put_slice(SIGNAL_PREFIX);
out.put_u16(self.signal_type.0);
out.put_u16(body_len);
out.put(body);
Ok(out)
}
pub fn decode(mut src: Bytes) -> Result<Self, CodecError> {
ensure_len(src.remaining(), SIGNAL_HEADER_LEN)?;
let mut prefix = [0u8; 4];
src.copy_to_slice(&mut prefix);
if &prefix != SIGNAL_PREFIX {
return Err(CodecError::MissingSignalPrefix);
}
let signal_type = SignalType(src.get_u16());
let declared = src.get_u16() as usize;
if src.remaining() != declared {
return Err(CodecError::LengthMismatch {
declared,
remaining: src.remaining(),
});
}
let mut body = src.split_to(declared);
let mut data_items = Vec::new();
while body.has_remaining() {
let raw = RawDataItem::decode(&mut body)?;
data_items.push(DataItem::decode(raw)?);
}
Ok(Signal {
signal_type,
data_items,
})
}
}
impl Message {
pub fn encode(&self) -> Result<BytesMut, CodecError> {
let mut body = BytesMut::new();
for item in &self.data_items {
item.encode(&mut body)?;
}
let body_len = u16_length(field::MESSAGE_BODY_LENGTH, body.len())?;
let mut out = BytesMut::with_capacity(MESSAGE_HEADER_LEN + body.len());
out.put_u16(self.message_type.0);
out.put_u16(body_len);
out.put(body);
Ok(out)
}
pub fn decode(mut src: Bytes) -> Result<Self, CodecError> {
ensure_len(src.remaining(), MESSAGE_HEADER_LEN)?;
let message_type = MessageType(src.get_u16());
let declared = src.get_u16() as usize;
if src.remaining() < declared {
return Err(CodecError::LengthMismatch {
declared,
remaining: src.remaining(),
});
}
let mut body = src.split_to(declared);
let mut data_items = Vec::new();
while body.has_remaining() {
let raw = RawDataItem::decode(&mut body)?;
data_items.push(DataItem::decode(raw)?);
}
Ok(Message {
message_type,
data_items,
})
}
}
fn ensure_len(have: usize, needed: usize) -> Result<(), CodecError> {
if have < needed {
Err(CodecError::Truncated { needed, have })
} else {
Ok(())
}
}
fn expect_exact(kind: DataItemType, got: usize, expected: usize) -> Result<(), CodecError> {
if got == expected {
Ok(())
} else {
Err(CodecError::InvalidDataItemLength {
kind,
expected: ExpectedLen::Exact(expected),
got,
})
}
}
fn read_u64_be(value: &[u8]) -> u64 {
let mut buf = [0u8; 8];
buf.copy_from_slice(&value[..8]);
u64::from_be_bytes(buf)
}
fn validate_flags(byte: u8) -> Result<(), CodecError> {
if byte & 0xfe != 0 {
return Err(CodecError::OutOfRange {
field: field::DATA_ITEM_FLAGS,
value: byte.into(),
});
}
Ok(())
}
fn encode_cp_flags(flags: ConnectionPointFlags) -> u8 {
u8::from(flags.use_tls)
}
fn decode_cp_flags(byte: u8) -> ConnectionPointFlags {
ConnectionPointFlags {
use_tls: (byte & 0x01) != 0,
}
}
fn encode_peer_flags(flags: PeerFlags) -> u8 {
u8::from(flags.smi)
}
fn decode_peer_flags(byte: u8) -> PeerFlags {
PeerFlags {
smi: (byte & 0x01) != 0,
}
}
fn check_percent(field: &'static str, pct: u8) -> Result<(), CodecError> {
if pct > 100 {
Err(CodecError::OutOfRange {
field,
value: pct as u64,
})
} else {
Ok(())
}
}
fn decode_percent(kind: DataItemType, value: &[u8], field: &'static str) -> Result<u8, CodecError> {
expect_exact(kind, value.len(), 1)?;
let pct = value[0];
check_percent(field, pct)?;
Ok(pct)
}
fn u16_length(field: &'static str, len: usize) -> Result<u16, CodecError> {
u16::try_from(len).map_err(|_| CodecError::OutOfRange {
field,
value: len as u64,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn encode_one(item: DataItem) -> Vec<u8> {
let mut buf = BytesMut::new();
item.encode(&mut buf).expect("encode should not fail");
buf.to_vec()
}
fn decode_one(bytes: &[u8]) -> DataItem {
let mut b = Bytes::copy_from_slice(bytes);
let raw = RawDataItem::decode(&mut b).unwrap();
DataItem::decode(raw).unwrap()
}
#[test]
fn empty_message_roundtrips() {
let m = Message::new(MessageType::HEARTBEAT);
let bytes = m.encode().unwrap().freeze();
let decoded = Message::decode(bytes).unwrap();
assert_eq!(decoded.message_type, MessageType::HEARTBEAT);
assert!(decoded.data_items.is_empty());
}
#[test]
fn empty_signal_roundtrips() {
let s = Signal::new(SignalType::PEER_DISCOVERY);
let bytes = s.encode().unwrap().freeze();
let decoded = Signal::decode(bytes).unwrap();
assert_eq!(decoded.signal_type, SignalType::PEER_DISCOVERY);
}
#[test]
fn signal_rejects_bad_prefix() {
let mut bad = BytesMut::from(&b"XLEP"[..]);
bad.put_u16(1);
bad.put_u16(0);
let err = Signal::decode(bad.freeze()).unwrap_err();
assert!(matches!(err, CodecError::MissingSignalPrefix));
}
#[test]
fn status_encodes_with_text() {
let item = DataItem::Status {
code: StatusCode::SUCCESS,
text: "ok".into(),
};
assert_eq!(
encode_one(item),
vec![0x00, 0x01, 0x00, 0x03, 0x00, b'o', b'k']
);
}
#[test]
fn status_roundtrips_terminate_code() {
let item = DataItem::Status {
code: StatusCode::TIMED_OUT,
text: "deadline".into(),
};
let bytes = encode_one(item);
let DataItem::Status { code, text } = decode_one(&bytes) else {
panic!("wrong variant")
};
assert_eq!(code, StatusCode::TIMED_OUT);
assert_eq!(text, "deadline");
}
#[test]
fn status_with_empty_text_roundtrips() {
let item = DataItem::Status {
code: StatusCode::SUCCESS,
text: String::new(),
};
let bytes = encode_one(item);
assert_eq!(bytes, vec![0x00, 0x01, 0x00, 0x01, 0x00]);
let DataItem::Status { code, text } = decode_one(&bytes) else {
panic!()
};
assert_eq!(code, StatusCode::SUCCESS);
assert!(text.is_empty());
}
#[test]
fn status_with_zero_length_value_rejected() {
let raw = RawDataItem {
type_id: DataItemType::STATUS,
value: Bytes::new(),
};
let err = DataItem::decode(raw).unwrap_err();
assert!(matches!(
err,
CodecError::InvalidDataItemLength {
kind: DataItemType::STATUS,
..
}
));
}
#[test]
fn ipv4_connection_point_with_port_encodes() {
let item = DataItem::Ipv4ConnectionPoint {
flags: ConnectionPointFlags { use_tls: true },
addr: Ipv4Addr::new(10, 0, 0, 1),
port: Some(854),
};
assert_eq!(
encode_one(item),
vec![0x00, 0x02, 0x00, 0x07, 0x01, 10, 0, 0, 1, 0x03, 0x56]
);
}
#[test]
fn ipv4_connection_point_length_between_valid_forms_rejected() {
let raw = RawDataItem {
type_id: DataItemType::IPV4_CONNECTION_POINT,
value: Bytes::from_static(&[0, 1, 2, 3, 4, 5]),
};
match DataItem::decode(raw).unwrap_err() {
CodecError::InvalidDataItemLength {
kind: DataItemType::IPV4_CONNECTION_POINT,
expected: ExpectedLen::OneOf(&[5, 7]),
got: 6,
} => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn ipv6_connection_point_length_between_valid_forms_rejected() {
let raw = RawDataItem {
type_id: DataItemType::IPV6_CONNECTION_POINT,
value: Bytes::from_static(&[0u8; 18]),
};
match DataItem::decode(raw).unwrap_err() {
CodecError::InvalidDataItemLength {
kind: DataItemType::IPV6_CONNECTION_POINT,
expected: ExpectedLen::OneOf(&[17, 19]),
got: 18,
} => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn status_at_least_one_byte_error_carries_atleast() {
let raw = RawDataItem {
type_id: DataItemType::STATUS,
value: Bytes::new(),
};
match DataItem::decode(raw).unwrap_err() {
CodecError::InvalidDataItemLength {
kind: DataItemType::STATUS,
expected: ExpectedLen::AtLeast(1),
got: 0,
} => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn extensions_supported_odd_length_error_carries_multiple() {
let raw = RawDataItem {
type_id: DataItemType::EXTENSIONS_SUPPORTED,
value: Bytes::from_static(&[0x00, 0x01, 0x00]),
};
match DataItem::decode(raw).unwrap_err() {
CodecError::InvalidDataItemLength {
kind: DataItemType::EXTENSIONS_SUPPORTED,
expected: ExpectedLen::Multiple(2),
got: 3,
} => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn ipv4_connection_point_without_port_roundtrips() {
let item = DataItem::Ipv4ConnectionPoint {
flags: ConnectionPointFlags::default(),
addr: Ipv4Addr::new(192, 168, 1, 1),
port: None,
};
let bytes = encode_one(item);
assert_eq!(bytes.len(), 4 + 5);
let DataItem::Ipv4ConnectionPoint { flags, addr, port } = decode_one(&bytes) else {
panic!()
};
assert!(!flags.use_tls);
assert_eq!(addr, Ipv4Addr::new(192, 168, 1, 1));
assert_eq!(port, None);
}
#[test]
fn ipv6_connection_point_with_port_roundtrips() {
let item = DataItem::Ipv6ConnectionPoint {
flags: ConnectionPointFlags { use_tls: true },
addr: "fe80::1".parse().unwrap(),
port: Some(854),
};
let bytes = encode_one(item);
assert_eq!(bytes.len(), 4 + 19);
let DataItem::Ipv6ConnectionPoint { flags, addr, port } = decode_one(&bytes) else {
panic!()
};
assert!(flags.use_tls);
assert_eq!(addr, "fe80::1".parse::<Ipv6Addr>().unwrap());
assert_eq!(port, Some(854));
}
#[test]
fn ipv6_connection_point_without_port_roundtrips() {
let item = DataItem::Ipv6ConnectionPoint {
flags: ConnectionPointFlags::default(),
addr: Ipv6Addr::LOCALHOST,
port: None,
};
let bytes = encode_one(item);
assert_eq!(bytes.len(), 4 + 17);
let DataItem::Ipv6ConnectionPoint { port, .. } = decode_one(&bytes) else {
panic!()
};
assert_eq!(port, None);
}
#[test]
fn peer_type_encodes() {
let item = DataItem::PeerType {
flags: PeerFlags { smi: true },
description: "modem".into(),
};
assert_eq!(
encode_one(item),
vec![0x00, 0x04, 0x00, 0x06, 0x01, b'm', b'o', b'd', b'e', b'm']
);
}
#[test]
fn peer_type_with_empty_description_roundtrips() {
let item = DataItem::PeerType {
flags: PeerFlags::default(),
description: String::new(),
};
let bytes = encode_one(item);
let DataItem::PeerType { flags, description } = decode_one(&bytes) else {
panic!()
};
assert!(!flags.smi);
assert!(description.is_empty());
}
#[test]
fn heartbeat_interval_encodes() {
let item = DataItem::HeartbeatInterval(Duration::from_millis(60_000));
assert_eq!(
encode_one(item),
vec![0x00, 0x05, 0x00, 0x04, 0x00, 0x00, 0xEA, 0x60]
);
}
#[test]
fn heartbeat_interval_below_rfc_minimum_rejected_on_encode() {
for ms in [0, MIN_HEARTBEAT_INTERVAL_MS - 1] {
let item = DataItem::HeartbeatInterval(Duration::from_millis(ms.into()));
let mut buf = BytesMut::new();
let err = item.encode(&mut buf).unwrap_err();
assert!(matches!(
err,
CodecError::OutOfRange {
field: field::HEARTBEAT_INTERVAL_MS,
..
}
));
assert!(buf.is_empty());
}
}
#[test]
fn heartbeat_interval_below_rfc_minimum_rejected_on_decode() {
for ms in [0, MIN_HEARTBEAT_INTERVAL_MS - 1] {
let raw = RawDataItem {
type_id: DataItemType::HEARTBEAT_INTERVAL,
value: Bytes::copy_from_slice(&ms.to_be_bytes()),
};
let err = DataItem::decode(raw).unwrap_err();
assert!(matches!(
err,
CodecError::OutOfRange {
field: field::HEARTBEAT_INTERVAL_MS,
..
}
));
}
}
#[test]
fn heartbeat_interval_overflow_rejected_on_encode() {
let item = DataItem::HeartbeatInterval(Duration::from_secs(u64::MAX / 1000));
let mut buf = BytesMut::new();
let err = item.encode(&mut buf).unwrap_err();
assert!(matches!(err, CodecError::OutOfRange { .. }));
assert!(buf.is_empty());
}
#[test]
fn heartbeat_interval_at_u32_max_encodes() {
let item = DataItem::HeartbeatInterval(Duration::from_millis(u32::MAX.into()));
let bytes = encode_one(item);
let DataItem::HeartbeatInterval(d) = decode_one(&bytes) else {
panic!()
};
assert_eq!(d, Duration::from_millis(u32::MAX.into()));
}
#[test]
fn extensions_supported_encodes_three_ids() {
let item = DataItem::ExtensionsSupported(vec![
ExtensionId(1),
ExtensionId(2),
ExtensionId(0xFFFF),
]);
assert_eq!(
encode_one(item),
vec![0x00, 0x06, 0x00, 0x06, 0x00, 0x01, 0x00, 0x02, 0xFF, 0xFF]
);
}
#[test]
fn extensions_supported_empty_roundtrips() {
let item = DataItem::ExtensionsSupported(Vec::new());
let bytes = encode_one(item);
assert_eq!(bytes, vec![0x00, 0x06, 0x00, 0x00]);
let DataItem::ExtensionsSupported(ids) = decode_one(&bytes) else {
panic!()
};
assert!(ids.is_empty());
}
#[test]
fn extensions_supported_odd_length_rejected() {
let raw = RawDataItem {
type_id: DataItemType::EXTENSIONS_SUPPORTED,
value: Bytes::from_static(&[0x00, 0x01, 0x00]),
};
let err = DataItem::decode(raw).unwrap_err();
assert!(matches!(
err,
CodecError::InvalidDataItemLength {
kind: DataItemType::EXTENSIONS_SUPPORTED,
..
}
));
}
#[test]
fn mac_address_eui48_encodes() {
let item = DataItem::MacAddress(MacAddress::Eui48([0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01]));
assert_eq!(
encode_one(item),
vec![0x00, 0x07, 0x00, 0x06, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01]
);
}
#[test]
fn mac_address_eui64_encodes() {
let item = DataItem::MacAddress(MacAddress::Eui64([
0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE,
]));
assert_eq!(
encode_one(item),
vec![
0x00, 0x07, 0x00, 0x08, 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE,
]
);
}
#[test]
fn mac_address_eui48_roundtrips() {
let bytes = encode_one(DataItem::MacAddress(MacAddress::Eui48([
0x02, 0x00, 0x00, 0x00, 0x00, 0x01,
])));
let DataItem::MacAddress(mac) = decode_one(&bytes) else {
panic!("expected MacAddress variant")
};
assert!(mac.is_eui48());
assert_eq!(mac.as_bytes(), &[0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
}
#[test]
fn mac_address_eui64_roundtrips() {
let bytes = encode_one(DataItem::MacAddress(MacAddress::Eui64([
0x02, 0x00, 0x00, 0xFF, 0xFE, 0x00, 0x00, 0x01,
])));
let DataItem::MacAddress(mac) = decode_one(&bytes) else {
panic!("expected MacAddress variant")
};
assert!(mac.is_eui64());
assert_eq!(
mac.as_bytes(),
&[0x02, 0x00, 0x00, 0xFF, 0xFE, 0x00, 0x00, 0x01]
);
}
#[test]
fn mac_address_wrong_length_rejected() {
for bad_len in [0usize, 1, 5, 7, 9, 16] {
let raw = RawDataItem {
type_id: DataItemType::MAC_ADDRESS,
value: Bytes::from(vec![0u8; bad_len]),
};
match DataItem::decode(raw).unwrap_err() {
CodecError::InvalidDataItemLength {
kind: DataItemType::MAC_ADDRESS,
expected: ExpectedLen::OneOf(&[6, 8]),
got,
} => assert_eq!(got, bad_len),
other => panic!("len={bad_len} got unexpected error {other:?}"),
}
}
}
#[test]
fn ipv4_address_add_drop_flag_roundtrips() {
let add = DataItem::Ipv4Address {
add: true,
addr: Ipv4Addr::new(1, 2, 3, 4),
};
let bytes = encode_one(add);
assert_eq!(bytes, vec![0x00, 0x08, 0x00, 0x05, 0x01, 1, 2, 3, 4]);
let DataItem::Ipv4Address { add, addr } = decode_one(&bytes) else {
panic!()
};
assert!(add);
assert_eq!(addr, Ipv4Addr::new(1, 2, 3, 4));
let drop = DataItem::Ipv4Address {
add: false,
addr: Ipv4Addr::new(1, 2, 3, 4),
};
let bytes = encode_one(drop);
assert_eq!(bytes[4], 0x00);
let DataItem::Ipv4Address { add, .. } = decode_one(&bytes) else {
panic!()
};
assert!(!add);
}
#[test]
fn ipv6_address_roundtrips() {
let item = DataItem::Ipv6Address {
add: true,
addr: "2001:db8::1".parse().unwrap(),
};
let bytes = encode_one(item);
assert_eq!(bytes.len(), 4 + 17);
let DataItem::Ipv6Address { add, addr } = decode_one(&bytes) else {
panic!()
};
assert!(add);
assert_eq!(addr, "2001:db8::1".parse::<Ipv6Addr>().unwrap());
}
#[test]
fn ipv4_attached_subnet_encodes() {
let item = DataItem::Ipv4AttachedSubnet {
add: true,
subnet: "10.0.0.0/24".parse().unwrap(),
};
assert_eq!(
encode_one(item),
vec![0x00, 0x0A, 0x00, 0x06, 0x01, 10, 0, 0, 0, 24]
);
}
#[test]
fn ipv4_attached_subnet_bad_prefix_rejected() {
let raw = RawDataItem {
type_id: DataItemType::IPV4_ATTACHED_SUBNET,
value: Bytes::from_static(&[0x01, 10, 0, 0, 0, 33]),
};
let err = DataItem::decode(raw).unwrap_err();
assert!(matches!(err, CodecError::OutOfRange { .. }));
}
#[test]
fn ipv6_attached_subnet_roundtrips() {
let item = DataItem::Ipv6AttachedSubnet {
add: true,
subnet: "2001:db8::/32".parse().unwrap(),
};
let bytes = encode_one(item);
assert_eq!(bytes.len(), 4 + 18);
let DataItem::Ipv6AttachedSubnet { add, subnet } = decode_one(&bytes) else {
panic!()
};
assert!(add);
assert_eq!(subnet, "2001:db8::/32".parse::<Ipv6Net>().unwrap());
}
#[test]
fn data_rate_variants_encode_as_u64() {
let pairs = [
(
DataItem::MaxDataRateReceive(1_000_000_000),
DataItemType::MAXIMUM_DATA_RATE_RECEIVE,
),
(
DataItem::MaxDataRateTransmit(1_000_000_000),
DataItemType::MAXIMUM_DATA_RATE_TRANSMIT,
),
(
DataItem::CurrentDataRateReceive(500_000_000),
DataItemType::CURRENT_DATA_RATE_RECEIVE,
),
(
DataItem::CurrentDataRateTransmit(500_000_000),
DataItemType::CURRENT_DATA_RATE_TRANSMIT,
),
];
for (item, ty) in pairs {
let bytes = encode_one(item);
assert_eq!(bytes.len(), 4 + 8);
assert_eq!(u16::from_be_bytes([bytes[0], bytes[1]]), ty.0);
assert_eq!(u16::from_be_bytes([bytes[2], bytes[3]]), 8);
}
}
#[test]
fn latency_encodes_microseconds() {
let item = DataItem::Latency(Duration::from_micros(0xDEAD_BEEF));
let bytes = encode_one(item);
assert_eq!(&bytes[..4], &[0x00, 0x10, 0x00, 0x08]);
let DataItem::Latency(d) = decode_one(&bytes) else {
panic!()
};
assert_eq!(d, Duration::from_micros(0xDEAD_BEEF));
}
#[test]
fn resources_encodes_single_byte() {
let item = DataItem::Resources(75);
assert_eq!(encode_one(item), vec![0x00, 0x11, 0x00, 0x01, 0x4B]);
}
#[test]
fn resources_above_100_rejected_on_decode() {
let raw = RawDataItem {
type_id: DataItemType::RESOURCES,
value: Bytes::from_static(&[150]),
};
let err = DataItem::decode(raw).unwrap_err();
assert!(matches!(err, CodecError::OutOfRange { .. }));
}
#[test]
fn resources_above_100_rejected_on_encode() {
let mut buf = BytesMut::new();
let err = DataItem::Resources(150).encode(&mut buf).unwrap_err();
assert!(matches!(err, CodecError::OutOfRange { .. }));
assert!(buf.is_empty());
}
#[test]
fn rlq_above_100_rejected_on_encode() {
for item in [
DataItem::RelativeLinkQualityReceive(101),
DataItem::RelativeLinkQualityTransmit(255),
] {
let mut buf = BytesMut::new();
let err = item.encode(&mut buf).unwrap_err();
assert!(matches!(err, CodecError::OutOfRange { .. }));
assert!(buf.is_empty());
}
}
#[test]
fn latency_overflow_rejected_on_encode() {
let mut buf = BytesMut::from(&b"prefix"[..]);
let snapshot = buf.clone();
let err = DataItem::Latency(Duration::MAX)
.encode(&mut buf)
.unwrap_err();
assert!(matches!(err, CodecError::OutOfRange { .. }));
assert_eq!(buf, snapshot);
}
#[test]
fn heartbeat_interval_overflow_restores_nonempty_buffer() {
let mut buf = BytesMut::from(&b"existing"[..]);
let snapshot = buf.clone();
let item = DataItem::HeartbeatInterval(Duration::from_secs(u64::MAX / 1000));
let err = item.encode(&mut buf).unwrap_err();
assert!(matches!(err, CodecError::OutOfRange { .. }));
assert_eq!(buf, snapshot);
}
#[test]
fn oversized_data_item_value_rejected() {
let item = DataItem::Status {
code: StatusCode::SUCCESS,
text: "x".repeat(70_000),
};
let mut buf = BytesMut::new();
let err = item.encode(&mut buf).unwrap_err();
match err {
CodecError::OutOfRange { field, value } => {
assert_eq!(field, "data_item_value_length");
assert!(value > u16::MAX as u64);
}
other => panic!("unexpected error: {other:?}"),
}
assert!(buf.is_empty());
}
#[test]
fn oversized_signal_body_rejected() {
let s = Signal::new(SignalType::PEER_OFFER)
.with_item(DataItem::Status {
code: StatusCode::SUCCESS,
text: "x".repeat(40_000),
})
.with_item(DataItem::Status {
code: StatusCode::SUCCESS,
text: "y".repeat(40_000),
});
match s.encode().unwrap_err() {
CodecError::OutOfRange { field, .. } => {
assert_eq!(field, "signal_body_length");
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn oversized_message_body_rejected() {
let m = Message::new(MessageType::SESSION_UPDATE)
.with_item(DataItem::Status {
code: StatusCode::SUCCESS,
text: "x".repeat(40_000),
})
.with_item(DataItem::Status {
code: StatusCode::SUCCESS,
text: "y".repeat(40_000),
});
match m.encode().unwrap_err() {
CodecError::OutOfRange { field, .. } => {
assert_eq!(field, "message_body_length");
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn mtu_wrong_length_carries_exact() {
let raw = RawDataItem {
type_id: DataItemType::MTU,
value: Bytes::from_static(&[0x05]),
};
match DataItem::decode(raw).unwrap_err() {
CodecError::InvalidDataItemLength {
kind: DataItemType::MTU,
expected: ExpectedLen::Exact(2),
got: 1,
} => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn ipv4_attached_subnet_decode_normalizes_host_bits() {
let raw = RawDataItem {
type_id: DataItemType::IPV4_ATTACHED_SUBNET,
value: Bytes::from_static(&[0x01, 10, 0, 0, 5, 24]),
};
let DataItem::Ipv4AttachedSubnet { add, subnet } = DataItem::decode(raw).unwrap() else {
panic!()
};
assert!(add);
assert_eq!(subnet, "10.0.0.0/24".parse::<Ipv4Net>().unwrap());
}
#[test]
fn ipv6_attached_subnet_decode_normalizes_host_bits() {
let mut wire = vec![0x01u8];
wire.extend_from_slice(&"2001:db8::1234".parse::<Ipv6Addr>().unwrap().octets());
wire.push(32);
let raw = RawDataItem {
type_id: DataItemType::IPV6_ATTACHED_SUBNET,
value: Bytes::from(wire),
};
let DataItem::Ipv6AttachedSubnet { add, subnet } = DataItem::decode(raw).unwrap() else {
panic!()
};
assert!(add);
assert_eq!(subnet, "2001:db8::/32".parse::<Ipv6Net>().unwrap());
}
#[test]
fn non_canonical_subnet_wire_bytes_round_trip_to_canonical() {
let non_canonical = [0x01u8, 10, 0, 0, 5, 24];
let canonical = [0x01u8, 10, 0, 0, 0, 24];
let raw = RawDataItem {
type_id: DataItemType::IPV4_ATTACHED_SUBNET,
value: Bytes::copy_from_slice(&non_canonical),
};
let item = DataItem::decode(raw).unwrap();
let mut out = BytesMut::new();
item.encode(&mut out).unwrap();
assert_eq!(&out[4..], &canonical[..]);
}
#[test]
fn oversized_peer_type_value_rejected() {
let item = DataItem::PeerType {
flags: PeerFlags::default(),
description: "x".repeat(70_000),
};
let mut buf = BytesMut::new();
let err = item.encode(&mut buf).unwrap_err();
match err {
CodecError::OutOfRange { field, value } => {
assert_eq!(field, "data_item_value_length");
assert!(value > u16::MAX as u64);
}
other => panic!("unexpected error: {other:?}"),
}
assert!(buf.is_empty());
}
#[test]
fn oversized_unknown_data_item_value_rejected() {
let item = DataItem::Unknown(RawDataItem {
type_id: DataItemType(4242),
value: Bytes::from(vec![0u8; 70_000]),
});
let mut buf = BytesMut::new();
let err = item.encode(&mut buf).unwrap_err();
match err {
CodecError::OutOfRange { field, value } => {
assert_eq!(field, "data_item_value_length");
assert_eq!(value, 70_000);
}
other => panic!("unexpected error: {other:?}"),
}
assert!(buf.is_empty());
}
#[test]
fn oversized_extensions_supported_value_rejected() {
let item = DataItem::ExtensionsSupported(vec![ExtensionId(0); 33_000]);
let mut buf = BytesMut::new();
let err = item.encode(&mut buf).unwrap_err();
match err {
CodecError::OutOfRange { field, value } => {
assert_eq!(field, "data_item_value_length");
assert!(value > u16::MAX as u64);
}
other => panic!("unexpected error: {other:?}"),
}
assert!(buf.is_empty());
}
#[test]
fn encode_failure_does_not_corrupt_existing_buffer() {
let mut buf = BytesMut::from(&b"prefix"[..]);
let snapshot = buf.clone();
let err = DataItem::Resources(200).encode(&mut buf).unwrap_err();
assert!(matches!(err, CodecError::OutOfRange { .. }));
assert_eq!(buf, snapshot);
}
#[test]
fn signal_encode_propagates_data_item_error() {
let s = Signal::new(SignalType::PEER_OFFER).with_item(DataItem::Resources(200));
let err = s.encode().unwrap_err();
assert!(matches!(err, CodecError::OutOfRange { .. }));
}
#[test]
fn message_encode_propagates_data_item_error() {
let m = Message::new(MessageType::SESSION_UPDATE)
.with_item(DataItem::HeartbeatInterval(Duration::from_millis(1000)))
.with_item(DataItem::RelativeLinkQualityReceive(120));
let err = m.encode().unwrap_err();
assert!(matches!(err, CodecError::OutOfRange { .. }));
}
#[test]
fn relative_link_quality_variants_roundtrip() {
for v in [0u8, 50, 100] {
let rx = DataItem::RelativeLinkQualityReceive(v);
let DataItem::RelativeLinkQualityReceive(out) = decode_one(&encode_one(rx)) else {
panic!()
};
assert_eq!(out, v);
let tx = DataItem::RelativeLinkQualityTransmit(v);
let DataItem::RelativeLinkQualityTransmit(out) = decode_one(&encode_one(tx)) else {
panic!()
};
assert_eq!(out, v);
}
}
#[test]
fn mtu_encodes() {
let item = DataItem::Mtu(1500);
assert_eq!(encode_one(item), vec![0x00, 0x14, 0x00, 0x02, 0x05, 0xDC]);
}
#[test]
fn unknown_data_item_passes_through() {
let raw_in = RawDataItem {
type_id: DataItemType(4242),
value: Bytes::from_static(&[0xAA, 0xBB, 0xCC]),
};
let item = DataItem::decode(raw_in.clone()).unwrap();
match &item {
DataItem::Unknown(r) => {
assert_eq!(r.type_id, DataItemType(4242));
assert_eq!(&r.value[..], &[0xAA, 0xBB, 0xCC]);
}
_ => panic!("expected Unknown"),
}
let mut buf = BytesMut::new();
item.encode(&mut buf).unwrap();
assert_eq!(&buf[..], &[0x10, 0x92, 0x00, 0x03, 0xAA, 0xBB, 0xCC]);
}
#[test]
fn decoder_skips_unknown_data_items_inside_message() {
let m = Message::new(MessageType::SESSION_INITIALIZATION)
.with_item(DataItem::HeartbeatInterval(Duration::from_millis(1000)))
.with_item(DataItem::Unknown(RawDataItem {
type_id: DataItemType(9999),
value: Bytes::from_static(&[0x42]),
}));
let decoded = Message::decode(m.encode().unwrap().freeze()).unwrap();
assert_eq!(decoded.message_type, MessageType::SESSION_INITIALIZATION);
assert_eq!(decoded.data_items.len(), 2);
assert!(matches!(
decoded.data_items[0],
DataItem::HeartbeatInterval(_)
));
assert!(matches!(decoded.data_items[1], DataItem::Unknown(_)));
}
#[test]
fn signal_with_multiple_unknown_items_round_trips() {
let s = Signal::new(SignalType::PEER_OFFER)
.with_item(DataItem::PeerType {
flags: PeerFlags::default(),
description: "router".into(),
})
.with_item(DataItem::Unknown(RawDataItem {
type_id: DataItemType(7777),
value: Bytes::from_static(&[1, 2, 3]),
}))
.with_item(DataItem::Unknown(RawDataItem {
type_id: DataItemType(8888),
value: Bytes::new(),
}));
let decoded = Signal::decode(s.encode().unwrap().freeze()).unwrap();
assert_eq!(decoded.data_items.len(), 3);
assert!(matches!(decoded.data_items[0], DataItem::PeerType { .. }));
match &decoded.data_items[1] {
DataItem::Unknown(r) => {
assert_eq!(r.type_id, DataItemType(7777));
assert_eq!(&r.value[..], &[1, 2, 3]);
}
_ => panic!(),
}
match &decoded.data_items[2] {
DataItem::Unknown(r) => {
assert_eq!(r.type_id, DataItemType(8888));
assert!(r.value.is_empty());
}
_ => panic!(),
}
}
#[test]
fn signal_with_multiple_items_preserves_order() {
let s = Signal::new(SignalType::PEER_OFFER)
.with_item(DataItem::PeerType {
flags: PeerFlags::default(),
description: "router".into(),
})
.with_item(DataItem::Ipv4ConnectionPoint {
flags: ConnectionPointFlags { use_tls: false },
addr: Ipv4Addr::new(127, 0, 0, 1),
port: Some(854),
});
let decoded = Signal::decode(s.encode().unwrap().freeze()).unwrap();
assert_eq!(decoded.signal_type, SignalType::PEER_OFFER);
assert_eq!(decoded.data_items.len(), 2);
assert!(matches!(decoded.data_items[0], DataItem::PeerType { .. }));
assert!(matches!(
decoded.data_items[1],
DataItem::Ipv4ConnectionPoint { .. }
));
}
#[test]
fn truncated_message_buffer_rejected() {
let mut buf = BytesMut::new();
buf.put_u16(MessageType::HEARTBEAT.0);
buf.put_u16(4);
buf.put_u8(0xAB);
buf.put_u8(0xCD);
let err = Message::decode(buf.freeze()).unwrap_err();
assert!(matches!(err, CodecError::LengthMismatch { .. }));
}
#[test]
fn truncated_signal_header_rejected() {
let buf = Bytes::from_static(b"DLE");
let err = Signal::decode(buf).unwrap_err();
assert!(matches!(err, CodecError::Truncated { .. }));
}
#[test]
fn invalid_utf8_in_status_text_rejected() {
let raw = RawDataItem {
type_id: DataItemType::STATUS,
value: Bytes::from_static(&[0x00, 0xFF, 0xFE]),
};
let err = DataItem::decode(raw).unwrap_err();
assert!(matches!(err, CodecError::InvalidUtf8(_)));
}
}