Skip to main content

bacnet_encoding/apdu/
mod.rs

1//! APDU encoding and decoding per ASHRAE 135-2020 Clause 20.1.
2//!
3//! Covers all eight PDU types:
4//! - [`ConfirmedRequest`] (Clause 20.1.2)
5//! - [`UnconfirmedRequest`] (Clause 20.1.3)
6//! - [`SimpleAck`] (Clause 20.1.4)
7//! - [`ComplexAck`] (Clause 20.1.5)
8//! - [`SegmentAck`] (Clause 20.1.6)
9//! - [`ErrorPdu`] (Clause 20.1.7)
10//! - [`RejectPdu`] (Clause 20.1.8)
11//! - [`AbortPdu`] (Clause 20.1.9)
12
13use bacnet_types::enums::{
14    AbortReason, ConfirmedServiceChoice, ErrorClass, ErrorCode, PduType, RejectReason,
15    UnconfirmedServiceChoice,
16};
17use bacnet_types::error::Error;
18use bytes::{BufMut, Bytes, BytesMut};
19
20use crate::primitives;
21use crate::tags;
22
23// ---------------------------------------------------------------------------
24// Max-segments encoding
25// ---------------------------------------------------------------------------
26
27/// Decoded max-segments values indexed by the 3-bit field (0-7).
28/// `None` means unspecified (0).
29const MAX_SEGMENTS_DECODE: [Option<u8>; 8] = [
30    None,      // 0 = unspecified
31    Some(2),   // 1
32    Some(4),   // 2
33    Some(8),   // 3
34    Some(16),  // 4
35    Some(32),  // 5
36    Some(64),  // 6
37    Some(255), // 7 = >64 segments accepted
38];
39
40/// Encode a max-segments value to a 3-bit field.
41fn encode_max_segments(value: Option<u8>) -> u8 {
42    match value {
43        None => 0,
44        Some(2) => 1,
45        Some(4) => 2,
46        Some(8) => 3,
47        Some(16) => 4,
48        Some(32) => 5,
49        Some(64) => 6,
50        Some(_) => 7, // >64
51    }
52}
53
54/// Decode a 3-bit max-segments field.
55fn decode_max_segments(value: u8) -> Option<u8> {
56    MAX_SEGMENTS_DECODE[(value & 0x07) as usize]
57}
58
59// ---------------------------------------------------------------------------
60// Max-APDU-length encoding
61// ---------------------------------------------------------------------------
62
63/// Decoded max-APDU-length values indexed by the 4-bit field.
64const MAX_APDU_DECODE: [u16; 6] = [50, 128, 206, 480, 1024, 1476];
65
66/// Return true when `value` is one of the BACnet max-APDU-length encodings
67/// defined by ASHRAE 135-2020 Clause 20.1.2.5.
68pub fn is_valid_max_apdu_length(value: u16) -> bool {
69    matches!(value, 50 | 128 | 206 | 480 | 1024 | 1476)
70}
71
72/// Validate a locally configured max-APDU-length value.
73pub fn validate_max_apdu_length(value: u16) -> Result<(), Error> {
74    if is_valid_max_apdu_length(value) {
75        Ok(())
76    } else {
77        Err(Error::Encoding(format!(
78            "invalid max-APDU-length {value}; expected one of 50, 128, 206, 480, 1024, 1476"
79        )))
80    }
81}
82
83/// Encode a max-APDU-length to a 4-bit field.
84fn encode_max_apdu(value: u16) -> Result<u8, Error> {
85    validate_max_apdu_length(value)?;
86    match value {
87        50 => Ok(0),
88        128 => Ok(1),
89        206 => Ok(2),
90        480 => Ok(3),
91        1024 => Ok(4),
92        1476 => Ok(5),
93        _ => unreachable!("validated max-APDU-length"),
94    }
95}
96
97/// Decode a 4-bit max-APDU-length field.
98fn decode_max_apdu(value: u8) -> Result<u16, Error> {
99    let idx = (value & 0x0F) as usize;
100    if idx < MAX_APDU_DECODE.len() {
101        Ok(MAX_APDU_DECODE[idx])
102    } else {
103        Err(Error::decoding(
104            1,
105            format!("reserved max-APDU-length field value {idx}"),
106        ))
107    }
108}
109
110// ---------------------------------------------------------------------------
111// PDU structs
112// ---------------------------------------------------------------------------
113
114/// Confirmed-Request PDU (Clause 20.1.2).
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct ConfirmedRequest {
117    pub segmented: bool,
118    pub more_follows: bool,
119    pub segmented_response_accepted: bool,
120    pub max_segments: Option<u8>,
121    pub max_apdu_length: u16,
122    pub invoke_id: u8,
123    pub sequence_number: Option<u8>,
124    pub proposed_window_size: Option<u8>,
125    pub service_choice: ConfirmedServiceChoice,
126    pub service_request: Bytes,
127}
128
129/// Unconfirmed-Request PDU (Clause 20.1.3).
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct UnconfirmedRequest {
132    pub service_choice: UnconfirmedServiceChoice,
133    pub service_request: Bytes,
134}
135
136/// SimpleACK PDU (Clause 20.1.4).
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct SimpleAck {
139    pub invoke_id: u8,
140    pub service_choice: ConfirmedServiceChoice,
141}
142
143/// ComplexACK PDU (Clause 20.1.5).
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct ComplexAck {
146    pub segmented: bool,
147    pub more_follows: bool,
148    pub invoke_id: u8,
149    pub sequence_number: Option<u8>,
150    pub proposed_window_size: Option<u8>,
151    pub service_choice: ConfirmedServiceChoice,
152    pub service_ack: Bytes,
153}
154
155/// SegmentACK PDU (Clause 20.1.6).
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct SegmentAck {
158    pub negative_ack: bool,
159    pub sent_by_server: bool,
160    pub invoke_id: u8,
161    pub sequence_number: u8,
162    pub actual_window_size: u8,
163}
164
165/// Error PDU (Clause 20.1.7).
166#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct ErrorPdu {
168    pub invoke_id: u8,
169    pub service_choice: ConfirmedServiceChoice,
170    pub error_class: ErrorClass,
171    pub error_code: ErrorCode,
172    pub error_data: Bytes,
173}
174
175/// Reject PDU (Clause 20.1.8).
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct RejectPdu {
178    pub invoke_id: u8,
179    pub reject_reason: RejectReason,
180}
181
182/// Abort PDU (Clause 20.1.9).
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct AbortPdu {
185    pub sent_by_server: bool,
186    pub invoke_id: u8,
187    pub abort_reason: AbortReason,
188}
189
190/// Sum type for all APDU PDU types.
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub enum Apdu {
193    ConfirmedRequest(ConfirmedRequest),
194    UnconfirmedRequest(UnconfirmedRequest),
195    SimpleAck(SimpleAck),
196    ComplexAck(ComplexAck),
197    SegmentAck(SegmentAck),
198    Error(ErrorPdu),
199    Reject(RejectPdu),
200    Abort(AbortPdu),
201}
202
203// ---------------------------------------------------------------------------
204// Encoding
205// ---------------------------------------------------------------------------
206
207/// Encode an APDU to wire format.
208pub fn encode_apdu(buf: &mut BytesMut, apdu: &Apdu) -> Result<(), Error> {
209    match apdu {
210        Apdu::ConfirmedRequest(pdu) => encode_confirmed_request(buf, pdu),
211        Apdu::UnconfirmedRequest(pdu) => {
212            encode_unconfirmed_request(buf, pdu);
213            Ok(())
214        }
215        Apdu::SimpleAck(pdu) => {
216            encode_simple_ack(buf, pdu);
217            Ok(())
218        }
219        Apdu::ComplexAck(pdu) => encode_complex_ack(buf, pdu),
220        Apdu::SegmentAck(pdu) => encode_segment_ack(buf, pdu),
221        Apdu::Error(pdu) => {
222            encode_error(buf, pdu);
223            Ok(())
224        }
225        Apdu::Reject(pdu) => {
226            encode_reject(buf, pdu);
227            Ok(())
228        }
229        Apdu::Abort(pdu) => {
230            encode_abort(buf, pdu);
231            Ok(())
232        }
233    }
234}
235
236fn encode_confirmed_request(buf: &mut BytesMut, pdu: &ConfirmedRequest) -> Result<(), Error> {
237    let mut byte0 = PduType::CONFIRMED_REQUEST.to_raw() << 4;
238    if pdu.segmented {
239        byte0 |= 0x08;
240    }
241    if pdu.more_follows {
242        byte0 |= 0x04;
243    }
244    if pdu.segmented_response_accepted {
245        byte0 |= 0x02;
246    }
247    buf.put_u8(byte0);
248
249    let byte1 =
250        (encode_max_segments(pdu.max_segments) << 4) | encode_max_apdu(pdu.max_apdu_length)?;
251    buf.put_u8(byte1);
252
253    buf.put_u8(pdu.invoke_id);
254
255    if pdu.segmented {
256        buf.put_u8(pdu.sequence_number.unwrap_or(0));
257        buf.put_u8(valid_window_size(
258            "ConfirmedRequest proposed-window-size",
259            pdu.proposed_window_size.unwrap_or(1),
260        )?);
261    }
262
263    buf.put_u8(pdu.service_choice.to_raw());
264    buf.put_slice(&pdu.service_request);
265    Ok(())
266}
267
268fn encode_unconfirmed_request(buf: &mut BytesMut, pdu: &UnconfirmedRequest) {
269    buf.put_u8(PduType::UNCONFIRMED_REQUEST.to_raw() << 4);
270    buf.put_u8(pdu.service_choice.to_raw());
271    buf.put_slice(&pdu.service_request);
272}
273
274fn encode_simple_ack(buf: &mut BytesMut, pdu: &SimpleAck) {
275    buf.put_u8(PduType::SIMPLE_ACK.to_raw() << 4);
276    buf.put_u8(pdu.invoke_id);
277    buf.put_u8(pdu.service_choice.to_raw());
278}
279
280fn encode_complex_ack(buf: &mut BytesMut, pdu: &ComplexAck) -> Result<(), Error> {
281    let mut byte0 = PduType::COMPLEX_ACK.to_raw() << 4;
282    if pdu.segmented {
283        byte0 |= 0x08;
284    }
285    if pdu.more_follows {
286        byte0 |= 0x04;
287    }
288    buf.put_u8(byte0);
289
290    buf.put_u8(pdu.invoke_id);
291
292    if pdu.segmented {
293        buf.put_u8(pdu.sequence_number.unwrap_or(0));
294        buf.put_u8(valid_window_size(
295            "ComplexAck proposed-window-size",
296            pdu.proposed_window_size.unwrap_or(1),
297        )?);
298    }
299
300    buf.put_u8(pdu.service_choice.to_raw());
301    buf.put_slice(&pdu.service_ack);
302    Ok(())
303}
304
305fn encode_segment_ack(buf: &mut BytesMut, pdu: &SegmentAck) -> Result<(), Error> {
306    let mut byte0 = PduType::SEGMENT_ACK.to_raw() << 4;
307    if pdu.negative_ack {
308        byte0 |= 0x02;
309    }
310    if pdu.sent_by_server {
311        byte0 |= 0x01;
312    }
313    buf.put_u8(byte0);
314    buf.put_u8(pdu.invoke_id);
315    buf.put_u8(pdu.sequence_number);
316    buf.put_u8(valid_window_size(
317        "SegmentACK actual-window-size",
318        pdu.actual_window_size,
319    )?);
320    Ok(())
321}
322
323fn valid_window_size(field: &str, value: u8) -> Result<u8, Error> {
324    if (1..=127).contains(&value) {
325        Ok(value)
326    } else {
327        Err(Error::Encoding(format!(
328            "{field} {value} outside BACnet range 1..=127"
329        )))
330    }
331}
332
333fn encode_error(buf: &mut BytesMut, pdu: &ErrorPdu) {
334    buf.put_u8(PduType::ERROR.to_raw() << 4);
335    buf.put_u8(pdu.invoke_id);
336    buf.put_u8(pdu.service_choice.to_raw());
337    primitives::encode_app_enumerated(buf, pdu.error_class.to_raw() as u32);
338    primitives::encode_app_enumerated(buf, pdu.error_code.to_raw() as u32);
339    if !pdu.error_data.is_empty() {
340        buf.put_slice(&pdu.error_data);
341    }
342}
343
344fn encode_reject(buf: &mut BytesMut, pdu: &RejectPdu) {
345    buf.put_u8(PduType::REJECT.to_raw() << 4);
346    buf.put_u8(pdu.invoke_id);
347    buf.put_u8(pdu.reject_reason.to_raw());
348}
349
350fn encode_abort(buf: &mut BytesMut, pdu: &AbortPdu) {
351    let mut byte0 = PduType::ABORT.to_raw() << 4;
352    if pdu.sent_by_server {
353        byte0 |= 0x01;
354    }
355    buf.put_u8(byte0);
356    buf.put_u8(pdu.invoke_id);
357    buf.put_u8(pdu.abort_reason.to_raw());
358}
359
360// ---------------------------------------------------------------------------
361// Decoding
362// ---------------------------------------------------------------------------
363
364/// Decode an APDU from raw bytes.
365pub fn decode_apdu(data: Bytes) -> Result<Apdu, Error> {
366    if data.is_empty() {
367        return Err(Error::decoding(0, "APDU data is empty"));
368    }
369
370    let pdu_type_raw = (data[0] >> 4) & 0x0F;
371    let pdu_type = PduType::from_raw(pdu_type_raw);
372
373    if pdu_type == PduType::CONFIRMED_REQUEST {
374        decode_confirmed_request(data).map(Apdu::ConfirmedRequest)
375    } else if pdu_type == PduType::UNCONFIRMED_REQUEST {
376        decode_unconfirmed_request(data).map(Apdu::UnconfirmedRequest)
377    } else if pdu_type == PduType::SIMPLE_ACK {
378        decode_simple_ack(data).map(Apdu::SimpleAck)
379    } else if pdu_type == PduType::COMPLEX_ACK {
380        decode_complex_ack(data).map(Apdu::ComplexAck)
381    } else if pdu_type == PduType::SEGMENT_ACK {
382        decode_segment_ack(data).map(Apdu::SegmentAck)
383    } else if pdu_type == PduType::ERROR {
384        decode_error(data).map(Apdu::Error)
385    } else if pdu_type == PduType::REJECT {
386        decode_reject(data).map(Apdu::Reject)
387    } else if pdu_type == PduType::ABORT {
388        decode_abort(data).map(Apdu::Abort)
389    } else {
390        Err(Error::decoding(
391            0,
392            format!("unknown PDU type nibble: {:#x}", pdu_type_raw),
393        ))
394    }
395}
396
397fn decode_confirmed_request(data: Bytes) -> Result<ConfirmedRequest, Error> {
398    if data.len() < 4 {
399        return Err(Error::buffer_too_short(4, data.len()));
400    }
401
402    let byte0 = data[0];
403    let segmented = byte0 & 0x08 != 0;
404    let more_follows = byte0 & 0x04 != 0;
405    let segmented_response_accepted = byte0 & 0x02 != 0;
406
407    let byte1 = data[1];
408    let max_segments = decode_max_segments((byte1 >> 4) & 0x07);
409    let max_apdu_length = decode_max_apdu(byte1 & 0x0F)?;
410
411    let invoke_id = data[2];
412    let mut offset = 3;
413
414    let (sequence_number, proposed_window_size) = if segmented {
415        if data.len() < 6 {
416            return Err(Error::decoding(
417                offset,
418                "segmented ConfirmedRequest too short for sequence/window fields",
419            ));
420        }
421        let seq = data[offset];
422        let win = data[offset + 1];
423        offset += 2;
424        (Some(seq), Some(win))
425    } else {
426        (None, None)
427    };
428
429    if offset >= data.len() {
430        return Err(Error::decoding(
431            offset,
432            "ConfirmedRequest missing service choice",
433        ));
434    }
435    let service_choice = ConfirmedServiceChoice::from_raw(data[offset]);
436    offset += 1;
437
438    let service_request = data.slice(offset..);
439
440    Ok(ConfirmedRequest {
441        segmented,
442        more_follows,
443        segmented_response_accepted,
444        max_segments,
445        max_apdu_length,
446        invoke_id,
447        sequence_number,
448        proposed_window_size,
449        service_choice,
450        service_request,
451    })
452}
453
454fn decode_unconfirmed_request(data: Bytes) -> Result<UnconfirmedRequest, Error> {
455    if data.len() < 2 {
456        return Err(Error::buffer_too_short(2, data.len()));
457    }
458
459    let service_choice = UnconfirmedServiceChoice::from_raw(data[1]);
460    let service_request = data.slice(2..);
461
462    Ok(UnconfirmedRequest {
463        service_choice,
464        service_request,
465    })
466}
467
468fn decode_simple_ack(data: Bytes) -> Result<SimpleAck, Error> {
469    if data.len() < 3 {
470        return Err(Error::buffer_too_short(3, data.len()));
471    }
472
473    Ok(SimpleAck {
474        invoke_id: data[1],
475        service_choice: ConfirmedServiceChoice::from_raw(data[2]),
476    })
477}
478
479fn decode_complex_ack(data: Bytes) -> Result<ComplexAck, Error> {
480    if data.len() < 3 {
481        return Err(Error::buffer_too_short(3, data.len()));
482    }
483
484    let byte0 = data[0];
485    let segmented = byte0 & 0x08 != 0;
486    let more_follows = byte0 & 0x04 != 0;
487
488    let invoke_id = data[1];
489    let mut offset = 2;
490
491    let (sequence_number, proposed_window_size) = if segmented {
492        if data.len() < 5 {
493            return Err(Error::decoding(
494                offset,
495                "segmented ComplexAck too short for sequence/window fields",
496            ));
497        }
498        let seq = data[offset];
499        let win = data[offset + 1];
500        offset += 2;
501        (Some(seq), Some(win))
502    } else {
503        (None, None)
504    };
505
506    if offset >= data.len() {
507        return Err(Error::decoding(offset, "ComplexAck missing service choice"));
508    }
509    let service_choice = ConfirmedServiceChoice::from_raw(data[offset]);
510    offset += 1;
511
512    let service_ack = data.slice(offset..);
513
514    Ok(ComplexAck {
515        segmented,
516        more_follows,
517        invoke_id,
518        sequence_number,
519        proposed_window_size,
520        service_choice,
521        service_ack,
522    })
523}
524
525fn decode_segment_ack(data: Bytes) -> Result<SegmentAck, Error> {
526    if data.len() < 4 {
527        return Err(Error::buffer_too_short(4, data.len()));
528    }
529
530    let byte0 = data[0];
531    let raw_window = data[3];
532    if raw_window == 0 || raw_window > 127 {
533        return Err(Error::decoding(
534            3,
535            format!("SegmentACK actual-window-size {raw_window} outside range 1..=127"),
536        ));
537    }
538    Ok(SegmentAck {
539        negative_ack: byte0 & 0x02 != 0,
540        sent_by_server: byte0 & 0x01 != 0,
541        invoke_id: data[1],
542        sequence_number: data[2],
543        actual_window_size: raw_window,
544    })
545}
546
547fn decode_error(data: Bytes) -> Result<ErrorPdu, Error> {
548    if data.len() < 5 {
549        return Err(Error::buffer_too_short(5, data.len()));
550    }
551
552    let invoke_id = data[1];
553    let service_choice = ConfirmedServiceChoice::from_raw(data[2]);
554
555    let mut offset = 3;
556    let (tag, tag_end) = tags::decode_tag(&data, offset)?;
557    if tag.class != tags::TagClass::Application || tag.number != tags::app_tag::ENUMERATED {
558        return Err(Error::decoding(
559            offset,
560            "ErrorPDU error class: expected application-tagged enumerated",
561        ));
562    }
563    let class_end = tag_end
564        .checked_add(tag.length as usize)
565        .ok_or_else(|| Error::decoding(tag_end, "ErrorPDU error class length overflow"))?;
566    if class_end > data.len() {
567        return Err(Error::decoding(
568            tag_end,
569            "ErrorPDU truncated at error class",
570        ));
571    }
572    let error_class_raw = primitives::decode_unsigned(&data[tag_end..class_end])? as u16;
573    offset = class_end;
574
575    let (tag, tag_end) = tags::decode_tag(&data, offset)?;
576    if tag.class != tags::TagClass::Application || tag.number != tags::app_tag::ENUMERATED {
577        return Err(Error::decoding(
578            offset,
579            "ErrorPDU error code: expected application-tagged enumerated",
580        ));
581    }
582    let code_end = tag_end
583        .checked_add(tag.length as usize)
584        .ok_or_else(|| Error::decoding(tag_end, "ErrorPDU error code length overflow"))?;
585    if code_end > data.len() {
586        return Err(Error::decoding(tag_end, "ErrorPDU truncated at error code"));
587    }
588    let error_code_raw = primitives::decode_unsigned(&data[tag_end..code_end])? as u16;
589    offset = code_end;
590
591    let error_data = if offset < data.len() {
592        data.slice(offset..)
593    } else {
594        Bytes::new()
595    };
596
597    Ok(ErrorPdu {
598        invoke_id,
599        service_choice,
600        error_class: ErrorClass::from_raw(error_class_raw),
601        error_code: ErrorCode::from_raw(error_code_raw),
602        error_data,
603    })
604}
605
606fn decode_reject(data: Bytes) -> Result<RejectPdu, Error> {
607    if data.len() < 3 {
608        return Err(Error::buffer_too_short(3, data.len()));
609    }
610
611    Ok(RejectPdu {
612        invoke_id: data[1],
613        reject_reason: RejectReason::from_raw(data[2]),
614    })
615}
616
617fn decode_abort(data: Bytes) -> Result<AbortPdu, Error> {
618    if data.len() < 3 {
619        return Err(Error::buffer_too_short(3, data.len()));
620    }
621
622    let byte0 = data[0];
623    Ok(AbortPdu {
624        sent_by_server: byte0 & 0x01 != 0,
625        invoke_id: data[1],
626        abort_reason: AbortReason::from_raw(data[2]),
627    })
628}
629
630// ---------------------------------------------------------------------------
631// Tests
632// ---------------------------------------------------------------------------
633
634#[cfg(test)]
635mod tests;