1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
//! 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)
}