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