Skip to main content

bacnet_services/
file.rs

1//! AtomicReadFile / AtomicWriteFile services per ASHRAE 135-2020 Clauses 15.1–15.2.
2
3use 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
10/// Decode a tag and validate the resulting slice bounds.
11fn 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// ---------------------------------------------------------------------------
76// AtomicReadFile-Request
77// ---------------------------------------------------------------------------
78
79/// AtomicReadFile-Request — stream or record access.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct AtomicReadFileRequest {
82    pub file_identifier: ObjectIdentifier,
83    pub access: FileAccessMethod,
84}
85
86/// AtomicWriteFile-Request.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct AtomicWriteFileRequest {
89    pub file_identifier: ObjectIdentifier,
90    pub access: FileWriteAccessMethod,
91}
92
93/// File access method for reads.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum FileAccessMethod {
96    /// Stream access: file_start_position, requested_octet_count.
97    Stream {
98        file_start_position: i32,
99        requested_octet_count: u32,
100    },
101    /// Record access: file_start_record, requested_record_count.
102    Record {
103        file_start_record: i32,
104        requested_record_count: u32,
105    },
106}
107
108/// File access method for writes.
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum FileWriteAccessMethod {
111    /// Stream access: file_start_position, file_data.
112    Stream {
113        file_start_position: i32,
114        file_data: Vec<u8>,
115    },
116    /// Record access: file_start_record, record_count, file_record_data.
117    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// ---------------------------------------------------------------------------
297// AtomicReadFile-ACK
298// ---------------------------------------------------------------------------
299
300/// AtomicReadFile-ACK — response for stream or record access.
301#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct AtomicReadFileAck {
303    pub end_of_file: bool,
304    pub access: FileReadAckMethod,
305}
306
307/// Read-ACK access method.
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub enum FileReadAckMethod {
310    /// Stream access: file_start_position + returned data.
311    Stream {
312        file_start_position: i32,
313        file_data: Vec<u8>,
314    },
315    /// Record access: file_start_record + returned records.
316    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// ---------------------------------------------------------------------------
466// AtomicWriteFile-ACK
467// ---------------------------------------------------------------------------
468
469/// AtomicWriteFile-ACK — response for stream or record access.
470#[derive(Debug, Clone, PartialEq, Eq)]
471pub struct AtomicWriteFileAck {
472    pub access: FileWriteAckMethod,
473}
474
475/// Write-ACK access method.
476#[derive(Debug, Clone, PartialEq, Eq)]
477pub enum FileWriteAckMethod {
478    /// Stream: confirmed file_start_position.
479    Stream { file_start_position: i32 },
480    /// Record: confirmed file_start_record.
481    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    // -----------------------------------------------------------------------
591    // Malformed-input decode error tests
592    // -----------------------------------------------------------------------
593
594    #[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        // Craft a packet where extract_context_value succeeds but inner tag within
703        // the content claims more bytes than the content slice contains.
704        // Application signed tag (tag 3), lvt=5 (extended length), length=50 → only 2 bytes present.
705        let data = [
706            0xC4, 0x02, 0x80, 0x00, 0x01, // object identifier (FILE:1)
707            // Opening tag [0]
708            0x0E,
709            // App signed tag (tag 3=0x30), extended len (lvt=5 → 0x05): 0x35, len byte: 50
710            0x35, 50, // Only 2 data bytes instead of 50
711            0x01, 0x02, // Closing tag [0]
712            0x0F,
713        ];
714        assert!(AtomicReadFileRequest::decode(&data).is_err());
715    }
716
717    #[test]
718    fn atomic_write_file_request_truncated_inner_tag() {
719        // Same technique for AtomicWriteFile: inner tag claims too many bytes
720        let data = [
721            0xC4, 0x02, 0x80, 0x00, 0x01, // object identifier (FILE:1)
722            // Opening tag [0]
723            0x0E, // App signed tag, extended len, claims 80 bytes
724            0x35, 80,   // Only 1 data byte instead of 80
725            0x01, // Closing tag [0]
726            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;