Skip to main content

bacnet_endpoint/
source_profile.rs

1//! Complete source profile preflight, before mutation or ingress ownership.
2use super::*;
3impl<T: TransportPort + 'static> EndpointSession<T> {
4    /// Write the active source profile's Device Audit recipient as a trusted local operation.
5    ///
6    /// `Some(value)` validates and changes the actual Device value, atomically
7    /// admitting its two notifications: to the old and the new recipient, or
8    /// to the new one and by global broadcast when the old is an Address the
9    /// session's number does not name. `None` is NULL relinquishment: it
10    /// succeeds unchanged after the same live-owner checks. Equal values are also
11    /// no-ops. This is runtime mutation, not initial provisioning or route setup.
12    ///
13    /// Available in a running `ClientOnly` or `Both` source session. Local calls
14    /// identify the local Device and have no network invoke ID; they do not invoke
15    /// the inbound network authorizer. Unavailable routes or precommit resource
16    /// failures leave state unchanged. Success establishes commit and owned
17    /// delivery admission, not remote receipt. The session's sealed state is
18    /// rechecked after acquiring the database; pre-start, stopping, stopped and
19    /// sessions without a source owner reject the operation.
20    pub async fn write_audit_recipient(
21        &self,
22        recipient: Option<bacnet_types::constructed::BACnetRecipient>,
23    ) -> Result<(), Error> {
24        let unavailable = || Error::Encoding("endpoint source Audit owner is not running".into());
25        if !self.is_running() {
26            return Err(unavailable());
27        }
28        let database = self.database.as_ref().ok_or_else(unavailable)?;
29        let mut db = database.write().await;
30        let runtime = self
31            .source_recipient
32            .as_ref()
33            .filter(|runtime| self.is_running() && runtime.owner.is_active())
34            .ok_or_else(unavailable)?;
35        runtime.write_local(&mut db, recipient)
36    }
37
38    pub(super) fn prepare_source_audit_reporter(
39        &mut self,
40    ) -> Result<Option<crate::source_audit::recipient::SourceRoutes>, Error> {
41        let Some(selected) = self.source_audit_reporter else {
42            return Ok(None);
43        };
44        if self.role == SessionRole::ServerOnly {
45            return Err(Error::Encoding(
46                "source Audit Reporter requires a client role".into(),
47            ));
48        }
49        if self.role == SessionRole::Both && self.device_write_authorizer.is_none() {
50            return Err(Error::Encoding(
51                "source Audit with Both roles requires an explicit Device write authorizer".into(),
52            ));
53        }
54        let broadcast = self
55            .ingress
56            .as_ref()
57            .and_then(|ingress| ingress.bip_broadcast_endpoint())
58            .ok_or_else(|| Error::Encoding("source Audit requires IPv4 B/IP".into()))?;
59        let routes = crate::source_audit::recipient::SourceRoutes::new(
60            &self.source_audit_bindings,
61            broadcast,
62        )?;
63        let db = self.database.as_mut().ok_or_else(|| {
64            Error::Encoding("source Audit Reporter requires an attached local database".into())
65        })?;
66        // Ready sessions have not shared the Arc with a responder. Synchronous,
67        // exclusive validation has no await/cancellation or concurrent mutation.
68        let db = Arc::get_mut(db)
69            .expect("database is unshared before startup")
70            .get_mut();
71        // The local Device is the audit identity. Source ownership refuses any
72        // new Device and keeps this one, so it stays the lowest while owned.
73        let device = db.local_device().identifier().ok_or_else(|| {
74            Error::Encoding("source Audit Reporter requires a concrete local Device".into())
75        })?;
76        if self
77            .identity
78            .as_ref()
79            .is_some_and(|identity| device.instance_number() != identity.instance())
80        {
81            return Err(Error::Encoding(
82                "source Audit Reporter local Device does not match session identity".into(),
83            ));
84        }
85        let recipient = db
86            .get_mut(&device)
87            .and_then(|object| object.device_authority_internal())
88            .filter(|authority| authority.object_identifier() == device)
89            .and_then(|authority| authority.provisioned_audit_recipient().cloned())
90            .ok_or_else(|| {
91                Error::Encoding(
92                    "source Audit requires a provisioned built-in Device recipient".into(),
93                )
94            })?;
95        routes.validate_initial(&recipient)?;
96        if selected.object_type() != ObjectType::AUDIT_REPORTER {
97            return Err(Error::Encoding(
98                "source selection must be an Audit Reporter".into(),
99            ));
100        }
101        let object = db.get(&selected).ok_or_else(|| {
102            Error::Encoding("selected source Audit Reporter is absent from the database".into())
103        })?;
104        if !object
105            .audit_reporter_internal()
106            .is_some_and(|reporter| reporter.object_identifier() == selected)
107        {
108            return Err(Error::Encoding(
109                "selected object lacks the Audit Reporter capability".into(),
110            ));
111        }
112        if object.audit_reporter_internal().is_some_and(|reporter| {
113            reporter
114                .property_list()
115                .contains(&PropertyIdentifier::MONITORED_OBJECTS)
116                || reporter
117                    .configuration_internal()
118                    .maximum_send_delay
119                    .is_some()
120        }) {
121            return Err(Error::Encoding(
122                "source Audit does not support Monitored_Objects or delayed notifications".into(),
123            ));
124        }
125        for (oid, object) in db.iter_objects() {
126            if oid != selected && oid.object_type() == ObjectType::AUDIT_REPORTER {
127                match object.read_property(PropertyIdentifier::AUDIT_SOURCE_REPORTER, None) {
128                    Ok(PropertyValue::Boolean(false)) => {}
129                    Ok(PropertyValue::Boolean(true)) => {
130                        return Err(Error::Encoding(
131                            "database already contains a conflicting source Audit Reporter".into(),
132                        ));
133                    }
134                    _ => {
135                        return Err(Error::Encoding(
136                            "cannot determine another Audit Reporter's source ownership".into(),
137                        ));
138                    }
139                }
140            }
141        }
142        Ok(Some(routes))
143    }
144}