Skip to main content

bacnet_services/
file.rs

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