use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;
use crate::constants;
use crate::transport::types::PacketBytes;
pub trait PacketStorage: AsRef<[u8]> {
fn to_shared(&self) -> PacketBytes;
}
impl PacketStorage for Vec<u8> {
fn to_shared(&self) -> PacketBytes {
PacketBytes::from(self.as_slice())
}
}
impl PacketStorage for &[u8] {
fn to_shared(&self) -> PacketBytes {
PacketBytes::from(*self)
}
}
impl PacketStorage for Arc<[u8]> {
fn to_shared(&self) -> PacketBytes {
self.clone()
}
}
#[derive(Debug)]
pub enum PacketError {
TooShort,
ExceedsMtu,
MissingTransportId,
InvalidHeaderType,
ZeroLengthData,
InvalidHopCount(u8),
}
impl fmt::Display for PacketError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PacketError::TooShort => write!(f, "Packet too short"),
PacketError::ExceedsMtu => write!(f, "Packet exceeds MTU"),
PacketError::MissingTransportId => write!(f, "HEADER_2 requires transport_id"),
PacketError::InvalidHeaderType => write!(f, "Invalid header type"),
PacketError::ZeroLengthData => write!(f, "Zero-length data field"),
PacketError::InvalidHopCount(hops) => write!(f, "Invalid hop count: {}", hops),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PacketFlags {
pub header_type: u8,
pub context_flag: u8,
pub transport_type: u8,
pub destination_type: u8,
pub packet_type: u8,
}
impl PacketFlags {
pub fn pack(&self) -> u8 {
(self.header_type << 6)
| (self.context_flag << 5)
| (self.transport_type << 4)
| (self.destination_type << 2)
| self.packet_type
}
pub fn unpack(byte: u8) -> Self {
PacketFlags {
header_type: (byte & 0b01000000) >> 6,
context_flag: (byte & 0b00100000) >> 5,
transport_type: (byte & 0b00010000) >> 4,
destination_type: (byte & 0b00001100) >> 2,
packet_type: byte & 0b00000011,
}
}
}
#[derive(Debug, Clone)]
pub struct RawPacket<B = Vec<u8>> {
pub flags: PacketFlags,
pub hops: u8,
pub transport_id: Option<[u8; 16]>,
pub destination_hash: [u8; 16],
pub context: u8,
pub raw: B,
pub packet_hash: [u8; 32],
pub rssi: Option<i16>,
pub snr: Option<f32>,
}
impl RawPacket {
#[inline]
pub const fn data_offset(header_type: u8) -> usize {
if header_type == constants::HEADER_2 {
2 + 2 * (constants::TRUNCATED_HASHLENGTH / 8) + 1
} else {
2 + constants::TRUNCATED_HASHLENGTH / 8 + 1
}
}
pub fn is_unhashed_link_resource_wire(raw: &[u8]) -> bool {
let Some(&flags) = raw.first() else {
return false;
};
let flags = PacketFlags::unpack(flags);
raw.get(Self::data_offset(flags.header_type) - 1)
.is_some_and(|&context| Self::is_unhashed_link_resource_header(&flags, context))
}
#[inline]
fn is_unhashed_link_resource_header(flags: &PacketFlags, context: u8) -> bool {
flags.destination_type == constants::DESTINATION_LINK
&& match context {
constants::CONTEXT_RESOURCE | constants::CONTEXT_RESOURCE_REQ => {
flags.packet_type == constants::PACKET_TYPE_DATA
}
constants::CONTEXT_RESOURCE_PRF => {
flags.packet_type == constants::PACKET_TYPE_PROOF
}
_ => false,
}
}
pub fn pack(
flags: PacketFlags,
hops: u8,
destination_hash: &[u8; 16],
transport_id: Option<&[u8; 16]>,
context: u8,
data: &[u8],
) -> Result<Self, PacketError> {
Self::pack_with_max_mtu(
flags,
hops,
destination_hash,
transport_id,
context,
data,
constants::MTU,
)
}
pub fn pack_raw_with_hash(
flags: PacketFlags,
hops: u8,
destination_hash: &[u8; 16],
transport_id: Option<&[u8; 16]>,
context: u8,
data: &[u8],
) -> Result<(Vec<u8>, [u8; 32]), PacketError> {
Self::pack_raw_with_hash_with_max_mtu(
flags,
hops,
destination_hash,
transport_id,
context,
data,
constants::MTU,
)
}
pub fn pack_with_max_mtu(
flags: PacketFlags,
hops: u8,
destination_hash: &[u8; 16],
transport_id: Option<&[u8; 16]>,
context: u8,
data: &[u8],
max_mtu: usize,
) -> Result<Self, PacketError> {
let (raw, packet_hash) = Self::pack_raw_with_hash_with_max_mtu(
flags,
hops,
destination_hash,
transport_id,
context,
data,
max_mtu,
)?;
Ok(RawPacket {
flags,
hops,
transport_id: transport_id.copied(),
destination_hash: *destination_hash,
context,
raw,
packet_hash,
rssi: None,
snr: None,
})
}
pub fn pack_raw_with_hash_with_max_mtu(
flags: PacketFlags,
hops: u8,
destination_hash: &[u8; 16],
transport_id: Option<&[u8; 16]>,
context: u8,
data: &[u8],
max_mtu: usize,
) -> Result<(Vec<u8>, [u8; 32]), PacketError> {
let raw = Self::pack_raw_with_max_mtu(
flags,
hops,
destination_hash,
transport_id,
context,
data,
max_mtu,
)?;
let packet_hash = Self::compute_packet_hash(flags.header_type, &raw);
Ok((raw, packet_hash))
}
#[allow(clippy::too_many_arguments)]
pub fn pack_raw_shared_with_hash_with_max_mtu(
flags: PacketFlags,
hops: u8,
destination_hash: &[u8; 16],
transport_id: Option<&[u8; 16]>,
context: u8,
data: &[u8],
max_mtu: usize,
) -> Result<(PacketBytes, [u8; 32]), PacketError> {
let raw = Self::pack_raw_shared_with_max_mtu(
flags,
hops,
destination_hash,
transport_id,
context,
data,
max_mtu,
)?;
let packet_hash = Self::compute_packet_hash(flags.header_type, &raw);
Ok((raw, packet_hash))
}
pub fn packet_hash_of(header_type: u8, raw: &[u8]) -> [u8; 32] {
Self::compute_packet_hash(header_type, raw)
}
#[allow(clippy::too_many_arguments)]
pub fn pack_raw_shared_filled_with_max_mtu(
flags: PacketFlags,
hops: u8,
destination_hash: &[u8; 16],
transport_id: Option<&[u8; 16]>,
context: u8,
payload_len: usize,
max_mtu: usize,
fill: impl FnOnce(&mut [u8]),
) -> Result<PacketBytes, PacketError> {
if flags.header_type == constants::HEADER_2 && transport_id.is_none() {
return Err(PacketError::MissingTransportId);
}
let transport_id = transport_id.filter(|_| flags.header_type == constants::HEADER_2);
let header_len = 2 + transport_id.map_or(0, |id| id.len()) + destination_hash.len() + 1;
let len = header_len + payload_len;
if len > max_mtu {
return Err(PacketError::ExceedsMtu);
}
let mut shared = Arc::<[u8]>::new_uninit_slice(len);
let out = Arc::get_mut(&mut shared).expect("a new Arc is unique");
let out: &mut [u8] = unsafe {
core::ptr::write_bytes(out.as_mut_ptr(), 0, len);
core::slice::from_raw_parts_mut(out.as_mut_ptr().cast::<u8>(), len)
};
out[0] = flags.pack();
out[1] = hops;
let mut at = 2;
if let Some(transport_id) = transport_id {
out[at..at + 16].copy_from_slice(transport_id);
at += 16;
}
out[at..at + 16].copy_from_slice(destination_hash);
at += 16;
out[at] = context;
fill(&mut out[header_len..]);
Ok(unsafe { shared.assume_init() })
}
pub fn pack_raw_shared_with_max_mtu(
flags: PacketFlags,
hops: u8,
destination_hash: &[u8; 16],
transport_id: Option<&[u8; 16]>,
context: u8,
data: &[u8],
max_mtu: usize,
) -> Result<PacketBytes, PacketError> {
if flags.header_type == constants::HEADER_2 && transport_id.is_none() {
return Err(PacketError::MissingTransportId);
}
let transport_id = transport_id.filter(|_| flags.header_type == constants::HEADER_2);
let len =
2 + transport_id.map_or(0, |id| id.len()) + destination_hash.len() + 1 + data.len();
if len > max_mtu {
return Err(PacketError::ExceedsMtu);
}
let mut shared = Arc::<[u8]>::new_uninit_slice(len);
let out = Arc::get_mut(&mut shared).expect("a new Arc is unique");
let mut at = 0;
let mut put = |bytes: &[u8]| {
let slots = &mut out[at..at + bytes.len()];
unsafe {
core::ptr::copy_nonoverlapping(
bytes.as_ptr(),
slots.as_mut_ptr().cast::<u8>(),
bytes.len(),
)
};
at += bytes.len();
};
put(&[flags.pack(), hops]);
if let Some(transport_id) = transport_id {
put(transport_id);
}
put(destination_hash);
put(&[context]);
put(data);
assert_eq!(at, len);
Ok(unsafe { shared.assume_init() })
}
pub fn pack_raw_with_max_mtu(
flags: PacketFlags,
hops: u8,
destination_hash: &[u8; 16],
transport_id: Option<&[u8; 16]>,
context: u8,
data: &[u8],
max_mtu: usize,
) -> Result<Vec<u8>, PacketError> {
if flags.header_type == constants::HEADER_2 && transport_id.is_none() {
return Err(PacketError::MissingTransportId);
}
let transport_id = transport_id.filter(|_| flags.header_type == constants::HEADER_2);
let len =
2 + transport_id.map_or(0, |id| id.len()) + destination_hash.len() + 1 + data.len();
if len > max_mtu {
return Err(PacketError::ExceedsMtu);
}
let mut raw = Vec::with_capacity(len);
raw.push(flags.pack());
raw.push(hops);
if let Some(transport_id) = transport_id {
raw.extend_from_slice(transport_id);
}
raw.extend_from_slice(destination_hash);
raw.push(context);
raw.extend_from_slice(data);
debug_assert_eq!(raw.len(), len);
Ok(raw)
}
pub fn unpack(raw: &[u8]) -> Result<Self, PacketError> {
RawPacket::<&[u8]>::parse::<true>(raw).map(|packet| packet.map_raw(<[u8]>::to_vec))
}
pub fn unpack_owned(raw: Vec<u8>) -> Result<Self, PacketError> {
Self::parse::<true>(raw)
}
pub(crate) fn unpack_unhashed(raw: Vec<u8>) -> Result<Self, PacketError> {
Self::parse::<false>(raw)
}
pub fn unpack_outbound(raw: Vec<u8>) -> Result<Self, PacketError> {
let mut packet = Self::unpack_unhashed(raw)?;
if !packet.is_unhashed_link_resource() {
packet.hash_in_place();
}
Ok(packet)
}
pub fn unpack_shared(raw: PacketBytes) -> Result<RawPacket<PacketBytes>, PacketError> {
RawPacket::<PacketBytes>::parse::<true>(raw)
}
pub(crate) fn unpack_shared_unhashed(
raw: PacketBytes,
) -> Result<RawPacket<PacketBytes>, PacketError> {
RawPacket::<PacketBytes>::parse::<false>(raw)
}
pub fn unpack_outbound_shared(raw: PacketBytes) -> Result<RawPacket<PacketBytes>, PacketError> {
let mut packet = RawPacket::<PacketBytes>::parse::<false>(raw)?;
if !packet.is_unhashed_link_resource() {
packet.hash_in_place();
}
Ok(packet)
}
pub fn unpack_prehashed(raw: &[u8], packet_hash: [u8; 32]) -> Result<Self, PacketError> {
RawPacket::view_prehashed(raw, packet_hash).map(|packet| packet.map_raw(<[u8]>::to_vec))
}
fn hashable_tail(header_type: u8, raw: &[u8]) -> &[u8] {
if header_type == constants::HEADER_2 {
&raw[(constants::TRUNCATED_HASHLENGTH / 8 + 2)..]
} else {
&raw[2..]
}
}
fn compute_hashable_part(header_type: u8, raw: &[u8]) -> Vec<u8> {
let tail = Self::hashable_tail(header_type, raw);
let mut hashable = Vec::with_capacity(1 + tail.len());
hashable.push(raw[0] & 0b00001111);
hashable.extend_from_slice(tail);
hashable
}
fn compute_packet_hash(header_type: u8, raw: &[u8]) -> [u8; 32] {
let mut hasher = rns_crypto::sha256::Sha256::new();
hasher.update(&[raw[0] & 0b00001111]);
hasher.update(Self::hashable_tail(header_type, raw));
hasher.digest()
}
}
impl<B: AsRef<[u8]>> RawPacket<B> {
#[inline]
pub fn data(&self) -> &[u8] {
&self.raw.as_ref()[<RawPacket>::data_offset(self.flags.header_type)..]
}
#[inline]
pub fn is_unhashed_link_resource(&self) -> bool {
<RawPacket>::is_unhashed_link_resource_header(&self.flags, self.context)
}
pub fn is_unproven_link_data(&self) -> bool {
self.flags.packet_type == constants::PACKET_TYPE_DATA
&& self.flags.destination_type == constants::DESTINATION_LINK
&& matches!(
self.context,
constants::CONTEXT_REQUEST
| constants::CONTEXT_RESPONSE
| constants::CONTEXT_RESOURCE
| constants::CONTEXT_RESOURCE_ADV
| constants::CONTEXT_RESOURCE_REQ
| constants::CONTEXT_RESOURCE_HMU
| constants::CONTEXT_RESOURCE_ICL
| constants::CONTEXT_RESOURCE_RCL
| constants::CONTEXT_KEEPALIVE
| constants::CONTEXT_LINKIDENTIFY
| constants::CONTEXT_LRRTT
| constants::CONTEXT_LINKCLOSE
)
}
pub fn get_hashable_part(&self) -> Vec<u8> {
<RawPacket>::compute_hashable_part(self.flags.header_type, self.raw.as_ref())
}
pub(crate) fn hash_in_place(&mut self) {
self.packet_hash =
<RawPacket>::compute_packet_hash(self.flags.header_type, self.raw.as_ref());
}
pub fn get_hash(&self) -> [u8; 32] {
self.packet_hash
}
pub fn get_truncated_hash(&self) -> [u8; 16] {
let mut result = [0u8; 16];
result.copy_from_slice(&self.packet_hash[..16]);
result
}
pub fn as_view(&self) -> RawPacket<&[u8]> {
RawPacket {
flags: self.flags,
hops: self.hops,
transport_id: self.transport_id,
destination_hash: self.destination_hash,
context: self.context,
raw: self.raw.as_ref(),
packet_hash: self.packet_hash,
rssi: self.rssi,
snr: self.snr,
}
}
pub fn to_owned_packet(&self) -> RawPacket {
self.as_view().map_raw(<[u8]>::to_vec)
}
pub fn map_raw<C>(self, f: impl FnOnce(B) -> C) -> RawPacket<C> {
RawPacket {
flags: self.flags,
hops: self.hops,
transport_id: self.transport_id,
destination_hash: self.destination_hash,
context: self.context,
raw: f(self.raw),
packet_hash: self.packet_hash,
rssi: self.rssi,
snr: self.snr,
}
}
fn parse<const HASH: bool>(input: B) -> Result<Self, PacketError> {
let raw = input.as_ref();
if raw.len() < constants::HEADER_MINSIZE {
return Err(PacketError::TooShort);
}
let flags = PacketFlags::unpack(raw[0]);
let hops = raw[1];
if hops >= constants::PATHFINDER_M {
return Err(PacketError::InvalidHopCount(hops));
}
let dst_len = constants::TRUNCATED_HASHLENGTH / 8;
if flags.header_type == constants::HEADER_2 {
let min_len = 2 + dst_len * 2 + 1;
if raw.len() < min_len {
return Err(PacketError::TooShort);
}
let mut transport_id = [0u8; 16];
transport_id.copy_from_slice(&raw[2..2 + dst_len]);
let mut destination_hash = [0u8; 16];
destination_hash.copy_from_slice(&raw[2 + dst_len..2 + 2 * dst_len]);
let context = raw[2 + 2 * dst_len];
if raw.len() == 2 + 2 * dst_len + 1 {
return Err(PacketError::ZeroLengthData);
}
let packet_hash = if HASH {
<RawPacket>::compute_packet_hash(flags.header_type, raw)
} else {
[0; 32]
};
Ok(RawPacket {
flags,
hops,
transport_id: Some(transport_id),
destination_hash,
context,
raw: input,
packet_hash,
rssi: None,
snr: None,
})
} else if flags.header_type == constants::HEADER_1 {
let min_len = 2 + dst_len + 1;
if raw.len() < min_len {
return Err(PacketError::TooShort);
}
let mut destination_hash = [0u8; 16];
destination_hash.copy_from_slice(&raw[2..2 + dst_len]);
let context = raw[2 + dst_len];
if raw.len() == 2 + dst_len + 1 {
return Err(PacketError::ZeroLengthData);
}
let packet_hash = if HASH {
<RawPacket>::compute_packet_hash(flags.header_type, raw)
} else {
[0; 32]
};
Ok(RawPacket {
flags,
hops,
transport_id: None,
destination_hash,
context,
raw: input,
packet_hash,
rssi: None,
snr: None,
})
} else {
Err(PacketError::InvalidHeaderType)
}
}
}
impl<'a> RawPacket<&'a [u8]> {
pub fn view_prehashed(raw: &'a [u8], packet_hash: [u8; 32]) -> Result<Self, PacketError> {
let mut packet = Self::parse::<false>(raw)?;
packet.packet_hash = packet_hash;
Ok(packet)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unpack_prehashed_matches_unpack_but_keeps_the_given_hash() {
let flags = PacketFlags {
header_type: constants::HEADER_1,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_BROADCAST,
destination_type: constants::DESTINATION_LINK,
packet_type: constants::PACKET_TYPE_DATA,
};
let packet = RawPacket::pack(
flags,
3,
&[0x42; 16],
None,
constants::CONTEXT_RESOURCE,
&[9; 300],
)
.unwrap();
let full = RawPacket::unpack(&packet.raw).unwrap();
let known = RawPacket::unpack_prehashed(&packet.raw, full.packet_hash).unwrap();
assert_eq!(known.raw, full.raw);
assert_eq!(known.packet_hash, full.packet_hash);
assert_eq!(known.destination_hash, full.destination_hash);
assert_eq!((known.hops, known.context), (full.hops, full.context));
assert_eq!(known.data(), full.data());
let other = RawPacket::unpack_prehashed(&packet.raw, [7; 32]).unwrap();
assert_eq!(other.packet_hash, [7; 32]);
assert_eq!(
format!(
"{:?}",
RawPacket::unpack_prehashed(&packet.raw[..10], [0; 32]).unwrap_err()
),
format!("{:?}", RawPacket::unpack(&packet.raw[..10]).unwrap_err())
);
}
#[test]
fn only_link_resource_transfers_are_sent_unhashed() {
let cases = [
(
constants::DESTINATION_LINK,
constants::PACKET_TYPE_DATA,
constants::CONTEXT_RESOURCE,
true,
),
(
constants::DESTINATION_LINK,
constants::PACKET_TYPE_DATA,
constants::CONTEXT_CHANNEL,
false,
),
(
constants::DESTINATION_LINK,
constants::PACKET_TYPE_DATA,
constants::CONTEXT_RESOURCE_REQ,
true,
),
(
constants::DESTINATION_LINK,
constants::PACKET_TYPE_PROOF,
constants::CONTEXT_RESOURCE_PRF,
true,
),
(
constants::DESTINATION_LINK,
constants::PACKET_TYPE_DATA,
constants::CONTEXT_RESOURCE_PRF,
false,
),
(
constants::DESTINATION_LINK,
constants::PACKET_TYPE_DATA,
constants::CONTEXT_RESOURCE_HMU,
false,
),
(
constants::DESTINATION_LINK,
constants::PACKET_TYPE_PROOF,
constants::CONTEXT_RESOURCE,
false,
),
(
constants::DESTINATION_LINK,
constants::PACKET_TYPE_PROOF,
constants::CONTEXT_NONE,
false,
),
(
constants::DESTINATION_SINGLE,
constants::PACKET_TYPE_DATA,
constants::CONTEXT_RESOURCE,
false,
),
];
for header_type in [constants::HEADER_1, constants::HEADER_2] {
for (destination_type, packet_type, context, part) in cases {
let flags = PacketFlags {
header_type,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_BROADCAST,
destination_type,
packet_type,
};
let transport_id = (header_type == constants::HEADER_2).then_some([0x11; 16]);
let packet = RawPacket::pack(
flags,
0,
&[0x42; 16],
transport_id.as_ref(),
context,
&[9; 300],
)
.unwrap();
assert_eq!(packet.is_unhashed_link_resource(), part);
assert_eq!(RawPacket::is_unhashed_link_resource_wire(&packet.raw), part);
let sent = RawPacket::unpack_outbound(packet.raw.clone()).unwrap();
assert_eq!(sent.raw, packet.raw);
let expected = if part { [0; 32] } else { packet.packet_hash };
assert_eq!(sent.packet_hash, expected);
}
}
let part = RawPacket::pack(
PacketFlags {
header_type: constants::HEADER_1,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_BROADCAST,
destination_type: constants::DESTINATION_LINK,
packet_type: constants::PACKET_TYPE_DATA,
},
0,
&[0x42; 16],
None,
constants::CONTEXT_RESOURCE,
&[9; 4],
)
.unwrap();
let context_at = RawPacket::data_offset(constants::HEADER_1) - 1;
assert!(RawPacket::is_unhashed_link_resource_wire(
&part.raw[..=context_at]
));
assert!(!RawPacket::is_unhashed_link_resource_wire(
&part.raw[..context_at]
));
assert!(!RawPacket::is_unhashed_link_resource_wire(&[]));
}
#[test]
fn only_plain_link_data_and_channel_messages_await_proofs() {
let packet = |destination_type, packet_type, context| {
RawPacket::pack(
PacketFlags {
header_type: constants::HEADER_1,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_BROADCAST,
destination_type,
packet_type,
},
0,
&[0x42; 16],
None,
context,
b"payload",
)
.unwrap()
};
let link_data = |context| {
packet(
constants::DESTINATION_LINK,
constants::PACKET_TYPE_DATA,
context,
)
};
for context in [constants::CONTEXT_NONE, constants::CONTEXT_CHANNEL] {
assert!(
!link_data(context).is_unproven_link_data(),
"context {context}"
);
}
for context in [
constants::CONTEXT_REQUEST,
constants::CONTEXT_RESPONSE,
constants::CONTEXT_RESOURCE,
constants::CONTEXT_RESOURCE_ADV,
constants::CONTEXT_RESOURCE_REQ,
constants::CONTEXT_RESOURCE_HMU,
constants::CONTEXT_RESOURCE_ICL,
constants::CONTEXT_RESOURCE_RCL,
constants::CONTEXT_KEEPALIVE,
constants::CONTEXT_LINKIDENTIFY,
constants::CONTEXT_LRRTT,
constants::CONTEXT_LINKCLOSE,
] {
assert!(
link_data(context).is_unproven_link_data(),
"context {context}"
);
}
let request = constants::CONTEXT_REQUEST;
assert!(!packet(
constants::DESTINATION_SINGLE,
constants::PACKET_TYPE_DATA,
request
)
.is_unproven_link_data());
assert!(!packet(
constants::DESTINATION_LINK,
constants::PACKET_TYPE_PROOF,
request
)
.is_unproven_link_data());
}
#[test]
fn shared_packing_matches_owned_packing_and_unpacks_alike() {
let flags = PacketFlags {
header_type: constants::HEADER_1,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_BROADCAST,
destination_type: constants::DESTINATION_LINK,
packet_type: constants::PACKET_TYPE_DATA,
};
let header2 = PacketFlags {
header_type: constants::HEADER_2,
..flags
};
let dest = [0x11; 16];
let tid = [0x22; 16];
for (flags, tid) in [(flags, None), (flags, Some(&tid)), (header2, Some(&tid))] {
for len in [1usize, 2, 15, 16, 400, 465] {
let data: Vec<u8> = (0..len).map(|i| i as u8).collect();
for mtu in [len + 18, len + 35, 500] {
let owned =
RawPacket::pack_raw_with_max_mtu(flags, 3, &dest, tid, 0x07, &data, mtu);
let shared = RawPacket::pack_raw_shared_with_max_mtu(
flags, 3, &dest, tid, 0x07, &data, mtu,
);
let filled = RawPacket::pack_raw_shared_filled_with_max_mtu(
flags,
3,
&dest,
tid,
0x07,
data.len(),
mtu,
|payload| payload.copy_from_slice(&data),
);
assert_eq!(format!("{shared:?}"), format!("{filled:?}"));
match (owned, shared) {
(Ok(owned), Ok(shared)) => {
assert_eq!(&owned[..], &shared[..]);
let a = RawPacket::unpack_outbound(owned).unwrap();
let b = RawPacket::unpack_outbound_shared(shared).unwrap();
assert_eq!(a.packet_hash, b.packet_hash);
assert_eq!(a.data(), b.data());
}
(Err(PacketError::ExceedsMtu), Err(PacketError::ExceedsMtu)) => {}
other => panic!("diverged: {other:?}"),
}
}
}
}
assert!(matches!(
RawPacket::pack_raw_shared_with_max_mtu(header2, 0, &dest, None, 0, &[1], 500),
Err(PacketError::MissingTransportId)
));
assert!(matches!(
RawPacket::pack_raw_shared_filled_with_max_mtu(
header2,
0,
&dest,
None,
0,
1,
500,
|_| {}
),
Err(PacketError::MissingTransportId)
));
}
#[test]
fn raw_only_packing_preserves_wire_layout_and_limits() {
for header in [constants::HEADER_1, constants::HEADER_2] {
for transport in [None, Some([0xBB; 16])] {
for len in [0, 1, 464, 16348] {
let flags = PacketFlags {
header_type: header,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_BROADCAST,
destination_type: constants::DESTINATION_LINK,
packet_type: constants::PACKET_TYPE_DATA,
};
let data = vec![0xCC; len];
let mut expected = vec![flags.pack(), 7];
if header == constants::HEADER_2 {
expected.extend_from_slice(&[0xBB; 16]);
}
expected.extend_from_slice(&[0xAA; 16]);
expected.push(constants::CONTEXT_RESOURCE);
expected.extend_from_slice(&data);
for limit in [0, expected.len() - 1, expected.len(), expected.len() + 1] {
let result = RawPacket::pack_raw_with_max_mtu(
flags,
7,
&[0xAA; 16],
transport.as_ref(),
constants::CONTEXT_RESOURCE,
&data,
limit,
);
if header == constants::HEADER_2 && transport.is_none() {
assert!(matches!(result, Err(PacketError::MissingTransportId)));
} else if limit < expected.len() {
assert!(matches!(result, Err(PacketError::ExceedsMtu)));
} else {
assert_eq!(result.unwrap(), expected);
let (raw, hash) = RawPacket::pack_raw_with_hash_with_max_mtu(
flags,
7,
&[0xAA; 16],
transport.as_ref(),
constants::CONTEXT_RESOURCE,
&data,
limit,
)
.unwrap();
assert_eq!(raw, expected);
if len == 0 {
assert!(matches!(
RawPacket::unpack(&raw),
Err(PacketError::ZeroLengthData)
));
} else {
assert_eq!(RawPacket::unpack(&raw).unwrap().packet_hash, hash);
}
}
}
}
}
}
}
#[test]
fn test_flags_pack_header1_data_single_broadcast() {
let flags = PacketFlags {
header_type: constants::HEADER_1,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_BROADCAST,
destination_type: constants::DESTINATION_SINGLE,
packet_type: constants::PACKET_TYPE_DATA,
};
assert_eq!(flags.pack(), 0x00);
}
#[test]
fn test_flags_pack_header2_announce_single_transport() {
let flags = PacketFlags {
header_type: constants::HEADER_2,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_TRANSPORT,
destination_type: constants::DESTINATION_SINGLE,
packet_type: constants::PACKET_TYPE_ANNOUNCE,
};
assert_eq!(flags.pack(), 0x51);
}
#[test]
fn test_flags_roundtrip() {
for byte in 0..=0x7Fu8 {
let flags = PacketFlags::unpack(byte);
assert_eq!(flags.pack(), byte);
}
}
#[test]
fn test_pack_header1() {
let dest_hash = [0xAA; 16];
let data = b"hello";
let flags = PacketFlags {
header_type: constants::HEADER_1,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_BROADCAST,
destination_type: constants::DESTINATION_SINGLE,
packet_type: constants::PACKET_TYPE_DATA,
};
let pkt =
RawPacket::pack(flags, 0, &dest_hash, None, constants::CONTEXT_NONE, data).unwrap();
assert_eq!(pkt.raw.len(), 24);
assert_eq!(pkt.raw[0], 0x00); assert_eq!(pkt.raw[1], 0x00); assert_eq!(&pkt.raw[2..18], &dest_hash); assert_eq!(pkt.raw[18], 0x00); assert_eq!(&pkt.raw[19..], b"hello"); }
#[test]
fn test_pack_header2() {
let dest_hash = [0xAA; 16];
let transport_id = [0xBB; 16];
let data = b"world";
let flags = PacketFlags {
header_type: constants::HEADER_2,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_TRANSPORT,
destination_type: constants::DESTINATION_SINGLE,
packet_type: constants::PACKET_TYPE_ANNOUNCE,
};
let pkt = RawPacket::pack(
flags,
3,
&dest_hash,
Some(&transport_id),
constants::CONTEXT_NONE,
data,
)
.unwrap();
assert_eq!(pkt.raw.len(), 40);
assert_eq!(pkt.raw[0], flags.pack());
assert_eq!(pkt.raw[1], 3);
assert_eq!(&pkt.raw[2..18], &transport_id);
assert_eq!(&pkt.raw[18..34], &dest_hash);
assert_eq!(pkt.raw[34], 0x00);
assert_eq!(&pkt.raw[35..], b"world");
}
#[test]
fn test_unpack_roundtrip_header1() {
let dest_hash = [0x11; 16];
let data = b"test data";
let flags = PacketFlags {
header_type: constants::HEADER_1,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_BROADCAST,
destination_type: constants::DESTINATION_SINGLE,
packet_type: constants::PACKET_TYPE_DATA,
};
let pkt = RawPacket::pack(
flags,
5,
&dest_hash,
None,
constants::CONTEXT_RESOURCE,
data,
)
.unwrap();
let unpacked = RawPacket::unpack(&pkt.raw).unwrap();
assert_eq!(unpacked.flags, flags);
assert_eq!(unpacked.hops, 5);
assert!(unpacked.transport_id.is_none());
assert_eq!(unpacked.destination_hash, dest_hash);
assert_eq!(unpacked.context, constants::CONTEXT_RESOURCE);
assert_eq!(unpacked.data(), data);
assert_eq!(unpacked.packet_hash, pkt.packet_hash);
}
#[test]
fn test_unpack_roundtrip_header2() {
let dest_hash = [0x22; 16];
let transport_id = [0x33; 16];
let data = b"transported";
let flags = PacketFlags {
header_type: constants::HEADER_2,
context_flag: constants::FLAG_SET,
transport_type: constants::TRANSPORT_TRANSPORT,
destination_type: constants::DESTINATION_SINGLE,
packet_type: constants::PACKET_TYPE_ANNOUNCE,
};
let pkt = RawPacket::pack(
flags,
2,
&dest_hash,
Some(&transport_id),
constants::CONTEXT_NONE,
data,
)
.unwrap();
let unpacked = RawPacket::unpack(&pkt.raw).unwrap();
assert_eq!(unpacked.flags, flags);
assert_eq!(unpacked.hops, 2);
assert_eq!(unpacked.transport_id.unwrap(), transport_id);
assert_eq!(unpacked.destination_hash, dest_hash);
assert_eq!(unpacked.context, constants::CONTEXT_NONE);
assert_eq!(unpacked.data(), data);
assert_eq!(unpacked.packet_hash, pkt.packet_hash);
}
#[test]
fn truncated_hash_is_derived_from_cached_packet_hash() {
let mut packet = RawPacket::pack(
PacketFlags {
header_type: constants::HEADER_1,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_BROADCAST,
destination_type: constants::DESTINATION_SINGLE,
packet_type: constants::PACKET_TYPE_DATA,
},
1,
&[0x42; 16],
None,
constants::CONTEXT_NONE,
b"cached hash",
)
.unwrap();
let expected: [u8; 16] = packet.packet_hash[..16].try_into().unwrap();
let last = packet.raw.len() - 1;
packet.raw[last] ^= 0xff;
assert_eq!(packet.get_truncated_hash(), expected);
}
#[test]
fn test_unpack_too_short() {
assert!(RawPacket::unpack(&[0x00; 5]).is_err());
}
#[test]
fn unpack_rejects_zero_length_data_for_both_header_types() {
for header_type in [constants::HEADER_1, constants::HEADER_2] {
let packet = RawPacket::pack(
PacketFlags {
header_type,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_BROADCAST,
destination_type: constants::DESTINATION_SINGLE,
packet_type: constants::PACKET_TYPE_DATA,
},
0,
&[0x42; 16],
(header_type == constants::HEADER_2).then_some(&[0x24; 16]),
constants::CONTEXT_NONE,
b"",
)
.unwrap();
assert!(matches!(
RawPacket::unpack(&packet.raw),
Err(PacketError::ZeroLengthData)
));
}
}
#[test]
fn unpack_enforces_pathfinder_hop_boundary_but_pack_remains_permissive() {
let flags = PacketFlags {
header_type: constants::HEADER_1,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_BROADCAST,
destination_type: constants::DESTINATION_SINGLE,
packet_type: constants::PACKET_TYPE_DATA,
};
let accepted = RawPacket::pack(flags, 127, &[1; 16], None, 0, b"x").unwrap();
assert_eq!(RawPacket::unpack(&accepted.raw).unwrap().hops, 127);
for hops in [128, 255] {
let packed = RawPacket::pack(flags, hops, &[1; 16], None, 0, b"x").unwrap();
assert!(matches!(
RawPacket::unpack(&packed.raw),
Err(PacketError::InvalidHopCount(value)) if value == hops
));
}
}
#[test]
fn test_pack_exceeds_mtu() {
let flags = PacketFlags {
header_type: constants::HEADER_1,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_BROADCAST,
destination_type: constants::DESTINATION_SINGLE,
packet_type: constants::PACKET_TYPE_DATA,
};
let data = [0u8; 500]; let result = RawPacket::pack(flags, 0, &[0; 16], None, 0, &data);
assert!(result.is_err());
}
#[test]
fn test_header2_missing_transport_id() {
let flags = PacketFlags {
header_type: constants::HEADER_2,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_TRANSPORT,
destination_type: constants::DESTINATION_SINGLE,
packet_type: constants::PACKET_TYPE_ANNOUNCE,
};
let result = RawPacket::pack(flags, 0, &[0; 16], None, 0, b"data");
assert!(result.is_err());
}
#[test]
fn test_hashable_part_header1_masks_upper_flags() {
let dest_hash = [0xCC; 16];
let flags = PacketFlags {
header_type: constants::HEADER_1,
context_flag: constants::FLAG_SET,
transport_type: constants::TRANSPORT_BROADCAST,
destination_type: constants::DESTINATION_SINGLE,
packet_type: constants::PACKET_TYPE_DATA,
};
let pkt =
RawPacket::pack(flags, 0, &dest_hash, None, constants::CONTEXT_NONE, b"test").unwrap();
let hashable = pkt.get_hashable_part();
assert_eq!(hashable[0], pkt.raw[0] & 0x0F);
assert_eq!(&hashable[1..], &pkt.raw[2..]);
}
#[test]
fn test_hashable_part_header2_strips_transport_id() {
let dest_hash = [0xDD; 16];
let transport_id = [0xEE; 16];
let flags = PacketFlags {
header_type: constants::HEADER_2,
context_flag: constants::FLAG_UNSET,
transport_type: constants::TRANSPORT_TRANSPORT,
destination_type: constants::DESTINATION_SINGLE,
packet_type: constants::PACKET_TYPE_ANNOUNCE,
};
let pkt = RawPacket::pack(
flags,
0,
&dest_hash,
Some(&transport_id),
constants::CONTEXT_NONE,
b"data",
)
.unwrap();
let hashable = pkt.get_hashable_part();
assert_eq!(hashable[0], pkt.raw[0] & 0x0F);
assert_eq!(&hashable[1..], &pkt.raw[18..]);
}
}