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}