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::{DecodeError, DecodingKind, Error};
12use bacnet_types::primitives::ObjectIdentifier;
13use bytes::BytesMut;
14
15use crate::common::MAX_DECODED_ITEMS;
16
17/// A record's refusal naming its index, built without formatting (#1645,
18/// #1776): the shared reader's context names the list, and the index says
19/// which record.
20fn indexed_record(error: Error, index: u32) -> Error {
21    match (error, u16::try_from(index)) {
22        (Error::Decoding(refused), Ok(index)) => refused.at_item(index).into(),
23        (other, _) => other,
24    }
25}
26
27/// The application-tagged INTEGER at `offset` (a file-start-position or
28/// file-start-record) and the offset past it.
29fn decode_start(data: &[u8], offset: usize, what: &'static str) -> Result<(i32, usize), Error> {
30    let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::SIGNED, what)?;
31    Ok((primitives::decode_signed(octets)?, end))
32}
33
34/// The fault where `what`'s access-method choice, `[0]` or `[1]`, is due at
35/// `offset` and neither opens there: a missing member when the data ends
36/// after the file identifier, any other tag a wrong one.
37fn unknown_access_method(data: &[u8], offset: usize, what: &'static str) -> Error {
38    let refused = if offset >= data.len() {
39        DecodeError::new(DecodingKind::Missing, offset, "access method missing")
40    } else {
41        DecodeError::new(DecodingKind::InvalidTag, offset, "unknown access method")
42    };
43    refused.within(what).into()
44}
45
46// ---------------------------------------------------------------------------
47// AtomicReadFile-Request
48// ---------------------------------------------------------------------------
49
50/// AtomicReadFile-Request — stream or record access.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct AtomicReadFileRequest {
53    /// File object to read from; should be of object type File (not checked here).
54    pub file_identifier: ObjectIdentifier,
55    /// Stream or record access, with the position and amount requested.
56    pub access: FileAccessMethod,
57}
58
59/// AtomicWriteFile-Request.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct AtomicWriteFileRequest {
62    /// File object to write to; should be of object type File (not checked here).
63    pub file_identifier: ObjectIdentifier,
64    /// Stream or record access, with the data to write.
65    pub access: FileWriteAccessMethod,
66}
67
68/// File access method for reads.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum FileAccessMethod {
71    /// Stream access: file_start_position, requested_octet_count.
72    Stream {
73        /// Octet offset at which reading starts (0 is the first octet).
74        file_start_position: i32,
75        /// Maximum number of octets the responder should return.
76        requested_octet_count: u32,
77    },
78    /// Record access: file_start_record, requested_record_count.
79    Record {
80        /// Record number at which reading starts (0 is the first record).
81        file_start_record: i32,
82        /// Maximum number of records the responder should return.
83        requested_record_count: u32,
84    },
85}
86
87/// File access method for writes.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum FileWriteAccessMethod {
90    /// Stream access: `file_data` is written beginning at octet offset
91    /// `file_start_position`, or appended when that offset is -1.
92    Stream {
93        /// Octet offset at which writing starts, or -1 to append to the end of the file.
94        file_start_position: i32,
95        /// Octets to write.
96        file_data: Vec<u8>,
97    },
98    /// Record access: `record_count` records taken from `file_record_data` are
99    /// written beginning at record number `file_start_record`, or appended when
100    /// that record number is -1.
101    Record {
102        /// Record number at which writing starts, or -1 to append after the last record.
103        file_start_record: i32,
104        /// Number of records supplied in `file_record_data`. Encode writes it as given; decode
105        /// rejects a count that doesn't match the records.
106        record_count: u32,
107        /// Records to write, each one an opaque octet string.
108        file_record_data: Vec<Vec<u8>>,
109    },
110}
111
112impl AtomicReadFileRequest {
113    /// Encode the request parameters (Clause 14.1.2) into `buf`.
114    pub fn encode(&self, buf: &mut BytesMut) {
115        primitives::encode_app_object_id(buf, &self.file_identifier);
116        match &self.access {
117            FileAccessMethod::Stream {
118                file_start_position,
119                requested_octet_count,
120            } => {
121                tags::encode_opening_tag(buf, 0);
122                primitives::encode_app_signed(buf, *file_start_position);
123                primitives::encode_app_unsigned(buf, *requested_octet_count as u64);
124                tags::encode_closing_tag(buf, 0);
125            }
126            FileAccessMethod::Record {
127                file_start_record,
128                requested_record_count,
129            } => {
130                tags::encode_opening_tag(buf, 1);
131                primitives::encode_app_signed(buf, *file_start_record);
132                primitives::encode_app_unsigned(buf, *requested_record_count as u64);
133                tags::encode_closing_tag(buf, 1);
134            }
135        }
136    }
137
138    /// Decode the request from service-request octets; fails on malformed or truncated input,
139    /// on a member under any tag but its application tag, and on octets after the access
140    /// frame's members or after the frame.
141    pub fn decode(data: &[u8]) -> Result<Self, Error> {
142        let (file_identifier, offset) =
143            decode_app_object_id(data, 0, "AtomicReadFile file-identifier")?;
144
145        let (access, end) = if next_is_opening(data, offset, 0)? {
146            const WHAT: &str = "AtomicReadFile stream";
147            let (content, end) = decode_ctx_constructed(data, offset, 0, WHAT)?;
148            let (file_start_position, inner) =
149                decode_start(content, 0, "AtomicReadFile stream file-start-position")?;
150            let (requested_octet_count, inner) = decode_app_unsigned::<u32>(
151                content,
152                inner,
153                "AtomicReadFile stream requested-octet-count",
154            )?;
155            expect_end(content, inner, offset, WHAT)?;
156            let access = FileAccessMethod::Stream {
157                file_start_position,
158                requested_octet_count,
159            };
160            (access, end)
161        } else if next_is_opening(data, offset, 1)? {
162            const WHAT: &str = "AtomicReadFile record";
163            let (content, end) = decode_ctx_constructed(data, offset, 1, WHAT)?;
164            let (file_start_record, inner) =
165                decode_start(content, 0, "AtomicReadFile record file-start-record")?;
166            let (requested_record_count, inner) = decode_app_unsigned::<u32>(
167                content,
168                inner,
169                "AtomicReadFile record requested-record-count",
170            )?;
171            expect_end(content, inner, offset, WHAT)?;
172            let access = FileAccessMethod::Record {
173                file_start_record,
174                requested_record_count,
175            };
176            (access, end)
177        } else {
178            return Err(unknown_access_method(data, offset, "AtomicReadFile"));
179        };
180        expect_end(data, end, end, "AtomicReadFile")?;
181
182        Ok(Self {
183            file_identifier,
184            access,
185        })
186    }
187}
188
189impl AtomicWriteFileRequest {
190    /// Encode the request parameters (Clause 14.2.2) into `buf`.
191    pub fn encode(&self, buf: &mut BytesMut) {
192        primitives::encode_app_object_id(buf, &self.file_identifier);
193        match &self.access {
194            FileWriteAccessMethod::Stream {
195                file_start_position,
196                file_data,
197            } => {
198                tags::encode_opening_tag(buf, 0);
199                primitives::encode_app_signed(buf, *file_start_position);
200                primitives::encode_app_octet_string(buf, file_data);
201                tags::encode_closing_tag(buf, 0);
202            }
203            FileWriteAccessMethod::Record {
204                file_start_record,
205                record_count,
206                file_record_data,
207            } => {
208                tags::encode_opening_tag(buf, 1);
209                primitives::encode_app_signed(buf, *file_start_record);
210                primitives::encode_app_unsigned(buf, *record_count as u64);
211                for record in file_record_data {
212                    primitives::encode_app_octet_string(buf, record);
213                }
214                tags::encode_closing_tag(buf, 1);
215            }
216        }
217    }
218
219    /// Decode the request from service-request octets; fails on malformed or truncated input,
220    /// on a member under any tag but its application tag, and on octets after the stream
221    /// frame's file data or after the frame. In record access, a record list that doesn't
222    /// match its count is a Reject: MISSING_REQUIRED_PARAMETER when records are missing,
223    /// TOO_MANY_ARGUMENTS when anything follows the counted ones.
224    pub fn decode(data: &[u8]) -> Result<Self, Error> {
225        let (file_identifier, offset) =
226            decode_app_object_id(data, 0, "AtomicWriteFile file-identifier")?;
227
228        let (access, end) = if next_is_opening(data, offset, 0)? {
229            const WHAT: &str = "AtomicWriteFile stream";
230            let (content, end) = decode_ctx_constructed(data, offset, 0, WHAT)?;
231            let (file_start_position, inner) =
232                decode_start(content, 0, "AtomicWriteFile stream file-start-position")?;
233            let (file_data, inner) = decode_app_primitive(
234                content,
235                inner,
236                tags::app_tag::OCTET_STRING,
237                "AtomicWriteFile stream file-data",
238            )?;
239            expect_end(content, inner, offset, WHAT)?;
240            let access = FileWriteAccessMethod::Stream {
241                file_start_position,
242                file_data: file_data.to_vec(),
243            };
244            (access, end)
245        } else if next_is_opening(data, offset, 1)? {
246            let (content, end) = decode_ctx_constructed(data, offset, 1, "AtomicWriteFile record")?;
247            let (file_start_record, mut inner) =
248                decode_start(content, 0, "AtomicWriteFile record file-start-record")?;
249            let (record_count, new_inner) =
250                decode_app_unsigned::<u32>(content, inner, "AtomicWriteFile record record-count")?;
251            inner = new_inner;
252            if record_count as usize > MAX_DECODED_ITEMS {
253                return Err(Error::overflow(0, "record count exceeds maximum"));
254            }
255            let mut file_record_data = Vec::new();
256            for i in 0..record_count {
257                if inner >= content.len() {
258                    return Err(DecodeError::new(
259                        DecodingKind::Missing,
260                        inner,
261                        "AtomicWriteFile record: record {} of the count is missing",
262                    )
263                    .with_value(i.into())
264                    .into());
265                }
266                let (record, new_inner) = decode_app_primitive(
267                    content,
268                    inner,
269                    tags::app_tag::OCTET_STRING,
270                    "AtomicWriteFile record data",
271                )
272                .map_err(|error| indexed_record(error, i))?;
273                file_record_data.push(record.to_vec());
274                inner = new_inner;
275            }
276            expect_end(content, inner, inner, "AtomicWriteFile record")?;
277            let access = FileWriteAccessMethod::Record {
278                file_start_record,
279                record_count,
280                file_record_data,
281            };
282            (access, end)
283        } else {
284            return Err(unknown_access_method(data, offset, "AtomicWriteFile"));
285        };
286        expect_end(data, end, end, "AtomicWriteFile")?;
287
288        Ok(Self {
289            file_identifier,
290            access,
291        })
292    }
293}
294
295// ---------------------------------------------------------------------------
296// AtomicReadFile-ACK
297// ---------------------------------------------------------------------------
298
299/// AtomicReadFile-ACK — response for stream or record access.
300#[derive(Debug, Clone, PartialEq, Eq)]
301pub struct AtomicReadFileAck {
302    /// True when the returned data reaches the end of the file.
303    pub end_of_file: bool,
304    /// Stream or record data returned, with its starting position.
305    pub access: FileReadAckMethod,
306}
307
308/// Read-ACK access method.
309#[derive(Debug, Clone, PartialEq, Eq)]
310pub enum FileReadAckMethod {
311    /// Stream access: file_start_position + returned data.
312    Stream {
313        /// Octet offset within the file of the first returned octet.
314        file_start_position: i32,
315        /// Octets read from the file.
316        file_data: Vec<u8>,
317    },
318    /// Record access: file_start_record + returned records.
319    Record {
320        /// Record number of the first returned record.
321        file_start_record: i32,
322        /// Number of records returned in `file_record_data`.
323        returned_record_count: u32,
324        /// Records read from the file, each one an opaque octet string.
325        file_record_data: Vec<Vec<u8>>,
326    },
327}
328
329impl AtomicReadFileAck {
330    /// Encode the acknowledgment parameters into `buf`.
331    pub fn encode(&self, buf: &mut BytesMut) {
332        primitives::encode_app_boolean(buf, self.end_of_file);
333        match &self.access {
334            FileReadAckMethod::Stream {
335                file_start_position,
336                file_data,
337            } => {
338                tags::encode_opening_tag(buf, 0);
339                primitives::encode_app_signed(buf, *file_start_position);
340                primitives::encode_app_octet_string(buf, file_data);
341                tags::encode_closing_tag(buf, 0);
342            }
343            FileReadAckMethod::Record {
344                file_start_record,
345                returned_record_count,
346                file_record_data,
347            } => {
348                tags::encode_opening_tag(buf, 1);
349                primitives::encode_app_signed(buf, *file_start_record);
350                primitives::encode_app_unsigned(buf, *returned_record_count as u64);
351                for record in file_record_data {
352                    primitives::encode_app_octet_string(buf, record);
353                }
354                tags::encode_closing_tag(buf, 1);
355            }
356        }
357    }
358
359    /// Decode the acknowledgment from its service-ack octets; fails on malformed or truncated
360    /// input.
361    pub fn decode(data: &[u8]) -> Result<Self, Error> {
362        let mut offset = 0;
363
364        let (tag, pos) = tags::decode_tag(data, offset)?;
365        if tag.class != tags::TagClass::Application
366            || tag.number != tags::app_tag::BOOLEAN
367            || tag.is_opening
368            || tag.is_closing
369            || data[offset] & 0x07 > 1
370        {
371            return Err(Error::decoding(
372                offset,
373                "AtomicReadFileAck expected application Boolean end-of-file",
374            ));
375        }
376        let end_of_file = tag.length != 0;
377        offset = pos;
378
379        let (tag, tag_end) = tags::decode_tag(data, offset)?;
380        let (access, access_end) = if tag.is_opening_tag(0) {
381            let (content, access_end) = tags::extract_context_value(data, tag_end, 0)?;
382            let (file_start_position, inner) =
383                decode_start(content, 0, "AtomicReadFileAck stream file-start-position")?;
384            let (slice, inner) = decode_app_primitive(
385                content,
386                inner,
387                tags::app_tag::OCTET_STRING,
388                "AtomicReadFileAck stream file-data",
389            )?;
390            expect_end(content, inner, inner, "AtomicReadFileAck stream")?;
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 (file_start_record, mut inner) =
402                decode_start(content, 0, "AtomicReadFileAck record file-start-record")?;
403            let (returned_record_count, new_inner) = decode_app_unsigned::<u32>(
404                content,
405                inner,
406                "AtomicReadFileAck record returned-record-count",
407            )?;
408            inner = new_inner;
409            if returned_record_count as usize > MAX_DECODED_ITEMS {
410                return Err(Error::overflow(0, "record count exceeds maximum"));
411            }
412            let mut file_record_data = Vec::new();
413            for i in 0..returned_record_count {
414                if inner >= content.len() {
415                    return Err(DecodeError::new(
416                        DecodingKind::Missing,
417                        inner,
418                        "AtomicReadFileAck record: record {} of the count is missing",
419                    )
420                    .with_value(i.into())
421                    .into());
422                }
423                let (slice, new_inner) = decode_app_primitive(
424                    content,
425                    inner,
426                    tags::app_tag::OCTET_STRING,
427                    "AtomicReadFileAck record data",
428                )
429                .map_err(|error| indexed_record(error, i))?;
430                file_record_data.push(slice.to_vec());
431                inner = new_inner;
432            }
433            expect_end(content, inner, inner, "AtomicReadFileAck record")?;
434            (
435                FileReadAckMethod::Record {
436                    file_start_record,
437                    returned_record_count,
438                    file_record_data,
439                },
440                access_end,
441            )
442        } else {
443            return Err(Error::decoding(
444                offset,
445                "Unknown read file ACK access method",
446            ));
447        };
448        expect_end(data, access_end, access_end, "AtomicReadFileAck")?;
449
450        Ok(Self {
451            end_of_file,
452            access,
453        })
454    }
455}
456
457// ---------------------------------------------------------------------------
458// AtomicWriteFile-ACK
459// ---------------------------------------------------------------------------
460
461/// AtomicWriteFile-ACK — response for stream or record access.
462#[derive(Debug, Clone, PartialEq, Eq)]
463pub struct AtomicWriteFileAck {
464    /// Stream or record form of the acknowledgment, echoing where the write took place.
465    pub access: FileWriteAckMethod,
466}
467
468/// Write-ACK access method.
469#[derive(Debug, Clone, PartialEq, Eq)]
470pub enum FileWriteAckMethod {
471    /// Stream: confirmed file_start_position.
472    Stream {
473        /// Octet offset at which the data was actually written.
474        file_start_position: i32,
475    },
476    /// Record: confirmed file_start_record.
477    Record {
478        /// Record number at which the records were actually written.
479        file_start_record: i32,
480    },
481}
482
483impl AtomicWriteFileAck {
484    /// Encode the acknowledgment parameters into `buf`.
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    /// Decode the acknowledgment from its service-ack octets; fails on malformed or truncated
499    /// input and on octets after the choice.
500    pub fn decode(data: &[u8]) -> Result<Self, Error> {
501        let (access, end) = if next_is_context(data, 0, 0)? {
502            let (octets, end) =
503                decode_ctx_primitive(data, 0, 0, "AtomicWriteFileAck file-start-position")?;
504            let access = FileWriteAckMethod::Stream {
505                file_start_position: primitives::decode_signed(octets)?,
506            };
507            (access, end)
508        } else if next_is_context(data, 0, 1)? {
509            let (octets, end) =
510                decode_ctx_primitive(data, 0, 1, "AtomicWriteFileAck file-start-record")?;
511            let access = FileWriteAckMethod::Record {
512                file_start_record: primitives::decode_signed(octets)?,
513            };
514            (access, end)
515        } else {
516            return Err(Error::decoding(0, "Unknown write file ACK access method"));
517        };
518        expect_end(data, end, end, "AtomicWriteFileAck")?;
519
520        Ok(Self { access })
521    }
522}
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527    use bacnet_types::enums::ObjectType;
528
529    fn file_oid() -> ObjectIdentifier {
530        ObjectIdentifier::new(ObjectType::FILE, 1).unwrap()
531    }
532
533    #[test]
534    fn atomic_read_stream_round_trip() {
535        let req = AtomicReadFileRequest {
536            file_identifier: file_oid(),
537            access: FileAccessMethod::Stream {
538                file_start_position: 0,
539                requested_octet_count: 1024,
540            },
541        };
542        let mut buf = BytesMut::new();
543        req.encode(&mut buf);
544        let decoded = AtomicReadFileRequest::decode(&buf).unwrap();
545        assert_eq!(decoded, req);
546    }
547
548    #[test]
549    fn atomic_read_record_round_trip() {
550        let req = AtomicReadFileRequest {
551            file_identifier: file_oid(),
552            access: FileAccessMethod::Record {
553                file_start_record: 5,
554                requested_record_count: 10,
555            },
556        };
557        let mut buf = BytesMut::new();
558        req.encode(&mut buf);
559        let decoded = AtomicReadFileRequest::decode(&buf).unwrap();
560        assert_eq!(decoded, req);
561    }
562
563    #[test]
564    fn atomic_write_stream_round_trip() {
565        let req = AtomicWriteFileRequest {
566            file_identifier: file_oid(),
567            access: FileWriteAccessMethod::Stream {
568                file_start_position: 100,
569                file_data: vec![0x01, 0x02, 0x03, 0x04],
570            },
571        };
572        let mut buf = BytesMut::new();
573        req.encode(&mut buf);
574        let decoded = AtomicWriteFileRequest::decode(&buf).unwrap();
575        assert_eq!(decoded, req);
576    }
577
578    #[test]
579    fn atomic_write_record_round_trip() {
580        let req = AtomicWriteFileRequest {
581            file_identifier: file_oid(),
582            access: FileWriteAccessMethod::Record {
583                file_start_record: 0,
584                record_count: 2,
585                file_record_data: vec![vec![0xAA, 0xBB], vec![0xCC, 0xDD]],
586            },
587        };
588        let mut buf = BytesMut::new();
589        req.encode(&mut buf);
590        let decoded = AtomicWriteFileRequest::decode(&buf).unwrap();
591        assert_eq!(decoded, req);
592    }
593
594    // -----------------------------------------------------------------------
595    // Malformed-input decode error tests
596    // -----------------------------------------------------------------------
597
598    #[test]
599    fn test_decode_atomic_read_file_empty_input() {
600        assert!(AtomicReadFileRequest::decode(&[]).is_err());
601    }
602
603    #[test]
604    fn test_decode_atomic_read_file_truncated_1_byte() {
605        let req = AtomicReadFileRequest {
606            file_identifier: file_oid(),
607            access: FileAccessMethod::Stream {
608                file_start_position: 0,
609                requested_octet_count: 1024,
610            },
611        };
612        let mut buf = BytesMut::new();
613        req.encode(&mut buf);
614        assert!(AtomicReadFileRequest::decode(&buf[..1]).is_err());
615    }
616
617    #[test]
618    fn test_decode_atomic_read_file_truncated_3_bytes() {
619        let req = AtomicReadFileRequest {
620            file_identifier: file_oid(),
621            access: FileAccessMethod::Stream {
622                file_start_position: 0,
623                requested_octet_count: 1024,
624            },
625        };
626        let mut buf = BytesMut::new();
627        req.encode(&mut buf);
628        assert!(AtomicReadFileRequest::decode(&buf[..3]).is_err());
629    }
630
631    #[test]
632    fn test_decode_atomic_read_file_truncated_half() {
633        let req = AtomicReadFileRequest {
634            file_identifier: file_oid(),
635            access: FileAccessMethod::Stream {
636                file_start_position: 0,
637                requested_octet_count: 1024,
638            },
639        };
640        let mut buf = BytesMut::new();
641        req.encode(&mut buf);
642        let half = buf.len() / 2;
643        assert!(AtomicReadFileRequest::decode(&buf[..half]).is_err());
644    }
645
646    #[test]
647    fn test_decode_atomic_read_file_invalid_tag() {
648        assert!(AtomicReadFileRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
649    }
650
651    #[test]
652    fn test_decode_atomic_write_file_empty_input() {
653        assert!(AtomicWriteFileRequest::decode(&[]).is_err());
654    }
655
656    #[test]
657    fn test_decode_atomic_write_file_truncated_1_byte() {
658        let req = AtomicWriteFileRequest {
659            file_identifier: file_oid(),
660            access: FileWriteAccessMethod::Stream {
661                file_start_position: 100,
662                file_data: vec![0x01, 0x02, 0x03, 0x04],
663            },
664        };
665        let mut buf = BytesMut::new();
666        req.encode(&mut buf);
667        assert!(AtomicWriteFileRequest::decode(&buf[..1]).is_err());
668    }
669
670    #[test]
671    fn test_decode_atomic_write_file_truncated_3_bytes() {
672        let req = AtomicWriteFileRequest {
673            file_identifier: file_oid(),
674            access: FileWriteAccessMethod::Stream {
675                file_start_position: 100,
676                file_data: vec![0x01, 0x02, 0x03, 0x04],
677            },
678        };
679        let mut buf = BytesMut::new();
680        req.encode(&mut buf);
681        assert!(AtomicWriteFileRequest::decode(&buf[..3]).is_err());
682    }
683
684    #[test]
685    fn test_decode_atomic_write_file_truncated_half() {
686        let req = AtomicWriteFileRequest {
687            file_identifier: file_oid(),
688            access: FileWriteAccessMethod::Stream {
689                file_start_position: 100,
690                file_data: vec![0x01, 0x02, 0x03, 0x04],
691            },
692        };
693        let mut buf = BytesMut::new();
694        req.encode(&mut buf);
695        let half = buf.len() / 2;
696        assert!(AtomicWriteFileRequest::decode(&buf[..half]).is_err());
697    }
698
699    #[test]
700    fn test_decode_atomic_write_file_invalid_tag() {
701        assert!(AtomicWriteFileRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
702    }
703
704    #[test]
705    fn atomic_read_file_request_truncated_inner_tag() {
706        // Craft a packet where extract_context_value succeeds but inner tag within
707        // the content claims more bytes than the content slice contains.
708        // Application signed tag (tag 3), lvt=5 (extended length), length=50 → only 2 bytes present.
709        let data = [
710            0xC4, 0x02, 0x80, 0x00, 0x01, // object identifier (FILE:1)
711            // Opening tag [0]
712            0x0E,
713            // App signed tag (tag 3=0x30), extended len (lvt=5 → 0x05): 0x35, len byte: 50
714            0x35, 50, // Only 2 data bytes instead of 50
715            0x01, 0x02, // Closing tag [0]
716            0x0F,
717        ];
718        assert!(AtomicReadFileRequest::decode(&data).is_err());
719    }
720
721    #[test]
722    fn atomic_write_file_request_truncated_inner_tag() {
723        // Same technique for AtomicWriteFile: inner tag claims too many bytes
724        let data = [
725            0xC4, 0x02, 0x80, 0x00, 0x01, // object identifier (FILE:1)
726            // Opening tag [0]
727            0x0E, // App signed tag, extended len, claims 80 bytes
728            0x35, 80,   // Only 1 data byte instead of 80
729            0x01, // Closing tag [0]
730            0x0F,
731        ];
732        assert!(AtomicWriteFileRequest::decode(&data).is_err());
733    }
734}
735
736#[cfg(test)]
737#[path = "file_width_tests.rs"]
738mod width_tests;
739
740#[cfg(test)]
741#[path = "file_ack_strict_tests.rs"]
742mod ack_strict_tests;
743
744#[cfg(test)]
745#[path = "file_ack_roundtrip_tests.rs"]
746mod ack_roundtrip_tests;
747#[path = "file_ack_size.rs"]
748mod ack_size;