Skip to main content

bacnet_services/
file.rs

1//! AtomicReadFile / AtomicWriteFile services per ASHRAE 135-2020 Clauses 14.1–14.2.
2
3#[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
17/// The application-tagged INTEGER at `offset` (a file-start-position or
18/// file-start-record) and the offset past it.
19fn 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
24/// The fault where `what`'s access-method choice, `[0]` or `[1]`, is due at
25/// `offset` and neither opens there: a missing member when the data ends
26/// after the file identifier, any other tag a wrong one.
27fn 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// ---------------------------------------------------------------------------
36// AtomicReadFile-Request
37// ---------------------------------------------------------------------------
38
39/// AtomicReadFile-Request — stream or record access.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct AtomicReadFileRequest {
42    /// File object to read from; should be of object type File (not checked here).
43    pub file_identifier: ObjectIdentifier,
44    /// Stream or record access, with the position and amount requested.
45    pub access: FileAccessMethod,
46}
47
48/// AtomicWriteFile-Request.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct AtomicWriteFileRequest {
51    /// File object to write to; should be of object type File (not checked here).
52    pub file_identifier: ObjectIdentifier,
53    /// Stream or record access, with the data to write.
54    pub access: FileWriteAccessMethod,
55}
56
57/// File access method for reads.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum FileAccessMethod {
60    /// Stream access: file_start_position, requested_octet_count.
61    Stream {
62        /// Octet offset at which reading starts (0 is the first octet).
63        file_start_position: i32,
64        /// Maximum number of octets the responder should return.
65        requested_octet_count: u32,
66    },
67    /// Record access: file_start_record, requested_record_count.
68    Record {
69        /// Record number at which reading starts (0 is the first record).
70        file_start_record: i32,
71        /// Maximum number of records the responder should return.
72        requested_record_count: u32,
73    },
74}
75
76/// File access method for writes.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum FileWriteAccessMethod {
79    /// Stream access: `file_data` is written beginning at octet offset
80    /// `file_start_position`, or appended when that offset is -1.
81    Stream {
82        /// Octet offset at which writing starts, or -1 to append to the end of the file.
83        file_start_position: i32,
84        /// Octets to write.
85        file_data: Vec<u8>,
86    },
87    /// Record access: `record_count` records taken from `file_record_data` are
88    /// written beginning at record number `file_start_record`, or appended when
89    /// that record number is -1.
90    Record {
91        /// Record number at which writing starts, or -1 to append after the last record.
92        file_start_record: i32,
93        /// Number of records supplied in `file_record_data`. Encode writes it as given; decode
94        /// rejects a count that doesn't match the records.
95        record_count: u32,
96        /// Records to write, each one an opaque octet string.
97        file_record_data: Vec<Vec<u8>>,
98    },
99}
100
101impl AtomicReadFileRequest {
102    /// Encode the request parameters (Clause 14.1.2) into `buf`.
103    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    /// Decode the request from service-request octets; fails on malformed or truncated input,
128    /// on a member under any tag but its application tag, and on octets after the access
129    /// frame's members or after the frame.
130    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    /// Encode the request parameters (Clause 14.2.2) into `buf`.
180    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    /// Decode the request from service-request octets; fails on malformed or truncated input,
209    /// on a member under any tag but its application tag, and on octets after the stream
210    /// frame's file data or after the frame. In record access, a record list that doesn't
211    /// match its count is a Reject: MISSING_REQUIRED_PARAMETER when records are missing,
212    /// TOO_MANY_ARGUMENTS when anything follows the counted ones.
213    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                    // Formatted only if the record fails to decode (#1645).
257                    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// ---------------------------------------------------------------------------
282// AtomicReadFile-ACK
283// ---------------------------------------------------------------------------
284
285/// AtomicReadFile-ACK — response for stream or record access.
286#[derive(Debug, Clone, PartialEq, Eq)]
287pub struct AtomicReadFileAck {
288    /// True when the returned data reaches the end of the file.
289    pub end_of_file: bool,
290    /// Stream or record data returned, with its starting position.
291    pub access: FileReadAckMethod,
292}
293
294/// Read-ACK access method.
295#[derive(Debug, Clone, PartialEq, Eq)]
296pub enum FileReadAckMethod {
297    /// Stream access: file_start_position + returned data.
298    Stream {
299        /// Octet offset within the file of the first returned octet.
300        file_start_position: i32,
301        /// Octets read from the file.
302        file_data: Vec<u8>,
303    },
304    /// Record access: file_start_record + returned records.
305    Record {
306        /// Record number of the first returned record.
307        file_start_record: i32,
308        /// Number of records returned in `file_record_data`.
309        returned_record_count: u32,
310        /// Records read from the file, each one an opaque octet string.
311        file_record_data: Vec<Vec<u8>>,
312    },
313}
314
315impl AtomicReadFileAck {
316    /// Encode the acknowledgment parameters into `buf`.
317    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    /// Decode the acknowledgment from its service-ack octets; fails on malformed or truncated
346    /// input.
347    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                    // Formatted only if the record fails to decode (#1645).
411                    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// ---------------------------------------------------------------------------
441// AtomicWriteFile-ACK
442// ---------------------------------------------------------------------------
443
444/// AtomicWriteFile-ACK — response for stream or record access.
445#[derive(Debug, Clone, PartialEq, Eq)]
446pub struct AtomicWriteFileAck {
447    /// Stream or record form of the acknowledgment, echoing where the write took place.
448    pub access: FileWriteAckMethod,
449}
450
451/// Write-ACK access method.
452#[derive(Debug, Clone, PartialEq, Eq)]
453pub enum FileWriteAckMethod {
454    /// Stream: confirmed file_start_position.
455    Stream {
456        /// Octet offset at which the data was actually written.
457        file_start_position: i32,
458    },
459    /// Record: confirmed file_start_record.
460    Record {
461        /// Record number at which the records were actually written.
462        file_start_record: i32,
463    },
464}
465
466impl AtomicWriteFileAck {
467    /// Encode the acknowledgment parameters into `buf`.
468    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    /// Decode the acknowledgment from its service-ack octets; fails on malformed or truncated
482    /// input and on octets after the choice.
483    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    // -----------------------------------------------------------------------
578    // Malformed-input decode error tests
579    // -----------------------------------------------------------------------
580
581    #[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        // Craft a packet where extract_context_value succeeds but inner tag within
690        // the content claims more bytes than the content slice contains.
691        // Application signed tag (tag 3), lvt=5 (extended length), length=50 → only 2 bytes present.
692        let data = [
693            0xC4, 0x02, 0x80, 0x00, 0x01, // object identifier (FILE:1)
694            // Opening tag [0]
695            0x0E,
696            // App signed tag (tag 3=0x30), extended len (lvt=5 → 0x05): 0x35, len byte: 50
697            0x35, 50, // Only 2 data bytes instead of 50
698            0x01, 0x02, // Closing tag [0]
699            0x0F,
700        ];
701        assert!(AtomicReadFileRequest::decode(&data).is_err());
702    }
703
704    #[test]
705    fn atomic_write_file_request_truncated_inner_tag() {
706        // Same technique for AtomicWriteFile: inner tag claims too many bytes
707        let data = [
708            0xC4, 0x02, 0x80, 0x00, 0x01, // object identifier (FILE:1)
709            // Opening tag [0]
710            0x0E, // App signed tag, extended len, claims 80 bytes
711            0x35, 80,   // Only 1 data byte instead of 80
712            0x01, // Closing tag [0]
713            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;