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