Skip to main content

bacnet_objects/access_control/
rights_persistence.rs

1//! Keeping what peers write to an Access Rights object across a restart
2//! (#1392).
3//!
4//! Clause 12.34 says nothing about restarts, so this is the stack's own
5//! choice, made because peers provision access rules over the network
6//! (#1330, #1332): a head end writes the rules once and expects the
7//! controller to keep them. As for the Notification Class
8//! ([`NotificationClassPersistence`](crate::notification_class::NotificationClassPersistence)),
9//! the storage belongs to the application. The object loads it once, when it
10//! is built, and saves on its own writer thread each time a write changes
11//! Positive_Access_Rules, Negative_Access_Rules, Enable or Accompaniment
12//! (#1393). The bundled server waits for that save with the database guard
13//! dropped; a write nobody staged waits where it is ([`crate::durable`] lists
14//! those paths).
15
16use crate::prelude::*;
17#[cfg(feature = "std")]
18use std::path::Path;
19
20#[cfg(feature = "std")]
21use bacnet_encoding::constructed::tagged::{
22    decode_ctx_boolean, expect_closing, expect_opening, next_is_context, next_is_opening,
23};
24#[cfg(feature = "std")]
25use bacnet_encoding::constructed::{
26    decode_access_rule, decode_device_object_reference, encode_access_rule,
27    encode_device_object_reference,
28};
29#[cfg(feature = "std")]
30use bacnet_encoding::{primitives, tags};
31use bacnet_types::constructed::{BACnetAccessRule, BACnetDeviceObjectReference};
32use bacnet_types::error::Error;
33use bacnet_types::primitives::ObjectIdentifier;
34#[cfg(feature = "std")]
35use bytes::BytesMut;
36
37#[cfg(feature = "std")]
38use super::MAX_ACCESS_RULES;
39#[cfg(feature = "std")]
40use crate::durable::file::ObjectFile;
41
42/// What an Access Rights object keeps across a restart. Each member is
43/// `None` until a write sets it: what the application configures applies at
44/// each start until then.
45#[derive(Clone, Debug, Default, PartialEq)]
46pub struct AccessRightsSnapshot {
47    /// Positive_Access_Rules, in array order, once a write has set them.
48    pub positive_access_rules: Option<Vec<BACnetAccessRule>>,
49    /// Negative_Access_Rules, in array order, once a write has set them.
50    pub negative_access_rules: Option<Vec<BACnetAccessRule>>,
51    /// Enable, once a write has set it.
52    pub enable: Option<bool>,
53    /// Accompaniment, once a write has set it (#1393).
54    pub accompaniment: Option<BACnetDeviceObjectReference>,
55}
56
57/// Application-owned storage for one Access Rights object's rule arrays,
58/// Enable and Accompaniment.
59///
60/// The object calls it from its writer thread, one call at a time. A written
61/// value lands in storage a moment before the object serves it: see [storage
62/// leads the served state](crate::durable#storage-leads-the-served-state).
63pub trait AccessRightsPersistence: Send + Sync {
64    /// What was last saved for `rights`, or `None` when nothing was saved.
65    fn load(&self, rights: ObjectIdentifier) -> Result<Option<AccessRightsSnapshot>, Error>;
66
67    /// Durably replace what is saved for `rights` with `snapshot`.
68    fn save(&self, rights: ObjectIdentifier, snapshot: &AccessRightsSnapshot) -> Result<(), Error>;
69}
70
71/// The format's tag. A Notification Class's `RBNNCL01` and a forwarder's
72/// `RBNFWD01` files are siblings with the same header; a new layout gets a
73/// new tag. Accompaniment (#1393) is an optional member after the others,
74/// so a file written before it existed is one with that member absent.
75#[cfg(feature = "std")]
76const MAGIC: &[u8; 8] = b"RBNACR01";
77/// A rule this object accepts encodes in at most 40 octets, so two full
78/// arrays of [`MAX_ACCESS_RULES`] take about 80 KiB, and Accompaniment at
79/// most 12 octets more. Anything much larger is not a file this backend
80/// wrote.
81#[cfg(feature = "std")]
82pub(super) const MAX_FILE_BYTES: u64 = 128 * 1024;
83/// The context tags of the four members, in file order.
84#[cfg(feature = "std")]
85const POSITIVE_TAG: u8 = 0;
86#[cfg(feature = "std")]
87const NEGATIVE_TAG: u8 = 1;
88#[cfg(feature = "std")]
89const ENABLE_TAG: u8 = 2;
90#[cfg(feature = "std")]
91const ACCOMPANIMENT_TAG: u8 = 3;
92/// The label decoding errors carry.
93#[cfg(feature = "std")]
94const WHAT: &str = "Access Rights file";
95
96/// A file holding one Access Rights object's written rule arrays, Enable and
97/// Accompaniment, replaced whole on each save, the same way as
98/// [`FileNotificationClassPersistence`]: a failed save leaves the previous
99/// state in place, and a completed one survives a power loss.
100///
101/// After the magic tag `RBNACR01` and the object's identifier, the file
102/// holds BACnet encodings of up to four members, in this order, each only
103/// once a write has set it:
104///
105/// - context tag 0, opening, then each Positive_Access_Rules rule in its
106///   Clause 21 `BACnetAccessRule` encoding, then context tag 0, closing;
107/// - the same for Negative_Access_Rules under context tag 1;
108/// - Enable as a context-tagged BOOLEAN with tag 2;
109/// - context tag 3, opening, then Accompaniment in its Clause 21
110///   `BACnetDeviceObjectReference` encoding, then context tag 3, closing.
111///
112/// Loading refuses a file past 128 KiB, an array of more than
113/// [`MAX_ACCESS_RULES`] rules, members out of order or repeated, a rule or
114/// reference that does not decode, and octets after the last member. It
115/// does not check the rules or the reference themselves; the object does,
116/// as it loads them. It does not coordinate between processes.
117///
118/// [`FileNotificationClassPersistence`]: crate::notification_class::FileNotificationClassPersistence
119#[derive(Clone, Debug)]
120#[cfg(feature = "std")]
121pub struct FileAccessRightsPersistence {
122    file: ObjectFile,
123}
124
125#[cfg(feature = "std")]
126impl FileAccessRightsPersistence {
127    /// Keep the rule arrays, Enable and Accompaniment in the file at `path`.
128    pub fn new(path: impl AsRef<Path>) -> Result<Self, Error> {
129        Ok(Self {
130            file: ObjectFile::new(path.as_ref(), MAGIC, MAX_FILE_BYTES, "Access Rights")?,
131        })
132    }
133
134    /// The file the rule arrays, Enable and Accompaniment are kept in.
135    pub fn path(&self) -> &Path {
136        self.file.path()
137    }
138
139    fn undecodable(&self) -> Error {
140        self.file.corrupt("holds an entry that does not decode")
141    }
142
143    /// The rule array framed by context tag `tag` at `*pos`, if one is there,
144    /// moving `*pos` past it.
145    fn decode_array(
146        &self,
147        body: &[u8],
148        pos: &mut usize,
149        tag: u8,
150    ) -> Result<Option<Vec<BACnetAccessRule>>, Error> {
151        if !next_is_opening(body, *pos, tag).map_err(|_| self.undecodable())? {
152            return Ok(None);
153        }
154        let mut at = expect_opening(body, *pos, tag, WHAT).map_err(|_| self.undecodable())?;
155        let mut rules = Vec::new();
156        loop {
157            // Running out of octets here means the frame never closed.
158            let (next, after) = tags::decode_tag(body, at).map_err(|_| self.undecodable())?;
159            if next.is_closing_tag(tag) {
160                *pos = after;
161                return Ok(Some(rules));
162            }
163            if rules.len() == MAX_ACCESS_RULES {
164                return Err(self.file.corrupt("holds more entries than the cap"));
165            }
166            let (rule, end) = decode_access_rule(body, at).map_err(|_| self.undecodable())?;
167            rules.push(rule);
168            at = end;
169        }
170    }
171
172    /// The Accompaniment framed by context tag 3 at `*pos`, if one is there,
173    /// moving `*pos` past it.
174    fn decode_accompaniment(
175        &self,
176        body: &[u8],
177        pos: &mut usize,
178    ) -> Result<Option<BACnetDeviceObjectReference>, Error> {
179        if !next_is_opening(body, *pos, ACCOMPANIMENT_TAG).map_err(|_| self.undecodable())? {
180            return Ok(None);
181        }
182        let decoded = expect_opening(body, *pos, ACCOMPANIMENT_TAG, WHAT)
183            .and_then(|at| decode_device_object_reference(body, at))
184            .and_then(|(reference, at)| {
185                expect_closing(body, at, ACCOMPANIMENT_TAG, WHAT).map(|end| (reference, end))
186            });
187        let (reference, end) = decoded.map_err(|_| self.undecodable())?;
188        *pos = end;
189        Ok(Some(reference))
190    }
191}
192
193#[cfg(feature = "std")]
194impl AccessRightsPersistence for FileAccessRightsPersistence {
195    fn load(&self, rights: ObjectIdentifier) -> Result<Option<AccessRightsSnapshot>, Error> {
196        let Some(body) = self.file.load(rights)? else {
197            return Ok(None);
198        };
199        let mut pos = 0;
200        let positive_access_rules = self.decode_array(&body, &mut pos, POSITIVE_TAG)?;
201        let negative_access_rules = self.decode_array(&body, &mut pos, NEGATIVE_TAG)?;
202        let enable = if next_is_context(&body, pos, ENABLE_TAG).map_err(|_| self.undecodable())? {
203            let (enable, end) =
204                decode_ctx_boolean(&body, pos, ENABLE_TAG, WHAT).map_err(|_| self.undecodable())?;
205            pos = end;
206            Some(enable)
207        } else {
208            None
209        };
210        let accompaniment = self.decode_accompaniment(&body, &mut pos)?;
211        if pos != body.len() {
212            return Err(self.undecodable());
213        }
214        Ok(Some(AccessRightsSnapshot {
215            positive_access_rules,
216            negative_access_rules,
217            enable,
218            accompaniment,
219        }))
220    }
221
222    fn save(&self, rights: ObjectIdentifier, snapshot: &AccessRightsSnapshot) -> Result<(), Error> {
223        let mut body = BytesMut::new();
224        for (tag, rules) in [
225            (POSITIVE_TAG, &snapshot.positive_access_rules),
226            (NEGATIVE_TAG, &snapshot.negative_access_rules),
227        ] {
228            if let Some(rules) = rules {
229                tags::encode_opening_tag(&mut body, tag);
230                for rule in rules {
231                    encode_access_rule(&mut body, rule);
232                }
233                tags::encode_closing_tag(&mut body, tag);
234            }
235        }
236        if let Some(enable) = snapshot.enable {
237            primitives::encode_ctx_boolean(&mut body, ENABLE_TAG, enable);
238        }
239        if let Some(reference) = &snapshot.accompaniment {
240            tags::encode_opening_tag(&mut body, ACCOMPANIMENT_TAG);
241            encode_device_object_reference(&mut body, reference);
242            tags::encode_closing_tag(&mut body, ACCOMPANIMENT_TAG);
243        }
244        self.file.save(rights, &body)
245    }
246}