1use bacnet_encoding::{primitives, tags};
4use bacnet_types::error::Error;
5use bacnet_types::primitives::ObjectIdentifier;
6use bytes::BytesMut;
7
8use crate::common::MAX_DECODED_ITEMS;
9
10fn checked_slice<'a>(
12 content: &'a [u8],
13 offset: usize,
14 context: &str,
15) -> Result<(&'a [u8], usize), Error> {
16 let (t, p) = tags::decode_tag(content, offset)?;
17 let end = p + t.length as usize;
18 if end > content.len() {
19 return Err(Error::decoding(p, format!("{context} truncated")));
20 }
21 Ok((&content[p..end], end))
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct AtomicReadFileRequest {
31 pub file_identifier: ObjectIdentifier,
32 pub access: FileAccessMethod,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct AtomicWriteFileRequest {
38 pub file_identifier: ObjectIdentifier,
39 pub access: FileWriteAccessMethod,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum FileAccessMethod {
45 Stream {
47 file_start_position: i32,
48 requested_octet_count: u32,
49 },
50 Record {
52 file_start_record: i32,
53 requested_record_count: u32,
54 },
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum FileWriteAccessMethod {
60 Stream {
62 file_start_position: i32,
63 file_data: Vec<u8>,
64 },
65 Record {
67 file_start_record: i32,
68 record_count: u32,
69 file_record_data: Vec<Vec<u8>>,
70 },
71}
72
73impl AtomicReadFileRequest {
74 pub fn encode(&self, buf: &mut BytesMut) {
75 primitives::encode_app_object_id(buf, &self.file_identifier);
76 match &self.access {
77 FileAccessMethod::Stream {
78 file_start_position,
79 requested_octet_count,
80 } => {
81 tags::encode_opening_tag(buf, 0);
82 primitives::encode_app_signed(buf, *file_start_position);
83 primitives::encode_app_unsigned(buf, *requested_octet_count as u64);
84 tags::encode_closing_tag(buf, 0);
85 }
86 FileAccessMethod::Record {
87 file_start_record,
88 requested_record_count,
89 } => {
90 tags::encode_opening_tag(buf, 1);
91 primitives::encode_app_signed(buf, *file_start_record);
92 primitives::encode_app_unsigned(buf, *requested_record_count as u64);
93 tags::encode_closing_tag(buf, 1);
94 }
95 }
96 }
97
98 pub fn decode(data: &[u8]) -> Result<Self, Error> {
99 let mut offset = 0;
100
101 let (tag, pos) = tags::decode_tag(data, offset)?;
103 let end = pos + tag.length as usize;
104 if end > data.len() {
105 return Err(Error::buffer_too_short(end, data.len()));
106 }
107 let file_identifier = ObjectIdentifier::decode(&data[pos..end])?;
108 offset = end;
109
110 let (tag, tag_end) = tags::decode_tag(data, offset)?;
112 let access = if tag.is_opening_tag(0) {
113 let (content, _) = tags::extract_context_value(data, tag_end, 0)?;
114 let (slice, inner) =
115 checked_slice(content, 0, "AtomicReadFile stream file-start-position")?;
116 let file_start_position = primitives::decode_signed(slice)?;
117 let (slice, _) = checked_slice(
118 content,
119 inner,
120 "AtomicReadFile stream requested-octet-count",
121 )?;
122 let requested_octet_count = primitives::decode_unsigned(slice)? as u32;
123 FileAccessMethod::Stream {
124 file_start_position,
125 requested_octet_count,
126 }
127 } else if tag.is_opening_tag(1) {
128 let (content, _) = tags::extract_context_value(data, tag_end, 1)?;
129 let (slice, inner) =
130 checked_slice(content, 0, "AtomicReadFile record file-start-record")?;
131 let file_start_record = primitives::decode_signed(slice)?;
132 let (slice, _) = checked_slice(
133 content,
134 inner,
135 "AtomicReadFile record requested-record-count",
136 )?;
137 let requested_record_count = primitives::decode_unsigned(slice)? as u32;
138 FileAccessMethod::Record {
139 file_start_record,
140 requested_record_count,
141 }
142 } else {
143 return Err(Error::decoding(offset, "Unknown file access method"));
144 };
145
146 Ok(Self {
147 file_identifier,
148 access,
149 })
150 }
151}
152
153impl AtomicWriteFileRequest {
154 pub fn encode(&self, buf: &mut BytesMut) {
155 primitives::encode_app_object_id(buf, &self.file_identifier);
156 match &self.access {
157 FileWriteAccessMethod::Stream {
158 file_start_position,
159 file_data,
160 } => {
161 tags::encode_opening_tag(buf, 0);
162 primitives::encode_app_signed(buf, *file_start_position);
163 primitives::encode_app_octet_string(buf, file_data);
164 tags::encode_closing_tag(buf, 0);
165 }
166 FileWriteAccessMethod::Record {
167 file_start_record,
168 record_count,
169 file_record_data,
170 } => {
171 tags::encode_opening_tag(buf, 1);
172 primitives::encode_app_signed(buf, *file_start_record);
173 primitives::encode_app_unsigned(buf, *record_count as u64);
174 for record in file_record_data {
175 primitives::encode_app_octet_string(buf, record);
176 }
177 tags::encode_closing_tag(buf, 1);
178 }
179 }
180 }
181
182 pub fn decode(data: &[u8]) -> Result<Self, Error> {
183 let mut offset = 0;
184
185 let (tag, pos) = tags::decode_tag(data, offset)?;
186 let end = pos + tag.length as usize;
187 if end > data.len() {
188 return Err(Error::buffer_too_short(end, data.len()));
189 }
190 let file_identifier = ObjectIdentifier::decode(&data[pos..end])?;
191 offset = end;
192
193 let (tag, tag_end) = tags::decode_tag(data, offset)?;
194 let access = if tag.is_opening_tag(0) {
195 let (content, _) = tags::extract_context_value(data, tag_end, 0)?;
196 let (slice, inner) =
197 checked_slice(content, 0, "AtomicWriteFile stream file-start-position")?;
198 let file_start_position = primitives::decode_signed(slice)?;
199 let (slice, _) = checked_slice(content, inner, "AtomicWriteFile stream file-data")?;
200 let file_data = slice.to_vec();
201 FileWriteAccessMethod::Stream {
202 file_start_position,
203 file_data,
204 }
205 } else if tag.is_opening_tag(1) {
206 let (content, _) = tags::extract_context_value(data, tag_end, 1)?;
207 let (slice, mut inner) =
208 checked_slice(content, 0, "AtomicWriteFile record file-start-record")?;
209 let file_start_record = primitives::decode_signed(slice)?;
210 let (slice, new_inner) =
211 checked_slice(content, inner, "AtomicWriteFile record record-count")?;
212 let record_count = primitives::decode_unsigned(slice)? as u32;
213 inner = new_inner;
214 if record_count as usize > MAX_DECODED_ITEMS {
215 return Err(Error::decoding(0, "record count exceeds maximum"));
216 }
217 let mut file_record_data = Vec::new();
218 for i in 0..record_count {
219 if inner >= content.len() {
220 break;
221 }
222 let (slice, new_inner) =
223 checked_slice(content, inner, &format!("AtomicWriteFile record data[{i}]"))?;
224 file_record_data.push(slice.to_vec());
225 inner = new_inner;
226 }
227 FileWriteAccessMethod::Record {
228 file_start_record,
229 record_count,
230 file_record_data,
231 }
232 } else {
233 return Err(Error::decoding(offset, "Unknown file write access method"));
234 };
235
236 Ok(Self {
237 file_identifier,
238 access,
239 })
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
249pub struct AtomicReadFileAck {
250 pub end_of_file: bool,
251 pub access: FileReadAckMethod,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
256pub enum FileReadAckMethod {
257 Stream {
259 file_start_position: i32,
260 file_data: Vec<u8>,
261 },
262 Record {
264 file_start_record: i32,
265 returned_record_count: u32,
266 file_record_data: Vec<Vec<u8>>,
267 },
268}
269
270impl AtomicReadFileAck {
271 pub fn encode(&self, buf: &mut BytesMut) {
272 primitives::encode_app_boolean(buf, self.end_of_file);
273 match &self.access {
274 FileReadAckMethod::Stream {
275 file_start_position,
276 file_data,
277 } => {
278 tags::encode_opening_tag(buf, 0);
279 primitives::encode_app_signed(buf, *file_start_position);
280 primitives::encode_app_octet_string(buf, file_data);
281 tags::encode_closing_tag(buf, 0);
282 }
283 FileReadAckMethod::Record {
284 file_start_record,
285 returned_record_count,
286 file_record_data,
287 } => {
288 tags::encode_opening_tag(buf, 1);
289 primitives::encode_app_signed(buf, *file_start_record);
290 primitives::encode_app_unsigned(buf, *returned_record_count as u64);
291 for record in file_record_data {
292 primitives::encode_app_octet_string(buf, record);
293 }
294 tags::encode_closing_tag(buf, 1);
295 }
296 }
297 }
298
299 pub fn decode(data: &[u8]) -> Result<Self, Error> {
300 let mut offset = 0;
301
302 let (tag, pos) = tags::decode_tag(data, offset)?;
304 let end_of_file = tag.length != 0;
305 offset = pos;
306
307 let (tag, tag_end) = tags::decode_tag(data, offset)?;
309 let access = if tag.is_opening_tag(0) {
310 let (content, _) = tags::extract_context_value(data, tag_end, 0)?;
311 let (slice, inner) =
312 checked_slice(content, 0, "AtomicReadFileAck stream file-start-position")?;
313 let file_start_position = primitives::decode_signed(slice)?;
314 let (slice, _) = checked_slice(content, inner, "AtomicReadFileAck stream file-data")?;
315 let file_data = slice.to_vec();
316 FileReadAckMethod::Stream {
317 file_start_position,
318 file_data,
319 }
320 } else if tag.is_opening_tag(1) {
321 let (content, _) = tags::extract_context_value(data, tag_end, 1)?;
322 let (slice, mut inner) =
323 checked_slice(content, 0, "AtomicReadFileAck record file-start-record")?;
324 let file_start_record = primitives::decode_signed(slice)?;
325 let (slice, new_inner) = checked_slice(
326 content,
327 inner,
328 "AtomicReadFileAck record returned-record-count",
329 )?;
330 let returned_record_count = primitives::decode_unsigned(slice)? as u32;
331 inner = new_inner;
332 if returned_record_count as usize > MAX_DECODED_ITEMS {
333 return Err(Error::decoding(0, "record count exceeds maximum"));
334 }
335 let mut file_record_data = Vec::new();
336 for i in 0..returned_record_count {
337 if inner >= content.len() {
338 break;
339 }
340 let (slice, new_inner) = checked_slice(
341 content,
342 inner,
343 &format!("AtomicReadFileAck record data[{i}]"),
344 )?;
345 file_record_data.push(slice.to_vec());
346 inner = new_inner;
347 }
348 FileReadAckMethod::Record {
349 file_start_record,
350 returned_record_count,
351 file_record_data,
352 }
353 } else {
354 return Err(Error::decoding(
355 offset,
356 "Unknown read file ACK access method",
357 ));
358 };
359
360 Ok(Self {
361 end_of_file,
362 access,
363 })
364 }
365}
366
367#[derive(Debug, Clone, PartialEq, Eq)]
373pub struct AtomicWriteFileAck {
374 pub access: FileWriteAckMethod,
375}
376
377#[derive(Debug, Clone, PartialEq, Eq)]
379pub enum FileWriteAckMethod {
380 Stream { file_start_position: i32 },
382 Record { file_start_record: i32 },
384}
385
386impl AtomicWriteFileAck {
387 pub fn encode(&self, buf: &mut BytesMut) {
388 match &self.access {
389 FileWriteAckMethod::Stream {
390 file_start_position,
391 } => {
392 primitives::encode_ctx_signed(buf, 0, *file_start_position);
393 }
394 FileWriteAckMethod::Record { file_start_record } => {
395 primitives::encode_ctx_signed(buf, 1, *file_start_record);
396 }
397 }
398 }
399
400 pub fn decode(data: &[u8]) -> Result<Self, Error> {
401 let (tag, pos) = tags::decode_tag(data, 0)?;
402 let end = pos + tag.length as usize;
403 if end > data.len() {
404 return Err(Error::buffer_too_short(end, data.len()));
405 }
406 let access = if tag.is_context(0) {
407 let file_start_position = primitives::decode_signed(&data[pos..end])?;
408 FileWriteAckMethod::Stream {
409 file_start_position,
410 }
411 } else if tag.is_context(1) {
412 let file_start_record = primitives::decode_signed(&data[pos..end])?;
413 FileWriteAckMethod::Record { file_start_record }
414 } else {
415 return Err(Error::decoding(0, "Unknown write file ACK access method"));
416 };
417
418 Ok(Self { access })
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use bacnet_types::enums::ObjectType;
426
427 fn file_oid() -> ObjectIdentifier {
428 ObjectIdentifier::new(ObjectType::FILE, 1).unwrap()
429 }
430
431 #[test]
432 fn atomic_read_stream_round_trip() {
433 let req = AtomicReadFileRequest {
434 file_identifier: file_oid(),
435 access: FileAccessMethod::Stream {
436 file_start_position: 0,
437 requested_octet_count: 1024,
438 },
439 };
440 let mut buf = BytesMut::new();
441 req.encode(&mut buf);
442 let decoded = AtomicReadFileRequest::decode(&buf).unwrap();
443 assert_eq!(decoded, req);
444 }
445
446 #[test]
447 fn atomic_read_record_round_trip() {
448 let req = AtomicReadFileRequest {
449 file_identifier: file_oid(),
450 access: FileAccessMethod::Record {
451 file_start_record: 5,
452 requested_record_count: 10,
453 },
454 };
455 let mut buf = BytesMut::new();
456 req.encode(&mut buf);
457 let decoded = AtomicReadFileRequest::decode(&buf).unwrap();
458 assert_eq!(decoded, req);
459 }
460
461 #[test]
462 fn atomic_write_stream_round_trip() {
463 let req = AtomicWriteFileRequest {
464 file_identifier: file_oid(),
465 access: FileWriteAccessMethod::Stream {
466 file_start_position: 100,
467 file_data: vec![0x01, 0x02, 0x03, 0x04],
468 },
469 };
470 let mut buf = BytesMut::new();
471 req.encode(&mut buf);
472 let decoded = AtomicWriteFileRequest::decode(&buf).unwrap();
473 assert_eq!(decoded, req);
474 }
475
476 #[test]
477 fn atomic_write_record_round_trip() {
478 let req = AtomicWriteFileRequest {
479 file_identifier: file_oid(),
480 access: FileWriteAccessMethod::Record {
481 file_start_record: 0,
482 record_count: 2,
483 file_record_data: vec![vec![0xAA, 0xBB], vec![0xCC, 0xDD]],
484 },
485 };
486 let mut buf = BytesMut::new();
487 req.encode(&mut buf);
488 let decoded = AtomicWriteFileRequest::decode(&buf).unwrap();
489 assert_eq!(decoded, req);
490 }
491
492 #[test]
497 fn test_decode_atomic_read_file_empty_input() {
498 assert!(AtomicReadFileRequest::decode(&[]).is_err());
499 }
500
501 #[test]
502 fn test_decode_atomic_read_file_truncated_1_byte() {
503 let req = AtomicReadFileRequest {
504 file_identifier: file_oid(),
505 access: FileAccessMethod::Stream {
506 file_start_position: 0,
507 requested_octet_count: 1024,
508 },
509 };
510 let mut buf = BytesMut::new();
511 req.encode(&mut buf);
512 assert!(AtomicReadFileRequest::decode(&buf[..1]).is_err());
513 }
514
515 #[test]
516 fn test_decode_atomic_read_file_truncated_3_bytes() {
517 let req = AtomicReadFileRequest {
518 file_identifier: file_oid(),
519 access: FileAccessMethod::Stream {
520 file_start_position: 0,
521 requested_octet_count: 1024,
522 },
523 };
524 let mut buf = BytesMut::new();
525 req.encode(&mut buf);
526 assert!(AtomicReadFileRequest::decode(&buf[..3]).is_err());
527 }
528
529 #[test]
530 fn test_decode_atomic_read_file_truncated_half() {
531 let req = AtomicReadFileRequest {
532 file_identifier: file_oid(),
533 access: FileAccessMethod::Stream {
534 file_start_position: 0,
535 requested_octet_count: 1024,
536 },
537 };
538 let mut buf = BytesMut::new();
539 req.encode(&mut buf);
540 let half = buf.len() / 2;
541 assert!(AtomicReadFileRequest::decode(&buf[..half]).is_err());
542 }
543
544 #[test]
545 fn test_decode_atomic_read_file_invalid_tag() {
546 assert!(AtomicReadFileRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
547 }
548
549 #[test]
550 fn test_decode_atomic_write_file_empty_input() {
551 assert!(AtomicWriteFileRequest::decode(&[]).is_err());
552 }
553
554 #[test]
555 fn test_decode_atomic_write_file_truncated_1_byte() {
556 let req = AtomicWriteFileRequest {
557 file_identifier: file_oid(),
558 access: FileWriteAccessMethod::Stream {
559 file_start_position: 100,
560 file_data: vec![0x01, 0x02, 0x03, 0x04],
561 },
562 };
563 let mut buf = BytesMut::new();
564 req.encode(&mut buf);
565 assert!(AtomicWriteFileRequest::decode(&buf[..1]).is_err());
566 }
567
568 #[test]
569 fn test_decode_atomic_write_file_truncated_3_bytes() {
570 let req = AtomicWriteFileRequest {
571 file_identifier: file_oid(),
572 access: FileWriteAccessMethod::Stream {
573 file_start_position: 100,
574 file_data: vec![0x01, 0x02, 0x03, 0x04],
575 },
576 };
577 let mut buf = BytesMut::new();
578 req.encode(&mut buf);
579 assert!(AtomicWriteFileRequest::decode(&buf[..3]).is_err());
580 }
581
582 #[test]
583 fn test_decode_atomic_write_file_truncated_half() {
584 let req = AtomicWriteFileRequest {
585 file_identifier: file_oid(),
586 access: FileWriteAccessMethod::Stream {
587 file_start_position: 100,
588 file_data: vec![0x01, 0x02, 0x03, 0x04],
589 },
590 };
591 let mut buf = BytesMut::new();
592 req.encode(&mut buf);
593 let half = buf.len() / 2;
594 assert!(AtomicWriteFileRequest::decode(&buf[..half]).is_err());
595 }
596
597 #[test]
598 fn test_decode_atomic_write_file_invalid_tag() {
599 assert!(AtomicWriteFileRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
600 }
601
602 #[test]
603 fn atomic_read_file_request_truncated_inner_tag() {
604 let data = [
608 0xC4, 0x02, 0x80, 0x00, 0x01, 0x0E,
611 0x35, 50, 0x01, 0x02, 0x0F,
615 ];
616 assert!(AtomicReadFileRequest::decode(&data).is_err());
617 }
618
619 #[test]
620 fn atomic_write_file_request_truncated_inner_tag() {
621 let data = [
623 0xC4, 0x02, 0x80, 0x00, 0x01, 0x0E, 0x35, 80, 0x01, 0x0F,
629 ];
630 assert!(AtomicWriteFileRequest::decode(&data).is_err());
631 }
632
633 #[test]
638 fn atomic_read_file_ack_stream_round_trip() {
639 let ack = AtomicReadFileAck {
640 end_of_file: false,
641 access: FileReadAckMethod::Stream {
642 file_start_position: 0,
643 file_data: vec![0x48, 0x65, 0x6C, 0x6C, 0x6F],
644 },
645 };
646 let mut buf = BytesMut::new();
647 ack.encode(&mut buf);
648 let decoded = AtomicReadFileAck::decode(&buf).unwrap();
649 assert_eq!(decoded, ack);
650 }
651
652 #[test]
653 fn atomic_read_file_ack_stream_eof_true() {
654 let ack = AtomicReadFileAck {
655 end_of_file: true,
656 access: FileReadAckMethod::Stream {
657 file_start_position: 512,
658 file_data: vec![0xDE, 0xAD],
659 },
660 };
661 let mut buf = BytesMut::new();
662 ack.encode(&mut buf);
663 let decoded = AtomicReadFileAck::decode(&buf).unwrap();
664 assert_eq!(decoded, ack);
665 }
666
667 #[test]
668 fn atomic_read_file_ack_record_round_trip() {
669 let ack = AtomicReadFileAck {
670 end_of_file: true,
671 access: FileReadAckMethod::Record {
672 file_start_record: 5,
673 returned_record_count: 2,
674 file_record_data: vec![vec![0xAA, 0xBB], vec![0xCC, 0xDD, 0xEE]],
675 },
676 };
677 let mut buf = BytesMut::new();
678 ack.encode(&mut buf);
679 let decoded = AtomicReadFileAck::decode(&buf).unwrap();
680 assert_eq!(decoded, ack);
681 }
682
683 #[test]
684 fn atomic_read_file_ack_record_empty() {
685 let ack = AtomicReadFileAck {
686 end_of_file: true,
687 access: FileReadAckMethod::Record {
688 file_start_record: 0,
689 returned_record_count: 0,
690 file_record_data: vec![],
691 },
692 };
693 let mut buf = BytesMut::new();
694 ack.encode(&mut buf);
695 let decoded = AtomicReadFileAck::decode(&buf).unwrap();
696 assert_eq!(decoded, ack);
697 }
698
699 #[test]
704 fn atomic_write_file_ack_stream_round_trip() {
705 let ack = AtomicWriteFileAck {
706 access: FileWriteAckMethod::Stream {
707 file_start_position: 100,
708 },
709 };
710 let mut buf = BytesMut::new();
711 ack.encode(&mut buf);
712 let decoded = AtomicWriteFileAck::decode(&buf).unwrap();
713 assert_eq!(decoded, ack);
714 }
715
716 #[test]
717 fn atomic_write_file_ack_stream_negative_position() {
718 let ack = AtomicWriteFileAck {
719 access: FileWriteAckMethod::Stream {
720 file_start_position: -1,
721 },
722 };
723 let mut buf = BytesMut::new();
724 ack.encode(&mut buf);
725 let decoded = AtomicWriteFileAck::decode(&buf).unwrap();
726 assert_eq!(decoded, ack);
727 }
728
729 #[test]
730 fn atomic_write_file_ack_record_round_trip() {
731 let ack = AtomicWriteFileAck {
732 access: FileWriteAckMethod::Record {
733 file_start_record: 42,
734 },
735 };
736 let mut buf = BytesMut::new();
737 ack.encode(&mut buf);
738 let decoded = AtomicWriteFileAck::decode(&buf).unwrap();
739 assert_eq!(decoded, ack);
740 }
741
742 #[test]
747 fn test_decode_read_file_ack_empty_input() {
748 assert!(AtomicReadFileAck::decode(&[]).is_err());
749 }
750
751 #[test]
752 fn test_decode_read_file_ack_truncated() {
753 let ack = AtomicReadFileAck {
754 end_of_file: false,
755 access: FileReadAckMethod::Stream {
756 file_start_position: 0,
757 file_data: vec![0x01, 0x02, 0x03],
758 },
759 };
760 let mut buf = BytesMut::new();
761 ack.encode(&mut buf);
762 assert!(AtomicReadFileAck::decode(&buf[..1]).is_err());
764 let half = buf.len() / 2;
766 assert!(AtomicReadFileAck::decode(&buf[..half]).is_err());
767 }
768
769 #[test]
770 fn test_decode_write_file_ack_empty_input() {
771 assert!(AtomicWriteFileAck::decode(&[]).is_err());
772 }
773
774 #[test]
775 fn test_decode_write_file_ack_truncated() {
776 let ack = AtomicWriteFileAck {
777 access: FileWriteAckMethod::Stream {
778 file_start_position: 100,
779 },
780 };
781 let mut buf = BytesMut::new();
782 ack.encode(&mut buf);
783 if buf.len() > 1 {
785 assert!(AtomicWriteFileAck::decode(&buf[..1]).is_err());
786 }
787 }
788}