Skip to main content

bacnet_services/
virtual_terminal_error.rs

1//! VTClose-Error (Clause 21): `[0]` Error, then an optional `[1]` frame of
2//! application Unsigned8 VT session identifiers.
3
4#[cfg(not(feature = "std"))]
5use alloc::vec::Vec;
6
7use crate::common::error_type::{
8    decode_error_pdu, decode_error_type, encode_error_type, error_pdu,
9};
10use crate::common::MAX_DECODED_ITEMS;
11use bacnet_encoding::apdu::ErrorPdu;
12use bacnet_encoding::constructed::tagged::{
13    decode_app_unsigned, decode_ctx_constructed, expect_end,
14};
15use bacnet_encoding::{primitives, tags};
16use bacnet_types::enums::{ConfirmedServiceChoice, ErrorClass, ErrorCode};
17use bacnet_types::error::{Error, ErrorDetail};
18use bytes::BytesMut;
19
20const WHAT: &str = "VTClose-Error";
21
22/// The Result(-) body of VT-Close (Clause 17.3.1.3).
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct VTCloseError {
25    /// Error class from the inner BACnetError.
26    pub error_class: ErrorClass,
27    /// Error code from the inner BACnetError.
28    pub error_code: ErrorCode,
29    /// The requester's local identifiers of the VT sessions that could not
30    /// be closed. The responder includes the list with
31    /// VT_SESSION_TERMINATION_FAILURE and omits it (`None`) otherwise.
32    pub list_of_vt_session_identifiers: Option<Vec<u8>>,
33}
34
35impl VTCloseError {
36    /// Encode the error, then the session list when present.
37    pub fn encode(&self, buf: &mut BytesMut) {
38        encode_error_type(buf, self.error_class, self.error_code);
39        if let Some(sessions) = &self.list_of_vt_session_identifiers {
40            tags::encode_opening_tag(buf, 1);
41            for &session in sessions {
42                primitives::encode_app_unsigned(buf, u64::from(session));
43            }
44            tags::encode_closing_tag(buf, 1);
45        }
46    }
47
48    /// Decode one complete body with no trailing content.
49    pub fn decode(data: &[u8]) -> Result<Self, Error> {
50        let ((error_class, error_code), mut offset) = decode_error_type(data, WHAT)?;
51        let mut list_of_vt_session_identifiers = None;
52        if offset < data.len() {
53            let (frame, end) = decode_ctx_constructed(data, offset, 1, WHAT)?;
54            let mut sessions = Vec::new();
55            let mut position = 0;
56            while position < frame.len() {
57                if sessions.len() >= MAX_DECODED_ITEMS {
58                    return Err(Error::overflow(position, "VTClose-Error too many sessions"));
59                }
60                let (session, next) =
61                    decode_app_unsigned::<u8>(frame, position, "VTClose-Error session-identifier")?;
62                sessions.push(session);
63                position = next;
64            }
65            list_of_vt_session_identifiers = Some(sessions);
66            offset = end;
67        }
68        expect_end(data, offset, offset, WHAT)?;
69        Ok(Self {
70            error_class,
71            error_code,
72            list_of_vt_session_identifiers,
73        })
74    }
75
76    /// Build the VT-Close Error PDU carrying this body.
77    pub fn to_error_pdu(&self, invoke_id: u8) -> ErrorPdu {
78        error_pdu(
79            invoke_id,
80            ConfirmedServiceChoice::VT_CLOSE,
81            (self.error_class, self.error_code),
82            |body| self.encode(body),
83        )
84    }
85}
86
87impl TryFrom<&ErrorPdu> for VTCloseError {
88    type Error = Error;
89
90    /// Decode and verify the body of a VT-Close Error PDU. A plain class/code
91    /// error, which older devices send, is refused.
92    fn try_from(pdu: &ErrorPdu) -> Result<Self, Self::Error> {
93        decode_error_pdu(
94            pdu,
95            &[ConfirmedServiceChoice::VT_CLOSE],
96            WHAT,
97            Self::decode,
98            |error| (error.error_class, error.error_code),
99        )
100    }
101}
102
103impl From<VTCloseError> for Error {
104    fn from(error: VTCloseError) -> Self {
105        Error::with_detail(
106            error.error_class,
107            error.error_code,
108            error
109                .list_of_vt_session_identifiers
110                .map(ErrorDetail::VtSessionIdentifiers),
111        )
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use bacnet_types::error::RemoteError;
118    use super::*;
119    use bytes::Bytes;
120
121    fn sample(sessions: Option<Vec<u8>>) -> VTCloseError {
122        VTCloseError {
123            error_class: ErrorClass::SERVICES,
124            error_code: ErrorCode::VT_SESSION_TERMINATION_FAILURE,
125            list_of_vt_session_identifiers: sessions,
126        }
127    }
128
129    fn encoded(error: &VTCloseError) -> Vec<u8> {
130        let mut body = BytesMut::new();
131        error.encode(&mut body);
132        body.to_vec()
133    }
134
135    #[test]
136    fn vt_close_error_round_trips_with_exact_shape() {
137        // [0] { 5, 39 }, then [1] around application Unsigned (0x21) values.
138        let head = [0x0E, 0x91, 5, 0x91, 39, 0x0F];
139        for (sessions, tail) in [
140            (None, &[][..]),
141            (Some(vec![1, 4]), &[0x1E, 0x21, 1, 0x21, 4, 0x1F]),
142            (Some(vec![255]), &[0x1E, 0x21, 255, 0x1F]),
143        ] {
144            let error = sample(sessions);
145            let wire = [&head[..], tail].concat();
146            assert_eq!(encoded(&error), wire);
147            assert_eq!(VTCloseError::decode(&wire).unwrap(), error);
148            let pdu = error.to_error_pdu(2);
149            assert_eq!(VTCloseError::try_from(&pdu).unwrap(), error);
150        }
151    }
152
153    #[test]
154    fn only_a_present_list_is_a_detail() {
155        assert!(matches!(
156            Error::from(sample(None)),
157            Error::Remote(RemoteError::Error { class: ErrorClass::SERVICES, code: ErrorCode::VT_SESSION_TERMINATION_FAILURE })
158        ));
159        assert!(matches!(
160            Error::from(sample(Some(vec![1, 4]))),
161            Error::Structured { class: ErrorClass::SERVICES, code: ErrorCode::VT_SESSION_TERMINATION_FAILURE, detail }
162                if *detail == ErrorDetail::VtSessionIdentifiers(vec![1, 4])
163        ));
164    }
165
166    #[test]
167    fn vt_close_error_refuses_malformed_plain_and_mismatched() {
168        let body = encoded(&sample(Some(vec![1, 4])));
169        for malformed in [
170            body[..body.len() - 1].to_vec(),
171            [&body[..], &[0x00][..]].concat(),
172            [&body[..6], &[0x21, 1][..]].concat(),
173            [&body[..6], &[0x1E, 0x22, 1, 0, 0x1F][..]].concat(),
174            [&body[..6], &[0x1E, 0x09, 1, 0x1F][..]].concat(),
175        ] {
176            assert!(
177                VTCloseError::decode(&malformed).is_err(),
178                "{malformed:02x?}"
179            );
180        }
181
182        let mut pdu = sample(None).to_error_pdu(1);
183        pdu.error_code = ErrorCode::UNKNOWN_VT_SESSION;
184        assert!(VTCloseError::try_from(&pdu).is_err());
185        let mut pdu = sample(None).to_error_pdu(1);
186        pdu.error_data = Bytes::new();
187        assert!(VTCloseError::try_from(&pdu).is_err());
188        let mut pdu = sample(None).to_error_pdu(1);
189        pdu.service_choice = ConfirmedServiceChoice::VT_OPEN;
190        assert!(VTCloseError::try_from(&pdu).is_err());
191    }
192}