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