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)]
25pub struct AuditNotificationRequest {
26 pub source_timestamp: BACnetTimeStamp,
28 pub target_timestamp: Option<BACnetTimeStamp>,
30 pub source_device: Vec<u8>,
32 pub source_object: Option<ObjectIdentifier>,
34 pub operation: u32,
36 pub source_comment: Option<String>,
38 pub target_comment: Option<String>,
40 pub invoke_id: Option<u8>,
42 pub source_user_info: Option<Vec<u8>>,
44 pub target_device: Vec<u8>,
46 pub target_object: Option<ObjectIdentifier>,
48 pub target_property: Option<PropertyReference>,
50 pub target_priority: Option<u8>,
52 pub target_value: Option<Vec<u8>>,
54 pub current_value: Option<Vec<u8>>,
56 pub result: Option<Vec<u8>>,
58}
59
60impl AuditNotificationRequest {
61 pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
62 primitives::encode_timestamp(buf, 0, &self.source_timestamp);
64 if let Some(ref ts) = self.target_timestamp {
66 primitives::encode_timestamp(buf, 1, ts);
67 }
68 tags::encode_opening_tag(buf, 2);
70 buf.extend_from_slice(&self.source_device);
71 tags::encode_closing_tag(buf, 2);
72 if let Some(ref oid) = self.source_object {
74 primitives::encode_ctx_object_id(buf, 3, oid);
75 }
76 primitives::encode_ctx_enumerated(buf, 4, self.operation);
78 if let Some(ref s) = self.source_comment {
80 primitives::encode_ctx_character_string(buf, 5, s)?;
81 }
82 if let Some(ref s) = self.target_comment {
84 primitives::encode_ctx_character_string(buf, 6, s)?;
85 }
86 if let Some(id) = self.invoke_id {
88 primitives::encode_ctx_unsigned(buf, 7, id as u64);
89 }
90 if let Some(ref raw) = self.source_user_info {
92 tags::encode_opening_tag(buf, 8);
93 buf.extend_from_slice(raw);
94 tags::encode_closing_tag(buf, 8);
95 }
96 tags::encode_opening_tag(buf, 9);
98 buf.extend_from_slice(&self.target_device);
99 tags::encode_closing_tag(buf, 9);
100 if let Some(ref oid) = self.target_object {
102 primitives::encode_ctx_object_id(buf, 10, oid);
103 }
104 if let Some(ref pr) = self.target_property {
106 tags::encode_opening_tag(buf, 11);
107 pr.encode(buf);
108 tags::encode_closing_tag(buf, 11);
109 }
110 if let Some(prio) = self.target_priority {
112 primitives::encode_ctx_unsigned(buf, 12, prio as u64);
113 }
114 if let Some(ref raw) = self.target_value {
116 tags::encode_opening_tag(buf, 13);
117 buf.extend_from_slice(raw);
118 tags::encode_closing_tag(buf, 13);
119 }
120 if let Some(ref raw) = self.current_value {
122 tags::encode_opening_tag(buf, 14);
123 buf.extend_from_slice(raw);
124 tags::encode_closing_tag(buf, 14);
125 }
126 if let Some(ref raw) = self.result {
128 tags::encode_opening_tag(buf, 15);
129 buf.extend_from_slice(raw);
130 tags::encode_closing_tag(buf, 15);
131 }
132 Ok(())
133 }
134
135 pub fn decode(data: &[u8]) -> Result<Self, Error> {
136 let mut offset = 0;
137
138 let (source_timestamp, new_off) = primitives::decode_timestamp(data, offset, 0)?;
140 offset = new_off;
141
142 let mut target_timestamp = None;
144 if offset < data.len() {
145 let (peek, _) = tags::decode_tag(data, offset)?;
146 if peek.is_opening_tag(1) {
147 let (ts, new_off) = primitives::decode_timestamp(data, offset, 1)?;
148 target_timestamp = Some(ts);
149 offset = new_off;
150 }
151 }
152
153 let (tag, tag_end) = tags::decode_tag(data, offset)?;
155 if !tag.is_opening_tag(2) {
156 return Err(Error::decoding(
157 offset,
158 "AuditNotification expected opening tag 2 for source-device",
159 ));
160 }
161 let (raw, new_off) = tags::extract_context_value(data, tag_end, 2)?;
162 let source_device = raw.to_vec();
163 offset = new_off;
164
165 let mut source_object = None;
167 if offset < data.len() {
168 let (opt, new_off) = tags::decode_optional_context(data, offset, 3)?;
169 if let Some(content) = opt {
170 source_object = Some(ObjectIdentifier::decode(content)?);
171 offset = new_off;
172 }
173 }
174
175 let (tag, pos) = tags::decode_tag(data, offset)?;
177 let end = pos + tag.length as usize;
178 if end > data.len() {
179 return Err(Error::decoding(
180 pos,
181 "AuditNotification truncated at operation",
182 ));
183 }
184 let operation = primitives::decode_unsigned(&data[pos..end])? as u32;
185 offset = end;
186
187 let mut source_comment = None;
189 if offset < data.len() {
190 let (opt, new_off) = tags::decode_optional_context(data, offset, 5)?;
191 if let Some(content) = opt {
192 source_comment = Some(primitives::decode_character_string(content)?);
193 offset = new_off;
194 }
195 }
196
197 let mut target_comment = None;
199 if offset < data.len() {
200 let (opt, new_off) = tags::decode_optional_context(data, offset, 6)?;
201 if let Some(content) = opt {
202 target_comment = Some(primitives::decode_character_string(content)?);
203 offset = new_off;
204 }
205 }
206
207 let mut invoke_id = None;
209 if offset < data.len() {
210 let (opt, new_off) = tags::decode_optional_context(data, offset, 7)?;
211 if let Some(content) = opt {
212 invoke_id = Some(primitives::decode_unsigned(content)? as u8);
213 offset = new_off;
214 }
215 }
216
217 let mut source_user_info = None;
219 if offset < data.len() {
220 let (peek, _) = tags::decode_tag(data, offset)?;
221 if peek.is_opening_tag(8) {
222 let (_, inner_start) = tags::decode_tag(data, offset)?;
223 let (raw, new_off) = tags::extract_context_value(data, inner_start, 8)?;
224 source_user_info = Some(raw.to_vec());
225 offset = new_off;
226 }
227 }
228
229 let (tag, tag_end) = tags::decode_tag(data, offset)?;
231 if !tag.is_opening_tag(9) {
232 return Err(Error::decoding(
233 offset,
234 "AuditNotification expected opening tag 9 for target-device",
235 ));
236 }
237 let (raw, new_off) = tags::extract_context_value(data, tag_end, 9)?;
238 let target_device = raw.to_vec();
239 offset = new_off;
240
241 let mut target_object = None;
243 if offset < data.len() {
244 let (opt, new_off) = tags::decode_optional_context(data, offset, 10)?;
245 if let Some(content) = opt {
246 target_object = Some(ObjectIdentifier::decode(content)?);
247 offset = new_off;
248 }
249 }
250
251 let mut target_property = None;
253 if offset < data.len() {
254 let (peek, _) = tags::decode_tag(data, offset)?;
255 if peek.is_opening_tag(11) {
256 let (_, inner_start) = tags::decode_tag(data, offset)?;
257 let (pr, pr_end) = PropertyReference::decode(data, inner_start)?;
258 target_property = Some(pr);
259 let (_tag, tag_end) = tags::decode_tag(data, pr_end)?;
260 offset = tag_end;
261 }
262 }
263
264 let mut target_priority = None;
266 if offset < data.len() {
267 let (opt, new_off) = tags::decode_optional_context(data, offset, 12)?;
268 if let Some(content) = opt {
269 target_priority = Some(primitives::decode_unsigned(content)? as u8);
270 offset = new_off;
271 }
272 }
273
274 let mut target_value = None;
276 if offset < data.len() {
277 let (peek, _) = tags::decode_tag(data, offset)?;
278 if peek.is_opening_tag(13) {
279 let (_, inner_start) = tags::decode_tag(data, offset)?;
280 let (raw, new_off) = tags::extract_context_value(data, inner_start, 13)?;
281 target_value = Some(raw.to_vec());
282 offset = new_off;
283 }
284 }
285
286 let mut current_value = None;
288 if offset < data.len() {
289 let (peek, _) = tags::decode_tag(data, offset)?;
290 if peek.is_opening_tag(14) {
291 let (_, inner_start) = tags::decode_tag(data, offset)?;
292 let (raw, new_off) = tags::extract_context_value(data, inner_start, 14)?;
293 current_value = Some(raw.to_vec());
294 offset = new_off;
295 }
296 }
297
298 let mut result = None;
300 if offset < data.len() {
301 let (peek, _) = tags::decode_tag(data, offset)?;
302 if peek.is_opening_tag(15) {
303 let (_, inner_start) = tags::decode_tag(data, offset)?;
304 let (raw, new_off) = tags::extract_context_value(data, inner_start, 15)?;
305 result = Some(raw.to_vec());
306 offset = new_off;
307 }
308 }
309 let _ = offset;
310
311 Ok(Self {
312 source_timestamp,
313 target_timestamp,
314 source_device,
315 source_object,
316 operation,
317 source_comment,
318 target_comment,
319 invoke_id,
320 source_user_info,
321 target_device,
322 target_object,
323 target_property,
324 target_priority,
325 target_value,
326 current_value,
327 result,
328 })
329 }
330}
331
332#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct AuditLogQueryRequest {
339 pub acknowledgment_filter: u32,
341 pub query_options_raw: Vec<u8>,
343}
344
345impl AuditLogQueryRequest {
346 pub fn encode(&self, buf: &mut BytesMut) {
347 primitives::encode_ctx_enumerated(buf, 0, self.acknowledgment_filter);
349 buf.extend_from_slice(&self.query_options_raw);
350 }
351
352 pub fn decode(data: &[u8]) -> Result<Self, Error> {
353 let mut offset = 0;
354
355 let (tag, pos) = tags::decode_tag(data, offset)?;
357 let end = pos + tag.length as usize;
358 if end > data.len() {
359 return Err(Error::decoding(
360 pos,
361 "AuditLogQuery truncated at acknowledgment-filter",
362 ));
363 }
364 let acknowledgment_filter = primitives::decode_unsigned(&data[pos..end])? as u32;
365 offset = end;
366
367 let query_options_raw = data[offset..].to_vec();
368
369 Ok(Self {
370 acknowledgment_filter,
371 query_options_raw,
372 })
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use bacnet_types::enums::{ObjectType, PropertyIdentifier};
380 use bacnet_types::primitives::Time;
381
382 #[test]
383 fn audit_notification_round_trip() {
384 let req = AuditNotificationRequest {
385 source_timestamp: BACnetTimeStamp::SequenceNumber(100),
386 target_timestamp: None,
387 source_device: vec![0x09, 0x01], source_object: Some(ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap()),
389 operation: 3,
390 source_comment: Some("test audit".to_string()),
391 target_comment: None,
392 invoke_id: Some(5),
393 source_user_info: None,
394 target_device: vec![0x09, 0x02], target_object: Some(ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap()),
396 target_property: Some(PropertyReference {
397 property_identifier: PropertyIdentifier::PRESENT_VALUE,
398 property_array_index: None,
399 }),
400 target_priority: Some(8),
401 target_value: Some(vec![0x44, 0x42, 0x90, 0x00, 0x00]),
402 current_value: Some(vec![0x44, 0x00, 0x00, 0x00, 0x00]),
403 result: None,
404 };
405 let mut buf = BytesMut::new();
406 req.encode(&mut buf).unwrap();
407 let decoded = AuditNotificationRequest::decode(&buf).unwrap();
408 assert_eq!(req, decoded);
409 }
410
411 #[test]
412 fn audit_notification_minimal() {
413 let req = AuditNotificationRequest {
414 source_timestamp: BACnetTimeStamp::Time(Time {
415 hour: 10,
416 minute: 0,
417 second: 0,
418 hundredths: 0,
419 }),
420 target_timestamp: None,
421 source_device: vec![0x09, 0x01],
422 source_object: None,
423 operation: 0,
424 source_comment: None,
425 target_comment: None,
426 invoke_id: None,
427 source_user_info: None,
428 target_device: vec![0x09, 0x02],
429 target_object: None,
430 target_property: None,
431 target_priority: None,
432 target_value: None,
433 current_value: None,
434 result: None,
435 };
436 let mut buf = BytesMut::new();
437 req.encode(&mut buf).unwrap();
438 let decoded = AuditNotificationRequest::decode(&buf).unwrap();
439 assert_eq!(req, decoded);
440 }
441
442 #[test]
443 fn audit_notification_empty_input() {
444 assert!(AuditNotificationRequest::decode(&[]).is_err());
445 }
446
447 #[test]
448 fn audit_log_query_round_trip() {
449 let req = AuditLogQueryRequest {
450 acknowledgment_filter: 1,
451 query_options_raw: vec![0x19, 0x05, 0x29, 0x0A],
452 };
453 let mut buf = BytesMut::new();
454 req.encode(&mut buf);
455 let decoded = AuditLogQueryRequest::decode(&buf).unwrap();
456 assert_eq!(req, decoded);
457 }
458
459 #[test]
460 fn audit_log_query_no_options() {
461 let req = AuditLogQueryRequest {
462 acknowledgment_filter: 0,
463 query_options_raw: vec![],
464 };
465 let mut buf = BytesMut::new();
466 req.encode(&mut buf);
467 let decoded = AuditLogQueryRequest::decode(&buf).unwrap();
468 assert_eq!(req, decoded);
469 }
470
471 #[test]
472 fn audit_log_query_empty_input() {
473 assert!(AuditLogQueryRequest::decode(&[]).is_err());
474 }
475}