Skip to main content

bacnet_services/wpm/
error.rs

1//! Formal WritePropertyMultiple Error service body (Clause 21).
2
3use crate::common::error_type::{decode_error_type, encode_error_type};
4use bacnet_encoding::apdu::ErrorPdu;
5use bacnet_encoding::constructed::tagged::{decode_ctx_constructed, expect_end};
6use bacnet_encoding::constructed::{
7    decode_object_property_reference, encode_object_property_reference,
8};
9use bacnet_encoding::tags;
10use bacnet_types::constructed::BACnetObjectPropertyReference;
11use bacnet_types::enums::{ConfirmedServiceChoice, ErrorClass, ErrorCode};
12use bacnet_types::error::{Error, ErrorDetail};
13use bytes::BytesMut;
14
15/// Formal service-16 Result(-) body with the first failed write coordinate.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct WritePropertyMultipleError {
18    /// Error class from the inner BACnetError.
19    pub error_class: ErrorClass,
20    /// Error code from the inner BACnetError.
21    pub error_code: ErrorCode,
22    /// Complete coordinate of the first failed write attempt.
23    pub first_failed_write_attempt: BACnetObjectPropertyReference,
24}
25
26impl WritePropertyMultipleError {
27    /// Encode `[0] Error` followed by `[1] BACnetObjectPropertyReference`.
28    pub fn encode(&self, buf: &mut BytesMut) {
29        encode_error_type(buf, self.error_class, self.error_code);
30        tags::encode_opening_tag(buf, 1);
31        encode_object_property_reference(buf, &self.first_failed_write_attempt);
32        tags::encode_closing_tag(buf, 1);
33    }
34
35    /// Decode one complete formal service body with no trailing content.
36    pub fn decode(data: &[u8]) -> Result<Self, Error> {
37        let ((error_class, error_code), offset) = decode_error_type(data, "WPM Error")?;
38        let (reference_body, end) = decode_ctx_constructed(data, offset, 1, "WPM Error")?;
39        expect_end(data, end, end, "WPM Error")?;
40        let first_failed_write_attempt = decode_object_property_reference(reference_body)?;
41        Ok(Self {
42            error_class,
43            error_code,
44            first_failed_write_attempt,
45        })
46    }
47
48    /// Build an existing low-level ErrorPdu while retaining the formal body.
49    pub fn to_error_pdu(&self, invoke_id: u8) -> ErrorPdu {
50        let mut body = BytesMut::new();
51        self.encode(&mut body);
52        ErrorPdu {
53            invoke_id,
54            service_choice: ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE,
55            error_class: self.error_class,
56            error_code: self.error_code,
57            error_data: body.freeze(),
58        }
59    }
60
61    /// Decode and verify a formal WPM ErrorPdu.
62    pub fn from_error_pdu(pdu: &ErrorPdu) -> Result<Self, Error> {
63        Self::try_from(pdu)
64    }
65}
66
67impl TryFrom<&ErrorPdu> for WritePropertyMultipleError {
68    type Error = Error;
69
70    fn try_from(pdu: &ErrorPdu) -> Result<Self, Self::Error> {
71        if pdu.service_choice != ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE {
72            return Err(Error::decoding(
73                0,
74                "ErrorPdu is not a WritePropertyMultiple result",
75            ));
76        }
77        let decoded = Self::decode(&pdu.error_data)?;
78        if decoded.error_class != pdu.error_class || decoded.error_code != pdu.error_code {
79            return Err(Error::decoding(
80                0,
81                "formal WPM Error body disagrees with ErrorPdu class/code",
82            ));
83        }
84        Ok(decoded)
85    }
86}
87
88impl From<WritePropertyMultipleError> for Error {
89    fn from(error: WritePropertyMultipleError) -> Self {
90        Error::with_detail(
91            error.error_class,
92            error.error_code,
93            Some(ErrorDetail::FirstFailedWriteAttempt(
94                error.first_failed_write_attempt,
95            )),
96        )
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use bacnet_types::enums::{ObjectType, PropertyIdentifier};
104    use bacnet_types::primitives::ObjectIdentifier;
105    use bytes::Bytes;
106
107    fn sample(index: Option<u32>) -> WritePropertyMultipleError {
108        WritePropertyMultipleError {
109            error_class: ErrorClass::PROPERTY,
110            error_code: ErrorCode::WRITE_ACCESS_DENIED,
111            first_failed_write_attempt: BACnetObjectPropertyReference {
112                object_identifier: ObjectIdentifier::new(ObjectType::EVENT_ENROLLMENT, 7).unwrap(),
113                property_identifier: PropertyIdentifier::ACKED_TRANSITIONS.to_raw(),
114                property_array_index: index,
115            },
116        }
117    }
118
119    #[test]
120    fn indexed_and_unindexed_round_trip_with_exact_outer_shape() {
121        for index in [None, Some(3)] {
122            let error = sample(index);
123            let mut body = BytesMut::new();
124            error.encode(&mut body);
125            assert_eq!(
126                &body[..7],
127                &[
128                    0x0e,
129                    0x91,
130                    ErrorClass::PROPERTY.to_raw() as u8,
131                    0x91,
132                    ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u8,
133                    0x0f,
134                    0x1e,
135                ]
136            );
137            assert_eq!(body.last(), Some(&0x1f));
138            assert!(!body.iter().any(|byte| matches!(byte, 0x4e | 0x4f)));
139            assert_eq!(WritePropertyMultipleError::decode(&body).unwrap(), error);
140        }
141    }
142
143    #[test]
144    fn error_pdu_conversion_retains_complete_formal_body() {
145        let error = sample(None);
146        let pdu = error.to_error_pdu(9);
147        assert_eq!(pdu.invoke_id, 9);
148        assert_eq!(pdu.error_data[0], 0x0e);
149        assert_eq!(WritePropertyMultipleError::try_from(&pdu).unwrap(), error);
150        assert!(matches!(
151            Error::from(error.clone()),
152            Error::Structured { class: ErrorClass::PROPERTY, code: ErrorCode::WRITE_ACCESS_DENIED, detail }
153                if *detail == ErrorDetail::FirstFailedWriteAttempt(
154                    error.first_failed_write_attempt.clone()
155                )
156        ));
157    }
158
159    #[test]
160    fn strict_decode_rejects_missing_duplicate_wrong_and_trailing_fields() {
161        let error = sample(None);
162        let mut body = BytesMut::new();
163        error.encode(&mut body);
164
165        for malformed in [
166            body[..body.len() - 1].to_vec(),
167            body[5..].to_vec(),
168            {
169                let mut bytes = body.to_vec();
170                bytes[6] = 0x2e;
171                bytes
172            },
173            {
174                let mut bytes = body.to_vec();
175                bytes.extend_from_slice(&[0x1e, 0x1f]);
176                bytes
177            },
178            {
179                let mut bytes = body.to_vec();
180                bytes.splice(3..3, [0x91, 0x02]);
181                bytes
182            },
183            {
184                let mut bytes = body.to_vec();
185                let property = bytes
186                    .windows(2)
187                    .rposition(|window| window == [0x19, 0x00])
188                    .unwrap();
189                bytes.drain(property..property + 2);
190                bytes
191            },
192            {
193                let mut bytes = body.to_vec();
194                let property = bytes
195                    .windows(2)
196                    .rposition(|window| window == [0x19, 0x00])
197                    .unwrap();
198                bytes.splice(property..property, [0x19, 0x00]);
199                bytes
200            },
201            {
202                let mut bytes = body.to_vec();
203                let property = bytes
204                    .windows(2)
205                    .rposition(|window| window == [0x19, 0x00])
206                    .unwrap();
207                bytes[property] = 0x29;
208                bytes
209            },
210        ] {
211            assert!(WritePropertyMultipleError::decode(&malformed).is_err());
212        }
213    }
214
215    #[test]
216    fn conversion_rejects_mismatched_projection() {
217        let error = sample(None);
218        let mut pdu = error.to_error_pdu(1);
219        pdu.error_code = ErrorCode::UNKNOWN_PROPERTY;
220        assert!(WritePropertyMultipleError::from_error_pdu(&pdu).is_err());
221
222        pdu.error_data = Bytes::new();
223        assert!(WritePropertyMultipleError::from_error_pdu(&pdu).is_err());
224    }
225}