//! Functions that simplify producing tagged CBOR values through minicbor.
use core::borrow::Borrow;
/// An IP address with a zone identifier associated with it
///
/// For minicbor serialization purposes, the zone identifier is only expressed if the IP address is
/// link local.
#[derive(Debug, Clone)]
pub(crate) struct IpWithZone<A: Borrow<core::net::Ipv6Addr> = core::net::Ipv6Addr> {
pub(crate) ip: A,
pub(crate) zone: Option<core::num::NonZero<u16>>,
}
// TODO replace with Ipv6Addr's is_unicast_link_local once https://github.com/rust-lang/rust/pull/129238 is merged
fn is_unicast_link_local(addr: &core::net::Ipv6Addr) -> bool {
// Copied from Ipv6Addr::is_unicast_link_local, whose stabilization is pending
(addr.segments()[0] & 0xffc0) == 0xfe80
}
impl<C, A: Borrow<core::net::Ipv6Addr>> minicbor::encode::Encode<C> for IpWithZone<A> {
fn encode<W: minicbor::encode::Write>(
&self,
e: &mut minicbor::Encoder<W>,
ctx: &mut C,
) -> Result<(), minicbor::encode::Error<W::Error>> {
// Could also go back to the RIOT type and use its is_link_local function, but this is
// easier here and about to be stabilized.
if self.zone.is_none() || !is_unicast_link_local(self.ip.borrow()) {
return encode_as_ip(&self.ip.borrow().octets().as_slice(), e, ctx);
}
let zone = self.zone.map(u16::from).unwrap_or_default(); // or unwrap -- we've proven it is not None already
let e = e.tag(minicbor::data::Tag::new(54))?;
let e = e.array(3)?;
let e = e.bytes(self.ip.borrow().octets().as_slice())?;
let e = e.null()?;
let _ = e.u16(zone.into())?;
Ok(())
}
}
// For the time being we only implement decoding into an owned IpWithZone. I have a rough guess
// that IP address might be aligned, so storing a reference to the buffer would fail down the road
// at some point.
impl<'b, C> minicbor::decode::Decode<'b, C> for IpWithZone {
fn decode(
d: &mut minicbor::decode::Decoder<'b>,
_ctx: &mut C,
) -> Result<Self, minicbor::decode::Error> {
if !d.tag().is_ok_and(|t| t.as_u64() == 54) {
return Err(minicbor::decode::Error::message(
"Expecting tag54 IP address",
));
}
// We can't just match on `d.array()` because on Err, the pointer will have advanced
let items = if d.datatype()? == minicbor::data::Type::Array {
match d.array()? {
Some(n) if (1..=3).contains(&n) => n,
_ => {
return Err(minicbor::decode::Error::message(
"Unexpected tag54 array length",
))
}
}
} else {
1
};
let address = d.bytes()?;
let address: [u8; 16] = address
.try_into()
// "the value of omitting trailing zeros for the pure address format was considered
// nonessential"
.map_err(|_| minicbor::decode::Error::message("Invalid IP address length"))?;
let mut result = IpWithZone {
ip: core::net::Ipv6Addr::from(address),
zone: None,
};
if items == 1 {
return Ok(result);
}
d.null()?;
if items == 2 {
// I think this case is allowed
return Ok(result);
}
let zone = d.u16()?;
result.zone = zone.try_into().ok();
return Ok(result);
}
}
/// Encode a `&[u8]` as a tagged IP address (with the family determined by the length).
///
/// Slices with lengths that are neither IPv4 nor IPv6 are emitted as plain byte strings.
#[inline]
pub(crate) fn encode_as_ip<C, W: minicbor::encode::Write>(
xs: &&[u8],
e: &mut minicbor::Encoder<W>,
ctx: &mut C,
) -> Result<(), minicbor::encode::Error<W::Error>> {
match xs.len() {
4 => e.tag(minicbor::data::Tag::new(52))?,
16 => e.tag(minicbor::data::Tag::new(54))?,
_ => e,
};
minicbor::bytes::encode::<C, &[u8], W>(xs, e, ctx)
}
/// Encode a `&[u8]` as a tagged MAC address.
pub(crate) fn encode_as_mac<C, W: minicbor::encode::Write>(
xs: &&[u8],
e: &mut minicbor::Encoder<W>,
ctx: &mut C,
) -> Result<(), minicbor::encode::Error<W::Error>> {
e.tag(minicbor::data::Tag::new(48))?;
minicbor::bytes::encode::<C, &[u8], W>(xs, e, ctx)
}