Skip to main content

bacnet_objects/
file.rs

1//! File (type 10) object per ASHRAE 135-2020 Clause 12.11.
2//!
3//! Backs AtomicReadFile and AtomicWriteFile services. Supports both
4//! stream-access and record-access modes.
5
6use bacnet_types::enums::{ObjectType, PropertyIdentifier};
7use bacnet_types::error::Error;
8use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
9use std::borrow::Cow;
10
11use crate::common::{self, read_common_properties};
12use crate::traits::BACnetObject;
13
14// ---------------------------------------------------------------------------
15// FileObject (type 10)
16// ---------------------------------------------------------------------------
17
18/// BACnet File object.
19///
20/// Represents a file accessible via AtomicReadFile / AtomicWriteFile services.
21/// The `file_access_method` determines whether the file is accessed as a
22/// byte stream (0) or as a sequence of fixed-length records (1).
23pub struct FileObject {
24    oid: ObjectIdentifier,
25    name: String,
26    description: String,
27    file_type: String,
28    file_size: u64,
29    modification_date: (Date, Time),
30    archive: bool,
31    read_only: bool,
32    /// 0 = stream access, 1 = record access.
33    file_access_method: u32,
34    /// Record count (only meaningful for record-access files).
35    record_count: Option<u64>,
36    /// Stream data (used when file_access_method == 0).
37    data: Vec<u8>,
38    /// Record data (used when file_access_method == 1).
39    records: Vec<Vec<u8>>,
40    status_flags: StatusFlags,
41    out_of_service: bool,
42    /// Reliability: 0 = NO_FAULT_DETECTED.
43    reliability: u32,
44}
45
46impl FileObject {
47    /// Create a new File object.
48    ///
49    /// Defaults to stream access (file_access_method = 0), empty data,
50    /// not read-only, archive = false.
51    pub fn new(
52        instance: u32,
53        name: impl Into<String>,
54        file_type: impl Into<String>,
55    ) -> Result<Self, Error> {
56        let oid = ObjectIdentifier::new(ObjectType::FILE, instance)?;
57        Ok(Self {
58            oid,
59            name: name.into(),
60            description: String::new(),
61            file_type: file_type.into(),
62            file_size: 0,
63            modification_date: (
64                Date {
65                    year: 0xFF,
66                    month: 0xFF,
67                    day: 0xFF,
68                    day_of_week: 0xFF,
69                },
70                Time {
71                    hour: 0xFF,
72                    minute: 0xFF,
73                    second: 0xFF,
74                    hundredths: 0xFF,
75                },
76            ),
77            archive: false,
78            read_only: false,
79            file_access_method: 0,
80            record_count: None,
81            data: Vec::new(),
82            records: Vec::new(),
83            status_flags: StatusFlags::empty(),
84            out_of_service: false,
85            reliability: 0,
86        })
87    }
88
89    /// Set the description.
90    pub fn set_description(&mut self, desc: impl Into<String>) {
91        self.description = desc.into();
92    }
93
94    /// Set the file type string.
95    pub fn set_file_type(&mut self, ft: impl Into<String>) {
96        self.file_type = ft.into();
97    }
98
99    /// Set stream data and update file_size accordingly.
100    pub fn set_data(&mut self, data: Vec<u8>) {
101        self.file_size = data.len() as u64;
102        self.data = data;
103    }
104
105    /// Get a reference to the stream data.
106    pub fn data(&self) -> &[u8] {
107        &self.data
108    }
109
110    /// Set the file access method (0 = stream, 1 = record).
111    pub fn set_file_access_method(&mut self, method: u32) {
112        self.file_access_method = method;
113        if method == 1 {
114            self.record_count = Some(self.records.len() as u64);
115        } else {
116            self.record_count = None;
117        }
118    }
119
120    /// Set the records (for record-access files) and update record_count.
121    pub fn set_records(&mut self, records: Vec<Vec<u8>>) {
122        let total_size: u64 = records.iter().map(|r| r.len() as u64).sum();
123        self.file_size = total_size;
124        self.record_count = Some(records.len() as u64);
125        self.records = records;
126    }
127
128    /// Get a reference to the records.
129    pub fn records(&self) -> &[Vec<u8>] {
130        &self.records
131    }
132
133    /// Set the modification date.
134    pub fn set_modification_date(&mut self, date: Date, time: Time) {
135        self.modification_date = (date, time);
136    }
137
138    /// Set the archive flag.
139    pub fn set_archive(&mut self, archive: bool) {
140        self.archive = archive;
141    }
142
143    /// Set the read-only flag.
144    pub fn set_read_only(&mut self, read_only: bool) {
145        self.read_only = read_only;
146    }
147
148    /// Get the file size in bytes.
149    pub fn file_size(&self) -> u64 {
150        self.file_size
151    }
152
153    /// Get the archive flag.
154    pub fn archive(&self) -> bool {
155        self.archive
156    }
157
158    /// Get the read-only flag.
159    pub fn read_only(&self) -> bool {
160        self.read_only
161    }
162}
163
164impl BACnetObject for FileObject {
165    fn object_identifier(&self) -> ObjectIdentifier {
166        self.oid
167    }
168
169    fn object_name(&self) -> &str {
170        &self.name
171    }
172
173    fn read_property(
174        &self,
175        property: PropertyIdentifier,
176        array_index: Option<u32>,
177    ) -> Result<PropertyValue, Error> {
178        // Try common properties first.
179        if let Some(result) = read_common_properties!(self, property, array_index) {
180            return result;
181        }
182
183        match property {
184            p if p == PropertyIdentifier::OBJECT_TYPE => {
185                Ok(PropertyValue::Enumerated(ObjectType::FILE.to_raw()))
186            }
187            p if p == PropertyIdentifier::FILE_TYPE => {
188                Ok(PropertyValue::CharacterString(self.file_type.clone()))
189            }
190            p if p == PropertyIdentifier::FILE_SIZE => Ok(PropertyValue::Unsigned(self.file_size)),
191            p if p == PropertyIdentifier::MODIFICATION_DATE => Ok(PropertyValue::List(vec![
192                PropertyValue::Date(self.modification_date.0),
193                PropertyValue::Time(self.modification_date.1),
194            ])),
195            p if p == PropertyIdentifier::ARCHIVE => Ok(PropertyValue::Boolean(self.archive)),
196            p if p == PropertyIdentifier::READ_ONLY => Ok(PropertyValue::Boolean(self.read_only)),
197            p if p == PropertyIdentifier::FILE_ACCESS_METHOD => {
198                Ok(PropertyValue::Enumerated(self.file_access_method))
199            }
200            p if p == PropertyIdentifier::RECORD_COUNT => match self.record_count {
201                Some(count) => Ok(PropertyValue::Unsigned(count)),
202                None => Err(common::unknown_property_error()),
203            },
204            _ => Err(common::unknown_property_error()),
205        }
206    }
207
208    fn write_property(
209        &mut self,
210        property: PropertyIdentifier,
211        _array_index: Option<u32>,
212        value: PropertyValue,
213        _priority: Option<u8>,
214    ) -> Result<(), Error> {
215        // DESCRIPTION
216        if let Some(result) = common::write_description(&mut self.description, property, &value) {
217            return result;
218        }
219
220        // OUT_OF_SERVICE
221        if let Some(result) =
222            common::write_out_of_service(&mut self.out_of_service, property, &value)
223        {
224            return result;
225        }
226
227        match property {
228            p if p == PropertyIdentifier::ARCHIVE => {
229                if let PropertyValue::Boolean(v) = value {
230                    self.archive = v;
231                    Ok(())
232                } else {
233                    Err(common::invalid_data_type_error())
234                }
235            }
236            p if p == PropertyIdentifier::FILE_TYPE => {
237                if let PropertyValue::CharacterString(s) = value {
238                    self.file_type = s;
239                    Ok(())
240                } else {
241                    Err(common::invalid_data_type_error())
242                }
243            }
244            p if p == PropertyIdentifier::READ_ONLY => {
245                // Read-only is typically not writable from BACnet, but the
246                // application may need it. Deny remote writes.
247                Err(common::write_access_denied_error())
248            }
249            p if p == PropertyIdentifier::FILE_SIZE => Err(common::write_access_denied_error()),
250            p if p == PropertyIdentifier::FILE_ACCESS_METHOD => {
251                Err(common::write_access_denied_error())
252            }
253            p if p == PropertyIdentifier::MODIFICATION_DATE => {
254                Err(common::write_access_denied_error())
255            }
256            p if p == PropertyIdentifier::RECORD_COUNT => Err(common::write_access_denied_error()),
257            _ => Err(common::write_access_denied_error()),
258        }
259    }
260
261    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
262        let mut props = vec![
263            PropertyIdentifier::OBJECT_IDENTIFIER,
264            PropertyIdentifier::OBJECT_NAME,
265            PropertyIdentifier::OBJECT_TYPE,
266            PropertyIdentifier::DESCRIPTION,
267            PropertyIdentifier::FILE_TYPE,
268            PropertyIdentifier::FILE_SIZE,
269            PropertyIdentifier::MODIFICATION_DATE,
270            PropertyIdentifier::ARCHIVE,
271            PropertyIdentifier::READ_ONLY,
272            PropertyIdentifier::FILE_ACCESS_METHOD,
273            PropertyIdentifier::STATUS_FLAGS,
274            PropertyIdentifier::OUT_OF_SERVICE,
275            PropertyIdentifier::RELIABILITY,
276        ];
277        if self.record_count.is_some() {
278            props.push(PropertyIdentifier::RECORD_COUNT);
279        }
280        Cow::Owned(props)
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use bacnet_types::enums::ErrorCode;
288
289    #[test]
290    fn file_object_creation() {
291        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
292        assert_eq!(file.object_name(), "FILE-1");
293        assert_eq!(file.object_identifier().instance_number(), 1);
294    }
295
296    #[test]
297    fn file_read_object_type() {
298        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
299        let val = file
300            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
301            .unwrap();
302        assert_eq!(val, PropertyValue::Enumerated(ObjectType::FILE.to_raw()));
303    }
304
305    #[test]
306    fn file_read_object_identifier() {
307        let file = FileObject::new(42, "FILE-42", "application/octet-stream").unwrap();
308        let val = file
309            .read_property(PropertyIdentifier::OBJECT_IDENTIFIER, None)
310            .unwrap();
311        if let PropertyValue::ObjectIdentifier(oid) = val {
312            assert_eq!(oid.instance_number(), 42);
313        } else {
314            panic!("expected ObjectIdentifier");
315        }
316    }
317
318    #[test]
319    fn file_read_object_name() {
320        let file = FileObject::new(1, "MY-FILE", "text/plain").unwrap();
321        let val = file
322            .read_property(PropertyIdentifier::OBJECT_NAME, None)
323            .unwrap();
324        assert_eq!(val, PropertyValue::CharacterString("MY-FILE".into()));
325    }
326
327    #[test]
328    fn file_read_file_type() {
329        let file = FileObject::new(1, "FILE-1", "text/csv").unwrap();
330        let val = file
331            .read_property(PropertyIdentifier::FILE_TYPE, None)
332            .unwrap();
333        assert_eq!(val, PropertyValue::CharacterString("text/csv".into()));
334    }
335
336    #[test]
337    fn file_read_file_size_default_zero() {
338        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
339        let val = file
340            .read_property(PropertyIdentifier::FILE_SIZE, None)
341            .unwrap();
342        assert_eq!(val, PropertyValue::Unsigned(0));
343    }
344
345    #[test]
346    fn file_set_data_updates_file_size() {
347        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
348        file.set_data(vec![0x48, 0x65, 0x6C, 0x6C, 0x6F]); // "Hello"
349        let val = file
350            .read_property(PropertyIdentifier::FILE_SIZE, None)
351            .unwrap();
352        assert_eq!(val, PropertyValue::Unsigned(5));
353        assert_eq!(file.data(), &[0x48, 0x65, 0x6C, 0x6C, 0x6F]);
354    }
355
356    #[test]
357    fn file_read_archive_default_false() {
358        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
359        let val = file
360            .read_property(PropertyIdentifier::ARCHIVE, None)
361            .unwrap();
362        assert_eq!(val, PropertyValue::Boolean(false));
363    }
364
365    #[test]
366    fn file_set_and_read_archive() {
367        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
368        file.set_archive(true);
369        assert!(file.archive());
370        let val = file
371            .read_property(PropertyIdentifier::ARCHIVE, None)
372            .unwrap();
373        assert_eq!(val, PropertyValue::Boolean(true));
374    }
375
376    #[test]
377    fn file_read_read_only_default_false() {
378        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
379        let val = file
380            .read_property(PropertyIdentifier::READ_ONLY, None)
381            .unwrap();
382        assert_eq!(val, PropertyValue::Boolean(false));
383    }
384
385    #[test]
386    fn file_set_and_read_read_only() {
387        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
388        file.set_read_only(true);
389        assert!(file.read_only());
390        let val = file
391            .read_property(PropertyIdentifier::READ_ONLY, None)
392            .unwrap();
393        assert_eq!(val, PropertyValue::Boolean(true));
394    }
395
396    #[test]
397    fn file_read_modification_date_default_unspecified() {
398        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
399        let val = file
400            .read_property(PropertyIdentifier::MODIFICATION_DATE, None)
401            .unwrap();
402        if let PropertyValue::List(items) = val {
403            assert_eq!(items.len(), 2);
404            let unspec_date = Date {
405                year: 0xFF,
406                month: 0xFF,
407                day: 0xFF,
408                day_of_week: 0xFF,
409            };
410            let unspec_time = Time {
411                hour: 0xFF,
412                minute: 0xFF,
413                second: 0xFF,
414                hundredths: 0xFF,
415            };
416            assert_eq!(items[0], PropertyValue::Date(unspec_date));
417            assert_eq!(items[1], PropertyValue::Time(unspec_time));
418        } else {
419            panic!("expected PropertyValue::List");
420        }
421    }
422
423    #[test]
424    fn file_set_and_read_modification_date() {
425        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
426        let d = Date {
427            year: 126,
428            month: 3,
429            day: 1,
430            day_of_week: 7,
431        };
432        let t = Time {
433            hour: 14,
434            minute: 30,
435            second: 0,
436            hundredths: 0,
437        };
438        file.set_modification_date(d, t);
439        let val = file
440            .read_property(PropertyIdentifier::MODIFICATION_DATE, None)
441            .unwrap();
442        if let PropertyValue::List(items) = val {
443            assert_eq!(items[0], PropertyValue::Date(d));
444            assert_eq!(items[1], PropertyValue::Time(t));
445        } else {
446            panic!("expected PropertyValue::List");
447        }
448    }
449
450    #[test]
451    fn file_read_file_access_method_default_stream() {
452        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
453        let val = file
454            .read_property(PropertyIdentifier::FILE_ACCESS_METHOD, None)
455            .unwrap();
456        assert_eq!(val, PropertyValue::Enumerated(0));
457    }
458
459    #[test]
460    fn file_record_count_unavailable_for_stream() {
461        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
462        let result = file.read_property(PropertyIdentifier::RECORD_COUNT, None);
463        assert!(result.is_err());
464    }
465
466    #[test]
467    fn file_set_records_updates_record_count_and_size() {
468        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
469        file.set_file_access_method(1);
470        file.set_records(vec![vec![0x01, 0x02], vec![0x03, 0x04, 0x05]]);
471        let count = file
472            .read_property(PropertyIdentifier::RECORD_COUNT, None)
473            .unwrap();
474        assert_eq!(count, PropertyValue::Unsigned(2));
475        let size = file
476            .read_property(PropertyIdentifier::FILE_SIZE, None)
477            .unwrap();
478        assert_eq!(size, PropertyValue::Unsigned(5)); // 2 + 3 bytes
479        assert_eq!(file.records().len(), 2);
480    }
481
482    #[test]
483    fn file_read_status_flags_default() {
484        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
485        let val = file
486            .read_property(PropertyIdentifier::STATUS_FLAGS, None)
487            .unwrap();
488        if let PropertyValue::BitString { unused_bits, data } = val {
489            assert_eq!(unused_bits, 4);
490            assert_eq!(data, vec![0x00]);
491        } else {
492            panic!("expected BitString");
493        }
494    }
495
496    #[test]
497    fn file_read_out_of_service_default_false() {
498        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
499        let val = file
500            .read_property(PropertyIdentifier::OUT_OF_SERVICE, None)
501            .unwrap();
502        assert_eq!(val, PropertyValue::Boolean(false));
503    }
504
505    #[test]
506    fn file_read_reliability_default() {
507        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
508        let val = file
509            .read_property(PropertyIdentifier::RELIABILITY, None)
510            .unwrap();
511        assert_eq!(val, PropertyValue::Enumerated(0));
512    }
513
514    #[test]
515    fn file_read_description_default_empty() {
516        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
517        let val = file
518            .read_property(PropertyIdentifier::DESCRIPTION, None)
519            .unwrap();
520        assert_eq!(val, PropertyValue::CharacterString(String::new()));
521    }
522
523    #[test]
524    fn file_write_description() {
525        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
526        file.write_property(
527            PropertyIdentifier::DESCRIPTION,
528            None,
529            PropertyValue::CharacterString("A test file".into()),
530            None,
531        )
532        .unwrap();
533        let val = file
534            .read_property(PropertyIdentifier::DESCRIPTION, None)
535            .unwrap();
536        assert_eq!(val, PropertyValue::CharacterString("A test file".into()));
537    }
538
539    #[test]
540    fn file_write_archive() {
541        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
542        file.write_property(
543            PropertyIdentifier::ARCHIVE,
544            None,
545            PropertyValue::Boolean(true),
546            None,
547        )
548        .unwrap();
549        let val = file
550            .read_property(PropertyIdentifier::ARCHIVE, None)
551            .unwrap();
552        assert_eq!(val, PropertyValue::Boolean(true));
553    }
554
555    #[test]
556    fn file_write_archive_invalid_type() {
557        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
558        let result = file.write_property(
559            PropertyIdentifier::ARCHIVE,
560            None,
561            PropertyValue::Unsigned(1),
562            None,
563        );
564        assert!(result.is_err());
565    }
566
567    #[test]
568    fn file_write_file_type() {
569        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
570        file.write_property(
571            PropertyIdentifier::FILE_TYPE,
572            None,
573            PropertyValue::CharacterString("application/json".into()),
574            None,
575        )
576        .unwrap();
577        let val = file
578            .read_property(PropertyIdentifier::FILE_TYPE, None)
579            .unwrap();
580        assert_eq!(
581            val,
582            PropertyValue::CharacterString("application/json".into())
583        );
584    }
585
586    #[test]
587    fn file_write_out_of_service() {
588        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
589        file.write_property(
590            PropertyIdentifier::OUT_OF_SERVICE,
591            None,
592            PropertyValue::Boolean(true),
593            None,
594        )
595        .unwrap();
596        let val = file
597            .read_property(PropertyIdentifier::OUT_OF_SERVICE, None)
598            .unwrap();
599        assert_eq!(val, PropertyValue::Boolean(true));
600    }
601
602    #[test]
603    fn file_write_read_only_denied() {
604        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
605        let result = file.write_property(
606            PropertyIdentifier::READ_ONLY,
607            None,
608            PropertyValue::Boolean(true),
609            None,
610        );
611        assert!(result.is_err());
612    }
613
614    #[test]
615    fn file_write_file_size_denied() {
616        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
617        let result = file.write_property(
618            PropertyIdentifier::FILE_SIZE,
619            None,
620            PropertyValue::Unsigned(100),
621            None,
622        );
623        assert!(result.is_err());
624    }
625
626    #[test]
627    fn file_property_list_stream() {
628        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
629        let props = file.property_list();
630        assert!(props.contains(&PropertyIdentifier::OBJECT_IDENTIFIER));
631        assert!(props.contains(&PropertyIdentifier::OBJECT_NAME));
632        assert!(props.contains(&PropertyIdentifier::OBJECT_TYPE));
633        assert!(props.contains(&PropertyIdentifier::FILE_TYPE));
634        assert!(props.contains(&PropertyIdentifier::FILE_SIZE));
635        assert!(props.contains(&PropertyIdentifier::MODIFICATION_DATE));
636        assert!(props.contains(&PropertyIdentifier::ARCHIVE));
637        assert!(props.contains(&PropertyIdentifier::READ_ONLY));
638        assert!(props.contains(&PropertyIdentifier::FILE_ACCESS_METHOD));
639        assert!(props.contains(&PropertyIdentifier::STATUS_FLAGS));
640        assert!(props.contains(&PropertyIdentifier::OUT_OF_SERVICE));
641        assert!(props.contains(&PropertyIdentifier::RELIABILITY));
642        // RECORD_COUNT should NOT be in property list for stream-access files
643        assert!(!props.contains(&PropertyIdentifier::RECORD_COUNT));
644    }
645
646    #[test]
647    fn file_property_list_record_access() {
648        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
649        file.set_file_access_method(1);
650        let props = file.property_list();
651        assert!(props.contains(&PropertyIdentifier::RECORD_COUNT));
652    }
653
654    #[test]
655    fn file_unknown_property_error() {
656        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
657        let result = file.read_property(PropertyIdentifier::PRESENT_VALUE, None);
658        assert!(result.is_err());
659        if let Err(Error::Protocol { code, .. }) = result {
660            assert_eq!(code, ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32);
661        } else {
662            panic!("expected Protocol error");
663        }
664    }
665}