1use bacnet_encoding::{primitives, tags};
4use bacnet_types::primitives::ObjectIdentifier;
5use bacnet_types::{enums::RejectReason, error::Error};
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
24fn checked_application_slice<'a>(
25 content: &'a [u8],
26 offset: usize,
27 expected_tag: u8,
28 context: &str,
29) -> Result<(&'a [u8], usize), Error> {
30 let (tag, pos) = tags::decode_tag(content, offset)?;
31 if tag.class != tags::TagClass::Application
32 || tag.number != expected_tag
33 || tag.is_opening
34 || tag.is_closing
35 {
36 return Err(Error::decoding(
37 offset,
38 format!("{context} has the wrong application tag"),
39 ));
40 }
41 let end = pos
42 .checked_add(tag.length as usize)
43 .ok_or_else(|| Error::decoding(pos, format!("{context} length overflow")))?;
44 if end > content.len() {
45 return Err(Error::decoding(pos, format!("{context} truncated")));
46 }
47 Ok((&content[pos..end], end))
48}
49
50fn checked_unsigned_u32(
51 content: &[u8],
52 offset: usize,
53 context: &str,
54) -> Result<(u32, usize), Error> {
55 let (tag, pos) = tags::decode_tag(content, offset)?;
56 if tag.class != tags::TagClass::Application
57 || tag.number != tags::app_tag::UNSIGNED
58 || content[offset] & 0x07 > 5
59 {
60 return Err(Error::decoding(
61 offset,
62 format!("{context} expected application Unsigned"),
63 ));
64 }
65 let end = pos + tag.length as usize;
66 if end > content.len() {
67 return Err(Error::decoding(pos, format!("{context} truncated")));
68 }
69 let raw = primitives::decode_unsigned(&content[pos..end])?;
70 let value = u32::try_from(raw)
71 .map_err(|_| Error::decoding(pos, format!("{context} {raw} exceeds u32")))?;
72 Ok((value, end))
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct AtomicReadFileRequest {
82 pub file_identifier: ObjectIdentifier,
83 pub access: FileAccessMethod,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct AtomicWriteFileRequest {
89 pub file_identifier: ObjectIdentifier,
90 pub access: FileWriteAccessMethod,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum FileAccessMethod {
96 Stream {
98 file_start_position: i32,
99 requested_octet_count: u32,
100 },
101 Record {
103 file_start_record: i32,
104 requested_record_count: u32,
105 },
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum FileWriteAccessMethod {
111 Stream {
113 file_start_position: i32,
114 file_data: Vec<u8>,
115 },
116 Record {
118 file_start_record: i32,
119 record_count: u32,
120 file_record_data: Vec<Vec<u8>>,
121 },
122}
123
124impl AtomicReadFileRequest {
125 pub fn encode(&self, buf: &mut BytesMut) {
126 primitives::encode_app_object_id(buf, &self.file_identifier);
127 match &self.access {
128 FileAccessMethod::Stream {
129 file_start_position,
130 requested_octet_count,
131 } => {
132 tags::encode_opening_tag(buf, 0);
133 primitives::encode_app_signed(buf, *file_start_position);
134 primitives::encode_app_unsigned(buf, *requested_octet_count as u64);
135 tags::encode_closing_tag(buf, 0);
136 }
137 FileAccessMethod::Record {
138 file_start_record,
139 requested_record_count,
140 } => {
141 tags::encode_opening_tag(buf, 1);
142 primitives::encode_app_signed(buf, *file_start_record);
143 primitives::encode_app_unsigned(buf, *requested_record_count as u64);
144 tags::encode_closing_tag(buf, 1);
145 }
146 }
147 }
148
149 pub fn decode(data: &[u8]) -> Result<Self, Error> {
150 let mut offset = 0;
151
152 let (tag, pos) = tags::decode_tag(data, offset)?;
153 let end = pos + tag.length as usize;
154 if end > data.len() {
155 return Err(Error::buffer_too_short(end, data.len()));
156 }
157 let file_identifier = ObjectIdentifier::decode(&data[pos..end])?;
158 offset = end;
159
160 let (tag, tag_end) = tags::decode_tag(data, offset)?;
161 let access = if tag.is_opening_tag(0) {
162 let (content, _) = tags::extract_context_value(data, tag_end, 0)?;
163 let (slice, inner) =
164 checked_slice(content, 0, "AtomicReadFile stream file-start-position")?;
165 let file_start_position = primitives::decode_signed(slice)?;
166 let (requested_octet_count, _) = checked_unsigned_u32(
167 content,
168 inner,
169 "AtomicReadFile stream requested-octet-count",
170 )?;
171 FileAccessMethod::Stream {
172 file_start_position,
173 requested_octet_count,
174 }
175 } else if tag.is_opening_tag(1) {
176 let (content, _) = tags::extract_context_value(data, tag_end, 1)?;
177 let (slice, inner) =
178 checked_slice(content, 0, "AtomicReadFile record file-start-record")?;
179 let file_start_record = primitives::decode_signed(slice)?;
180 let (requested_record_count, _) = checked_unsigned_u32(
181 content,
182 inner,
183 "AtomicReadFile record requested-record-count",
184 )?;
185 FileAccessMethod::Record {
186 file_start_record,
187 requested_record_count,
188 }
189 } else {
190 return Err(Error::decoding(offset, "Unknown file access method"));
191 };
192
193 Ok(Self {
194 file_identifier,
195 access,
196 })
197 }
198}
199
200impl AtomicWriteFileRequest {
201 pub fn encode(&self, buf: &mut BytesMut) {
202 primitives::encode_app_object_id(buf, &self.file_identifier);
203 match &self.access {
204 FileWriteAccessMethod::Stream {
205 file_start_position,
206 file_data,
207 } => {
208 tags::encode_opening_tag(buf, 0);
209 primitives::encode_app_signed(buf, *file_start_position);
210 primitives::encode_app_octet_string(buf, file_data);
211 tags::encode_closing_tag(buf, 0);
212 }
213 FileWriteAccessMethod::Record {
214 file_start_record,
215 record_count,
216 file_record_data,
217 } => {
218 tags::encode_opening_tag(buf, 1);
219 primitives::encode_app_signed(buf, *file_start_record);
220 primitives::encode_app_unsigned(buf, *record_count as u64);
221 for record in file_record_data {
222 primitives::encode_app_octet_string(buf, record);
223 }
224 tags::encode_closing_tag(buf, 1);
225 }
226 }
227 }
228
229 pub fn decode(data: &[u8]) -> Result<Self, Error> {
230 let mut offset = 0;
231
232 let (tag, pos) = tags::decode_tag(data, offset)?;
233 let end = pos + tag.length as usize;
234 if end > data.len() {
235 return Err(Error::buffer_too_short(end, data.len()));
236 }
237 let file_identifier = ObjectIdentifier::decode(&data[pos..end])?;
238 offset = end;
239
240 let (tag, tag_end) = tags::decode_tag(data, offset)?;
241 let access = if tag.is_opening_tag(0) {
242 let (content, _) = tags::extract_context_value(data, tag_end, 0)?;
243 let (slice, inner) =
244 checked_slice(content, 0, "AtomicWriteFile stream file-start-position")?;
245 let file_start_position = primitives::decode_signed(slice)?;
246 let (slice, _) = checked_slice(content, inner, "AtomicWriteFile stream file-data")?;
247 let file_data = slice.to_vec();
248 FileWriteAccessMethod::Stream {
249 file_start_position,
250 file_data,
251 }
252 } else if tag.is_opening_tag(1) {
253 let (content, _) = tags::extract_context_value(data, tag_end, 1)?;
254 let (slice, mut inner) =
255 checked_slice(content, 0, "AtomicWriteFile record file-start-record")?;
256 let file_start_record = primitives::decode_signed(slice)?;
257 let (record_count, new_inner) =
258 checked_unsigned_u32(content, inner, "AtomicWriteFile record record-count")?;
259 inner = new_inner;
260 if record_count as usize > MAX_DECODED_ITEMS {
261 return Err(Error::decoding(0, "record count exceeds maximum"));
262 }
263 let mut file_record_data = Vec::new();
264 for i in 0..record_count {
265 if inner >= content.len() {
266 return Err(Error::Reject {
267 reason: RejectReason::MISSING_REQUIRED_PARAMETER.to_raw(),
268 });
269 }
270 let (slice, new_inner) =
271 checked_slice(content, inner, &format!("AtomicWriteFile record data[{i}]"))?;
272 file_record_data.push(slice.to_vec());
273 inner = new_inner;
274 }
275 if inner < content.len() {
276 return Err(Error::Reject {
277 reason: RejectReason::TOO_MANY_ARGUMENTS.to_raw(),
278 });
279 }
280 FileWriteAccessMethod::Record {
281 file_start_record,
282 record_count,
283 file_record_data,
284 }
285 } else {
286 return Err(Error::decoding(offset, "Unknown file write access method"));
287 };
288
289 Ok(Self {
290 file_identifier,
291 access,
292 })
293 }
294}
295
296#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct AtomicReadFileAck {
303 pub end_of_file: bool,
304 pub access: FileReadAckMethod,
305}
306
307#[derive(Debug, Clone, PartialEq, Eq)]
309pub enum FileReadAckMethod {
310 Stream {
312 file_start_position: i32,
313 file_data: Vec<u8>,
314 },
315 Record {
317 file_start_record: i32,
318 returned_record_count: u32,
319 file_record_data: Vec<Vec<u8>>,
320 },
321}
322
323impl AtomicReadFileAck {
324 pub fn encode(&self, buf: &mut BytesMut) {
325 primitives::encode_app_boolean(buf, self.end_of_file);
326 match &self.access {
327 FileReadAckMethod::Stream {
328 file_start_position,
329 file_data,
330 } => {
331 tags::encode_opening_tag(buf, 0);
332 primitives::encode_app_signed(buf, *file_start_position);
333 primitives::encode_app_octet_string(buf, file_data);
334 tags::encode_closing_tag(buf, 0);
335 }
336 FileReadAckMethod::Record {
337 file_start_record,
338 returned_record_count,
339 file_record_data,
340 } => {
341 tags::encode_opening_tag(buf, 1);
342 primitives::encode_app_signed(buf, *file_start_record);
343 primitives::encode_app_unsigned(buf, *returned_record_count as u64);
344 for record in file_record_data {
345 primitives::encode_app_octet_string(buf, record);
346 }
347 tags::encode_closing_tag(buf, 1);
348 }
349 }
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)?;
356 if tag.class != tags::TagClass::Application
357 || tag.number != tags::app_tag::BOOLEAN
358 || tag.is_opening
359 || tag.is_closing
360 || data[offset] & 0x07 > 1
361 {
362 return Err(Error::decoding(
363 offset,
364 "AtomicReadFileAck expected application Boolean end-of-file",
365 ));
366 }
367 let end_of_file = tag.length != 0;
368 offset = pos;
369
370 let (tag, tag_end) = tags::decode_tag(data, offset)?;
371 let (access, access_end) = if tag.is_opening_tag(0) {
372 let (content, access_end) = tags::extract_context_value(data, tag_end, 0)?;
373 let (slice, inner) = checked_application_slice(
374 content,
375 0,
376 tags::app_tag::SIGNED,
377 "AtomicReadFileAck stream file-start-position",
378 )?;
379 let file_start_position = primitives::decode_signed(slice)?;
380 let (slice, inner) = checked_application_slice(
381 content,
382 inner,
383 tags::app_tag::OCTET_STRING,
384 "AtomicReadFileAck stream file-data",
385 )?;
386 if inner != content.len() {
387 return Err(Error::Reject {
388 reason: RejectReason::TOO_MANY_ARGUMENTS.to_raw(),
389 });
390 }
391 let file_data = slice.to_vec();
392 (
393 FileReadAckMethod::Stream {
394 file_start_position,
395 file_data,
396 },
397 access_end,
398 )
399 } else if tag.is_opening_tag(1) {
400 let (content, access_end) = tags::extract_context_value(data, tag_end, 1)?;
401 let (slice, mut inner) = checked_application_slice(
402 content,
403 0,
404 tags::app_tag::SIGNED,
405 "AtomicReadFileAck record file-start-record",
406 )?;
407 let file_start_record = primitives::decode_signed(slice)?;
408 let (returned_record_count, new_inner) = checked_unsigned_u32(
409 content,
410 inner,
411 "AtomicReadFileAck record returned-record-count",
412 )?;
413 inner = new_inner;
414 if returned_record_count as usize > MAX_DECODED_ITEMS {
415 return Err(Error::decoding(0, "record count exceeds maximum"));
416 }
417 let mut file_record_data = Vec::new();
418 for i in 0..returned_record_count {
419 if inner >= content.len() {
420 return Err(Error::Reject {
421 reason: RejectReason::MISSING_REQUIRED_PARAMETER.to_raw(),
422 });
423 }
424 let (slice, new_inner) = checked_application_slice(
425 content,
426 inner,
427 tags::app_tag::OCTET_STRING,
428 &format!("AtomicReadFileAck record data[{i}]"),
429 )?;
430 file_record_data.push(slice.to_vec());
431 inner = new_inner;
432 }
433 if inner != content.len() {
434 return Err(Error::Reject {
435 reason: RejectReason::TOO_MANY_ARGUMENTS.to_raw(),
436 });
437 }
438 (
439 FileReadAckMethod::Record {
440 file_start_record,
441 returned_record_count,
442 file_record_data,
443 },
444 access_end,
445 )
446 } else {
447 return Err(Error::decoding(
448 offset,
449 "Unknown read file ACK access method",
450 ));
451 };
452 if access_end != data.len() {
453 return Err(Error::Reject {
454 reason: RejectReason::TOO_MANY_ARGUMENTS.to_raw(),
455 });
456 }
457
458 Ok(Self {
459 end_of_file,
460 access,
461 })
462 }
463}
464
465#[derive(Debug, Clone, PartialEq, Eq)]
471pub struct AtomicWriteFileAck {
472 pub access: FileWriteAckMethod,
473}
474
475#[derive(Debug, Clone, PartialEq, Eq)]
477pub enum FileWriteAckMethod {
478 Stream { file_start_position: i32 },
480 Record { file_start_record: i32 },
482}
483
484impl AtomicWriteFileAck {
485 pub fn encode(&self, buf: &mut BytesMut) {
486 match &self.access {
487 FileWriteAckMethod::Stream {
488 file_start_position,
489 } => {
490 primitives::encode_ctx_signed(buf, 0, *file_start_position);
491 }
492 FileWriteAckMethod::Record { file_start_record } => {
493 primitives::encode_ctx_signed(buf, 1, *file_start_record);
494 }
495 }
496 }
497
498 pub fn decode(data: &[u8]) -> Result<Self, Error> {
499 let (tag, pos) = tags::decode_tag(data, 0)?;
500 let end = pos + tag.length as usize;
501 if end > data.len() {
502 return Err(Error::buffer_too_short(end, data.len()));
503 }
504 let access = if tag.is_context(0) {
505 let file_start_position = primitives::decode_signed(&data[pos..end])?;
506 FileWriteAckMethod::Stream {
507 file_start_position,
508 }
509 } else if tag.is_context(1) {
510 let file_start_record = primitives::decode_signed(&data[pos..end])?;
511 FileWriteAckMethod::Record { file_start_record }
512 } else {
513 return Err(Error::decoding(0, "Unknown write file ACK access method"));
514 };
515
516 Ok(Self { access })
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523 use bacnet_types::enums::ObjectType;
524
525 fn file_oid() -> ObjectIdentifier {
526 ObjectIdentifier::new(ObjectType::FILE, 1).unwrap()
527 }
528
529 #[test]
530 fn atomic_read_stream_round_trip() {
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 decoded = AtomicReadFileRequest::decode(&buf).unwrap();
541 assert_eq!(decoded, req);
542 }
543
544 #[test]
545 fn atomic_read_record_round_trip() {
546 let req = AtomicReadFileRequest {
547 file_identifier: file_oid(),
548 access: FileAccessMethod::Record {
549 file_start_record: 5,
550 requested_record_count: 10,
551 },
552 };
553 let mut buf = BytesMut::new();
554 req.encode(&mut buf);
555 let decoded = AtomicReadFileRequest::decode(&buf).unwrap();
556 assert_eq!(decoded, req);
557 }
558
559 #[test]
560 fn atomic_write_stream_round_trip() {
561 let req = AtomicWriteFileRequest {
562 file_identifier: file_oid(),
563 access: FileWriteAccessMethod::Stream {
564 file_start_position: 100,
565 file_data: vec![0x01, 0x02, 0x03, 0x04],
566 },
567 };
568 let mut buf = BytesMut::new();
569 req.encode(&mut buf);
570 let decoded = AtomicWriteFileRequest::decode(&buf).unwrap();
571 assert_eq!(decoded, req);
572 }
573
574 #[test]
575 fn atomic_write_record_round_trip() {
576 let req = AtomicWriteFileRequest {
577 file_identifier: file_oid(),
578 access: FileWriteAccessMethod::Record {
579 file_start_record: 0,
580 record_count: 2,
581 file_record_data: vec![vec![0xAA, 0xBB], vec![0xCC, 0xDD]],
582 },
583 };
584 let mut buf = BytesMut::new();
585 req.encode(&mut buf);
586 let decoded = AtomicWriteFileRequest::decode(&buf).unwrap();
587 assert_eq!(decoded, req);
588 }
589
590 #[test]
595 fn test_decode_atomic_read_file_empty_input() {
596 assert!(AtomicReadFileRequest::decode(&[]).is_err());
597 }
598
599 #[test]
600 fn test_decode_atomic_read_file_truncated_1_byte() {
601 let req = AtomicReadFileRequest {
602 file_identifier: file_oid(),
603 access: FileAccessMethod::Stream {
604 file_start_position: 0,
605 requested_octet_count: 1024,
606 },
607 };
608 let mut buf = BytesMut::new();
609 req.encode(&mut buf);
610 assert!(AtomicReadFileRequest::decode(&buf[..1]).is_err());
611 }
612
613 #[test]
614 fn test_decode_atomic_read_file_truncated_3_bytes() {
615 let req = AtomicReadFileRequest {
616 file_identifier: file_oid(),
617 access: FileAccessMethod::Stream {
618 file_start_position: 0,
619 requested_octet_count: 1024,
620 },
621 };
622 let mut buf = BytesMut::new();
623 req.encode(&mut buf);
624 assert!(AtomicReadFileRequest::decode(&buf[..3]).is_err());
625 }
626
627 #[test]
628 fn test_decode_atomic_read_file_truncated_half() {
629 let req = AtomicReadFileRequest {
630 file_identifier: file_oid(),
631 access: FileAccessMethod::Stream {
632 file_start_position: 0,
633 requested_octet_count: 1024,
634 },
635 };
636 let mut buf = BytesMut::new();
637 req.encode(&mut buf);
638 let half = buf.len() / 2;
639 assert!(AtomicReadFileRequest::decode(&buf[..half]).is_err());
640 }
641
642 #[test]
643 fn test_decode_atomic_read_file_invalid_tag() {
644 assert!(AtomicReadFileRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
645 }
646
647 #[test]
648 fn test_decode_atomic_write_file_empty_input() {
649 assert!(AtomicWriteFileRequest::decode(&[]).is_err());
650 }
651
652 #[test]
653 fn test_decode_atomic_write_file_truncated_1_byte() {
654 let req = AtomicWriteFileRequest {
655 file_identifier: file_oid(),
656 access: FileWriteAccessMethod::Stream {
657 file_start_position: 100,
658 file_data: vec![0x01, 0x02, 0x03, 0x04],
659 },
660 };
661 let mut buf = BytesMut::new();
662 req.encode(&mut buf);
663 assert!(AtomicWriteFileRequest::decode(&buf[..1]).is_err());
664 }
665
666 #[test]
667 fn test_decode_atomic_write_file_truncated_3_bytes() {
668 let req = AtomicWriteFileRequest {
669 file_identifier: file_oid(),
670 access: FileWriteAccessMethod::Stream {
671 file_start_position: 100,
672 file_data: vec![0x01, 0x02, 0x03, 0x04],
673 },
674 };
675 let mut buf = BytesMut::new();
676 req.encode(&mut buf);
677 assert!(AtomicWriteFileRequest::decode(&buf[..3]).is_err());
678 }
679
680 #[test]
681 fn test_decode_atomic_write_file_truncated_half() {
682 let req = AtomicWriteFileRequest {
683 file_identifier: file_oid(),
684 access: FileWriteAccessMethod::Stream {
685 file_start_position: 100,
686 file_data: vec![0x01, 0x02, 0x03, 0x04],
687 },
688 };
689 let mut buf = BytesMut::new();
690 req.encode(&mut buf);
691 let half = buf.len() / 2;
692 assert!(AtomicWriteFileRequest::decode(&buf[..half]).is_err());
693 }
694
695 #[test]
696 fn test_decode_atomic_write_file_invalid_tag() {
697 assert!(AtomicWriteFileRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
698 }
699
700 #[test]
701 fn atomic_read_file_request_truncated_inner_tag() {
702 let data = [
706 0xC4, 0x02, 0x80, 0x00, 0x01, 0x0E,
709 0x35, 50, 0x01, 0x02, 0x0F,
713 ];
714 assert!(AtomicReadFileRequest::decode(&data).is_err());
715 }
716
717 #[test]
718 fn atomic_write_file_request_truncated_inner_tag() {
719 let data = [
721 0xC4, 0x02, 0x80, 0x00, 0x01, 0x0E, 0x35, 80, 0x01, 0x0F,
727 ];
728 assert!(AtomicWriteFileRequest::decode(&data).is_err());
729 }
730}
731
732#[cfg(test)]
733#[path = "file_width_tests.rs"]
734mod width_tests;
735
736#[cfg(test)]
737#[path = "file_ack_strict_tests.rs"]
738mod ack_strict_tests;
739
740#[cfg(test)]
741#[path = "file_ack_roundtrip_tests.rs"]
742mod ack_roundtrip_tests;