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}