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