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        if let Some(result) = read_common_properties!(self, property, array_index) {
179            return result;
180        }
181
182        match property {
183            p if p == PropertyIdentifier::OBJECT_TYPE => {
184                Ok(PropertyValue::Enumerated(ObjectType::FILE.to_raw()))
185            }
186            p if p == PropertyIdentifier::FILE_TYPE => {
187                Ok(PropertyValue::CharacterString(self.file_type.clone()))
188            }
189            p if p == PropertyIdentifier::FILE_SIZE => Ok(PropertyValue::Unsigned(self.file_size)),
190            p if p == PropertyIdentifier::MODIFICATION_DATE => Ok(PropertyValue::List(vec![
191                PropertyValue::Date(self.modification_date.0),
192                PropertyValue::Time(self.modification_date.1),
193            ])),
194            p if p == PropertyIdentifier::ARCHIVE => Ok(PropertyValue::Boolean(self.archive)),
195            p if p == PropertyIdentifier::READ_ONLY => Ok(PropertyValue::Boolean(self.read_only)),
196            p if p == PropertyIdentifier::FILE_ACCESS_METHOD => {
197                Ok(PropertyValue::Enumerated(self.file_access_method))
198            }
199            p if p == PropertyIdentifier::RECORD_COUNT => match self.record_count {
200                Some(count) => Ok(PropertyValue::Unsigned(count)),
201                None => Err(common::unknown_property_error()),
202            },
203            _ => Err(common::unknown_property_error()),
204        }
205    }
206
207    fn write_property(
208        &mut self,
209        property: PropertyIdentifier,
210        _array_index: Option<u32>,
211        value: PropertyValue,
212        _priority: Option<u8>,
213    ) -> Result<(), Error> {
214        if let Some(result) = common::write_description(&mut self.description, property, &value) {
215            return result;
216        }
217        if let Some(result) =
218            common::write_out_of_service(&mut self.out_of_service, property, &value)
219        {
220            return result;
221        }
222
223        match property {
224            p if p == PropertyIdentifier::ARCHIVE => {
225                if let PropertyValue::Boolean(v) = value {
226                    self.archive = v;
227                    Ok(())
228                } else {
229                    Err(common::invalid_data_type_error())
230                }
231            }
232            p if p == PropertyIdentifier::FILE_TYPE => {
233                if let PropertyValue::CharacterString(s) = value {
234                    self.file_type = s;
235                    Ok(())
236                } else {
237                    Err(common::invalid_data_type_error())
238                }
239            }
240            p if p == PropertyIdentifier::READ_ONLY => {
241                // Read-only is typically not writable from BACnet, but the
242                // application may need it. Deny remote writes.
243                Err(common::write_access_denied_error())
244            }
245            p if p == PropertyIdentifier::FILE_SIZE => Err(common::write_access_denied_error()),
246            p if p == PropertyIdentifier::FILE_ACCESS_METHOD => {
247                Err(common::write_access_denied_error())
248            }
249            p if p == PropertyIdentifier::MODIFICATION_DATE => {
250                Err(common::write_access_denied_error())
251            }
252            p if p == PropertyIdentifier::RECORD_COUNT => Err(common::write_access_denied_error()),
253            _ => Err(common::write_access_denied_error()),
254        }
255    }
256
257    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
258        let mut props = vec![
259            PropertyIdentifier::OBJECT_IDENTIFIER,
260            PropertyIdentifier::OBJECT_NAME,
261            PropertyIdentifier::OBJECT_TYPE,
262            PropertyIdentifier::DESCRIPTION,
263            PropertyIdentifier::FILE_TYPE,
264            PropertyIdentifier::FILE_SIZE,
265            PropertyIdentifier::MODIFICATION_DATE,
266            PropertyIdentifier::ARCHIVE,
267            PropertyIdentifier::READ_ONLY,
268            PropertyIdentifier::FILE_ACCESS_METHOD,
269            PropertyIdentifier::STATUS_FLAGS,
270            PropertyIdentifier::OUT_OF_SERVICE,
271            PropertyIdentifier::RELIABILITY,
272        ];
273        if self.record_count.is_some() {
274            props.push(PropertyIdentifier::RECORD_COUNT);
275        }
276        Cow::Owned(props)
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use bacnet_types::enums::ErrorCode;
284
285    #[test]
286    fn file_object_creation() {
287        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
288        assert_eq!(file.object_name(), "FILE-1");
289        assert_eq!(file.object_identifier().instance_number(), 1);
290    }
291
292    #[test]
293    fn file_read_object_type() {
294        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
295        let val = file
296            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
297            .unwrap();
298        assert_eq!(val, PropertyValue::Enumerated(ObjectType::FILE.to_raw()));
299    }
300
301    #[test]
302    fn file_read_object_identifier() {
303        let file = FileObject::new(42, "FILE-42", "application/octet-stream").unwrap();
304        let val = file
305            .read_property(PropertyIdentifier::OBJECT_IDENTIFIER, None)
306            .unwrap();
307        if let PropertyValue::ObjectIdentifier(oid) = val {
308            assert_eq!(oid.instance_number(), 42);
309        } else {
310            panic!("expected ObjectIdentifier");
311        }
312    }
313
314    #[test]
315    fn file_read_object_name() {
316        let file = FileObject::new(1, "MY-FILE", "text/plain").unwrap();
317        let val = file
318            .read_property(PropertyIdentifier::OBJECT_NAME, None)
319            .unwrap();
320        assert_eq!(val, PropertyValue::CharacterString("MY-FILE".into()));
321    }
322
323    #[test]
324    fn file_read_file_type() {
325        let file = FileObject::new(1, "FILE-1", "text/csv").unwrap();
326        let val = file
327            .read_property(PropertyIdentifier::FILE_TYPE, None)
328            .unwrap();
329        assert_eq!(val, PropertyValue::CharacterString("text/csv".into()));
330    }
331
332    #[test]
333    fn file_read_file_size_default_zero() {
334        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
335        let val = file
336            .read_property(PropertyIdentifier::FILE_SIZE, None)
337            .unwrap();
338        assert_eq!(val, PropertyValue::Unsigned(0));
339    }
340
341    #[test]
342    fn file_set_data_updates_file_size() {
343        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
344        file.set_data(vec![0x48, 0x65, 0x6C, 0x6C, 0x6F]); // "Hello"
345        let val = file
346            .read_property(PropertyIdentifier::FILE_SIZE, None)
347            .unwrap();
348        assert_eq!(val, PropertyValue::Unsigned(5));
349        assert_eq!(file.data(), &[0x48, 0x65, 0x6C, 0x6C, 0x6F]);
350    }
351
352    #[test]
353    fn file_read_archive_default_false() {
354        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
355        let val = file
356            .read_property(PropertyIdentifier::ARCHIVE, None)
357            .unwrap();
358        assert_eq!(val, PropertyValue::Boolean(false));
359    }
360
361    #[test]
362    fn file_set_and_read_archive() {
363        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
364        file.set_archive(true);
365        assert!(file.archive());
366        let val = file
367            .read_property(PropertyIdentifier::ARCHIVE, None)
368            .unwrap();
369        assert_eq!(val, PropertyValue::Boolean(true));
370    }
371
372    #[test]
373    fn file_read_read_only_default_false() {
374        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
375        let val = file
376            .read_property(PropertyIdentifier::READ_ONLY, None)
377            .unwrap();
378        assert_eq!(val, PropertyValue::Boolean(false));
379    }
380
381    #[test]
382    fn file_set_and_read_read_only() {
383        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
384        file.set_read_only(true);
385        assert!(file.read_only());
386        let val = file
387            .read_property(PropertyIdentifier::READ_ONLY, None)
388            .unwrap();
389        assert_eq!(val, PropertyValue::Boolean(true));
390    }
391
392    #[test]
393    fn file_read_modification_date_default_unspecified() {
394        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
395        let val = file
396            .read_property(PropertyIdentifier::MODIFICATION_DATE, None)
397            .unwrap();
398        if let PropertyValue::List(items) = val {
399            assert_eq!(items.len(), 2);
400            let unspec_date = Date {
401                year: 0xFF,
402                month: 0xFF,
403                day: 0xFF,
404                day_of_week: 0xFF,
405            };
406            let unspec_time = Time {
407                hour: 0xFF,
408                minute: 0xFF,
409                second: 0xFF,
410                hundredths: 0xFF,
411            };
412            assert_eq!(items[0], PropertyValue::Date(unspec_date));
413            assert_eq!(items[1], PropertyValue::Time(unspec_time));
414        } else {
415            panic!("expected PropertyValue::List");
416        }
417    }
418
419    #[test]
420    fn file_set_and_read_modification_date() {
421        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
422        let d = Date {
423            year: 126,
424            month: 3,
425            day: 1,
426            day_of_week: 7,
427        };
428        let t = Time {
429            hour: 14,
430            minute: 30,
431            second: 0,
432            hundredths: 0,
433        };
434        file.set_modification_date(d, t);
435        let val = file
436            .read_property(PropertyIdentifier::MODIFICATION_DATE, None)
437            .unwrap();
438        if let PropertyValue::List(items) = val {
439            assert_eq!(items[0], PropertyValue::Date(d));
440            assert_eq!(items[1], PropertyValue::Time(t));
441        } else {
442            panic!("expected PropertyValue::List");
443        }
444    }
445
446    #[test]
447    fn file_read_file_access_method_default_stream() {
448        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
449        let val = file
450            .read_property(PropertyIdentifier::FILE_ACCESS_METHOD, None)
451            .unwrap();
452        assert_eq!(val, PropertyValue::Enumerated(0));
453    }
454
455    #[test]
456    fn file_record_count_unavailable_for_stream() {
457        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
458        let result = file.read_property(PropertyIdentifier::RECORD_COUNT, None);
459        assert!(result.is_err());
460    }
461
462    #[test]
463    fn file_set_records_updates_record_count_and_size() {
464        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
465        file.set_file_access_method(1);
466        file.set_records(vec![vec![0x01, 0x02], vec![0x03, 0x04, 0x05]]);
467        let count = file
468            .read_property(PropertyIdentifier::RECORD_COUNT, None)
469            .unwrap();
470        assert_eq!(count, PropertyValue::Unsigned(2));
471        let size = file
472            .read_property(PropertyIdentifier::FILE_SIZE, None)
473            .unwrap();
474        assert_eq!(size, PropertyValue::Unsigned(5)); // 2 + 3 bytes
475        assert_eq!(file.records().len(), 2);
476    }
477
478    #[test]
479    fn file_read_status_flags_default() {
480        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
481        let val = file
482            .read_property(PropertyIdentifier::STATUS_FLAGS, None)
483            .unwrap();
484        if let PropertyValue::BitString { unused_bits, data } = val {
485            assert_eq!(unused_bits, 4);
486            assert_eq!(data, vec![0x00]);
487        } else {
488            panic!("expected BitString");
489        }
490    }
491
492    #[test]
493    fn file_read_out_of_service_default_false() {
494        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
495        let val = file
496            .read_property(PropertyIdentifier::OUT_OF_SERVICE, None)
497            .unwrap();
498        assert_eq!(val, PropertyValue::Boolean(false));
499    }
500
501    #[test]
502    fn file_read_reliability_default() {
503        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
504        let val = file
505            .read_property(PropertyIdentifier::RELIABILITY, None)
506            .unwrap();
507        assert_eq!(val, PropertyValue::Enumerated(0));
508    }
509
510    #[test]
511    fn file_read_description_default_empty() {
512        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
513        let val = file
514            .read_property(PropertyIdentifier::DESCRIPTION, None)
515            .unwrap();
516        assert_eq!(val, PropertyValue::CharacterString(String::new()));
517    }
518
519    #[test]
520    fn file_write_description() {
521        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
522        file.write_property(
523            PropertyIdentifier::DESCRIPTION,
524            None,
525            PropertyValue::CharacterString("A test file".into()),
526            None,
527        )
528        .unwrap();
529        let val = file
530            .read_property(PropertyIdentifier::DESCRIPTION, None)
531            .unwrap();
532        assert_eq!(val, PropertyValue::CharacterString("A test file".into()));
533    }
534
535    #[test]
536    fn file_write_archive() {
537        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
538        file.write_property(
539            PropertyIdentifier::ARCHIVE,
540            None,
541            PropertyValue::Boolean(true),
542            None,
543        )
544        .unwrap();
545        let val = file
546            .read_property(PropertyIdentifier::ARCHIVE, None)
547            .unwrap();
548        assert_eq!(val, PropertyValue::Boolean(true));
549    }
550
551    #[test]
552    fn file_write_archive_invalid_type() {
553        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
554        let result = file.write_property(
555            PropertyIdentifier::ARCHIVE,
556            None,
557            PropertyValue::Unsigned(1),
558            None,
559        );
560        assert!(result.is_err());
561    }
562
563    #[test]
564    fn file_write_file_type() {
565        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
566        file.write_property(
567            PropertyIdentifier::FILE_TYPE,
568            None,
569            PropertyValue::CharacterString("application/json".into()),
570            None,
571        )
572        .unwrap();
573        let val = file
574            .read_property(PropertyIdentifier::FILE_TYPE, None)
575            .unwrap();
576        assert_eq!(
577            val,
578            PropertyValue::CharacterString("application/json".into())
579        );
580    }
581
582    #[test]
583    fn file_write_out_of_service() {
584        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
585        file.write_property(
586            PropertyIdentifier::OUT_OF_SERVICE,
587            None,
588            PropertyValue::Boolean(true),
589            None,
590        )
591        .unwrap();
592        let val = file
593            .read_property(PropertyIdentifier::OUT_OF_SERVICE, None)
594            .unwrap();
595        assert_eq!(val, PropertyValue::Boolean(true));
596    }
597
598    #[test]
599    fn file_write_read_only_denied() {
600        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
601        let result = file.write_property(
602            PropertyIdentifier::READ_ONLY,
603            None,
604            PropertyValue::Boolean(true),
605            None,
606        );
607        assert!(result.is_err());
608    }
609
610    #[test]
611    fn file_write_file_size_denied() {
612        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
613        let result = file.write_property(
614            PropertyIdentifier::FILE_SIZE,
615            None,
616            PropertyValue::Unsigned(100),
617            None,
618        );
619        assert!(result.is_err());
620    }
621
622    #[test]
623    fn file_property_list_stream() {
624        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
625        let props = file.property_list();
626        assert!(props.contains(&PropertyIdentifier::OBJECT_IDENTIFIER));
627        assert!(props.contains(&PropertyIdentifier::OBJECT_NAME));
628        assert!(props.contains(&PropertyIdentifier::OBJECT_TYPE));
629        assert!(props.contains(&PropertyIdentifier::FILE_TYPE));
630        assert!(props.contains(&PropertyIdentifier::FILE_SIZE));
631        assert!(props.contains(&PropertyIdentifier::MODIFICATION_DATE));
632        assert!(props.contains(&PropertyIdentifier::ARCHIVE));
633        assert!(props.contains(&PropertyIdentifier::READ_ONLY));
634        assert!(props.contains(&PropertyIdentifier::FILE_ACCESS_METHOD));
635        assert!(props.contains(&PropertyIdentifier::STATUS_FLAGS));
636        assert!(props.contains(&PropertyIdentifier::OUT_OF_SERVICE));
637        assert!(props.contains(&PropertyIdentifier::RELIABILITY));
638        // RECORD_COUNT should NOT be in property list for stream-access files
639        assert!(!props.contains(&PropertyIdentifier::RECORD_COUNT));
640    }
641
642    #[test]
643    fn file_property_list_record_access() {
644        let mut file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
645        file.set_file_access_method(1);
646        let props = file.property_list();
647        assert!(props.contains(&PropertyIdentifier::RECORD_COUNT));
648    }
649
650    #[test]
651    fn file_unknown_property_error() {
652        let file = FileObject::new(1, "FILE-1", "text/plain").unwrap();
653        let result = file.read_property(PropertyIdentifier::PRESENT_VALUE, None);
654        assert!(result.is_err());
655        if let Err(Error::Protocol { code, .. }) = result {
656            assert_eq!(code, ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32);
657        } else {
658            panic!("expected Protocol error");
659        }
660    }
661}