Skip to main content

bacnet_bip_codec/
message.rs

1//! BVLL frames and messages: one-shot decode, and checked encode.
2
3use core::net::SocketAddrV4;
4
5use crate::{
6    address_from_octets, address_to_octets, BdtEntries, BvlcFunction, BvlcResultCode, BvllError,
7    BvllSink, FdtEntries, BVLC_TYPE_BIP, HEADER_LENGTH, MAX_BVLL_LENGTH,
8};
9
10/// A BVLL message whose header checks out, before its payload is read.
11///
12/// [`BvllFrame::decode`] checks the BVLC type and the Length field and
13/// accepts any function code, so a receiver can see a function it doesn't
14/// know, or decide how to answer a request whose payload is malformed,
15/// before [`BvllFrame::message`] reads the payload by the function's
16/// layout.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct BvllFrame<'a> {
19    /// The function octet.
20    pub function: BvlcFunction,
21    /// The octets after the 4-octet header.
22    pub payload: &'a [u8],
23}
24
25impl<'a> BvllFrame<'a> {
26    /// Check a datagram's BVLL header and split off its payload.
27    ///
28    /// The datagram must be one whole message: at least the 4-octet header,
29    /// BVLC type 0x81, and a Length field equal to the datagram's length,
30    /// since it counts the whole message (Annex J.2). Octets after the
31    /// declared length are refused rather than ignored, so a datagram can't
32    /// pass for a shorter message.
33    ///
34    /// ```
35    /// use bacnet_bip_codec::{BvlcFunction, BvllError, BvllFrame};
36    ///
37    /// // A Read-Foreign-Device-Table: header only.
38    /// let frame = BvllFrame::decode(&[0x81, 0x06, 0x00, 0x04])?;
39    /// assert_eq!(frame.function, BvlcFunction::READ_FOREIGN_DEVICE_TABLE);
40    /// assert!(frame.payload.is_empty());
41    ///
42    /// // An unknown function still has a frame, but no message.
43    /// let frame = BvllFrame::decode(&[0x81, 0x0C, 0x00, 0x05, 0xAA])?;
44    /// assert_eq!(frame.message(), Err(BvllError::UnknownFunction { function: 0x0C }));
45    ///
46    /// // A trailing octet the Length field doesn't count.
47    /// assert_eq!(
48    ///     BvllFrame::decode(&[0x81, 0x06, 0x00, 0x04, 0x00]),
49    ///     Err(BvllError::LengthMismatch { declared: 4, received: 5 }),
50    /// );
51    /// # Ok::<(), BvllError>(())
52    /// ```
53    pub fn decode(datagram: &'a [u8]) -> Result<Self, BvllError> {
54        let [bvlc_type, function, high, low, payload @ ..] = datagram else {
55            return Err(BvllError::TooShort {
56                length: datagram.len(),
57            });
58        };
59        if *bvlc_type != BVLC_TYPE_BIP {
60            return Err(BvllError::NotBip {
61                bvlc_type: *bvlc_type,
62            });
63        }
64        let declared = usize::from(u16::from_be_bytes([*high, *low]));
65        if declared != datagram.len() {
66            return Err(BvllError::LengthMismatch {
67                declared,
68                received: datagram.len(),
69            });
70        }
71        Ok(Self {
72            function: BvlcFunction::from_raw(*function),
73            payload,
74        })
75    }
76
77    /// Read the payload by the function's layout (see
78    /// [`BvlcFunction::payload_shape`]).
79    ///
80    /// Fails with [`BvllError::UnknownFunction`] for a function Annex J
81    /// doesn't define, and with [`BvllError::PayloadLength`] when the payload
82    /// is the wrong length for its function. Field values are not judged: an
83    /// undefined result code, a zero time-to-live, any mask or address, and
84    /// an empty NPDU all decode.
85    pub fn message(self) -> Result<Bvll<'a>, BvllError> {
86        let function = self.function;
87        let wrong_length = BvllError::PayloadLength {
88            function,
89            length: self.payload.len(),
90        };
91        Ok(match (function, self.payload) {
92            (BvlcFunction::BVLC_RESULT, &[high, low]) => {
93                Bvll::BvlcResult(BvlcResultCode::from_raw(u16::from_be_bytes([high, low])))
94            }
95            (BvlcFunction::WRITE_BROADCAST_DISTRIBUTION_TABLE, table) => {
96                Bvll::WriteBroadcastDistributionTable(
97                    BdtEntries::from_wire(table).map_err(|_| wrong_length)?,
98                )
99            }
100            (BvlcFunction::READ_BROADCAST_DISTRIBUTION_TABLE, []) => {
101                Bvll::ReadBroadcastDistributionTable
102            }
103            (BvlcFunction::READ_BROADCAST_DISTRIBUTION_TABLE_ACK, table) => {
104                Bvll::ReadBroadcastDistributionTableAck(
105                    BdtEntries::from_wire(table).map_err(|_| wrong_length)?,
106                )
107            }
108            (BvlcFunction::FORWARDED_NPDU, &[a, b, c, d, high, low, ref npdu @ ..]) => {
109                Bvll::ForwardedNpdu {
110                    origin: address_from_octets([a, b, c, d, high, low]),
111                    npdu,
112                }
113            }
114            (BvlcFunction::REGISTER_FOREIGN_DEVICE, &[high, low]) => Bvll::RegisterForeignDevice {
115                ttl: u16::from_be_bytes([high, low]),
116            },
117            (BvlcFunction::READ_FOREIGN_DEVICE_TABLE, []) => Bvll::ReadForeignDeviceTable,
118            (BvlcFunction::READ_FOREIGN_DEVICE_TABLE_ACK, table) => {
119                Bvll::ReadForeignDeviceTableAck(
120                    FdtEntries::from_wire(table).map_err(|_| wrong_length)?,
121                )
122            }
123            (BvlcFunction::DELETE_FOREIGN_DEVICE_TABLE_ENTRY, &[a, b, c, d, high, low]) => {
124                Bvll::DeleteForeignDeviceTableEntry(address_from_octets([a, b, c, d, high, low]))
125            }
126            (BvlcFunction::DISTRIBUTE_BROADCAST_TO_NETWORK, npdu) => {
127                Bvll::DistributeBroadcastToNetwork(npdu)
128            }
129            (BvlcFunction::ORIGINAL_UNICAST_NPDU, npdu) => Bvll::OriginalUnicastNpdu(npdu),
130            (BvlcFunction::ORIGINAL_BROADCAST_NPDU, npdu) => Bvll::OriginalBroadcastNpdu(npdu),
131            _ if function.is_defined() => return Err(wrong_length),
132            _ => {
133                return Err(BvllError::UnknownFunction {
134                    function: function.to_raw(),
135                })
136            }
137        })
138    }
139}
140
141/// Decode one datagram as a BVLL message: [`BvllFrame::decode`], then
142/// [`BvllFrame::message`].
143///
144/// ```
145/// use core::net::{Ipv4Addr, SocketAddrV4};
146/// use bacnet_bip_codec::{decode, Bvll, BvllError};
147///
148/// // A Forwarded-NPDU from 192.168.1.20:47808 carrying the NPDU 01 00.
149/// let wire = [0x81, 0x04, 0x00, 0x0C, 192, 168, 1, 20, 0xBA, 0xC0, 0x01, 0x00];
150/// assert_eq!(
151///     decode(&wire)?,
152///     Bvll::ForwardedNpdu {
153///         origin: SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 20), 47808),
154///         npdu: &[0x01, 0x00],
155///     },
156/// );
157///
158/// // A Register-Foreign-Device needs exactly a 2-octet time-to-live.
159/// assert!(matches!(
160///     decode(&[0x81, 0x05, 0x00, 0x05, 0x3C]),
161///     Err(BvllError::PayloadLength { length: 1, .. }),
162/// ));
163/// # Ok::<(), BvllError>(())
164/// ```
165pub fn decode(datagram: &[u8]) -> Result<Bvll<'_>, BvllError> {
166    BvllFrame::decode(datagram)?.message()
167}
168
169/// One B/IP BVLL message (Annex J.2), borrowing its NPDU or table.
170///
171/// [`decode`] produces one from a datagram, and
172/// [`encode_into`](Bvll::encode_into) or
173/// [`encode_to_sink`](Bvll::encode_to_sink) writes one. Each variant holds
174/// what its function's format clause lays out after the header.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum Bvll<'a> {
177    /// BVLC-Result (J.2.1).
178    BvlcResult(BvlcResultCode),
179    /// Write-Broadcast-Distribution-Table (J.2.2).
180    WriteBroadcastDistributionTable(BdtEntries<'a>),
181    /// Read-Broadcast-Distribution-Table (J.2.3).
182    ReadBroadcastDistributionTable,
183    /// Read-Broadcast-Distribution-Table-Ack (J.2.4).
184    ReadBroadcastDistributionTableAck(BdtEntries<'a>),
185    /// Forwarded-NPDU (J.2.5).
186    ForwardedNpdu {
187        /// Where the NPDU started: its originator's B/IP address. A receiver
188        /// takes it as the NPDU's source when it differs from the sender's
189        /// address (J.2.5).
190        origin: SocketAddrV4,
191        /// The NPDU.
192        npdu: &'a [u8],
193    },
194    /// Register-Foreign-Device (J.2.6).
195    RegisterForeignDevice {
196        /// Seconds within which the device will register again.
197        ttl: u16,
198    },
199    /// Read-Foreign-Device-Table (J.2.7).
200    ReadForeignDeviceTable,
201    /// Read-Foreign-Device-Table-Ack (J.2.8).
202    ReadForeignDeviceTableAck(FdtEntries<'a>),
203    /// Delete-Foreign-Device-Table-Entry (J.2.9), naming the entry's B/IP
204    /// address.
205    DeleteForeignDeviceTableEntry(SocketAddrV4),
206    /// Distribute-Broadcast-To-Network (J.2.10), carrying an NPDU.
207    DistributeBroadcastToNetwork(&'a [u8]),
208    /// Original-Unicast-NPDU (J.2.11), carrying an NPDU.
209    OriginalUnicastNpdu(&'a [u8]),
210    /// Original-Broadcast-NPDU (J.2.12), carrying an NPDU.
211    OriginalBroadcastNpdu(&'a [u8]),
212}
213
214impl<'a> Bvll<'a> {
215    /// The message's function code.
216    #[must_use]
217    pub const fn function(&self) -> BvlcFunction {
218        match self {
219            Self::BvlcResult(_) => BvlcFunction::BVLC_RESULT,
220            Self::WriteBroadcastDistributionTable(_) => {
221                BvlcFunction::WRITE_BROADCAST_DISTRIBUTION_TABLE
222            }
223            Self::ReadBroadcastDistributionTable => BvlcFunction::READ_BROADCAST_DISTRIBUTION_TABLE,
224            Self::ReadBroadcastDistributionTableAck(_) => {
225                BvlcFunction::READ_BROADCAST_DISTRIBUTION_TABLE_ACK
226            }
227            Self::ForwardedNpdu { .. } => BvlcFunction::FORWARDED_NPDU,
228            Self::RegisterForeignDevice { .. } => BvlcFunction::REGISTER_FOREIGN_DEVICE,
229            Self::ReadForeignDeviceTable => BvlcFunction::READ_FOREIGN_DEVICE_TABLE,
230            Self::ReadForeignDeviceTableAck(_) => BvlcFunction::READ_FOREIGN_DEVICE_TABLE_ACK,
231            Self::DeleteForeignDeviceTableEntry(_) => {
232                BvlcFunction::DELETE_FOREIGN_DEVICE_TABLE_ENTRY
233            }
234            Self::DistributeBroadcastToNetwork(_) => BvlcFunction::DISTRIBUTE_BROADCAST_TO_NETWORK,
235            Self::OriginalUnicastNpdu(_) => BvlcFunction::ORIGINAL_UNICAST_NPDU,
236            Self::OriginalBroadcastNpdu(_) => BvlcFunction::ORIGINAL_BROADCAST_NPDU,
237        }
238    }
239
240    /// The NPDU, for the four functions that carry one.
241    #[must_use]
242    pub const fn npdu(&self) -> Option<&'a [u8]> {
243        match *self {
244            Self::ForwardedNpdu { npdu, .. }
245            | Self::DistributeBroadcastToNetwork(npdu)
246            | Self::OriginalUnicastNpdu(npdu)
247            | Self::OriginalBroadcastNpdu(npdu) => Some(npdu),
248            _ => None,
249        }
250    }
251
252    /// A Forwarded-NPDU's originating address.
253    #[must_use]
254    pub const fn origin(&self) -> Option<SocketAddrV4> {
255        match *self {
256            Self::ForwardedNpdu { origin, .. } => Some(origin),
257            _ => None,
258        }
259    }
260
261    /// Octets after the header.
262    const fn payload_length(&self) -> usize {
263        match self {
264            Self::BvlcResult(_) | Self::RegisterForeignDevice { .. } => 2,
265            Self::ReadBroadcastDistributionTable | Self::ReadForeignDeviceTable => 0,
266            Self::WriteBroadcastDistributionTable(table)
267            | Self::ReadBroadcastDistributionTableAck(table) => table.wire_length(),
268            Self::ReadForeignDeviceTableAck(table) => table.wire_length(),
269            Self::DeleteForeignDeviceTableEntry(_) => 6,
270            Self::ForwardedNpdu { npdu, .. } => 6 + npdu.len(),
271            Self::DistributeBroadcastToNetwork(npdu)
272            | Self::OriginalUnicastNpdu(npdu)
273            | Self::OriginalBroadcastNpdu(npdu) => npdu.len(),
274        }
275    }
276
277    /// Check the message and return its wire length, header included.
278    ///
279    /// Encoding refuses two things: a message too long for the 16-bit Length
280    /// field ([`BvllError::TooLong`]), and a BVLC-Result whose code Annex
281    /// J.2.1.1 doesn't define ([`BvllError::UndefinedResultCode`]). Every
282    /// other shape the standard forbids can't be built: each variant holds
283    /// exactly its function's fields.
284    pub fn encoded_len(&self) -> Result<usize, BvllError> {
285        if let Self::BvlcResult(code) = self {
286            if !code.is_defined() {
287                return Err(BvllError::UndefinedResultCode {
288                    code: code.to_raw(),
289                });
290            }
291        }
292        let length = HEADER_LENGTH + self.payload_length();
293        if length > MAX_BVLL_LENGTH {
294            return Err(BvllError::TooLong { length });
295        }
296        Ok(length)
297    }
298
299    /// Encode the message at the start of `output` and return its wire
300    /// length. Nothing is written unless it passes the checks of
301    /// [`Bvll::encoded_len`] and fits.
302    ///
303    /// ```
304    /// use bacnet_bip_codec::{Bvll, BvlcResultCode, BvllError};
305    ///
306    /// let mut wire = [0u8; 6];
307    /// let nak = Bvll::BvlcResult(BvlcResultCode::REGISTER_FOREIGN_DEVICE_NAK);
308    /// assert_eq!(nak.encode_into(&mut wire)?, 6);
309    /// assert_eq!(wire, [0x81, 0x00, 0x00, 0x06, 0x00, 0x30]);
310    ///
311    /// // 0x0070 is not a result code Annex J defines.
312    /// assert_eq!(
313    ///     Bvll::BvlcResult(BvlcResultCode::from_raw(0x0070)).encode_into(&mut wire),
314    ///     Err(BvllError::UndefinedResultCode { code: 0x0070 }),
315    /// );
316    /// # Ok::<(), BvllError>(())
317    /// ```
318    pub fn encode_into(&self, output: &mut [u8]) -> Result<usize, BvllError> {
319        let mut sink = output;
320        self.encode_to_sink(&mut sink)
321    }
322
323    /// Encode the message into `sink` and return its wire length.
324    ///
325    /// The checks are those of [`Bvll::encoded_len`]. When the sink reports
326    /// a fixed capacity, a message that does not fit fails before anything
327    /// is written. A growable sink is written directly, so if one of its
328    /// writes fails, the octets written before it stay in the sink; see
329    /// [`BvllSink`].
330    pub fn encode_to_sink<S: BvllSink + ?Sized>(&self, sink: &mut S) -> Result<usize, BvllError> {
331        let length = self.encoded_len()?;
332        if let Some(available) = sink.remaining_capacity() {
333            if available < length {
334                return Err(BvllError::BufferExhausted {
335                    needed: length,
336                    available,
337                });
338            }
339        }
340        // `encoded_len` capped the length at 0xFFFF.
341        let [high, low] = (length as u16).to_be_bytes();
342        sink.write(&[BVLC_TYPE_BIP, self.function().to_raw(), high, low])?;
343        match self {
344            Self::BvlcResult(code) => sink.write(&code.to_raw().to_be_bytes())?,
345            Self::RegisterForeignDevice { ttl } => sink.write(&ttl.to_be_bytes())?,
346            Self::ReadBroadcastDistributionTable | Self::ReadForeignDeviceTable => {}
347            Self::WriteBroadcastDistributionTable(table)
348            | Self::ReadBroadcastDistributionTableAck(table) => table.write_to(sink)?,
349            Self::ReadForeignDeviceTableAck(table) => table.write_to(sink)?,
350            Self::DeleteForeignDeviceTableEntry(address) => {
351                sink.write(&address_to_octets(*address))?
352            }
353            Self::ForwardedNpdu { origin, npdu } => {
354                sink.write(&address_to_octets(*origin))?;
355                sink.write(npdu)?;
356            }
357            Self::DistributeBroadcastToNetwork(npdu)
358            | Self::OriginalUnicastNpdu(npdu)
359            | Self::OriginalBroadcastNpdu(npdu) => sink.write(npdu)?,
360        }
361        Ok(length)
362    }
363}