Skip to main content

bacnet_objects/
command_source.rs

1//! Explicit command origins and object-owned source tracking (Clause 19.5):
2//! per priority for a commandable Present_Value, and a single source for one
3//! with no priority array (#1552).
4//!
5//! Standalone callers assert their origin; validation establishes syntax, not
6//! authentication or database membership. Full servers derive remote origins
7//! from ingress and validate local membership under their mutation guard.
8use bacnet_types::constructed::{BACnetAddress, BACnetDeviceObjectReference, BACnetValueSource};
9use bacnet_types::enums::ObjectType;
10use bacnet_types::error::Error;
11use bacnet_types::primitives::ObjectIdentifier;
12
13mod single;
14mod tracking;
15pub(crate) use single::SingleValueSource;
16pub(crate) use tracking::write_sourced_priority;
17pub(crate) use tracking::ValueSourceTracking;
18
19/// Address-to-Device correlation, never authentication of a principal.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum CommandDeviceBinding {
22    /// No fresh unambiguous binding matches the actual address.
23    Unknown,
24    /// Exactly one Device is correlated with the address.
25    Unique(ObjectIdentifier),
26    /// More than one Device matches; corrections fail closed.
27    Ambiguous,
28}
29
30/// Actual command writer, kept separately from its correctable source claim.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum CommandOrigin {
33    /// Actual network writer with an admission-time correlation snapshot.
34    Remote {
35        /// Original source address; routed senders retain SNET/SADR.
36        actual_address: BACnetAddress,
37        /// Correlation does not authenticate the writer.
38        binding: CommandDeviceBinding,
39    },
40    /// Trusted local producer owned by the selected concrete Device.
41    Local {
42        /// Device that retains correction ownership.
43        owner_device: ObjectIdentifier,
44        /// Optional local initiator published as the command source.
45        initiating_object: Option<ObjectIdentifier>,
46    },
47}
48
49impl CommandOrigin {
50    /// Reject malformed actual writer identities; generic ValueSource claims
51    /// deliberately have a broader grammar than an actual command origin. A
52    /// remote writer's MAC must fit [`BACnetAddress::MAX_MAC_LEN`] octets, so
53    /// the Value_Source published from it always encodes (#1156).
54    pub fn validate(&self) -> Result<(), Error> {
55        let concrete =
56            |oid: ObjectIdentifier| oid.instance_number() != ObjectIdentifier::WILDCARD_INSTANCE;
57        let device =
58            |oid: ObjectIdentifier| concrete(oid) && oid.object_type() == ObjectType::DEVICE;
59        let valid = match self {
60            Self::Local {
61                owner_device,
62                initiating_object,
63            } => device(*owner_device) && initiating_object.is_none_or(concrete),
64            Self::Remote {
65                actual_address,
66                binding,
67            } => {
68                actual_address.network_number != u16::MAX
69                    && !actual_address.mac_address.is_empty()
70                    && actual_address.mac_address.len() <= BACnetAddress::MAX_MAC_LEN
71                    && match binding {
72                        CommandDeviceBinding::Unique(oid) => device(*oid),
73                        _ => true,
74                    }
75            }
76        };
77        if valid {
78            Ok(())
79        } else {
80            Err(crate::common::write_access_denied_error())
81        }
82    }
83
84    pub(crate) fn published_source(&self) -> BACnetValueSource {
85        match self {
86            Self::Local {
87                owner_device,
88                initiating_object,
89            } => BACnetValueSource::Object(BACnetDeviceObjectReference {
90                device_identifier: None,
91                object_identifier: initiating_object.unwrap_or(*owner_device),
92            }),
93            Self::Remote {
94                binding: CommandDeviceBinding::Unique(device),
95                ..
96            } => BACnetValueSource::Object(BACnetDeviceObjectReference {
97                device_identifier: None,
98                object_identifier: *device,
99            }),
100            Self::Remote { actual_address, .. } => {
101                BACnetValueSource::Address(actual_address.clone())
102            }
103        }
104    }
105
106    pub(crate) fn permits_correction_by(&self, incoming: &Self) -> bool {
107        match (self, incoming) {
108            (
109                Self::Local {
110                    owner_device: old, ..
111                },
112                Self::Local {
113                    owner_device: new, ..
114                },
115            ) => old == new,
116            (
117                Self::Remote {
118                    actual_address: old_address,
119                    binding: old,
120                },
121                Self::Remote {
122                    actual_address: new_address,
123                    binding: new,
124                },
125            ) => {
126                use CommandDeviceBinding::*;
127                match (old, new) {
128                    (Ambiguous, _) | (_, Ambiguous) => false,
129                    (Unique(a), Unique(b)) => a == b,
130                    _ => old_address == new_address,
131                }
132            }
133            _ => false,
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests;
140
141#[cfg(test)]
142pub(crate) fn test_origin() -> crate::command_source::CommandOrigin {
143    crate::command_source::CommandOrigin::Local {
144        owner_device: bacnet_types::primitives::ObjectIdentifier::new(
145            bacnet_types::enums::ObjectType::DEVICE,
146            1,
147        )
148        .unwrap(),
149        initiating_object: None,
150    }
151}