Skip to main content

bacnet_objects/
file.rs

1//! File (type 10) object per ASHRAE 135-2020 Clause 12.13.
2//!
3//! Backs AtomicReadFile and AtomicWriteFile services. Supports both
4//! stream-access and record-access modes.
5
6use bacnet_types::enums::{
7    ErrorClass, ErrorCode, FileAccessMethod, ObjectType, PropertyIdentifier,
8};
9use bacnet_types::error::Error;
10use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
11use std::borrow::Cow;
12use std::sync::Arc;
13
14use crate::clock::ClockReader;
15use crate::common::{self, read_common_properties};
16use crate::traits::{BACnetObject, WritePropertyRollback};
17
18mod resize;
19
20// ---------------------------------------------------------------------------
21// File storage (the Clause 14 service data behind a File object)
22// ---------------------------------------------------------------------------
23
24/// Default growth cap, in octets, for network writes to one File object.
25///
26/// Clause 14.2 requires a write whose 'File Start Position' exceeds the
27/// file size to extend the file to that size, and the position is a signed
28/// 32-bit INTEGER, so an unbounded implementation would zero-fill up to
29/// 2 GiB from one small request. Clause 18 defines FILE_FULL for exactly
30/// this bound: "when a File Object becomes filled to a designed limit, as
31/// opposed to a No Space Available / Out of Memory situation".
32pub const DEFAULT_MAX_FILE_SIZE: u64 = 1_048_576;
33
34/// Default growth cap, in records, for network writes to one record-access
35/// File object.
36///
37/// Bounds the record vector independently of its payload octets, which
38/// [`DEFAULT_MAX_FILE_SIZE`] bounds: extending to a far record index costs
39/// a `Vec` header per intervening empty record even when no octet is
40/// stored. The value is at most `bacnet_services::common::MAX_DECODED_ITEMS`
41/// — today exactly that ceiling — the largest SEQUENCE OF the workspace
42/// decoders accept in one ACK, so a record file read back at the cap still
43/// decodes on the client side.
44pub const DEFAULT_MAX_RECORD_COUNT: u64 = 10_000;
45
46/// Ceiling for [`FileObject::set_max_record_count`]; see
47/// [`DEFAULT_MAX_RECORD_COUNT`] for why it is the decoder's limit.
48const MAX_RECORD_CAP: u64 = DEFAULT_MAX_RECORD_COUNT;
49
50/// Trusted local configuration for the built-in in-memory File payload.
51///
52/// This narrow capability is separate from [`FileStorage`]: configuration may
53/// preload data regardless of `Read_Only` or the later network-write growth
54/// caps, while AtomicWriteFile continues to use `FileStorage` and enforce both.
55/// Payload operations reject a shape that does not match the active
56/// `File_Access_Method` before mutating the object.
57#[doc(hidden)]
58pub trait FileConfiguration: Send + Sync {
59    /// Select the active File access method without converting either payload.
60    fn set_access_method(&mut self, method: FileAccessMethod);
61
62    /// Replace the stream payload through the File object's accounting path.
63    fn set_stream_data(&mut self, data: Vec<u8>) -> Result<(), Error>;
64
65    /// Borrow the stream payload when stream access is active.
66    fn stream_data(&self) -> Result<&[u8], Error>;
67
68    /// Replace the record payload through the File object's accounting path.
69    fn set_record_data(&mut self, records: Vec<Vec<u8>>) -> Result<(), Error>;
70
71    /// Borrow the record payload when record access is active.
72    fn record_data(&self) -> Result<&[Vec<u8>], Error>;
73
74    /// Set and return the effective octet growth cap.
75    fn set_max_file_size(&mut self, max_octets: u64) -> u64;
76
77    /// Set and return the effective record-count growth cap.
78    fn set_max_record_count(&mut self, max_records: u64) -> u64;
79}
80
81/// One stream-access read window, as AtomicReadFile returns it.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct FileStreamRead {
84    /// The octets read: never more than requested, fewer when the file
85    /// ends first (Clause 14.1 Service Procedure).
86    pub data: Vec<u8>,
87    /// Clause 14.1.3.1 'End Of File'. The built-in empty file reports EOF at
88    /// its only valid start; an empty window in a non-empty file does not.
89    pub end_of_file: bool,
90}
91
92/// One record-access read window, as AtomicReadFile returns it.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct FileRecordRead {
95    /// The records read. Its length is the ACK's 'Returned Record Count'
96    /// (Clause 14.1.3.3), which may be less than the requested count.
97    pub records: Vec<Vec<u8>>,
98    /// 'End Of File' per the Clause 14.1 Service Procedure. The built-in
99    /// empty record file reports EOF at its only valid start.
100    pub end_of_file: bool,
101}
102
103/// Where an AtomicWriteFile write starts.
104///
105/// Clauses 14.2.2.2 and 14.2.2.3 give 'File Start Position' and 'File
106/// Start Record' the special value -1 for "an append to file operation";
107/// every other value is an offset from the beginning of the file.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum FileWriteStart {
110    /// Write at this octet or record offset, extending the file first if
111    /// the offset is past the current end (Clause 14.2 Service Procedure).
112    At(u64),
113    /// Write at the current end of the file.
114    Append,
115}
116
117/// The octet- and record-addressed storage behind a File object.
118///
119/// This is the **internal** channel AtomicReadFile and AtomicWriteFile use
120/// to reach file contents (Clauses 14.1 and 14.2). It is deliberately not a
121/// property: Table 12-16 (Clause 12.13) defines no File Data property, so
122/// file contents have no network route other than the File Access Services,
123/// and nothing here appears in `Property_List`.
124///
125/// The server reaches an implementation through
126/// [`BACnetObject::file_storage_internal`] and
127/// [`BACnetObject::file_storage_internal_mut`]. Every method has a default
128/// that refuses with SERVICES / FILE_ACCESS_DENIED — there is no
129/// implementation behind it, so the file is "otherwise not accessible" in
130/// Clause 18's words — and a stream-only implementation overrides only the
131/// two stream methods.
132///
133/// Error contract, using the Clause 14 pairs:
134///
135/// - SERVICES / INVALID_FILE_START_POSITION when a read starts past the
136///   current end (Clause 14.1 Service Procedure). Reading *at* the end is
137///   legal and yields an empty window. The built-in empty file reports
138///   `end_of_file` TRUE there; non-empty files report FALSE for such a window.
139/// - OBJECT / FILE_FULL when a write would grow the file past the
140///   implementation's designed limit (Clause 14.2.4.1; Clause 18).
141/// - SERVICES / INVALID_FILE_ACCESS_METHOD when the method does not match
142///   the object's `File_Access_Method`; only a genuine mismatch, never a
143///   missing implementation, reports this.
144/// - SERVICES / FILE_ACCESS_DENIED from the defaults above.
145///
146/// A write that returns `Err` must leave the storage unchanged: the service
147/// fails "in its entirety" (Clause 14.2.4), and the server encodes no ACK on
148/// the error path. Resolved write positions must fit the ACK's INTEGER, so
149/// implementations keep their limit at or below `i32::MAX`.
150pub trait FileStorage: Send + Sync {
151    /// Read up to `count` octets starting `start` octets into the file.
152    fn read_stream(&self, _start: u64, _count: u64) -> Result<FileStreamRead, Error> {
153        Err(file_access_denied())
154    }
155
156    /// Write `data` at `start`, extending the file first when `start` is
157    /// past the end; intervening octets are a local matter. Returns the
158    /// octet offset the data was written at, which for
159    /// [`FileWriteStart::Append`] is the previous file size.
160    fn write_stream(&mut self, _start: FileWriteStart, _data: &[u8]) -> Result<u64, Error> {
161        Err(file_access_denied())
162    }
163
164    /// Read up to `count` records starting at record `start`.
165    fn read_records(&self, _start: u64, _count: u64) -> Result<FileRecordRead, Error> {
166        Err(file_access_denied())
167    }
168
169    /// Write `records` starting at record `start`, replacing records in
170    /// place and extending the file first when `start` is past the end.
171    /// Returns the record index the data was written at.
172    fn write_records(
173        &mut self,
174        _start: FileWriteStart,
175        _records: &[Vec<u8>],
176    ) -> Result<u64, Error> {
177        Err(file_access_denied())
178    }
179}
180
181fn invalid_file_access_method() -> Error {
182    common::protocol_error(ErrorClass::SERVICES, ErrorCode::INVALID_FILE_ACCESS_METHOD)
183}
184
185fn file_access_denied() -> Error {
186    common::protocol_error(ErrorClass::SERVICES, ErrorCode::FILE_ACCESS_DENIED)
187}
188
189fn invalid_file_start_position() -> Error {
190    common::protocol_error(ErrorClass::SERVICES, ErrorCode::INVALID_FILE_START_POSITION)
191}
192
193fn file_full() -> Error {
194    common::protocol_error(ErrorClass::OBJECT, ErrorCode::FILE_FULL)
195}
196
197/// Resolved positions travel in the ACK as a BACnet INTEGER; caps above
198/// `i32::MAX` could produce one that does not fit.
199const MAX_REPRESENTABLE: u64 = i32::MAX as u64;
200
201fn octet_total(records: &[Vec<u8>]) -> u64 {
202    records.iter().map(|r| r.len() as u64).sum()
203}
204
205// ---------------------------------------------------------------------------
206// FileObject (type 10)
207// ---------------------------------------------------------------------------
208
209/// BACnet File object.
210///
211/// Represents a file accessible via AtomicReadFile / AtomicWriteFile services.
212/// The `file_access_method` determines whether the file is accessed as a
213/// byte stream ([`FileAccessMethod::STREAM_ACCESS`]) or as a sequence of
214/// fixed-length records ([`FileAccessMethod::RECORD_ACCESS`]).
215pub struct FileObject {
216    oid: ObjectIdentifier,
217    name: String,
218    description: String,
219    file_type: String,
220    file_size: u64,
221    modification_date: (Date, Time),
222    archive: bool,
223    read_only: bool,
224    /// Raw `BACnetFileAccessMethod` value; named in [`FileAccessMethod`]
225    /// (record-access = 0, stream-access = 1 per the Clause 21 production).
226    file_access_method: u32,
227    /// Record count (only meaningful for record-access files).
228    record_count: Option<u64>,
229    /// Stream data (used when file_access_method == STREAM_ACCESS).
230    data: Vec<u8>,
231    /// Record data (used when file_access_method == RECORD_ACCESS).
232    records: Vec<Vec<u8>>,
233    status_flags: StatusFlags,
234    out_of_service: bool,
235    /// Reliability: 0 = NO_FAULT_DETECTED.
236    reliability: u32,
237    /// Growth cap in octets for network writes; not a BACnet property.
238    max_file_size: u64,
239    /// Growth cap in records for network writes; not a BACnet property.
240    max_record_count: u64,
241    /// Database-owned coherent clock source used at successful mutations.
242    clock: Option<Arc<dyn ClockReader>>,
243}
244
245impl FileObject {
246    /// Create a new File object.
247    ///
248    /// Defaults to stream access (file_access_method = STREAM_ACCESS), empty
249    /// data, not read-only, archive = false.
250    pub fn new(
251        instance: u32,
252        name: impl Into<String>,
253        file_type: impl Into<String>,
254    ) -> Result<Self, Error> {
255        let oid = ObjectIdentifier::new(ObjectType::FILE, instance)?;
256        Ok(Self {
257            oid,
258            name: name.into(),
259            description: String::new(),
260            file_type: file_type.into(),
261            file_size: 0,
262            modification_date: (
263                Date {
264                    year: Date::UNSPECIFIED,
265                    month: Date::UNSPECIFIED,
266                    day: Date::UNSPECIFIED,
267                    day_of_week: Date::UNSPECIFIED,
268                },
269                Time {
270                    hour: Time::UNSPECIFIED,
271                    minute: Time::UNSPECIFIED,
272                    second: Time::UNSPECIFIED,
273                    hundredths: Time::UNSPECIFIED,
274                },
275            ),
276            archive: false,
277            read_only: false,
278            file_access_method: FileAccessMethod::STREAM_ACCESS.to_raw(),
279            record_count: None,
280            data: Vec::new(),
281            records: Vec::new(),
282            status_flags: StatusFlags::empty(),
283            out_of_service: false,
284            reliability: 0,
285            max_file_size: DEFAULT_MAX_FILE_SIZE,
286            max_record_count: DEFAULT_MAX_RECORD_COUNT,
287            clock: None,
288        })
289    }
290
291    /// Set the description.
292    pub fn set_description(&mut self, desc: impl Into<String>) {
293        self.description = desc.into();
294    }
295
296    /// Set the file type string.
297    pub fn set_file_type(&mut self, ft: impl Into<String>) {
298        self.file_type = ft.into();
299    }
300
301    /// Set stream data; File_Size follows it unless the object is
302    /// RECORD_ACCESS (any other access-method value, recognised or not,
303    /// is the stream channel, as in
304    /// [`set_file_access_method`](Self::set_file_access_method)).
305    pub fn set_data(&mut self, data: Vec<u8>) {
306        self.data = data;
307        if self.file_access_method != FileAccessMethod::RECORD_ACCESS.to_raw() {
308            self.file_size = self.data.len() as u64;
309        }
310        self.mark_modified();
311    }
312
313    /// Get a reference to the stream data.
314    pub fn data(&self) -> &[u8] {
315        &self.data
316    }
317
318    /// Set the file access method. Accepts the raw `BACnetFileAccessMethod`
319    /// value: [`FileAccessMethod::STREAM_ACCESS`] (1) or
320    /// [`FileAccessMethod::RECORD_ACCESS`] (0).
321    ///
322    /// File_Size and Record_Count are recomputed from the channel the
323    /// object switches to: Table 12-16 footnote 2 has Record_Count present
324    /// only under RECORD_ACCESS, and File_Size counts that channel's octets.
325    pub fn set_file_access_method(&mut self, method: u32) {
326        let old_file_size = self.file_size;
327        self.file_access_method = method;
328        if method == FileAccessMethod::RECORD_ACCESS.to_raw() {
329            self.record_count = Some(self.records.len() as u64);
330            self.file_size = octet_total(&self.records);
331        } else {
332            self.record_count = None;
333            self.file_size = self.data.len() as u64;
334        }
335        if self.file_size != old_file_size {
336            self.mark_modified();
337        }
338    }
339
340    /// Set the records; Record_Count and File_Size follow them while the
341    /// object is RECORD_ACCESS (Table 12-16 footnote 2).
342    pub fn set_records(&mut self, records: Vec<Vec<u8>>) {
343        self.records = records;
344        if self.file_access_method == FileAccessMethod::RECORD_ACCESS.to_raw() {
345            self.record_count = Some(self.records.len() as u64);
346            self.file_size = octet_total(&self.records);
347        }
348        self.mark_modified();
349    }
350
351    /// Get a reference to the records.
352    pub fn records(&self) -> &[Vec<u8>] {
353        &self.records
354    }
355
356    /// Set the modification date.
357    pub fn set_modification_date(&mut self, date: Date, time: Time) {
358        let modification_date = (date, time);
359        if self.modification_date != modification_date {
360            self.modification_date = modification_date;
361            self.archive = false;
362        }
363    }
364
365    /// Set the archive flag.
366    pub fn set_archive(&mut self, archive: bool) {
367        self.archive = archive;
368    }
369
370    /// Set the read-only flag.
371    pub fn set_read_only(&mut self, read_only: bool) {
372        self.read_only = read_only;
373    }
374
375    /// Get the file size in bytes.
376    pub fn file_size(&self) -> u64 {
377        self.file_size
378    }
379
380    /// Get the archive flag.
381    pub fn archive(&self) -> bool {
382        self.archive
383    }
384
385    /// Get the read-only flag.
386    pub fn read_only(&self) -> bool {
387        self.read_only
388    }
389
390    /// Set the growth cap, in octets, for network writes to this file.
391    ///
392    /// The cap bounds what AtomicWriteFile can add; it never invalidates
393    /// contents preloaded through [`set_data`](Self::set_data), which may
394    /// exceed it and still be overwritten in place. Clamped to `i32::MAX`
395    /// so every resolved write position fits the ACK.
396    pub fn set_max_file_size(&mut self, max_octets: u64) {
397        self.max_file_size = max_octets.min(MAX_REPRESENTABLE);
398    }
399
400    /// The growth cap, in octets, for network writes to this file.
401    pub fn max_file_size(&self) -> u64 {
402        self.max_file_size
403    }
404
405    /// Set the growth cap, in records, for network writes to this file.
406    ///
407    /// Same growth-only semantics as
408    /// [`set_max_file_size`](Self::set_max_file_size): records preloaded
409    /// through [`set_records`](Self::set_records) are never refused. Clamped
410    /// to [`DEFAULT_MAX_RECORD_COUNT`], the decoder ceiling, so a file grown
411    /// by network writes stays at or under what one AtomicReadFile-ACK
412    /// carries; a larger preloaded file reads back in windows of at most
413    /// that many records.
414    pub fn set_max_record_count(&mut self, max_records: u64) {
415        self.max_record_count = max_records.min(MAX_RECORD_CAP).min(MAX_REPRESENTABLE);
416    }
417
418    /// The growth cap, in records, for network writes to this file.
419    pub fn max_record_count(&self) -> u64 {
420        self.max_record_count
421    }
422
423    /// The handler gate is normative; this keeps `data` and `records` from
424    /// both filling up when a non-handler caller reaches the storage.
425    fn require_access(&self, method: FileAccessMethod) -> Result<(), Error> {
426        if self.file_access_method == method.to_raw() {
427            Ok(())
428        } else {
429            Err(invalid_file_access_method())
430        }
431    }
432
433    fn modification_datetime(&self) -> (Date, Time) {
434        let frame = self.clock.as_ref().and_then(|clock| clock.read_clock());
435        match frame {
436            Some(frame) if frame.is_valid_actual_datetime() => (frame.local_date, frame.local_time),
437            _ => (
438                Date {
439                    year: Date::UNSPECIFIED,
440                    month: Date::UNSPECIFIED,
441                    day: Date::UNSPECIFIED,
442                    day_of_week: Date::UNSPECIFIED,
443                },
444                Time {
445                    hour: Time::UNSPECIFIED,
446                    minute: Time::UNSPECIFIED,
447                    second: Time::UNSPECIFIED,
448                    hundredths: Time::UNSPECIFIED,
449                },
450            ),
451        }
452    }
453
454    fn mark_modified(&mut self) {
455        self.modification_date = self.modification_datetime();
456        self.archive = false;
457    }
458}
459
460impl FileStorage for FileObject {
461    fn read_stream(&self, start: u64, count: u64) -> Result<FileStreamRead, Error> {
462        self.require_access(FileAccessMethod::STREAM_ACCESS)?;
463        let len = self.data.len() as u64;
464        if start > len {
465            return Err(invalid_file_start_position());
466        }
467        let end = start.saturating_add(count).min(len);
468        Ok(FileStreamRead {
469            data: self.data[start as usize..end as usize].to_vec(),
470            end_of_file: len == 0 || (end >= len && end > start),
471        })
472    }
473
474    fn write_stream(&mut self, start: FileWriteStart, data: &[u8]) -> Result<u64, Error> {
475        self.require_access(FileAccessMethod::STREAM_ACCESS)?;
476        let len = self.data.len() as u64;
477        let start = match start {
478            FileWriteStart::At(offset) => offset,
479            FileWriteStart::Append => len,
480        };
481        let end = start.checked_add(data.len() as u64).ok_or_else(file_full)?;
482        if end > self.max_file_size.max(len) {
483            return Err(file_full());
484        }
485        let (start_idx, end_idx) = (
486            usize::try_from(start).map_err(|_| file_full())?,
487            usize::try_from(end).map_err(|_| file_full())?,
488        );
489        let mut updated = std::mem::take(&mut self.data);
490        if end_idx > updated.len() {
491            updated.resize(end_idx, 0);
492        }
493        updated[start_idx..end_idx].copy_from_slice(data);
494        self.set_data(updated);
495        Ok(start)
496    }
497
498    fn read_records(&self, start: u64, count: u64) -> Result<FileRecordRead, Error> {
499        self.require_access(FileAccessMethod::RECORD_ACCESS)?;
500        let len = self.records.len() as u64;
501        if start > len {
502            return Err(invalid_file_start_position());
503        }
504        let end = start.saturating_add(count).min(len);
505        Ok(FileRecordRead {
506            records: self.records[start as usize..end as usize].to_vec(),
507            end_of_file: len == 0 || (end >= len && end > start),
508        })
509    }
510
511    fn write_records(&mut self, start: FileWriteStart, records: &[Vec<u8>]) -> Result<u64, Error> {
512        self.require_access(FileAccessMethod::RECORD_ACCESS)?;
513        let len = self.records.len() as u64;
514        let start = match start {
515            FileWriteStart::At(index) => index,
516            FileWriteStart::Append => len,
517        };
518        let end = start
519            .checked_add(records.len() as u64)
520            .ok_or_else(file_full)?;
521        if end > self.max_record_count.max(len) {
522            return Err(file_full());
523        }
524        let (start_idx, end_idx) = (
525            usize::try_from(start).map_err(|_| file_full())?,
526            usize::try_from(end).map_err(|_| file_full())?,
527        );
528        // Octet cap on the projected total: existing octets, minus those
529        // in the records being replaced, plus the incoming payload.
530        let existing = octet_total(&self.records);
531        let replaced = octet_total(
532            &self.records[start_idx.min(self.records.len())..end_idx.min(self.records.len())],
533        );
534        let projected = existing - replaced + octet_total(records);
535        if projected > self.max_file_size.max(existing) {
536            return Err(file_full());
537        }
538        let mut updated = std::mem::take(&mut self.records);
539        if end_idx > updated.len() {
540            updated.resize(end_idx, Vec::new());
541        }
542        updated[start_idx..end_idx].clone_from_slice(records);
543        self.set_records(updated);
544        Ok(start)
545    }
546}
547
548impl FileConfiguration for FileObject {
549    fn set_access_method(&mut self, method: FileAccessMethod) {
550        FileObject::set_file_access_method(self, method.to_raw());
551    }
552
553    fn set_stream_data(&mut self, data: Vec<u8>) -> Result<(), Error> {
554        self.require_access(FileAccessMethod::STREAM_ACCESS)?;
555        FileObject::set_data(self, data);
556        Ok(())
557    }
558
559    fn stream_data(&self) -> Result<&[u8], Error> {
560        self.require_access(FileAccessMethod::STREAM_ACCESS)?;
561        Ok(FileObject::data(self))
562    }
563
564    fn set_record_data(&mut self, records: Vec<Vec<u8>>) -> Result<(), Error> {
565        self.require_access(FileAccessMethod::RECORD_ACCESS)?;
566        FileObject::set_records(self, records);
567        Ok(())
568    }
569
570    fn record_data(&self) -> Result<&[Vec<u8>], Error> {
571        self.require_access(FileAccessMethod::RECORD_ACCESS)?;
572        Ok(FileObject::records(self))
573    }
574
575    fn set_max_file_size(&mut self, max_octets: u64) -> u64 {
576        FileObject::set_max_file_size(self, max_octets);
577        FileObject::max_file_size(self)
578    }
579
580    fn set_max_record_count(&mut self, max_records: u64) -> u64 {
581        FileObject::set_max_record_count(self, max_records);
582        FileObject::max_record_count(self)
583    }
584}
585
586impl BACnetObject for FileObject {
587    fn object_identifier(&self) -> ObjectIdentifier {
588        self.oid
589    }
590
591    fn object_name(&self) -> &str {
592        &self.name
593    }
594
595    fn read_property(
596        &self,
597        property: PropertyIdentifier,
598        array_index: Option<u32>,
599    ) -> Result<PropertyValue, Error> {
600        if let Some(result) = read_common_properties!(self, property, array_index) {
601            return result;
602        }
603
604        match property {
605            p if p == PropertyIdentifier::OBJECT_TYPE => {
606                Ok(PropertyValue::Enumerated(ObjectType::FILE.to_raw()))
607            }
608            p if p == PropertyIdentifier::FILE_TYPE => {
609                Ok(PropertyValue::CharacterString(self.file_type.clone()))
610            }
611            p if p == PropertyIdentifier::FILE_SIZE => Ok(PropertyValue::Unsigned(self.file_size)),
612            p if p == PropertyIdentifier::MODIFICATION_DATE => Ok(PropertyValue::List(vec![
613                PropertyValue::Date(self.modification_date.0),
614                PropertyValue::Time(self.modification_date.1),
615            ])),
616            p if p == PropertyIdentifier::ARCHIVE => Ok(PropertyValue::Boolean(self.archive)),
617            p if p == PropertyIdentifier::READ_ONLY => Ok(PropertyValue::Boolean(self.read_only)),
618            p if p == PropertyIdentifier::FILE_ACCESS_METHOD => {
619                Ok(PropertyValue::Enumerated(self.file_access_method))
620            }
621            p if p == PropertyIdentifier::RECORD_COUNT => match self.record_count {
622                Some(count) => Ok(PropertyValue::Unsigned(count)),
623                None => Err(common::unknown_property_error()),
624            },
625            _ => Err(common::unknown_property_error()),
626        }
627    }
628
629    fn write_property(
630        &mut self,
631        property: PropertyIdentifier,
632        _array_index: Option<u32>,
633        value: PropertyValue,
634        _priority: Option<u8>,
635    ) -> Result<(), Error> {
636        if let Some(result) = common::write_description(&mut self.description, property, &value) {
637            return result;
638        }
639        if let Some(result) =
640            common::write_out_of_service(&mut self.out_of_service, property, &value)
641        {
642            return result;
643        }
644
645        match property {
646            p if p == PropertyIdentifier::ARCHIVE => {
647                if let PropertyValue::Boolean(v) = value {
648                    self.archive = v;
649                    Ok(())
650                } else {
651                    Err(common::invalid_data_type_error())
652                }
653            }
654            p if p == PropertyIdentifier::FILE_TYPE => {
655                if let PropertyValue::CharacterString(s) = value {
656                    self.file_type = s;
657                    Ok(())
658                } else {
659                    Err(common::invalid_data_type_error())
660                }
661            }
662            p if p == PropertyIdentifier::READ_ONLY => {
663                // Read-only is typically not writable from BACnet, but the
664                // application may need it. Deny remote writes.
665                Err(common::write_access_denied_error())
666            }
667            p if p == PropertyIdentifier::FILE_SIZE => resize::write_stream(self, value),
668            p if p == PropertyIdentifier::FILE_ACCESS_METHOD => {
669                Err(common::write_access_denied_error())
670            }
671            p if p == PropertyIdentifier::MODIFICATION_DATE => {
672                Err(common::write_access_denied_error())
673            }
674            p if p == PropertyIdentifier::RECORD_COUNT => resize::write_records(self, value),
675            _ => Err(common::write_access_denied_error()),
676        }
677    }
678
679    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
680        let mut props = vec![
681            PropertyIdentifier::OBJECT_IDENTIFIER,
682            PropertyIdentifier::OBJECT_NAME,
683            PropertyIdentifier::OBJECT_TYPE,
684            PropertyIdentifier::DESCRIPTION,
685            PropertyIdentifier::FILE_TYPE,
686            PropertyIdentifier::FILE_SIZE,
687            PropertyIdentifier::MODIFICATION_DATE,
688            PropertyIdentifier::ARCHIVE,
689            PropertyIdentifier::READ_ONLY,
690            PropertyIdentifier::FILE_ACCESS_METHOD,
691            PropertyIdentifier::STATUS_FLAGS,
692            PropertyIdentifier::OUT_OF_SERVICE,
693            PropertyIdentifier::RELIABILITY,
694        ];
695        if self.record_count.is_some() {
696            props.push(PropertyIdentifier::RECORD_COUNT);
697        }
698        Cow::Owned(props)
699    }
700
701    fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
702        resize::is_writable(self, property)
703    }
704
705    fn capture_write_property_rollback(
706        &mut self,
707        property: PropertyIdentifier,
708        value: &PropertyValue,
709    ) -> Option<WritePropertyRollback> {
710        resize::capture(self, property, value)
711    }
712
713    fn restore_write_property_rollback(
714        &mut self,
715        rollback: WritePropertyRollback,
716    ) -> Result<(), Error> {
717        resize::restore(self, rollback)
718    }
719
720    fn bind_clock_internal(&mut self, clock: Option<Arc<dyn ClockReader>>) {
721        self.clock = clock;
722    }
723
724    fn file_configuration_internal(&self) -> Option<&dyn FileConfiguration> {
725        Some(self)
726    }
727
728    fn file_configuration_internal_mut(&mut self) -> Option<&mut dyn FileConfiguration> {
729        Some(self)
730    }
731
732    fn file_storage_internal(&self) -> Option<&dyn FileStorage> {
733        Some(self)
734    }
735
736    fn file_storage_internal_mut(&mut self) -> Option<&mut dyn FileStorage> {
737        Some(self)
738    }
739}
740
741// ---------------------------------------------------------------------------
742// Tests
743// ---------------------------------------------------------------------------
744
745#[cfg(test)]
746mod resize_tests;
747#[cfg(test)]
748mod storage_tests;
749#[cfg(test)]
750mod tests;