Skip to main content

bacnet_endpoint/
device_writes.rs

1//! Pre-start execution profile and authority for the endpoint Device responder.
2
3use super::*;
4use bacnet_server::mutation::DeviceWriteAuthorizer;
5use bacnet_server::server::ReinitializeContext;
6use bacnet_types::enums::ServiceSupported;
7
8/// The services the session's server role, its bounded Device responder,
9/// executes on every session, as `BACnetServicesSupported` bits (Clause 21).
10///
11/// The responder is not the full `bacnet-server` service set: everything not
12/// here or in [`RESPONDER_OPT_IN_SERVICES`] is refused.
13pub const RESPONDER_SERVICES: &[ServiceSupported] = &[ServiceSupported::READ_PROPERTY];
14
15/// A service the session's server role executes only once an
16/// [`EndpointSession`] method enables it.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct ResponderOptIn {
19    /// The service, as its `BACnetServicesSupported` bit.
20    pub service: ServiceSupported,
21    /// The name of the [`EndpointSession`] method that enables it.
22    pub method: &'static str,
23}
24
25/// The services the server role executes only once enabled, with the method
26/// that enables each. The local Device and the session identity then
27/// advertise them too.
28pub const RESPONDER_OPT_IN_SERVICES: &[ResponderOptIn] = &[
29    ResponderOptIn {
30        service: ServiceSupported::WRITE_PROPERTY,
31        method: "with_device_writes",
32    },
33    ResponderOptIn {
34        service: ServiceSupported::REINITIALIZE_DEVICE,
35        method: "with_reinitialize",
36    },
37];
38
39impl<T: TransportPort + 'static> EndpointSession<T> {
40    /// Enables authorized writes to the local Device's `Description` and installed Audit recipient.
41    ///
42    /// The callback is mandatory and receives the existing redacted mutation
43    /// context. A refusal or panic denies the request before mutation. It must
44    /// be fast, nonblocking and side-effect-free; see [`DeviceWriteAuthorizer`].
45    /// Other objects/properties and WritePropertyMultiple remain unsupported.
46    /// An authorized NULL relinquishment succeeds without changing Description.
47    /// Recipient writes require the complete source runtime installed by source selection.
48    ///
49    /// Startup requires a server role and a concrete built-in local Device in
50    /// the attached database ([`ObjectDatabase::local_device`]: the lowest
51    /// instance when it holds several). Writes reach only that Device; any
52    /// other Device is out of scope. A composed identity must match it and
53    /// contain only the services the responder executes. Validation errors
54    /// precede configuration mutation and transport startup, allowing correction
55    /// and retry. The Device and identity then advertise exactly those services;
56    /// the default session remains ReadProperty-only.
57    ///
58    /// # Panics
59    /// Panics if startup has already consumed the session configuration.
60    pub fn with_device_writes(mut self, authorizer: DeviceWriteAuthorizer) -> Self {
61        self.assert_configurable();
62        self.device_write_authorizer = Some(authorizer);
63        self
64    }
65
66    /// Enables ReinitializeDevice, carried out by `handler` with the
67    /// requester's [`ReinitializeContext`] and the database write-locked.
68    ///
69    /// Startup requirements and the advertised services are those of
70    /// [`with_device_writes`](Self::with_device_writes). The handler follows
71    /// the rules on
72    /// [`ReinitializeHandler`](bacnet_server::server::ReinitializeHandler).
73    /// The SimpleACK goes out only after it returns, so register any restart
74    /// with [`ReinitializeContext::after_reply`] instead of restarting
75    /// inline: the hook runs once the egress has sent the SimpleACK, on the
76    /// same dispatch task, and never for a refusal. It runs synchronously
77    /// on the session's single dispatch task, so a slow or blocking handler
78    /// stalls the whole session: hand slow work such as backup files to a
79    /// task. Without a password set by
80    /// [`with_reinit_password`](Self::with_reinit_password) any peer reaches
81    /// it, and the Device-write authorizer doesn't cover this service, so
82    /// restrict sources through the context. Refuse with [`Error::protocol`];
83    /// a panic or an [`Error::reject`] is answered SERVICES / OTHER and the
84    /// session keeps serving.
85    ///
86    /// # Panics
87    /// Panics if startup has already consumed the session configuration.
88    pub fn with_reinitialize<F>(mut self, handler: F) -> Self
89    where
90        F: Fn(&ReinitializeContext, &mut ObjectDatabase) -> Result<(), Error>
91            + Send
92            + Sync
93            + 'static,
94    {
95        self.assert_configurable();
96        self.reinitialize = Some(Arc::new(handler));
97        self
98    }
99
100    /// Sets the password a ReinitializeDevice request must carry.
101    ///
102    /// It takes effect only with [`with_reinitialize`](Self::with_reinitialize):
103    /// startup refuses a password without a handler.
104    ///
105    /// # Panics
106    /// Panics if startup has already consumed the session configuration.
107    pub fn with_reinit_password(mut self, password: impl Into<String>) -> Self {
108        self.assert_configurable();
109        self.reinit_password = Some(password.into());
110        self
111    }
112
113    /// The services the responder executes with the capabilities enabled on
114    /// this session: [`RESPONDER_SERVICES`], then each enabled entry of
115    /// [`RESPONDER_OPT_IN_SERVICES`].
116    pub(crate) fn executed_services(&self) -> Vec<ServiceSupported> {
117        let enabled = |service: ServiceSupported| {
118            (service == ServiceSupported::WRITE_PROPERTY && self.device_write_authorizer.is_some())
119                || (service == ServiceSupported::REINITIALIZE_DEVICE && self.reinitialize.is_some())
120        };
121        let opt_in = RESPONDER_OPT_IN_SERVICES
122            .iter()
123            .map(|opt_in| opt_in.service)
124            .filter(|service| enabled(*service));
125        RESPONDER_SERVICES.iter().copied().chain(opt_in).collect()
126    }
127
128    pub(super) async fn validate_segmentation(&self) -> Result<(), Error> {
129        if self.identity.as_ref().is_some_and(|identity| {
130            identity.segmentation() != bacnet_types::enums::Segmentation::NONE
131        }) {
132            return Err(Error::Encoding(
133                "EndpointSession roles require segmentation NONE".into(),
134            ));
135        }
136        if let Some(database) = self.database.as_ref() {
137            let database = database.read().await;
138            if let Some(oid) = database.selected_device() {
139                let device = database
140                    .get(&oid)
141                    .expect("selected under same database guard");
142                let mode =
143                    device.read_property(PropertyIdentifier::SEGMENTATION_SUPPORTED, None)?;
144                if mode
145                    != PropertyValue::Enumerated(u32::from(
146                        bacnet_types::enums::Segmentation::NONE.to_raw(),
147                    ))
148                {
149                    return Err(Error::Encoding(
150                        "EndpointSession selected Device requires segmentation NONE".into(),
151                    ));
152                }
153            }
154        }
155        Ok(())
156    }
157
158    pub(super) fn validate_device_execution(&mut self) -> Result<Option<ObjectIdentifier>, Error> {
159        let writes = self.device_write_authorizer.is_some();
160        let reinitialize = self.reinitialize.is_some();
161        if self.reinit_password.is_some() && !reinitialize {
162            return Err(Error::Encoding(
163                "a ReinitializeDevice password requires a ReinitializeDevice handler".into(),
164            ));
165        }
166        // An empty password could never match: a request's empty password
167        // decodes as none (#2094).
168        if self.reinit_password.as_deref() == Some("") {
169            return Err(Error::Encoding(
170                "reinit_password is empty; leave it unset for no password".into(),
171            ));
172        }
173        // Past the 20-character limit no request could carry it (#2060).
174        if self
175            .reinit_password
176            .as_deref()
177            .is_some_and(|password| !bacnet_services::device_mgmt::password_fits(password))
178        {
179            return Err(Error::Encoding(format!(
180                "reinit_password exceeds the {}-character limit",
181                bacnet_services::device_mgmt::MAX_PASSWORD_CHARS
182            )));
183        }
184        // Names the enabled capabilities that need the Device.
185        let (capability, subject) = match (writes, reinitialize) {
186            (true, true) => (
187                "Device writes and ReinitializeDevice",
188                "Device writes and ReinitializeDevice require",
189            ),
190            (true, false) => ("Device writes", "Device writes require"),
191            (false, _) => ("ReinitializeDevice", "ReinitializeDevice requires"),
192        };
193        if self.role == SessionRole::ClientOnly {
194            return if writes || reinitialize {
195                Err(Error::Encoding(format!("{subject} a server role")))
196            } else {
197                Ok(None)
198            };
199        }
200        let allowed = self.executed_services();
201        if self.identity.as_ref().is_some_and(|identity| {
202            !identity
203                .services()
204                .contains(&ServiceSupported::READ_PROPERTY)
205                || identity
206                    .services()
207                    .iter()
208                    .any(|service| !allowed.contains(service))
209        }) {
210            return Err(Error::Encoding(
211                "Endpoint identity services do not match the responder's services".into(),
212            ));
213        }
214        if !writes && !reinitialize {
215            return Ok(None);
216        }
217        let db = self
218            .database
219            .as_mut()
220            .ok_or_else(|| Error::Encoding(format!("{subject} an attached local database")))?;
221        let db = Arc::get_mut(db)
222            .expect("database is unshared before startup")
223            .get_mut();
224        // The target is the selected Device, the one wildcard reads resolve
225        // to, and it must be concrete.
226        let oid = db
227            .local_device()
228            .identifier()
229            .ok_or_else(|| Error::Encoding(format!("{subject} a concrete local Device")))?;
230        if self
231            .identity
232            .as_ref()
233            .is_some_and(|identity| identity.device_oid() != oid)
234        {
235            return Err(Error::Encoding(format!(
236                "{capability} local Device does not match session identity"
237            )));
238        }
239        if !db
240            .get_mut(&oid)
241            .expect("Device exists")
242            .device_authority_internal()
243            .is_some_and(|device| device.object_identifier() == oid)
244        {
245            return Err(Error::Encoding(format!(
246                "{subject} the built-in Device authority"
247            )));
248        }
249        Ok(Some(oid))
250    }
251
252    // All fallible configuration validation (including source ownership) precedes
253    // this commit. No await/callback or second copy of Device state is involved.
254    pub(super) fn commit_device_profile(&mut self, target: Option<ObjectIdentifier>) {
255        let Some(oid) = target else { return };
256        let services = self.executed_services();
257        let db = Arc::get_mut(self.database.as_mut().expect("validated database"))
258            .expect("database is unshared before startup")
259            .get_mut();
260        db.get_mut(&oid)
261            .expect("validated Device")
262            .device_authority_internal()
263            .expect("validated Device authority")
264            .set_services_supported(&services);
265        if let Some(identity) = self.identity.take() {
266            self.identity = Some(identity.with_services(&services));
267        }
268    }
269}