Skip to main content

bacnet_bip_codec/
table.rs

1//! B/IP addresses, and the BDT and FDT entries built on them.
2
3use core::fmt;
4use core::iter::FusedIterator;
5use core::net::{Ipv4Addr, SocketAddrV4};
6
7use crate::{
8    BvllError, BvllSink, SinkFull, BDT_ENTRY_LENGTH, BIP_ADDRESS_LENGTH, FDT_ENTRY_LENGTH,
9};
10
11/// Octets in one table entry: both tables use the same size (Annex J.2.2.1,
12/// J.2.8.1), so one table type serves both.
13const ENTRY_LENGTH: usize = BDT_ENTRY_LENGTH;
14const _: () = assert!(FDT_ENTRY_LENGTH == ENTRY_LENGTH);
15
16/// The six octets of a B/IP address: the IPv4 address, then the UDP port,
17/// both most significant octet first (Annex J.1.2).
18///
19/// ```
20/// use core::net::{Ipv4Addr, SocketAddrV4};
21/// use bacnet_bip_codec::{address_from_octets, address_to_octets};
22///
23/// let address = SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 10), 47808);
24/// assert_eq!(address_to_octets(address), [192, 168, 1, 10, 0xBA, 0xC0]);
25/// assert_eq!(address_from_octets([192, 168, 1, 10, 0xBA, 0xC0]), address);
26/// ```
27#[must_use]
28pub const fn address_to_octets(address: SocketAddrV4) -> [u8; BIP_ADDRESS_LENGTH] {
29    let [a, b, c, d] = address.ip().octets();
30    let [high, low] = address.port().to_be_bytes();
31    [a, b, c, d, high, low]
32}
33
34/// The B/IP address six octets hold (Annex J.1.2).
35#[must_use]
36pub const fn address_from_octets(octets: [u8; BIP_ADDRESS_LENGTH]) -> SocketAddrV4 {
37    let [a, b, c, d, high, low] = octets;
38    SocketAddrV4::new(Ipv4Addr::new(a, b, c, d), u16::from_be_bytes([high, low]))
39}
40
41/// The B/IP address in `octets`, which must be exactly six octets long.
42pub fn address_from_slice(octets: &[u8]) -> Result<SocketAddrV4, BvllError> {
43    match <[u8; BIP_ADDRESS_LENGTH]>::try_from(octets) {
44        Ok(octets) => Ok(address_from_octets(octets)),
45        Err(_) => Err(BvllError::AddressLength {
46            length: octets.len(),
47        }),
48    }
49}
50
51/// One row of a broadcast distribution table: a BBMD's B/IP address and the
52/// broadcast distribution mask that says how to reach its subnet (Annex
53/// J.2.2.1, J.4.3.2).
54///
55/// A mask of all ones sends forwarded broadcasts to the BBMD itself
56/// (two-hop); the subnet's own mask sends them to its directed broadcast
57/// address (one-hop).
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub struct BdtEntry {
60    /// The BBMD's B/IP address.
61    pub address: SocketAddrV4,
62    /// The broadcast distribution mask.
63    pub mask: Ipv4Addr,
64}
65
66impl BdtEntry {
67    /// Construct an entry.
68    #[must_use]
69    pub const fn new(address: SocketAddrV4, mask: Ipv4Addr) -> Self {
70        Self { address, mask }
71    }
72
73    /// The entry's ten wire octets: address, then mask.
74    #[must_use]
75    pub const fn to_octets(self) -> [u8; ENTRY_LENGTH] {
76        let [a, b, c, d, high, low] = address_to_octets(self.address);
77        let [m0, m1, m2, m3] = self.mask.octets();
78        [a, b, c, d, high, low, m0, m1, m2, m3]
79    }
80
81    /// The entry ten wire octets hold.
82    #[must_use]
83    pub const fn from_octets(octets: [u8; ENTRY_LENGTH]) -> Self {
84        let [a, b, c, d, high, low, m0, m1, m2, m3] = octets;
85        Self {
86            address: address_from_octets([a, b, c, d, high, low]),
87            mask: Ipv4Addr::new(m0, m1, m2, m3),
88        }
89    }
90}
91
92/// One row of a foreign device table as Read-Foreign-Device-Table-Ack
93/// returns it (Annex J.2.8.1): the registrant's B/IP address, the
94/// time-to-live it registered with, and the seconds left before the BBMD
95/// purges it, which include the 30-second grace period of J.5.2.3.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97pub struct FdtEntry {
98    /// The foreign device's B/IP address.
99    pub address: SocketAddrV4,
100    /// The time-to-live, in seconds, the device registered with.
101    pub ttl: u16,
102    /// Seconds until the BBMD purges the entry.
103    pub remaining: u16,
104}
105
106impl FdtEntry {
107    /// Construct an entry.
108    #[must_use]
109    pub const fn new(address: SocketAddrV4, ttl: u16, remaining: u16) -> Self {
110        Self {
111            address,
112            ttl,
113            remaining,
114        }
115    }
116
117    /// The entry's ten wire octets: address, time-to-live, then remaining
118    /// seconds.
119    #[must_use]
120    pub const fn to_octets(self) -> [u8; ENTRY_LENGTH] {
121        let [a, b, c, d, high, low] = address_to_octets(self.address);
122        let [t0, t1] = self.ttl.to_be_bytes();
123        let [r0, r1] = self.remaining.to_be_bytes();
124        [a, b, c, d, high, low, t0, t1, r0, r1]
125    }
126
127    /// The entry ten wire octets hold.
128    #[must_use]
129    pub const fn from_octets(octets: [u8; ENTRY_LENGTH]) -> Self {
130        let [a, b, c, d, high, low, t0, t1, r0, r1] = octets;
131        Self {
132            address: address_from_octets([a, b, c, d, high, low]),
133            ttl: u16::from_be_bytes([t0, t1]),
134            remaining: u16::from_be_bytes([r0, r1]),
135        }
136    }
137}
138
139mod sealed {
140    pub trait Sealed {}
141    impl Sealed for super::BdtEntry {}
142    impl Sealed for super::FdtEntry {}
143}
144
145/// A table entry type: [`BdtEntry`] or [`FdtEntry`]. Sealed.
146pub trait TableEntry: Copy + sealed::Sealed {
147    /// The entry ten wire octets hold.
148    fn from_wire(octets: [u8; ENTRY_LENGTH]) -> Self;
149    /// The entry's ten wire octets.
150    fn to_wire(self) -> [u8; ENTRY_LENGTH];
151}
152
153impl TableEntry for BdtEntry {
154    fn from_wire(octets: [u8; ENTRY_LENGTH]) -> Self {
155        Self::from_octets(octets)
156    }
157    fn to_wire(self) -> [u8; ENTRY_LENGTH] {
158        self.to_octets()
159    }
160}
161
162impl TableEntry for FdtEntry {
163    fn from_wire(octets: [u8; ENTRY_LENGTH]) -> Self {
164        Self::from_octets(octets)
165    }
166    fn to_wire(self) -> [u8; ENTRY_LENGTH] {
167        self.to_octets()
168    }
169}
170
171/// A table carried in a BVLL payload, borrowed rather than copied: either
172/// the wire octets of a decoded message, or entries a caller wants to
173/// encode. Both read the same through [`Entries::iter`], and two tables
174/// compare equal when their entries do.
175#[derive(Clone, Copy)]
176pub struct Entries<'a, T> {
177    repr: Repr<'a, T>,
178}
179
180#[derive(Clone, Copy)]
181enum Repr<'a, T> {
182    Wire(&'a [[u8; ENTRY_LENGTH]]),
183    Typed(&'a [T]),
184}
185
186/// A broadcast distribution table (Annex J.2.2.1, J.2.4.1).
187pub type BdtEntries<'a> = Entries<'a, BdtEntry>;
188/// A foreign device table (Annex J.2.8.1).
189pub type FdtEntries<'a> = Entries<'a, FdtEntry>;
190
191impl<'a, T: TableEntry> Entries<'a, T> {
192    /// Read a table from its wire octets, which must be a whole number of
193    /// 10-octet entries. An empty table is allowed. The octets are borrowed,
194    /// not copied.
195    ///
196    /// ```
197    /// use bacnet_bip_codec::{BdtEntries, BvllError};
198    ///
199    /// let octets = [10, 0, 0, 1, 0xBA, 0xC0, 255, 255, 255, 255];
200    /// let table = BdtEntries::from_wire(&octets)?;
201    /// assert_eq!(table.len(), 1);
202    /// assert_eq!(table.iter().next().unwrap().address.port(), 0xBAC0);
203    ///
204    /// assert_eq!(
205    ///     BdtEntries::from_wire(&octets[..9]).unwrap_err(),
206    ///     BvllError::TableLength { length: 9, entry_length: 10 },
207    /// );
208    /// # Ok::<(), BvllError>(())
209    /// ```
210    pub fn from_wire(octets: &'a [u8]) -> Result<Self, BvllError> {
211        match octets.as_chunks::<ENTRY_LENGTH>() {
212            (entries, []) => Ok(Self {
213                repr: Repr::Wire(entries),
214            }),
215            _ => Err(BvllError::TableLength {
216                length: octets.len(),
217                entry_length: ENTRY_LENGTH,
218            }),
219        }
220    }
221
222    /// A table of these entries, for encoding.
223    #[must_use]
224    pub const fn from_entries(entries: &'a [T]) -> Self {
225        Self {
226            repr: Repr::Typed(entries),
227        }
228    }
229
230    /// Number of entries.
231    #[must_use]
232    pub const fn len(&self) -> usize {
233        match self.repr {
234            Repr::Wire(entries) => entries.len(),
235            Repr::Typed(entries) => entries.len(),
236        }
237    }
238
239    /// Whether the table has no entries.
240    #[must_use]
241    pub const fn is_empty(&self) -> bool {
242        self.len() == 0
243    }
244
245    /// The entries, in wire order.
246    #[must_use]
247    pub fn iter(&self) -> EntryIter<'a, T> {
248        EntryIter {
249            repr: match self.repr {
250                Repr::Wire(entries) => IterRepr::Wire(entries.iter()),
251                Repr::Typed(entries) => IterRepr::Typed(entries.iter()),
252            },
253        }
254    }
255
256    /// Octets the table takes on the wire.
257    pub(crate) const fn wire_length(&self) -> usize {
258        self.len() * ENTRY_LENGTH
259    }
260
261    /// Write the table's octets to `sink`.
262    pub(crate) fn write_to<S: BvllSink + ?Sized>(&self, sink: &mut S) -> Result<(), SinkFull> {
263        match self.repr {
264            Repr::Wire(entries) => sink.write(entries.as_flattened()),
265            Repr::Typed(entries) => entries
266                .iter()
267                .try_for_each(|entry| sink.write(&entry.to_wire())),
268        }
269    }
270}
271
272impl<T: TableEntry + PartialEq> PartialEq for Entries<'_, T> {
273    fn eq(&self, other: &Self) -> bool {
274        self.iter().eq(other.iter())
275    }
276}
277
278impl<T: TableEntry + Eq> Eq for Entries<'_, T> {}
279
280impl<T: TableEntry + fmt::Debug> fmt::Debug for Entries<'_, T> {
281    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282        f.debug_list().entries(self.iter()).finish()
283    }
284}
285
286impl<'a, T: TableEntry> IntoIterator for Entries<'a, T> {
287    type Item = T;
288    type IntoIter = EntryIter<'a, T>;
289
290    fn into_iter(self) -> Self::IntoIter {
291        self.iter()
292    }
293}
294
295impl<'a, T: TableEntry> IntoIterator for &Entries<'a, T> {
296    type Item = T;
297    type IntoIter = EntryIter<'a, T>;
298
299    fn into_iter(self) -> Self::IntoIter {
300        self.iter()
301    }
302}
303
304/// Iterator over a table's entries; see [`Entries::iter`].
305#[derive(Debug, Clone)]
306pub struct EntryIter<'a, T> {
307    repr: IterRepr<'a, T>,
308}
309
310#[derive(Debug, Clone)]
311enum IterRepr<'a, T> {
312    Wire(core::slice::Iter<'a, [u8; ENTRY_LENGTH]>),
313    Typed(core::slice::Iter<'a, T>),
314}
315
316impl<T: TableEntry> Iterator for EntryIter<'_, T> {
317    type Item = T;
318
319    fn next(&mut self) -> Option<T> {
320        match &mut self.repr {
321            IterRepr::Wire(entries) => entries.next().map(|octets| T::from_wire(*octets)),
322            IterRepr::Typed(entries) => entries.next().copied(),
323        }
324    }
325
326    fn size_hint(&self) -> (usize, Option<usize>) {
327        match &self.repr {
328            IterRepr::Wire(entries) => entries.size_hint(),
329            IterRepr::Typed(entries) => entries.size_hint(),
330        }
331    }
332}
333
334impl<T: TableEntry> DoubleEndedIterator for EntryIter<'_, T> {
335    fn next_back(&mut self) -> Option<T> {
336        match &mut self.repr {
337            IterRepr::Wire(entries) => entries.next_back().map(|octets| T::from_wire(*octets)),
338            IterRepr::Typed(entries) => entries.next_back().copied(),
339        }
340    }
341}
342
343impl<T: TableEntry> ExactSizeIterator for EntryIter<'_, T> {}
344
345impl<T: TableEntry> FusedIterator for EntryIter<'_, T> {}