bacnet_services/
text_message.rs1use bacnet_encoding::primitives;
5use bacnet_encoding::tags;
6use bacnet_types::enums::MessagePriority;
7use bacnet_types::error::Error;
8use bacnet_types::primitives::ObjectIdentifier;
9use bytes::BytesMut;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum MessageClass {
18 Numeric(u32),
19 Text(String),
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct TextMessageRequest {
30 pub source_device: ObjectIdentifier,
31 pub message_class: Option<MessageClass>,
32 pub message_priority: MessagePriority,
33 pub message: String,
34}
35
36impl TextMessageRequest {
37 pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
38 primitives::encode_ctx_object_id(buf, 0, &self.source_device);
40 if let Some(ref mc) = self.message_class {
42 match mc {
43 MessageClass::Numeric(n) => {
44 primitives::encode_ctx_unsigned(buf, 1, *n as u64);
45 }
46 MessageClass::Text(s) => {
47 primitives::encode_ctx_character_string(buf, 2, s)?;
48 }
49 }
50 }
51 primitives::encode_ctx_enumerated(buf, 3, self.message_priority.to_raw());
53 primitives::encode_ctx_character_string(buf, 4, &self.message)?;
55 Ok(())
56 }
57
58 pub fn decode(data: &[u8]) -> Result<Self, Error> {
59 let mut offset = 0;
60
61 let (tag, pos) = tags::decode_tag(data, offset)?;
63 let end = pos + tag.length as usize;
64 if end > data.len() {
65 return Err(Error::decoding(
66 pos,
67 "TextMessage truncated at sourceDevice",
68 ));
69 }
70 let source_device = ObjectIdentifier::decode(&data[pos..end])?;
71 offset = end;
72
73 let mut message_class = None;
75 if offset < data.len() {
76 let (tag, pos) = tags::decode_tag(data, offset)?;
77 if tag.is_context(1) {
78 let end = pos + tag.length as usize;
79 if end > data.len() {
80 return Err(Error::decoding(
81 pos,
82 "TextMessage truncated at messageClass numeric",
83 ));
84 }
85 message_class = Some(MessageClass::Numeric(primitives::decode_unsigned(
86 &data[pos..end],
87 )? as u32));
88 offset = end;
89 } else if tag.is_context(2) {
90 let end = pos + tag.length as usize;
91 if end > data.len() {
92 return Err(Error::decoding(
93 pos,
94 "TextMessage truncated at messageClass text",
95 ));
96 }
97 let s = primitives::decode_character_string(&data[pos..end])?;
98 message_class = Some(MessageClass::Text(s));
99 offset = end;
100 }
101 }
103
104 let (tag, pos) = tags::decode_tag(data, offset)?;
106 let end = pos + tag.length as usize;
107 if end > data.len() {
108 return Err(Error::decoding(
109 pos,
110 "TextMessage truncated at messagePriority",
111 ));
112 }
113 let message_priority =
114 MessagePriority::from_raw(primitives::decode_unsigned(&data[pos..end])? as u32);
115 offset = end;
116
117 let (tag, pos) = tags::decode_tag(data, offset)?;
119 let end = pos + tag.length as usize;
120 if end > data.len() {
121 return Err(Error::decoding(pos, "TextMessage truncated at message"));
122 }
123 let message = primitives::decode_character_string(&data[pos..end])?;
124
125 Ok(Self {
126 source_device,
127 message_class,
128 message_priority,
129 message,
130 })
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use bacnet_types::enums::ObjectType;
138
139 #[test]
140 fn request_numeric_class_round_trip() {
141 let req = TextMessageRequest {
142 source_device: ObjectIdentifier::new(ObjectType::DEVICE, 100).unwrap(),
143 message_class: Some(MessageClass::Numeric(5)),
144 message_priority: MessagePriority::URGENT,
145 message: "Fire alarm".into(),
146 };
147 let mut buf = BytesMut::new();
148 req.encode(&mut buf).unwrap();
149 let decoded = TextMessageRequest::decode(&buf).unwrap();
150 assert_eq!(req, decoded);
151 }
152
153 #[test]
154 fn request_text_class_round_trip() {
155 let req = TextMessageRequest {
156 source_device: ObjectIdentifier::new(ObjectType::DEVICE, 200).unwrap(),
157 message_class: Some(MessageClass::Text("maintenance".into())),
158 message_priority: MessagePriority::NORMAL,
159 message: "Scheduled shutdown".into(),
160 };
161 let mut buf = BytesMut::new();
162 req.encode(&mut buf).unwrap();
163 let decoded = TextMessageRequest::decode(&buf).unwrap();
164 assert_eq!(req, decoded);
165 }
166
167 #[test]
168 fn request_no_class_round_trip() {
169 let req = TextMessageRequest {
170 source_device: ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap(),
171 message_class: None,
172 message_priority: MessagePriority::NORMAL,
173 message: "Hello".into(),
174 };
175 let mut buf = BytesMut::new();
176 req.encode(&mut buf).unwrap();
177 let decoded = TextMessageRequest::decode(&buf).unwrap();
178 assert_eq!(req, decoded);
179 }
180
181 #[test]
186 fn test_decode_empty_input() {
187 assert!(TextMessageRequest::decode(&[]).is_err());
188 }
189
190 #[test]
191 fn test_decode_truncated_1_byte() {
192 let req = TextMessageRequest {
193 source_device: ObjectIdentifier::new(ObjectType::DEVICE, 100).unwrap(),
194 message_class: None,
195 message_priority: MessagePriority::NORMAL,
196 message: "Test".into(),
197 };
198 let mut buf = BytesMut::new();
199 req.encode(&mut buf).unwrap();
200 assert!(TextMessageRequest::decode(&buf[..1]).is_err());
201 }
202
203 #[test]
204 fn test_decode_truncated_half() {
205 let req = TextMessageRequest {
206 source_device: ObjectIdentifier::new(ObjectType::DEVICE, 100).unwrap(),
207 message_class: Some(MessageClass::Text("info".into())),
208 message_priority: MessagePriority::URGENT,
209 message: "Emergency".into(),
210 };
211 let mut buf = BytesMut::new();
212 req.encode(&mut buf).unwrap();
213 let half = buf.len() / 2;
214 assert!(TextMessageRequest::decode(&buf[..half]).is_err());
215 }
216
217 #[test]
218 fn test_decode_invalid_tag() {
219 assert!(TextMessageRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
220 }
221}