umsh_ulcp/
announce.rs

1//! On-demand announcement requests (`CMD_ANNOUNCE`).
2//!
3//! The payload is an option list in the `CMD_PROP_MULTI_SET` entry form
4//! (`LENGTH | OPTION | VALUE`), so an empty payload is a well-formed
5//! request carrying every default: an advertisement, broadcast, with no
6//! flood budget. Every option the device does not recognize is an error
7//! rather than something to ignore—a host that asked for a multicast
8//! must not get a broadcast on the air instead.
9
10use crate::frame::{FrameWriter, MultiEntries, WriteError};
11use crate::ids::MAX_ANNOUNCE_FLOOD_HOPS;
12use crate::items::CHANNEL_IDENTIFIER_LEN;
13use crate::status::Status;
14
15/// `CMD_ANNOUNCE` payload options.
16pub mod opt {
17    /// `ANNOUNCE_KIND`—UINT8, 0 advertisement, 1 beacon.
18    pub const KIND: u32 = 1;
19    /// `ANNOUNCE_FLOOD_HOPS`—UINT8, the flood budget, 0 to 15.
20    pub const FLOOD_HOPS: u32 = 2;
21    /// `ANNOUNCE_FULL_SOURCE`—BOOL, whether `SRC` carries the full
22    /// public key rather than the node hint.
23    pub const FULL_SOURCE: u32 = 3;
24    /// `ANNOUNCE_CHANNEL`—the 16-octet channel identifier of the device
25    /// channel to send on. Absent, the announcement is a broadcast.
26    pub const CHANNEL: u32 = 4;
27}
28
29/// What an announcement carries.
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub enum AnnouncementKind {
32    /// The device's signed node identity.
33    #[default]
34    Advertisement = 0,
35    /// No payload; a beacon publishes a path, not an identity.
36    Beacon = 1,
37}
38
39impl AnnouncementKind {
40    /// The wire code for this kind.
41    pub const fn code(self) -> u8 {
42        self as u8
43    }
44
45    /// Strict conversion from a decoded wire code.
46    pub const fn from_code(code: u8) -> Option<Self> {
47        match code {
48            0 => Some(Self::Advertisement),
49            1 => Some(Self::Beacon),
50            _ => None,
51        }
52    }
53
54    /// Whether this kind sends the full public key as `SRC` when the
55    /// host does not say.
56    ///
57    /// An advertisement's detached signature is only checkable against
58    /// the full key, so it defaults to carrying one; a beacon has
59    /// nothing to check and defaults to the hint.
60    pub const fn defaults_to_full_source(self) -> bool {
61        matches!(self, Self::Advertisement)
62    }
63}
64
65/// Why a `CMD_ANNOUNCE` payload could not be honored.
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub enum AnnounceError {
68    /// An unrecognized or repeated option, or a value of the wrong
69    /// length or out of range.
70    Invalid,
71    /// The entry list itself is malformed.
72    Parse,
73}
74
75impl AnnounceError {
76    /// The status a device answers with.
77    pub const fn status(self) -> Status {
78        match self {
79            Self::Invalid => Status::INVALID_ARGUMENT,
80            Self::Parse => Status::PARSE_ERROR,
81        }
82    }
83}
84
85/// A parsed `CMD_ANNOUNCE` request, with absent options resolved to
86/// their defaults.
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub struct Announcement {
89    pub kind: AnnouncementKind,
90    /// The flood budget. Zero reaches only nodes that hear the device
91    /// directly.
92    pub flood_hops: u8,
93    /// Whether `SRC` carries the full public key.
94    pub full_source: bool,
95    /// The channel to send on, named by its full channel identifier.
96    /// `None` is a broadcast.
97    pub channel: Option<[u8; CHANNEL_IDENTIFIER_LEN]>,
98}
99
100impl Default for Announcement {
101    fn default() -> Self {
102        Self::new(AnnouncementKind::Advertisement)
103    }
104}
105
106impl Announcement {
107    /// An announcement of `kind` with every other option at its default.
108    pub const fn new(kind: AnnouncementKind) -> Self {
109        Self {
110            kind,
111            flood_hops: 0,
112            full_source: kind.defaults_to_full_source(),
113            channel: None,
114        }
115    }
116
117    /// Whether this announcement floods beyond the device's neighbors.
118    pub const fn floods(&self) -> bool {
119        self.flood_hops > 0
120    }
121
122    /// Decode a `CMD_ANNOUNCE` payload.
123    pub fn parse(payload: &[u8]) -> Result<Self, AnnounceError> {
124        let mut request = Self::default();
125        let mut seen = 0u32;
126
127        for entry in MultiEntries::new(payload) {
128            let entry = entry.map_err(|_| AnnounceError::Parse)?;
129            let bit = match entry.key {
130                opt::KIND | opt::FLOOD_HOPS | opt::FULL_SOURCE | opt::CHANNEL => 1u32 << entry.key,
131                _ => return Err(AnnounceError::Invalid),
132            };
133            if seen & bit != 0 {
134                return Err(AnnounceError::Invalid);
135            }
136            seen |= bit;
137
138            match (entry.key, entry.value) {
139                (opt::KIND, &[code]) => {
140                    request.kind =
141                        AnnouncementKind::from_code(code).ok_or(AnnounceError::Invalid)?;
142                }
143                (opt::FLOOD_HOPS, &[hops]) if hops <= MAX_ANNOUNCE_FLOOD_HOPS => {
144                    request.flood_hops = hops;
145                }
146                (opt::FULL_SOURCE, &[0]) => request.full_source = false,
147                (opt::FULL_SOURCE, &[1]) => request.full_source = true,
148                (opt::CHANNEL, value) => {
149                    let identifier = value.try_into().map_err(|_| AnnounceError::Invalid)?;
150                    request.channel = Some(identifier);
151                }
152                _ => return Err(AnnounceError::Invalid),
153            }
154        }
155
156        if seen & (1 << opt::FULL_SOURCE) == 0 {
157            request.full_source = request.kind.defaults_to_full_source();
158        }
159        Ok(request)
160    }
161
162    /// Append this request's options to a frame, omitting those left at
163    /// their defaults.
164    pub fn write(&self, writer: &mut FrameWriter<'_>) -> Result<(), WriteError> {
165        if self.kind != AnnouncementKind::Advertisement {
166            writer.write_entry(opt::KIND, &[self.kind.code()])?;
167        }
168        if self.flood_hops != 0 {
169            writer.write_entry(opt::FLOOD_HOPS, &[self.flood_hops])?;
170        }
171        if self.full_source != self.kind.defaults_to_full_source() {
172            writer.write_entry(opt::FULL_SOURCE, &[u8::from(self.full_source)])?;
173        }
174        if let Some(channel) = &self.channel {
175            writer.write_entry(opt::CHANNEL, channel)?;
176        }
177        Ok(())
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::frame::{self, Cmd, Frame};
185
186    fn round_trip(request: &Announcement) -> Announcement {
187        let mut buf = [0u8; 64];
188        let len = frame::announce(&mut buf, 3, request).unwrap();
189        let parsed = Frame::parse(&buf[..len]).unwrap();
190        assert_eq!(parsed.command(), Some(Cmd::Announce));
191        assert_eq!(parsed.header.tid(), 3);
192        Announcement::parse(parsed.payload).unwrap()
193    }
194
195    #[test]
196    fn an_empty_payload_is_a_neighbor_advertisement() {
197        let request = Announcement::parse(&[]).unwrap();
198        assert_eq!(request.kind, AnnouncementKind::Advertisement);
199        assert_eq!(request.flood_hops, 0);
200        assert!(request.full_source);
201        assert_eq!(request.channel, None);
202        assert!(!request.floods());
203        assert_eq!(request, Announcement::default());
204
205        // The defaults cost nothing on the wire.
206        let mut buf = [0u8; 8];
207        assert_eq!(frame::announce(&mut buf, 0, &request).unwrap(), 2);
208    }
209
210    #[test]
211    fn a_beacon_defaults_to_the_source_hint() {
212        let request = Announcement::new(AnnouncementKind::Beacon);
213        assert!(!request.full_source);
214        assert_eq!(round_trip(&request), request);
215
216        // The kind's default is applied whichever order the options
217        // arrive in.
218        let mut buf = [0u8; 32];
219        let mut writer = FrameWriter::new(&mut buf, 0, Cmd::Announce).unwrap();
220        writer.write_entry(opt::FLOOD_HOPS, &[4]).unwrap();
221        writer
222            .write_entry(opt::KIND, &[AnnouncementKind::Beacon.code()])
223            .unwrap();
224        let len = writer.finish();
225        let parsed = Announcement::parse(Frame::parse(&buf[..len]).unwrap().payload).unwrap();
226        assert_eq!(parsed.kind, AnnouncementKind::Beacon);
227        assert!(!parsed.full_source);
228        assert_eq!(parsed.flood_hops, 4);
229    }
230
231    #[test]
232    fn every_option_round_trips() {
233        let requests = [
234            Announcement::default(),
235            Announcement {
236                kind: AnnouncementKind::Beacon,
237                flood_hops: MAX_ANNOUNCE_FLOOD_HOPS,
238                full_source: true,
239                channel: Some([0xAB; CHANNEL_IDENTIFIER_LEN]),
240            },
241            Announcement {
242                kind: AnnouncementKind::Advertisement,
243                flood_hops: 5,
244                full_source: false,
245                channel: None,
246            },
247        ];
248        for request in requests {
249            assert_eq!(round_trip(&request), request);
250        }
251    }
252
253    #[test]
254    fn unknown_and_repeated_options_are_refused() {
255        let mut buf = [0u8; 32];
256
257        let mut writer = FrameWriter::new(&mut buf, 0, Cmd::Announce).unwrap();
258        writer.write_entry(9, &[0]).unwrap();
259        let len = writer.finish();
260        assert_eq!(
261            Announcement::parse(Frame::parse(&buf[..len]).unwrap().payload),
262            Err(AnnounceError::Invalid)
263        );
264
265        let mut writer = FrameWriter::new(&mut buf, 0, Cmd::Announce).unwrap();
266        writer.write_entry(opt::FLOOD_HOPS, &[1]).unwrap();
267        writer.write_entry(opt::FLOOD_HOPS, &[2]).unwrap();
268        let len = writer.finish();
269        assert_eq!(
270            Announcement::parse(Frame::parse(&buf[..len]).unwrap().payload),
271            Err(AnnounceError::Invalid)
272        );
273    }
274
275    #[test]
276    fn out_of_range_and_misshaped_values_are_refused() {
277        let cases: [(u32, &[u8]); 6] = [
278            (opt::KIND, &[2]),
279            (opt::KIND, &[]),
280            (opt::FLOOD_HOPS, &[MAX_ANNOUNCE_FLOOD_HOPS + 1]),
281            (opt::FULL_SOURCE, &[2]),
282            (opt::CHANNEL, &[0xAB; 3]),
283            (opt::CHANNEL, &[0xAB; CHANNEL_IDENTIFIER_LEN + 1]),
284        ];
285        for (key, value) in cases {
286            let mut buf = [0u8; 32];
287            let mut writer = FrameWriter::new(&mut buf, 0, Cmd::Announce).unwrap();
288            writer.write_entry(key, value).unwrap();
289            let len = writer.finish();
290            assert_eq!(
291                Announcement::parse(Frame::parse(&buf[..len]).unwrap().payload),
292                Err(AnnounceError::Invalid),
293                "option {key} value {value:?}"
294            );
295        }
296    }
297
298    #[test]
299    fn a_truncated_entry_list_is_a_parse_error() {
300        // An entry claiming four bytes with two present.
301        assert_eq!(
302            Announcement::parse(&[4, opt::FLOOD_HOPS as u8, 1]),
303            Err(AnnounceError::Parse)
304        );
305    }
306
307    #[test]
308    fn errors_carry_the_status_the_device_answers() {
309        assert_eq!(AnnounceError::Invalid.status(), Status::INVALID_ARGUMENT);
310        assert_eq!(AnnounceError::Parse.status(), Status::PARSE_ERROR);
311    }
312}