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
23mod wpm_error;
24
25// ---------------------------------------------------------------------------
26// Max-segments encoding
27// ---------------------------------------------------------------------------
28
29/// Decoded max-segments values indexed by the 3-bit field (0-7).
30/// `None` means unspecified (0).
31const MAX_SEGMENTS_DECODE: [Option<u8>; 8] = [
32    None,      // 0 = unspecified
33    Some(2),   // 1
34    Some(4),   // 2
35    Some(8),   // 3
36    Some(16),  // 4
37    Some(32),  // 5
38    Some(64),  // 6
39    Some(255), // 7 = >64 segments accepted
40];
41
42/// Validate a configured maximum segment count for a Confirmed-Request header.
43///
44/// Clause 20.1.2.4 has no encoding for a finite capacity below two segments.
45/// To decline segmented responses, set `segmented-response-accepted = false`
46/// and use `None` rather than `Some(1)`.
47pub fn validate_max_segments(value: Option<u8>) -> Result<(), Error> {
48    match value {
49        Some(value @ 0..=1) => Err(Error::Encoding(format!(
50            "invalid max-segments-accepted {value}; expected 2..=255 or unspecified"
51        ))),
52        _ => Ok(()),
53    }
54}
55
56/// Encode a max-segments value to a 3-bit field.
57///
58/// Finite counts that fall between the Clause 20.1.2.4 rungs are rounded down
59/// so the wire header never promises more receive capacity than configured.
60fn encode_max_segments(value: Option<u8>) -> Result<u8, Error> {
61    validate_max_segments(value)?;
62    Ok(match value {
63        None => 0,
64        Some(2..=3) => 1,
65        Some(4..=7) => 2,
66        Some(8..=15) => 3,
67        Some(16..=31) => 4,
68        Some(32..=63) => 5,
69        Some(64) => 6,
70        Some(65..=u8::MAX) => 7, // >64
71        Some(0..=1) => unreachable!("validated above"),
72    })
73}
74
75/// Decode a 3-bit max-segments field.
76fn decode_max_segments(value: u8) -> Option<u8> {
77    MAX_SEGMENTS_DECODE[(value & 0x07) as usize]
78}
79
80/// The segment count a configured `max-segments-accepted` actually promises a peer.
81///
82/// Clause 20.1.2.4 carries this parameter in three bits, and only B'001'
83/// through B'110' name a number (2, 4, 8, 16, 32, 64). B'000' is "Unspecified
84/// number of segments accepted" and B'111' is "Greater than 64 segments
85/// accepted" — neither tells the peer a limit, so both yield `None`.
86///
87/// This deliberately round-trips through the wire encoding rather than reading
88/// `configured` directly: a value such as `Some(100)` encodes as B'111', so the
89/// peer was told "Greater than 64 segments accepted", not "100". Answering
90/// from the configured number would claim a promise that was never sent.
91/// Invalid finite capacities below two yield `None`; APDU encoding and client
92/// startup reject those values through [`validate_max_segments`].
93pub fn advertised_max_segments(configured: Option<u8>) -> Option<u8> {
94    let encoded = encode_max_segments(configured).ok()?;
95    match decode_max_segments(encoded) {
96        // The B'111' sentinel — "Greater than 64 segments accepted", which is
97        // open-ended rather than a bound.
98        Some(255) => None,
99        decoded => decoded,
100    }
101}
102
103// ---------------------------------------------------------------------------
104// Max-APDU-length encoding
105// ---------------------------------------------------------------------------
106
107/// MinimumMessageSize: the smallest APDU any BACnet device accepts.
108///
109/// Clause 20.1.2.5 spells the lowest max-APDU-length-accepted code, `B'0000'`,
110/// as "Up to MinimumMessageSize (50 octets)"; Clause 12.11.18 requires
111/// `Max_APDU_Length_Accepted` to be "greater than or equal to 50"; and Clause
112/// 5.2.1.2 requires the size accepted by a remote peer to be "at least 50
113/// octets".
114pub const MINIMUM_MESSAGE_SIZE: u16 = 50;
115
116/// Decoded max-APDU-length values indexed by the 4-bit field.
117const MAX_APDU_DECODE: [u16; 6] = [MINIMUM_MESSAGE_SIZE, 128, 206, 480, 1024, 1476];
118
119/// Return true when `value` is one of the BACnet max-APDU-length encodings
120/// defined by ASHRAE 135-2020 Clause 20.1.2.5.
121pub fn is_valid_max_apdu_length(value: u16) -> bool {
122    matches!(value, 50 | 128 | 206 | 480 | 1024 | 1476)
123}
124
125/// Validate a locally configured max-APDU-length value.
126pub fn validate_max_apdu_length(value: u16) -> Result<(), Error> {
127    if is_valid_max_apdu_length(value) {
128        Ok(())
129    } else {
130        Err(Error::Encoding(format!(
131            "invalid max-APDU-length {value}; expected one of 50, 128, 206, 480, 1024, 1476"
132        )))
133    }
134}
135
136/// Encode a max-APDU-length to a 4-bit field.
137fn encode_max_apdu(value: u16) -> Result<u8, Error> {
138    validate_max_apdu_length(value)?;
139    match value {
140        50 => Ok(0),
141        128 => Ok(1),
142        206 => Ok(2),
143        480 => Ok(3),
144        1024 => Ok(4),
145        1476 => Ok(5),
146        _ => unreachable!("validated max-APDU-length"),
147    }
148}
149
150/// Decode a 4-bit max-APDU-length field.
151fn decode_max_apdu(value: u8) -> Result<u16, Error> {
152    let idx = (value & 0x0F) as usize;
153    if idx < MAX_APDU_DECODE.len() {
154        Ok(MAX_APDU_DECODE[idx])
155    } else {
156        Err(Error::decoding(
157            1,
158            format!("reserved max-APDU-length field value {idx}"),
159        ))
160    }
161}
162
163// ---------------------------------------------------------------------------
164// PDU structs
165// ---------------------------------------------------------------------------
166
167/// Confirmed-Request PDU (Clause 20.1.2).
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct ConfirmedRequest {
170    pub segmented: bool,
171    pub more_follows: bool,
172    pub segmented_response_accepted: bool,
173    pub max_segments: Option<u8>,
174    pub max_apdu_length: u16,
175    pub invoke_id: u8,
176    pub sequence_number: Option<u8>,
177    pub proposed_window_size: Option<u8>,
178    pub service_choice: ConfirmedServiceChoice,
179    pub service_request: Bytes,
180}
181
182/// Unconfirmed-Request PDU (Clause 20.1.3).
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct UnconfirmedRequest {
185    pub service_choice: UnconfirmedServiceChoice,
186    pub service_request: Bytes,
187}
188
189/// SimpleACK PDU (Clause 20.1.4).
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct SimpleAck {
192    pub invoke_id: u8,
193    pub service_choice: ConfirmedServiceChoice,
194}
195
196/// ComplexACK PDU (Clause 20.1.5).
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct ComplexAck {
199    pub segmented: bool,
200    pub more_follows: bool,
201    pub invoke_id: u8,
202    pub sequence_number: Option<u8>,
203    pub proposed_window_size: Option<u8>,
204    pub service_choice: ConfirmedServiceChoice,
205    pub service_ack: Bytes,
206}
207
208/// SegmentACK PDU (Clause 20.1.6).
209#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct SegmentAck {
211    pub negative_ack: bool,
212    pub sent_by_server: bool,
213    pub invoke_id: u8,
214    pub sequence_number: u8,
215    pub actual_window_size: u8,
216}
217
218/// Error PDU (Clause 20.1.7).
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct ErrorPdu {
221    pub invoke_id: u8,
222    pub service_choice: ConfirmedServiceChoice,
223    pub error_class: ErrorClass,
224    pub error_code: ErrorCode,
225    pub error_data: Bytes,
226}
227
228/// Reject PDU (Clause 20.1.8).
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct RejectPdu {
231    pub invoke_id: u8,
232    pub reject_reason: RejectReason,
233}
234
235/// Abort PDU (Clause 20.1.9).
236#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct AbortPdu {
238    pub sent_by_server: bool,
239    pub invoke_id: u8,
240    pub abort_reason: AbortReason,
241}
242
243/// Sum type for all APDU PDU types.
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub enum Apdu {
246    ConfirmedRequest(ConfirmedRequest),
247    UnconfirmedRequest(UnconfirmedRequest),
248    SimpleAck(SimpleAck),
249    ComplexAck(ComplexAck),
250    SegmentAck(SegmentAck),
251    Error(ErrorPdu),
252    Reject(RejectPdu),
253    Abort(AbortPdu),
254}
255
256// ---------------------------------------------------------------------------
257// Encoding
258// ---------------------------------------------------------------------------
259
260/// Encode an APDU to wire format.
261pub fn encode_apdu(buf: &mut BytesMut, apdu: &Apdu) -> Result<(), Error> {
262    match apdu {
263        Apdu::ConfirmedRequest(pdu) => encode_confirmed_request(buf, pdu),
264        Apdu::UnconfirmedRequest(pdu) => {
265            encode_unconfirmed_request(buf, pdu);
266            Ok(())
267        }
268        Apdu::SimpleAck(pdu) => {
269            encode_simple_ack(buf, pdu);
270            Ok(())
271        }
272        Apdu::ComplexAck(pdu) => encode_complex_ack(buf, pdu),
273        Apdu::SegmentAck(pdu) => encode_segment_ack(buf, pdu),
274        Apdu::Error(pdu) => encode_error(buf, pdu),
275        Apdu::Reject(pdu) => {
276            encode_reject(buf, pdu);
277            Ok(())
278        }
279        Apdu::Abort(pdu) => {
280            encode_abort(buf, pdu);
281            Ok(())
282        }
283    }
284}
285
286fn encode_confirmed_request(buf: &mut BytesMut, pdu: &ConfirmedRequest) -> Result<(), Error> {
287    let max_segments = encode_max_segments(pdu.max_segments)?;
288    let max_apdu = encode_max_apdu(pdu.max_apdu_length)?;
289
290    let mut byte0 = PduType::CONFIRMED_REQUEST.to_raw() << 4;
291    if pdu.segmented {
292        byte0 |= 0x08;
293    }
294    if pdu.more_follows {
295        byte0 |= 0x04;
296    }
297    if pdu.segmented_response_accepted {
298        byte0 |= 0x02;
299    }
300    buf.put_u8(byte0);
301
302    let byte1 = (max_segments << 4) | max_apdu;
303    buf.put_u8(byte1);
304
305    buf.put_u8(pdu.invoke_id);
306
307    if pdu.segmented {
308        buf.put_u8(pdu.sequence_number.unwrap_or(0));
309        buf.put_u8(valid_window_size(
310            "ConfirmedRequest proposed-window-size",
311            pdu.proposed_window_size.unwrap_or(1),
312        )?);
313    }
314
315    buf.put_u8(pdu.service_choice.to_raw());
316    buf.put_slice(&pdu.service_request);
317    Ok(())
318}
319
320fn encode_unconfirmed_request(buf: &mut BytesMut, pdu: &UnconfirmedRequest) {
321    buf.put_u8(PduType::UNCONFIRMED_REQUEST.to_raw() << 4);
322    buf.put_u8(pdu.service_choice.to_raw());
323    buf.put_slice(&pdu.service_request);
324}
325
326fn encode_simple_ack(buf: &mut BytesMut, pdu: &SimpleAck) {
327    buf.put_u8(PduType::SIMPLE_ACK.to_raw() << 4);
328    buf.put_u8(pdu.invoke_id);
329    buf.put_u8(pdu.service_choice.to_raw());
330}
331
332fn encode_complex_ack(buf: &mut BytesMut, pdu: &ComplexAck) -> Result<(), Error> {
333    let mut byte0 = PduType::COMPLEX_ACK.to_raw() << 4;
334    if pdu.segmented {
335        byte0 |= 0x08;
336    }
337    if pdu.more_follows {
338        byte0 |= 0x04;
339    }
340    buf.put_u8(byte0);
341
342    buf.put_u8(pdu.invoke_id);
343
344    if pdu.segmented {
345        buf.put_u8(pdu.sequence_number.unwrap_or(0));
346        buf.put_u8(valid_window_size(
347            "ComplexAck proposed-window-size",
348            pdu.proposed_window_size.unwrap_or(1),
349        )?);
350    }
351
352    buf.put_u8(pdu.service_choice.to_raw());
353    buf.put_slice(&pdu.service_ack);
354    Ok(())
355}
356
357fn encode_segment_ack(buf: &mut BytesMut, pdu: &SegmentAck) -> Result<(), Error> {
358    let mut byte0 = PduType::SEGMENT_ACK.to_raw() << 4;
359    if pdu.negative_ack {
360        byte0 |= 0x02;
361    }
362    if pdu.sent_by_server {
363        byte0 |= 0x01;
364    }
365    buf.put_u8(byte0);
366    buf.put_u8(pdu.invoke_id);
367    buf.put_u8(pdu.sequence_number);
368    buf.put_u8(valid_window_size(
369        "SegmentACK actual-window-size",
370        pdu.actual_window_size,
371    )?);
372    Ok(())
373}
374
375fn valid_window_size(field: &str, value: u8) -> Result<u8, Error> {
376    if (1..=127).contains(&value) {
377        Ok(value)
378    } else {
379        Err(Error::Encoding(format!(
380            "{field} {value} outside BACnet range 1..=127"
381        )))
382    }
383}
384
385fn encode_error(buf: &mut BytesMut, pdu: &ErrorPdu) -> Result<(), Error> {
386    let formal = if pdu.service_choice == ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE {
387        // A legacy generic WPM Error may carry arbitrary service data beginning
388        // with context [0]. Only suppress the generic pair for a complete,
389        // structurally valid formal service body.
390        wpm_error::decode_formal_body(&pdu.error_data).unwrap_or(None)
391    } else {
392        None
393    };
394    if let Some((error_class, error_code)) = formal {
395        if error_class != pdu.error_class || error_code != pdu.error_code {
396            return Err(Error::Encoding(
397                "formal WPM Error body disagrees with ErrorPdu class/code".into(),
398            ));
399        }
400    }
401
402    buf.put_u8(PduType::ERROR.to_raw() << 4);
403    buf.put_u8(pdu.invoke_id);
404    buf.put_u8(pdu.service_choice.to_raw());
405    if formal.is_some() {
406        buf.put_slice(&pdu.error_data);
407    } else {
408        primitives::encode_app_enumerated(buf, pdu.error_class.to_raw() as u32);
409        primitives::encode_app_enumerated(buf, pdu.error_code.to_raw() as u32);
410        if !pdu.error_data.is_empty() {
411            buf.put_slice(&pdu.error_data);
412        }
413    }
414    Ok(())
415}
416
417fn encode_reject(buf: &mut BytesMut, pdu: &RejectPdu) {
418    buf.put_u8(PduType::REJECT.to_raw() << 4);
419    buf.put_u8(pdu.invoke_id);
420    buf.put_u8(pdu.reject_reason.to_raw());
421}
422
423fn encode_abort(buf: &mut BytesMut, pdu: &AbortPdu) {
424    let mut byte0 = PduType::ABORT.to_raw() << 4;
425    if pdu.sent_by_server {
426        byte0 |= 0x01;
427    }
428    buf.put_u8(byte0);
429    buf.put_u8(pdu.invoke_id);
430    buf.put_u8(pdu.abort_reason.to_raw());
431}
432
433// ---------------------------------------------------------------------------
434// Decoding
435// ---------------------------------------------------------------------------
436
437/// Decode an APDU from raw bytes.
438pub fn decode_apdu(data: Bytes) -> Result<Apdu, Error> {
439    if data.is_empty() {
440        return Err(Error::decoding(0, "APDU data is empty"));
441    }
442
443    let pdu_type_raw = (data[0] >> 4) & 0x0F;
444    let pdu_type = PduType::from_raw(pdu_type_raw);
445
446    if pdu_type == PduType::CONFIRMED_REQUEST {
447        decode_confirmed_request(data).map(Apdu::ConfirmedRequest)
448    } else if pdu_type == PduType::UNCONFIRMED_REQUEST {
449        decode_unconfirmed_request(data).map(Apdu::UnconfirmedRequest)
450    } else if pdu_type == PduType::SIMPLE_ACK {
451        decode_simple_ack(data).map(Apdu::SimpleAck)
452    } else if pdu_type == PduType::COMPLEX_ACK {
453        decode_complex_ack(data).map(Apdu::ComplexAck)
454    } else if pdu_type == PduType::SEGMENT_ACK {
455        decode_segment_ack(data).map(Apdu::SegmentAck)
456    } else if pdu_type == PduType::ERROR {
457        decode_error(data).map(Apdu::Error)
458    } else if pdu_type == PduType::REJECT {
459        decode_reject(data).map(Apdu::Reject)
460    } else if pdu_type == PduType::ABORT {
461        decode_abort(data).map(Apdu::Abort)
462    } else {
463        Err(Error::decoding(
464            0,
465            format!("unknown PDU type nibble: {:#x}", pdu_type_raw),
466        ))
467    }
468}
469
470fn decode_confirmed_request(data: Bytes) -> Result<ConfirmedRequest, Error> {
471    if data.len() < 4 {
472        return Err(Error::buffer_too_short(4, data.len()));
473    }
474
475    let byte0 = data[0];
476    let segmented = byte0 & 0x08 != 0;
477    let more_follows = byte0 & 0x04 != 0;
478    let segmented_response_accepted = byte0 & 0x02 != 0;
479
480    let byte1 = data[1];
481    let max_segments = decode_max_segments((byte1 >> 4) & 0x07);
482    let max_apdu_length = decode_max_apdu(byte1 & 0x0F)?;
483
484    let invoke_id = data[2];
485    let mut offset = 3;
486
487    let (sequence_number, proposed_window_size) = if segmented {
488        if data.len() < 6 {
489            return Err(Error::decoding(
490                offset,
491                "segmented ConfirmedRequest too short for sequence/window fields",
492            ));
493        }
494        let seq = data[offset];
495        let win = data[offset + 1];
496        offset += 2;
497        (Some(seq), Some(win))
498    } else {
499        (None, None)
500    };
501
502    if offset >= data.len() {
503        return Err(Error::decoding(
504            offset,
505            "ConfirmedRequest missing service choice",
506        ));
507    }
508    let service_choice = ConfirmedServiceChoice::from_raw(data[offset]);
509    offset += 1;
510
511    let service_request = data.slice(offset..);
512
513    Ok(ConfirmedRequest {
514        segmented,
515        more_follows,
516        segmented_response_accepted,
517        max_segments,
518        max_apdu_length,
519        invoke_id,
520        sequence_number,
521        proposed_window_size,
522        service_choice,
523        service_request,
524    })
525}
526
527fn decode_unconfirmed_request(data: Bytes) -> Result<UnconfirmedRequest, Error> {
528    if data.len() < 2 {
529        return Err(Error::buffer_too_short(2, data.len()));
530    }
531
532    let service_choice = UnconfirmedServiceChoice::from_raw(data[1]);
533    let service_request = data.slice(2..);
534
535    Ok(UnconfirmedRequest {
536        service_choice,
537        service_request,
538    })
539}
540
541fn decode_simple_ack(data: Bytes) -> Result<SimpleAck, Error> {
542    if data.len() < 3 {
543        return Err(Error::buffer_too_short(3, data.len()));
544    }
545
546    Ok(SimpleAck {
547        invoke_id: data[1],
548        service_choice: ConfirmedServiceChoice::from_raw(data[2]),
549    })
550}
551
552fn decode_complex_ack(data: Bytes) -> Result<ComplexAck, Error> {
553    if data.len() < 3 {
554        return Err(Error::buffer_too_short(3, data.len()));
555    }
556
557    let byte0 = data[0];
558    let segmented = byte0 & 0x08 != 0;
559    let more_follows = byte0 & 0x04 != 0;
560
561    let invoke_id = data[1];
562    let mut offset = 2;
563
564    let (sequence_number, proposed_window_size) = if segmented {
565        if data.len() < 5 {
566            return Err(Error::decoding(
567                offset,
568                "segmented ComplexAck too short for sequence/window fields",
569            ));
570        }
571        let seq = data[offset];
572        let win = data[offset + 1];
573        offset += 2;
574        (Some(seq), Some(win))
575    } else {
576        (None, None)
577    };
578
579    if offset >= data.len() {
580        return Err(Error::decoding(offset, "ComplexAck missing service choice"));
581    }
582    let service_choice = ConfirmedServiceChoice::from_raw(data[offset]);
583    offset += 1;
584
585    let service_ack = data.slice(offset..);
586
587    Ok(ComplexAck {
588        segmented,
589        more_follows,
590        invoke_id,
591        sequence_number,
592        proposed_window_size,
593        service_choice,
594        service_ack,
595    })
596}
597
598fn decode_segment_ack(data: Bytes) -> Result<SegmentAck, Error> {
599    if data.len() < 4 {
600        return Err(Error::buffer_too_short(4, data.len()));
601    }
602
603    let byte0 = data[0];
604    let raw_window = data[3];
605    if raw_window == 0 || raw_window > 127 {
606        return Err(Error::decoding(
607            3,
608            format!("SegmentACK actual-window-size {raw_window} outside range 1..=127"),
609        ));
610    }
611    Ok(SegmentAck {
612        negative_ack: byte0 & 0x02 != 0,
613        sent_by_server: byte0 & 0x01 != 0,
614        invoke_id: data[1],
615        sequence_number: data[2],
616        actual_window_size: raw_window,
617    })
618}
619
620fn decode_error(data: Bytes) -> Result<ErrorPdu, Error> {
621    if data.len() < 5 {
622        return Err(Error::buffer_too_short(5, data.len()));
623    }
624
625    let invoke_id = data[1];
626    let service_choice = ConfirmedServiceChoice::from_raw(data[2]);
627
628    if service_choice == ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE {
629        if let Some((error_class, error_code)) = wpm_error::decode_formal_body(&data[3..])? {
630            return Ok(ErrorPdu {
631                invoke_id,
632                service_choice,
633                error_class,
634                error_code,
635                error_data: data.slice(3..),
636            });
637        }
638    }
639
640    let mut offset = 3;
641    let (tag, tag_end) = tags::decode_tag(&data, offset)?;
642    if tag.class != tags::TagClass::Application || tag.number != tags::app_tag::ENUMERATED {
643        return Err(Error::decoding(
644            offset,
645            "ErrorPDU error class: expected application-tagged enumerated",
646        ));
647    }
648    let class_end = tag_end
649        .checked_add(tag.length as usize)
650        .ok_or_else(|| Error::decoding(tag_end, "ErrorPDU error class length overflow"))?;
651    if class_end > data.len() {
652        return Err(Error::decoding(
653            tag_end,
654            "ErrorPDU truncated at error class",
655        ));
656    }
657    let error_class_raw = primitives::decode_unsigned(&data[tag_end..class_end])?;
658    let error_class_raw = u16::try_from(error_class_raw).map_err(|_| {
659        Error::decoding(
660            tag_end,
661            format!("ErrorPDU error class {error_class_raw} exceeds u16"),
662        )
663    })?;
664    offset = class_end;
665
666    let (tag, tag_end) = tags::decode_tag(&data, offset)?;
667    if tag.class != tags::TagClass::Application || tag.number != tags::app_tag::ENUMERATED {
668        return Err(Error::decoding(
669            offset,
670            "ErrorPDU error code: expected application-tagged enumerated",
671        ));
672    }
673    let code_end = tag_end
674        .checked_add(tag.length as usize)
675        .ok_or_else(|| Error::decoding(tag_end, "ErrorPDU error code length overflow"))?;
676    if code_end > data.len() {
677        return Err(Error::decoding(tag_end, "ErrorPDU truncated at error code"));
678    }
679    let error_code_raw = primitives::decode_unsigned(&data[tag_end..code_end])?;
680    let error_code_raw = u16::try_from(error_code_raw).map_err(|_| {
681        Error::decoding(
682            tag_end,
683            format!("ErrorPDU error code {error_code_raw} exceeds u16"),
684        )
685    })?;
686    offset = code_end;
687
688    let error_data = if offset < data.len() {
689        data.slice(offset..)
690    } else {
691        Bytes::new()
692    };
693
694    Ok(ErrorPdu {
695        invoke_id,
696        service_choice,
697        error_class: ErrorClass::from_raw(error_class_raw),
698        error_code: ErrorCode::from_raw(error_code_raw),
699        error_data,
700    })
701}
702
703fn decode_reject(data: Bytes) -> Result<RejectPdu, Error> {
704    if data.len() < 3 {
705        return Err(Error::buffer_too_short(3, data.len()));
706    }
707
708    Ok(RejectPdu {
709        invoke_id: data[1],
710        reject_reason: RejectReason::from_raw(data[2]),
711    })
712}
713
714fn decode_abort(data: Bytes) -> Result<AbortPdu, Error> {
715    if data.len() < 3 {
716        return Err(Error::buffer_too_short(3, data.len()));
717    }
718
719    let byte0 = data[0];
720    Ok(AbortPdu {
721        sent_by_server: byte0 & 0x01 != 0,
722        invoke_id: data[1],
723        abort_reason: AbortReason::from_raw(data[2]),
724    })
725}
726
727// ---------------------------------------------------------------------------
728// Tests
729// ---------------------------------------------------------------------------
730
731#[cfg(test)]
732mod tests;
733#[cfg(test)]
734mod wpm_error_tests;