Skip to main content

bacnet_server/cov/
identity.rs

1use super::*;
2use bacnet_types::constructed::BACnetAddress;
3
4/// Delivery endpoint, including the immediate router for routed traffic.
5/// All COV family keys use the original client recipient as identity.
6/// Quota and notification accounting use that same [`CovRecipient`].
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub struct SubscriberEndpoint {
9    /// Immediate transport destination (the router when routed).
10    pub mac: MacAddr,
11    /// Original routed NPDU source, if any.
12    pub network: Option<NpduAddress>,
13}
14
15impl SubscriberEndpoint {
16    /// Capture both parts of the admitted transport endpoint.
17    pub fn new(mac: &[u8], network: Option<&NpduAddress>) -> Self {
18        Self {
19            mac: MacAddr::from_slice(mac),
20            network: network.cloned(),
21        }
22    }
23}
24
25/// Original BACnet client address for COV identity, quota and notification accounting.
26/// A routed address is independent of the immediate router; it is a claimed
27/// protocol address, not an authentication credential.
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub enum CovRecipient {
30    /// A local-network client, identified by its source MAC.
31    Direct(MacAddr),
32    /// The original SNET/SADR of a client behind a router.
33    Routed(NpduAddress),
34}
35
36impl CovRecipient {
37    /// Derive client identity separately from the current delivery route.
38    pub fn from_endpoint(mac: &[u8], network: Option<&NpduAddress>) -> Self {
39        match network {
40            Some(source) => Self::Routed(source.clone()),
41            None => Self::Direct(MacAddr::from_slice(mac)),
42        }
43    }
44    /// Table admission requires the same nonempty routed source MAC as NPDU
45    /// decoding. The address either form reports in the Device's COV lists
46    /// must also fit [`BACnetAddress::MAX_MAC_LEN`] octets, so every admitted
47    /// subscription encodes there (#1156). Off the wire both forms already
48    /// fit: the NPDU codec bounds a routed source (#1141) and the network
49    /// layer drops a frame from a longer link-layer MAC (#1198). The check
50    /// stays for subscriptions that reach the public table API another way.
51    pub(crate) fn validate(&self) -> Result<(), Error> {
52        if matches!(self, Self::Routed(source) if source.mac_address.is_empty()) {
53            return Err(Error::Encoding(
54                "COV routed recipient requires a nonempty source MAC".into(),
55            ));
56        }
57        let mac = match self {
58            Self::Direct(mac) => mac,
59            Self::Routed(source) => &source.mac_address,
60        };
61        if mac.len() > BACnetAddress::MAX_MAC_LEN {
62            return Err(Error::Encoding(format!(
63                "COV recipient MAC of {} octets exceeds the {}-octet BACnetAddress limit",
64                mac.len(),
65                BACnetAddress::MAX_MAC_LEN
66            )));
67        }
68        Ok(())
69    }
70}
71
72/// Multiple notification context. Confirmed and unconfirmed forms are independent.
73#[derive(Debug, Clone, PartialEq, Eq, Hash)]
74pub struct MultipleContextKey {
75    /// Original client address, without the immediate router.
76    pub recipient: CovRecipient,
77    /// Subscriber's process identifier.
78    pub process_id: u32,
79    /// Multiple notification form, unlike mutable ordinary/Single mode.
80    pub confirmed: bool,
81}
82
83/// Complete accepted subscription coordinates used by every table operation.
84#[derive(Debug, Clone, PartialEq, Eq, Hash)]
85pub enum CovSubscriptionKey {
86    /// Ordinary whole-object subscription.
87    Object {
88        /// Original client address, without the immediate router.
89        recipient: CovRecipient,
90        /// Subscriber's process identifier.
91        process_id: u32,
92        /// Monitored object identifier.
93        object: ObjectIdentifier,
94    },
95    /// Single-property subscription. Absent, zero and element indexes are distinct.
96    Property {
97        /// Original client address, without the immediate router.
98        recipient: CovRecipient,
99        /// Subscriber's process identifier.
100        process_id: u32,
101        /// Monitored object identifier.
102        object: ObjectIdentifier,
103        /// Monitored property identifier.
104        property: PropertyIdentifier,
105        /// Optional array coordinate; absence and zero remain distinct.
106        index: Option<u32>,
107    },
108    /// A property reference within a Multiple context.
109    Multiple {
110        /// Exact Multiple context, including notification form.
111        context: MultipleContextKey,
112        /// Monitored object identifier.
113        object: ObjectIdentifier,
114        /// Monitored property identifier.
115        property: PropertyIdentifier,
116        /// Optional array coordinate; absence and zero remain distinct.
117        index: Option<u32>,
118    },
119}
120
121impl CovSubscriptionKey {
122    /// Monitored object shared by all three subscription families.
123    pub fn object(&self) -> ObjectIdentifier {
124        match self {
125            Self::Object { object, .. }
126            | Self::Property { object, .. }
127            | Self::Multiple { object, .. } => *object,
128        }
129    }
130
131    /// Matching Multiple context, when this is a Multiple reference.
132    pub fn multiple_context(&self) -> Option<&MultipleContextKey> {
133        match self {
134            Self::Multiple { context, .. } => Some(context),
135            _ => None,
136        }
137    }
138}
139
140impl CovSubscription {
141    /// Current immediate transport route and original routed source.
142    pub fn endpoint(&self) -> SubscriberEndpoint {
143        SubscriberEndpoint::new(&self.subscriber_mac, self.subscriber_network.as_ref())
144    }
145
146    /// Validate and derive the sole table identity from this proposed subscription.
147    pub fn key(&self) -> Result<CovSubscriptionKey, Error> {
148        let recipient = self.recipient();
149        recipient.validate()?;
150        let process_id = self.subscriber_process_identifier;
151        let object = self.monitored_object_identifier;
152        let index = self.monitored_property_array_index;
153        match (self.notification_kind, self.monitored_property, index) {
154            (CovNotificationKind::Single, None, None) => Ok(CovSubscriptionKey::Object { recipient, process_id, object }),
155            (CovNotificationKind::Single, Some(property), _) => Ok(CovSubscriptionKey::Property { recipient, process_id, object, property, index }),
156            (CovNotificationKind::Multiple, Some(property), _) => Ok(CovSubscriptionKey::Multiple {
157                context: MultipleContextKey { recipient, process_id, confirmed: self.issue_confirmed_notifications }, object, property, index,
158            }),
159            _ => Err(Error::Encoding("COV reference requires a property; whole-object subscriptions cannot carry an index".into())),
160        }
161    }
162}
163
164/// Immutable accepted entry carried through initial notification and fanout work.
165/// Only a table can create it. Renewal/recreation invalidates earlier snapshots;
166/// snapshots from another table cannot complete this table's entries.
167#[derive(Debug, Clone)]
168pub struct CovSubscriptionSnapshot {
169    pub(super) key: CovSubscriptionKey,
170    pub(super) generation: u64,
171    pub(super) owner: Arc<ObservationOwner>,
172    // Zero is the initial marker; issued tickets start at one and never wrap.
173    pub(super) last_successful_ticket: u64,
174    // Outstanding confirmed report and hold-off of this entry's coordinate
175    // (#896): the entry itself for ordinary and Single subscriptions, the whole
176    // context for Multiple ones. A Multiple context shares it for one live route
177    // incarnation, so a route change fences old snapshots; a same-route refresh
178    // keeps it. Private so callers cannot forge completion authority.
179    pub(super) flight: super::confirmed::FlightMarker,
180    pub(super) subscription: CovSubscription,
181    /// Reported maximum notification delay of a Multiple reference; `None`
182    /// for ordinary and Single entries. The timed store keeps the context's
183    /// current value for its deadline backstop.
184    pub(super) max_notification_delay: Option<u32>,
185    /// Max-APDU-length-accepted the subscriber of a Multiple reference last
186    /// advertised in a SubscribeCOVPropertyMultiple request header, if known.
187    pub(super) subscriber_max_apdu: Option<u16>,
188}
189
190impl CovSubscriptionSnapshot {
191    /// Canonical identity captured at acceptance.
192    pub fn key(&self) -> &CovSubscriptionKey {
193        &self.key
194    }
195
196    /// Table generation of this accepted entry; renewal publishes a new one.
197    pub(crate) fn generation(&self) -> u64 {
198        self.generation
199    }
200
201    /// Maximum notification delay reported for a Multiple reference (`None`
202    /// for ordinary and Single entries). A table-held entry follows every
203    /// refresh of its context; a captured snapshot keeps its acceptance value.
204    pub fn max_notification_delay(&self) -> Option<u32> {
205        self.max_notification_delay
206    }
207
208    /// Maximum APDU the subscriber of a Multiple reference advertised, when
209    /// known; its notifications fit the smaller of it and the local maximum.
210    /// A table-held entry follows every refresh of its context.
211    pub fn subscriber_max_apdu(&self) -> Option<u16> {
212        self.subscriber_max_apdu
213    }
214}
215
216impl std::ops::Deref for CovSubscriptionSnapshot {
217    type Target = CovSubscription;
218    fn deref(&self) -> &Self::Target {
219        &self.subscription
220    }
221}