1#[cfg(not(feature = "std"))]
4use alloc::vec::Vec;
5
6use bacnet_encoding::constructed::tagged::{
7 decode_app_object_id, decode_app_primitive, decode_app_unsigned, decode_ctx_constructed,
8 decode_ctx_primitive, expect_end, next_is_context, next_is_opening,
9};
10use bacnet_encoding::{primitives, tags};
11use bacnet_types::error::Error;
12use bacnet_types::primitives::ObjectIdentifier;
13use bytes::BytesMut;
14
15use crate::common::MAX_DECODED_ITEMS;
16
17fn decode_start(data: &[u8], offset: usize, what: &str) -> Result<(i32, usize), Error> {
20 let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::SIGNED, what)?;
21 Ok((primitives::decode_signed(octets)?, end))
22}
23
24fn unknown_access_method(data: &[u8], offset: usize, what: &str) -> Error {
28 if offset >= data.len() {
29 Error::missing(offset, format!("{what}: access method missing"))
30 } else {
31 Error::invalid_tag(offset, format!("{what}: unknown access method"))
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct AtomicReadFileRequest {
42 pub file_identifier: ObjectIdentifier,
44 pub access: FileAccessMethod,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct AtomicWriteFileRequest {
51 pub file_identifier: ObjectIdentifier,
53 pub access: FileWriteAccessMethod,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum FileAccessMethod {
60 Stream {
62 file_start_position: i32,
64 requested_octet_count: u32,
66 },
67 Record {
69 file_start_record: i32,
71 requested_record_count: u32,
73 },
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum FileWriteAccessMethod {
79 Stream {
82 file_start_position: i32,
84 file_data: Vec<u8>,
86 },
87 Record {
91 file_start_record: i32,
93 record_count: u32,
96 file_record_data: Vec<Vec<u8>>,
98 },
99}
100
101impl AtomicReadFileRequest {
102 pub fn encode(&self, buf: &mut BytesMut) {
104 primitives::encode_app_object_id(buf, &self.file_identifier);
105 match &self.access {
106 FileAccessMethod::Stream {
107 file_start_position,
108 requested_octet_count,
109 } => {
110 tags::encode_opening_tag(buf, 0);
111 primitives::encode_app_signed(buf, *file_start_position);
112 primitives::encode_app_unsigned(buf, *requested_octet_count as u64);
113 tags::encode_closing_tag(buf, 0);
114 }
115 FileAccessMethod::Record {
116 file_start_record,
117 requested_record_count,
118 } => {
119 tags::encode_opening_tag(buf, 1);
120 primitives::encode_app_signed(buf, *file_start_record);
121 primitives::encode_app_unsigned(buf, *requested_record_count as u64);
122 tags::encode_closing_tag(buf, 1);
123 }
124 }
125 }
126
127 pub fn decode(data: &[u8]) -> Result<Self, Error> {
131 let (file_identifier, offset) =
132 decode_app_object_id(data, 0, "AtomicReadFile file-identifier")?;
133
134 let (access, end) = if next_is_opening(data, offset, 0)? {
135 const WHAT: &str = "AtomicReadFile stream";
136 let (content, end) = decode_ctx_constructed(data, offset, 0, WHAT)?;
137 let (file_start_position, inner) =
138 decode_start(content, 0, "AtomicReadFile stream file-start-position")?;
139 let (requested_octet_count, inner) = decode_app_unsigned::<u32>(
140 content,
141 inner,
142 "AtomicReadFile stream requested-octet-count",
143 )?;
144 expect_end(content, inner, offset, WHAT)?;
145 let access = FileAccessMethod::Stream {
146 file_start_position,
147 requested_octet_count,
148 };
149 (access, end)
150 } else if next_is_opening(data, offset, 1)? {
151 const WHAT: &str = "AtomicReadFile record";
152 let (content, end) = decode_ctx_constructed(data, offset, 1, WHAT)?;
153 let (file_start_record, inner) =
154 decode_start(content, 0, "AtomicReadFile record file-start-record")?;
155 let (requested_record_count, inner) = decode_app_unsigned::<u32>(
156 content,
157 inner,
158 "AtomicReadFile record requested-record-count",
159 )?;
160 expect_end(content, inner, offset, WHAT)?;
161 let access = FileAccessMethod::Record {
162 file_start_record,
163 requested_record_count,
164 };
165 (access, end)
166 } else {
167 return Err(unknown_access_method(data, offset, "AtomicReadFile"));
168 };
169 expect_end(data, end, end, "AtomicReadFile")?;
170
171 Ok(Self {
172 file_identifier,
173 access,
174 })
175 }
176}
177
178impl AtomicWriteFileRequest {
179 pub fn encode(&self, buf: &mut BytesMut) {
181 primitives::encode_app_object_id(buf, &self.file_identifier);
182 match &self.access {
183 FileWriteAccessMethod::Stream {
184 file_start_position,
185 file_data,
186 } => {
187 tags::encode_opening_tag(buf, 0);
188 primitives::encode_app_signed(buf, *file_start_position);
189 primitives::encode_app_octet_string(buf, file_data);
190 tags::encode_closing_tag(buf, 0);
191 }
192 FileWriteAccessMethod::Record {
193 file_start_record,
194 record_count,
195 file_record_data,
196 } => {
197 tags::encode_opening_tag(buf, 1);
198 primitives::encode_app_signed(buf, *file_start_record);
199 primitives::encode_app_unsigned(buf, *record_count as u64);
200 for record in file_record_data {
201 primitives::encode_app_octet_string(buf, record);
202 }
203 tags::encode_closing_tag(buf, 1);
204 }
205 }
206 }
207
208 pub fn decode(data: &[u8]) -> Result<Self, Error> {
214 let (file_identifier, offset) =
215 decode_app_object_id(data, 0, "AtomicWriteFile file-identifier")?;
216
217 let (access, end) = if next_is_opening(data, offset, 0)? {
218 const WHAT: &str = "AtomicWriteFile stream";
219 let (content, end) = decode_ctx_constructed(data, offset, 0, WHAT)?;
220 let (file_start_position, inner) =
221 decode_start(content, 0, "AtomicWriteFile stream file-start-position")?;
222 let (file_data, inner) = decode_app_primitive(
223 content,
224 inner,
225 tags::app_tag::OCTET_STRING,
226 "AtomicWriteFile stream file-data",
227 )?;
228 expect_end(content, inner, offset, WHAT)?;
229 let access = FileWriteAccessMethod::Stream {
230 file_start_position,
231 file_data: file_data.to_vec(),
232 };
233 (access, end)
234 } else if next_is_opening(data, offset, 1)? {
235 let (content, end) = decode_ctx_constructed(data, offset, 1, "AtomicWriteFile record")?;
236 let (file_start_record, mut inner) =
237 decode_start(content, 0, "AtomicWriteFile record file-start-record")?;
238 let (record_count, new_inner) =
239 decode_app_unsigned::<u32>(content, inner, "AtomicWriteFile record record-count")?;
240 inner = new_inner;
241 if record_count as usize > MAX_DECODED_ITEMS {
242 return Err(Error::overflow(0, "record count exceeds maximum"));
243 }
244 let mut file_record_data = Vec::new();
245 for i in 0..record_count {
246 if inner >= content.len() {
247 return Err(Error::missing(
248 inner,
249 format!("AtomicWriteFile record: record {i} of the count is missing"),
250 ));
251 }
252 let (record, new_inner) = decode_app_primitive(
253 content,
254 inner,
255 tags::app_tag::OCTET_STRING,
256 format_args!("AtomicWriteFile record data[{i}]"),
258 )?;
259 file_record_data.push(record.to_vec());
260 inner = new_inner;
261 }
262 expect_end(content, inner, inner, "AtomicWriteFile record")?;
263 let access = FileWriteAccessMethod::Record {
264 file_start_record,
265 record_count,
266 file_record_data,
267 };
268 (access, end)
269 } else {
270 return Err(unknown_access_method(data, offset, "AtomicWriteFile"));
271 };
272 expect_end(data, end, end, "AtomicWriteFile")?;
273
274 Ok(Self {
275 file_identifier,
276 access,
277 })
278 }
279}
280
281#[derive(Debug, Clone, PartialEq, Eq)]
287pub struct AtomicReadFileAck {
288 pub end_of_file: bool,
290 pub access: FileReadAckMethod,
292}
293
294#[derive(Debug, Clone, PartialEq, Eq)]
296pub enum FileReadAckMethod {
297 Stream {
299 file_start_position: i32,
301 file_data: Vec<u8>,
303 },
304 Record {
306 file_start_record: i32,
308 returned_record_count: u32,
310 file_record_data: Vec<Vec<u8>>,
312 },
313}
314
315impl AtomicReadFileAck {
316 pub fn encode(&self, buf: &mut BytesMut) {
318 primitives::encode_app_boolean(buf, self.end_of_file);
319 match &self.access {
320 FileReadAckMethod::Stream {
321 file_start_position,
322 file_data,
323 } => {
324 tags::encode_opening_tag(buf, 0);
325 primitives::encode_app_signed(buf, *file_start_position);
326 primitives::encode_app_octet_string(buf, file_data);
327 tags::encode_closing_tag(buf, 0);
328 }
329 FileReadAckMethod::Record {
330 file_start_record,
331 returned_record_count,
332 file_record_data,
333 } => {
334 tags::encode_opening_tag(buf, 1);
335 primitives::encode_app_signed(buf, *file_start_record);
336 primitives::encode_app_unsigned(buf, *returned_record_count as u64);
337 for record in file_record_data {
338 primitives::encode_app_octet_string(buf, record);
339 }
340 tags::encode_closing_tag(buf, 1);
341 }
342 }
343 }
344
345 pub fn decode(data: &[u8]) -> Result<Self, Error> {
348 let mut offset = 0;
349
350 let (tag, pos) = tags::decode_tag(data, offset)?;
351 if tag.class != tags::TagClass::Application
352 || tag.number != tags::app_tag::BOOLEAN
353 || tag.is_opening
354 || tag.is_closing
355 || data[offset] & 0x07 > 1
356 {
357 return Err(Error::decoding(
358 offset,
359 "AtomicReadFileAck expected application Boolean end-of-file",
360 ));
361 }
362 let end_of_file = tag.length != 0;
363 offset = pos;
364
365 let (tag, tag_end) = tags::decode_tag(data, offset)?;
366 let (access, access_end) = if tag.is_opening_tag(0) {
367 let (content, access_end) = tags::extract_context_value(data, tag_end, 0)?;
368 let (file_start_position, inner) =
369 decode_start(content, 0, "AtomicReadFileAck stream file-start-position")?;
370 let (slice, inner) = decode_app_primitive(
371 content,
372 inner,
373 tags::app_tag::OCTET_STRING,
374 "AtomicReadFileAck stream file-data",
375 )?;
376 expect_end(content, inner, inner, "AtomicReadFileAck stream")?;
377 let file_data = slice.to_vec();
378 (
379 FileReadAckMethod::Stream {
380 file_start_position,
381 file_data,
382 },
383 access_end,
384 )
385 } else if tag.is_opening_tag(1) {
386 let (content, access_end) = tags::extract_context_value(data, tag_end, 1)?;
387 let (file_start_record, mut inner) =
388 decode_start(content, 0, "AtomicReadFileAck record file-start-record")?;
389 let (returned_record_count, new_inner) = decode_app_unsigned::<u32>(
390 content,
391 inner,
392 "AtomicReadFileAck record returned-record-count",
393 )?;
394 inner = new_inner;
395 if returned_record_count as usize > MAX_DECODED_ITEMS {
396 return Err(Error::overflow(0, "record count exceeds maximum"));
397 }
398 let mut file_record_data = Vec::new();
399 for i in 0..returned_record_count {
400 if inner >= content.len() {
401 return Err(Error::missing(
402 inner,
403 format!("AtomicReadFileAck record: record {i} of the count is missing"),
404 ));
405 }
406 let (slice, new_inner) = decode_app_primitive(
407 content,
408 inner,
409 tags::app_tag::OCTET_STRING,
410 format_args!("AtomicReadFileAck record data[{i}]"),
412 )?;
413 file_record_data.push(slice.to_vec());
414 inner = new_inner;
415 }
416 expect_end(content, inner, inner, "AtomicReadFileAck record")?;
417 (
418 FileReadAckMethod::Record {
419 file_start_record,
420 returned_record_count,
421 file_record_data,
422 },
423 access_end,
424 )
425 } else {
426 return Err(Error::decoding(
427 offset,
428 "Unknown read file ACK access method",
429 ));
430 };
431 expect_end(data, access_end, access_end, "AtomicReadFileAck")?;
432
433 Ok(Self {
434 end_of_file,
435 access,
436 })
437 }
438}
439
440#[derive(Debug, Clone, PartialEq, Eq)]
446pub struct AtomicWriteFileAck {
447 pub access: FileWriteAckMethod,
449}
450
451#[derive(Debug, Clone, PartialEq, Eq)]
453pub enum FileWriteAckMethod {
454 Stream {
456 file_start_position: i32,
458 },
459 Record {
461 file_start_record: i32,
463 },
464}
465
466impl AtomicWriteFileAck {
467 pub fn encode(&self, buf: &mut BytesMut) {
469 match &self.access {
470 FileWriteAckMethod::Stream {
471 file_start_position,
472 } => {
473 primitives::encode_ctx_signed(buf, 0, *file_start_position);
474 }
475 FileWriteAckMethod::Record { file_start_record } => {
476 primitives::encode_ctx_signed(buf, 1, *file_start_record);
477 }
478 }
479 }
480
481 pub fn decode(data: &[u8]) -> Result<Self, Error> {
484 let (access, end) = if next_is_context(data, 0, 0)? {
485 let (octets, end) =
486 decode_ctx_primitive(data, 0, 0, "AtomicWriteFileAck file-start-position")?;
487 let access = FileWriteAckMethod::Stream {
488 file_start_position: primitives::decode_signed(octets)?,
489 };
490 (access, end)
491 } else if next_is_context(data, 0, 1)? {
492 let (octets, end) =
493 decode_ctx_primitive(data, 0, 1, "AtomicWriteFileAck file-start-record")?;
494 let access = FileWriteAckMethod::Record {
495 file_start_record: primitives::decode_signed(octets)?,
496 };
497 (access, end)
498 } else {
499 return Err(Error::decoding(0, "Unknown write file ACK access method"));
500 };
501 expect_end(data, end, end, "AtomicWriteFileAck")?;
502
503 Ok(Self { access })
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use bacnet_types::enums::ObjectType;
511
512 fn file_oid() -> ObjectIdentifier {
513 ObjectIdentifier::new(ObjectType::FILE, 1).unwrap()
514 }
515
516 #[test]
517 fn atomic_read_stream_round_trip() {
518 let req = AtomicReadFileRequest {
519 file_identifier: file_oid(),
520 access: FileAccessMethod::Stream {
521 file_start_position: 0,
522 requested_octet_count: 1024,
523 },
524 };
525 let mut buf = BytesMut::new();
526 req.encode(&mut buf);
527 let decoded = AtomicReadFileRequest::decode(&buf).unwrap();
528 assert_eq!(decoded, req);
529 }
530
531 #[test]
532 fn atomic_read_record_round_trip() {
533 let req = AtomicReadFileRequest {
534 file_identifier: file_oid(),
535 access: FileAccessMethod::Record {
536 file_start_record: 5,
537 requested_record_count: 10,
538 },
539 };
540 let mut buf = BytesMut::new();
541 req.encode(&mut buf);
542 let decoded = AtomicReadFileRequest::decode(&buf).unwrap();
543 assert_eq!(decoded, req);
544 }
545
546 #[test]
547 fn atomic_write_stream_round_trip() {
548 let req = AtomicWriteFileRequest {
549 file_identifier: file_oid(),
550 access: FileWriteAccessMethod::Stream {
551 file_start_position: 100,
552 file_data: vec![0x01, 0x02, 0x03, 0x04],
553 },
554 };
555 let mut buf = BytesMut::new();
556 req.encode(&mut buf);
557 let decoded = AtomicWriteFileRequest::decode(&buf).unwrap();
558 assert_eq!(decoded, req);
559 }
560
561 #[test]
562 fn atomic_write_record_round_trip() {
563 let req = AtomicWriteFileRequest {
564 file_identifier: file_oid(),
565 access: FileWriteAccessMethod::Record {
566 file_start_record: 0,
567 record_count: 2,
568 file_record_data: vec![vec![0xAA, 0xBB], vec![0xCC, 0xDD]],
569 },
570 };
571 let mut buf = BytesMut::new();
572 req.encode(&mut buf);
573 let decoded = AtomicWriteFileRequest::decode(&buf).unwrap();
574 assert_eq!(decoded, req);
575 }
576
577 #[test]
582 fn test_decode_atomic_read_file_empty_input() {
583 assert!(AtomicReadFileRequest::decode(&[]).is_err());
584 }
585
586 #[test]
587 fn test_decode_atomic_read_file_truncated_1_byte() {
588 let req = AtomicReadFileRequest {
589 file_identifier: file_oid(),
590 access: FileAccessMethod::Stream {
591 file_start_position: 0,
592 requested_octet_count: 1024,
593 },
594 };
595 let mut buf = BytesMut::new();
596 req.encode(&mut buf);
597 assert!(AtomicReadFileRequest::decode(&buf[..1]).is_err());
598 }
599
600 #[test]
601 fn test_decode_atomic_read_file_truncated_3_bytes() {
602 let req = AtomicReadFileRequest {
603 file_identifier: file_oid(),
604 access: FileAccessMethod::Stream {
605 file_start_position: 0,
606 requested_octet_count: 1024,
607 },
608 };
609 let mut buf = BytesMut::new();
610 req.encode(&mut buf);
611 assert!(AtomicReadFileRequest::decode(&buf[..3]).is_err());
612 }
613
614 #[test]
615 fn test_decode_atomic_read_file_truncated_half() {
616 let req = AtomicReadFileRequest {
617 file_identifier: file_oid(),
618 access: FileAccessMethod::Stream {
619 file_start_position: 0,
620 requested_octet_count: 1024,
621 },
622 };
623 let mut buf = BytesMut::new();
624 req.encode(&mut buf);
625 let half = buf.len() / 2;
626 assert!(AtomicReadFileRequest::decode(&buf[..half]).is_err());
627 }
628
629 #[test]
630 fn test_decode_atomic_read_file_invalid_tag() {
631 assert!(AtomicReadFileRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
632 }
633
634 #[test]
635 fn test_decode_atomic_write_file_empty_input() {
636 assert!(AtomicWriteFileRequest::decode(&[]).is_err());
637 }
638
639 #[test]
640 fn test_decode_atomic_write_file_truncated_1_byte() {
641 let req = AtomicWriteFileRequest {
642 file_identifier: file_oid(),
643 access: FileWriteAccessMethod::Stream {
644 file_start_position: 100,
645 file_data: vec![0x01, 0x02, 0x03, 0x04],
646 },
647 };
648 let mut buf = BytesMut::new();
649 req.encode(&mut buf);
650 assert!(AtomicWriteFileRequest::decode(&buf[..1]).is_err());
651 }
652
653 #[test]
654 fn test_decode_atomic_write_file_truncated_3_bytes() {
655 let req = AtomicWriteFileRequest {
656 file_identifier: file_oid(),
657 access: FileWriteAccessMethod::Stream {
658 file_start_position: 100,
659 file_data: vec![0x01, 0x02, 0x03, 0x04],
660 },
661 };
662 let mut buf = BytesMut::new();
663 req.encode(&mut buf);
664 assert!(AtomicWriteFileRequest::decode(&buf[..3]).is_err());
665 }
666
667 #[test]
668 fn test_decode_atomic_write_file_truncated_half() {
669 let req = AtomicWriteFileRequest {
670 file_identifier: file_oid(),
671 access: FileWriteAccessMethod::Stream {
672 file_start_position: 100,
673 file_data: vec![0x01, 0x02, 0x03, 0x04],
674 },
675 };
676 let mut buf = BytesMut::new();
677 req.encode(&mut buf);
678 let half = buf.len() / 2;
679 assert!(AtomicWriteFileRequest::decode(&buf[..half]).is_err());
680 }
681
682 #[test]
683 fn test_decode_atomic_write_file_invalid_tag() {
684 assert!(AtomicWriteFileRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
685 }
686
687 #[test]
688 fn atomic_read_file_request_truncated_inner_tag() {
689 let data = [
693 0xC4, 0x02, 0x80, 0x00, 0x01, 0x0E,
696 0x35, 50, 0x01, 0x02, 0x0F,
700 ];
701 assert!(AtomicReadFileRequest::decode(&data).is_err());
702 }
703
704 #[test]
705 fn atomic_write_file_request_truncated_inner_tag() {
706 let data = [
708 0xC4, 0x02, 0x80, 0x00, 0x01, 0x0E, 0x35, 80, 0x01, 0x0F,
714 ];
715 assert!(AtomicWriteFileRequest::decode(&data).is_err());
716 }
717}
718
719#[cfg(test)]
720#[path = "file_width_tests.rs"]
721mod width_tests;
722
723#[cfg(test)]
724#[path = "file_ack_strict_tests.rs"]
725mod ack_strict_tests;
726
727#[cfg(test)]
728#[path = "file_ack_roundtrip_tests.rs"]
729mod ack_roundtrip_tests;
730#[path = "file_ack_size.rs"]
731mod ack_size;