Skip to main content

bacnet_objects/
command_source.rs

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