1use bacnet_encoding::primitives;
8use bacnet_encoding::tags;
9use bacnet_types::error::Error;
10use bacnet_types::primitives::{BACnetTimeStamp, ObjectIdentifier};
11use bytes::BytesMut;
12
13use crate::common::PropertyReference;
14
15#[derive(Debug, Clone, PartialEq)]
26pub struct AuditNotificationRequest {
27 pub source_timestamp: BACnetTimeStamp,
29 pub target_timestamp: Option<BACnetTimeStamp>,
31 pub source_device: Vec<u8>,
33 pub source_object: Option<ObjectIdentifier>,
35 pub operation: u32,
37 pub source_comment: Option<String>,
39 pub target_comment: Option<String>,
41 pub invoke_id: Option<u8>,
43 pub source_user_info: Option<Vec<u8>>,
45 pub target_device: Vec<u8>,
47 pub target_object: Option<ObjectIdentifier>,
49 pub target_property: Option<PropertyReference>,
51 pub target_priority: Option<u8>,
53 pub target_value: Option<Vec<u8>>,
55 pub current_value: Option<Vec<u8>>,
57 pub result: Option<Vec<u8>>,
59}
60
61impl AuditNotificationRequest {
62 pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
63 primitives::encode_timestamp(buf, 0, &self.source_timestamp);
65 if let Some(ref ts) = self.target_timestamp {
67 primitives::encode_timestamp(buf, 1, ts);
68 }
69 tags::encode_opening_tag(buf, 2);
71 buf.extend_from_slice(&self.source_device);
72 tags::encode_closing_tag(buf, 2);
73 if let Some(ref oid) = self.source_object {
75 primitives::encode_ctx_object_id(buf, 3, oid);
76 }
77 primitives::encode_ctx_enumerated(buf, 4, self.operation);
79 if let Some(ref s) = self.source_comment {
81 primitives::encode_ctx_character_string(buf, 5, s)?;
82 }
83 if let Some(ref s) = self.target_comment {
85 primitives::encode_ctx_character_string(buf, 6, s)?;
86 }
87 if let Some(id) = self.invoke_id {
89 primitives::encode_ctx_unsigned(buf, 7, id as u64);
90 }
91 if let Some(ref raw) = self.source_user_info {
93 tags::encode_opening_tag(buf, 8);
94 buf.extend_from_slice(raw);
95 tags::encode_closing_tag(buf, 8);
96 }
97 tags::encode_opening_tag(buf, 9);
99 buf.extend_from_slice(&self.target_device);
100 tags::encode_closing_tag(buf, 9);
101 if let Some(ref oid) = self.target_object {
103 primitives::encode_ctx_object_id(buf, 10, oid);
104 }
105 if let Some(ref pr) = self.target_property {
107 tags::encode_opening_tag(buf, 11);
108 pr.encode(buf);
109 tags::encode_closing_tag(buf, 11);
110 }
111 if let Some(prio) = self.target_priority {
113 primitives::encode_ctx_unsigned(buf, 12, prio as u64);
114 }
115 if let Some(ref raw) = self.target_value {
117 tags::encode_opening_tag(buf, 13);
118 buf.extend_from_slice(raw);
119 tags::encode_closing_tag(buf, 13);
120 }
121 if let Some(ref raw) = self.current_value {
123 tags::encode_opening_tag(buf, 14);
124 buf.extend_from_slice(raw);
125 tags::encode_closing_tag(buf, 14);
126 }
127 if let Some(ref raw) = self.result {
129 tags::encode_opening_tag(buf, 15);
130 buf.extend_from_slice(raw);
131 tags::encode_closing_tag(buf, 15);
132 }
133 Ok(())
134 }
135
136 pub fn decode(data: &[u8]) -> Result<Self, Error> {
137 let mut offset = 0;
138
139 let (source_timestamp, new_off) = primitives::decode_timestamp(data, offset, 0)?;
141 offset = new_off;
142
143 let mut target_timestamp = None;
145 if offset < data.len() {
146 let (peek, _) = tags::decode_tag(data, offset)?;
147 if peek.is_opening_tag(1) {
148 let (ts, new_off) = primitives::decode_timestamp(data, offset, 1)?;
149 target_timestamp = Some(ts);
150 offset = new_off;
151 }
152 }
153
154 let (tag, tag_end) = tags::decode_tag(data, offset)?;
156 if !tag.is_opening_tag(2) {
157 return Err(Error::decoding(
158 offset,
159 "AuditNotification expected opening tag 2 for source-device",
160 ));
161 }
162 let (raw, new_off) = tags::extract_context_value(data, tag_end, 2)?;
163 let source_device = raw.to_vec();
164 offset = new_off;
165
166 let mut source_object = None;
168 if offset < data.len() {
169 let (opt, new_off) = tags::decode_optional_context(data, offset, 3)?;
170 if let Some(content) = opt {
171 source_object = Some(ObjectIdentifier::decode(content)?);
172 offset = new_off;
173 }
174 }
175
176 let (tag, pos) = tags::decode_tag(data, offset)?;
178 let end = pos + tag.length as usize;
179 if end > data.len() {
180 return Err(Error::decoding(
181 pos,
182 "AuditNotification truncated at operation",
183 ));
184 }
185 let operation = primitives::decode_unsigned(&data[pos..end])? as u32;
186 offset = end;
187
188 let mut source_comment = None;
190 if offset < data.len() {
191 let (opt, new_off) = tags::decode_optional_context(data, offset, 5)?;
192 if let Some(content) = opt {
193 source_comment = Some(primitives::decode_character_string(content)?);
194 offset = new_off;
195 }
196 }
197
198 let mut target_comment = None;
200 if offset < data.len() {
201 let (opt, new_off) = tags::decode_optional_context(data, offset, 6)?;
202 if let Some(content) = opt {
203 target_comment = Some(primitives::decode_character_string(content)?);
204 offset = new_off;
205 }
206 }
207
208 let mut invoke_id = None;
210 if offset < data.len() {
211 let (opt, new_off) = tags::decode_optional_context(data, offset, 7)?;
212 if let Some(content) = opt {
213 invoke_id = Some(primitives::decode_unsigned(content)? as u8);
214 offset = new_off;
215 }
216 }
217
218 let mut source_user_info = None;
220 if offset < data.len() {
221 let (peek, _) = tags::decode_tag(data, offset)?;
222 if peek.is_opening_tag(8) {
223 let (_, inner_start) = tags::decode_tag(data, offset)?;
224 let (raw, new_off) = tags::extract_context_value(data, inner_start, 8)?;
225 source_user_info = Some(raw.to_vec());
226 offset = new_off;
227 }
228 }
229
230 let (tag, tag_end) = tags::decode_tag(data, offset)?;
232 if !tag.is_opening_tag(9) {
233 return Err(Error::decoding(
234 offset,
235 "AuditNotification expected opening tag 9 for target-device",
236 ));
237 }
238 let (raw, new_off) = tags::extract_context_value(data, tag_end, 9)?;
239 let target_device = raw.to_vec();
240 offset = new_off;
241
242 let mut target_object = None;
244 if offset < data.len() {
245 let (opt, new_off) = tags::decode_optional_context(data, offset, 10)?;
246 if let Some(content) = opt {
247 target_object = Some(ObjectIdentifier::decode(content)?);
248 offset = new_off;
249 }
250 }
251
252 let mut target_property = None;
254 if offset < data.len() {
255 let (peek, _) = tags::decode_tag(data, offset)?;
256 if peek.is_opening_tag(11) {
257 let (_, inner_start) = tags::decode_tag(data, offset)?;
258 let (pr, pr_end) = PropertyReference::decode(data, inner_start)?;
259 target_property = Some(pr);
260 let (_tag, tag_end) = tags::decode_tag(data, pr_end)?;
262 offset = tag_end;
263 }
264 }
265
266 let mut target_priority = None;
268 if offset < data.len() {
269 let (opt, new_off) = tags::decode_optional_context(data, offset, 12)?;
270 if let Some(content) = opt {
271 target_priority = Some(primitives::decode_unsigned(content)? as u8);
272 offset = new_off;
273 }
274 }
275
276 let mut target_value = None;
278 if offset < data.len() {
279 let (peek, _) = tags::decode_tag(data, offset)?;
280 if peek.is_opening_tag(13) {
281 let (_, inner_start) = tags::decode_tag(data, offset)?;
282 let (raw, new_off) = tags::extract_context_value(data, inner_start, 13)?;
283 target_value = Some(raw.to_vec());
284 offset = new_off;
285 }
286 }
287
288 let mut current_value = None;
290 if offset < data.len() {
291 let (peek, _) = tags::decode_tag(data, offset)?;
292 if peek.is_opening_tag(14) {
293 let (_, inner_start) = tags::decode_tag(data, offset)?;
294 let (raw, new_off) = tags::extract_context_value(data, inner_start, 14)?;
295 current_value = Some(raw.to_vec());
296 offset = new_off;
297 }
298 }
299
300 let mut result = None;
302 if offset < data.len() {
303 let (peek, _) = tags::decode_tag(data, offset)?;
304 if peek.is_opening_tag(15) {
305 let (_, inner_start) = tags::decode_tag(data, offset)?;
306 let (raw, new_off) = tags::extract_context_value(data, inner_start, 15)?;
307 result = Some(raw.to_vec());
308 offset = new_off;
309 }
310 }
311 let _ = offset;
312
313 Ok(Self {
314 source_timestamp,
315 target_timestamp,
316 source_device,
317 source_object,
318 operation,
319 source_comment,
320 target_comment,
321 invoke_id,
322 source_user_info,
323 target_device,
324 target_object,
325 target_property,
326 target_priority,
327 target_value,
328 current_value,
329 result,
330 })
331 }
332}
333
334#[derive(Debug, Clone, PartialEq, Eq)]
340pub struct AuditLogQueryRequest {
341 pub acknowledgment_filter: u32,
343 pub query_options_raw: Vec<u8>,
345}
346
347impl AuditLogQueryRequest {
348 pub fn encode(&self, buf: &mut BytesMut) {
349 primitives::encode_ctx_enumerated(buf, 0, self.acknowledgment_filter);
351 buf.extend_from_slice(&self.query_options_raw);
353 }
354
355 pub fn decode(data: &[u8]) -> Result<Self, Error> {
356 let mut offset = 0;
357
358 let (tag, pos) = tags::decode_tag(data, offset)?;
360 let end = pos + tag.length as usize;
361 if end > data.len() {
362 return Err(Error::decoding(
363 pos,
364 "AuditLogQuery truncated at acknowledgment-filter",
365 ));
366 }
367 let acknowledgment_filter = primitives::decode_unsigned(&data[pos..end])? as u32;
368 offset = end;
369
370 let query_options_raw = data[offset..].to_vec();
372
373 Ok(Self {
374 acknowledgment_filter,
375 query_options_raw,
376 })
377 }
378}
379
380#[cfg(test)]
385mod tests {
386 use super::*;
387 use bacnet_types::enums::{ObjectType, PropertyIdentifier};
388 use bacnet_types::primitives::Time;
389
390 #[test]
391 fn audit_notification_round_trip() {
392 let req = AuditNotificationRequest {
393 source_timestamp: BACnetTimeStamp::SequenceNumber(100),
394 target_timestamp: None,
395 source_device: vec![0x09, 0x01], source_object: Some(ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap()),
397 operation: 3,
398 source_comment: Some("test audit".to_string()),
399 target_comment: None,
400 invoke_id: Some(5),
401 source_user_info: None,
402 target_device: vec![0x09, 0x02], target_object: Some(ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap()),
404 target_property: Some(PropertyReference {
405 property_identifier: PropertyIdentifier::PRESENT_VALUE,
406 property_array_index: None,
407 }),
408 target_priority: Some(8),
409 target_value: Some(vec![0x44, 0x42, 0x90, 0x00, 0x00]),
410 current_value: Some(vec![0x44, 0x00, 0x00, 0x00, 0x00]),
411 result: None,
412 };
413 let mut buf = BytesMut::new();
414 req.encode(&mut buf).unwrap();
415 let decoded = AuditNotificationRequest::decode(&buf).unwrap();
416 assert_eq!(req, decoded);
417 }
418
419 #[test]
420 fn audit_notification_minimal() {
421 let req = AuditNotificationRequest {
422 source_timestamp: BACnetTimeStamp::Time(Time {
423 hour: 10,
424 minute: 0,
425 second: 0,
426 hundredths: 0,
427 }),
428 target_timestamp: None,
429 source_device: vec![0x09, 0x01],
430 source_object: None,
431 operation: 0,
432 source_comment: None,
433 target_comment: None,
434 invoke_id: None,
435 source_user_info: None,
436 target_device: vec![0x09, 0x02],
437 target_object: None,
438 target_property: None,
439 target_priority: None,
440 target_value: None,
441 current_value: None,
442 result: None,
443 };
444 let mut buf = BytesMut::new();
445 req.encode(&mut buf).unwrap();
446 let decoded = AuditNotificationRequest::decode(&buf).unwrap();
447 assert_eq!(req, decoded);
448 }
449
450 #[test]
451 fn audit_notification_empty_input() {
452 assert!(AuditNotificationRequest::decode(&[]).is_err());
453 }
454
455 #[test]
456 fn audit_log_query_round_trip() {
457 let req = AuditLogQueryRequest {
458 acknowledgment_filter: 1,
459 query_options_raw: vec![0x19, 0x05, 0x29, 0x0A],
460 };
461 let mut buf = BytesMut::new();
462 req.encode(&mut buf);
463 let decoded = AuditLogQueryRequest::decode(&buf).unwrap();
464 assert_eq!(req, decoded);
465 }
466
467 #[test]
468 fn audit_log_query_no_options() {
469 let req = AuditLogQueryRequest {
470 acknowledgment_filter: 0,
471 query_options_raw: vec![],
472 };
473 let mut buf = BytesMut::new();
474 req.encode(&mut buf);
475 let decoded = AuditLogQueryRequest::decode(&buf).unwrap();
476 assert_eq!(req, decoded);
477 }
478
479 #[test]
480 fn audit_log_query_empty_input() {
481 assert!(AuditLogQueryRequest::decode(&[]).is_err());
482 }
483}