Skip to main content

bacnet_server/server/
audit_reporter.rs

1use super::event_recipient_route::ConfirmedRecipientRoute;
2use super::notification_transactions::{
3    AuditFailureContext, AuditFailureTicket, NotificationReservation, NotificationReserveError,
4};
5use super::*;
6use crate::handlers::{WriteCommitObserver, WriteTarget};
7use bacnet_objects::audit::AuditReporterStatus;
8use bacnet_services::audit::AuditNotificationRequest;
9use bacnet_types::constructed::{
10    AuditPropertyReference, BACnetAddress, BACnetAuditNotification, BACnetRecipient,
11};
12use bacnet_types::enums::{AuditLevel, AuditOperation};
13
14const DELIVERY_TIMEOUT: Duration = Duration::from_secs(3);
15
16#[path = "audit_reporter_read.rs"]
17mod read;
18
19#[path = "audit_reporter_dcc.rs"]
20mod dcc;
21
22#[path = "audit_reporter_write.rs"]
23mod write;
24use write::WriteSelection;
25
26#[path = "audit_policy_precommit.rs"]
27mod policy_precommit;
28
29/// A bounded set of target READ/WRITE/CREATE/DELETE Reporters sharing one Device recipient.
30///
31/// Reports successful inbound WP/WPM elements, AddListElement/RemoveListElement,
32/// AtomicWriteFile, CreateObject/DeleteObject operations, and authorized execution errors.
33/// Policy denials and undecoded/unattempted elements remain silent. Successes
34/// omit Result; execution failures include the mapped BACnet Error. There is no
35/// source-side reporting, forwarding, or durable outbox.
36/// Provision the typed recipient on the built-in Device before startup; configure
37/// Device routes with `device_binding`. Startup requires a concrete built-in
38/// local Device ([`ObjectDatabase::local_device`]: the lowest instance when the
39/// database holds several), a provision, and the selected Reporter capability.
40/// That Device names this device in every record and owns the recipient; any
41/// other Device in the database takes no part. Unresolved
42/// configured Device routes permit startup with CONFIGURATION_ERROR. Address
43/// choices require direct unicast IPv4 B/IP, on network zero or on this
44/// network's number once it is known (#1460); one naming a network the number
45/// in force does not name starts unresolved the same way. The active Device recipient is
46/// required/writable; actual local or authorized WP/WPM changes atomically reserve
47/// two attempts before commit, independently of ordinary reporting filters: to
48/// the old and the new recipient. The new route must be usable. An unavailable
49/// old route requires reconfiguration and restart, except an old Address the
50/// number does not name: an unconfirmed global broadcast takes its attempt
51/// (Clause 12.11.66). Active Device/Reporter membership is protected until quiescence.
52/// At most 64 deliveries are active per server. Optional object-owned
53/// Maximum_Send_Delay/Send_Now retains ordinary records in a bounded target queue:
54/// 256 records/256 KiB globally and 64 records/64 KiB per Reporter. Mandatory
55/// records remain immediate. Each send/ACK has one total three-second deadline
56/// and no retry. Known local losses use captured, bounded historical contexts;
57/// summary filtering remains the documented partial-profile policy. Unconfirmed
58/// success proves only transport acceptance. See docs/delayed-target-audit.md.
59///
60/// Audit_Source_Reporter remains false. AV/BV instance-owned overrides apply to
61/// target observations. Supported `write_local` operations share this observer;
62/// raw database authoring remains a bypass. Source reporting is a separate profile.
63/// Ordinary sensor samples and internal reliability updates never enter this
64/// producer. An enabled external write to a Reporter produces one record.
65/// Locally configured Monitored_Objects selects ordinary targets by exact object
66/// or object type. Omitted selection preserves catch-all behavior; an empty or
67/// all-NULL selection reports no ordinary targets. Reporter writes bypass it.
68/// Enabled nominal overlaps fault all affected Reporters; only the lowest instance
69/// emits, before ordinary filters. Live local setters and aggregate changes use
70/// atomic admission; equal configuration is silent. Network selection writes are
71/// not supported. See docs/target-audit-reporters.md for selected fallback policies.
72/// CREATE/DELETE require their operation bit, count as configuration operations,
73/// and ignore the priority filter. Records use the final/candidate created OID or
74/// captured deleted OID, with no property, priority, or values; initial values do
75/// not generate WRITE records. A failed by-type creation without an assigned,
76/// representable OID omits the target; only catch-all or type selection can match.
77/// Deleting or replacing the selected Reporter is denied until the target
78/// runtime has stopped and released membership protection.
79///
80/// List edits use WRITE, object/property/requested-index identity, no priority,
81/// the requested delta as raw Target_Value, and the known pre-image as Current_Value.
82/// Empty, structurally invalid, or over-32-octet values are omitted, never wrapped
83/// or truncated. Element and framed-list decoding precedes observation; valid
84/// execution failures retain the response-mapped Result. Successful no-op removals
85/// still report once. AUDIT_CONFIG admits implemented non-Present_Value lists;
86/// list services ignore priority filtering and reuse ordinary target selection.
87/// AtomicWriteFile uses WRITE and the known target OID, with no property,
88/// priority, or values (Table 19-5). AUDIT_CONFIG and AUDIT_ALL admit file writes;
89/// priority filtering is irrelevant. Admission follows the service decoder's
90/// acceptance boundary, which refuses octets after the request's last member.
91/// Decoder rejections and configured payload/count budget Aborts remain silent.
92/// An inbound WriteGroup reports WRITE (Table 19-5) once per Channel it writes,
93/// as a WriteProperty of that Channel's Present_Value at the priority used, with
94/// the requester as source and no invoke ID; a Channel write the mutation
95/// authorizer denied makes none (#1318). A Channel's Present_Value is
96/// commandable (Clause 12.53.5), so its records, a WriteProperty's included,
97/// carry the priority and Audit_Priority_Filter applies to them (Clause
98/// 19.6.3): one at a disabled priority is dropped.
99///
100/// READ covers completed, unsegmented RP/RPM responses, one record per
101/// returned property outcome in result order, including inline RPM errors.
102/// Read values, comments, and priorities are always omitted. READ requires its
103/// operation bit; AUDIT_CONFIG excludes Present_Value. RPM's existing result
104/// budget bounds provisional intents, which are discarded on whole-request
105/// failure. Admission follows release of the read guard and uses the same
106/// captured delivery, optional delay and bounded resource-loss policy.
107///
108/// ```no_run
109/// use bacnet_objects::{audit::AuditReporterObject, database::ObjectDatabase,
110///     device::{DeviceConfig, DeviceObject}};
111/// use bacnet_server::server::{AuditReportersConfig, BACnetServer, DeviceBinding};
112/// use bacnet_types::{bitstring::AuditOperationFlags, enums::{AuditLevel,
113///     AuditOperation, ObjectType}, primitives::ObjectIdentifier, constructed::BACnetRecipient};
114/// # async fn example() -> Result<(), bacnet_types::error::Error> {
115/// let mut db = ObjectDatabase::new();
116/// let recipient = ObjectIdentifier::new(ObjectType::DEVICE, 200)?;
117/// let mut device = DeviceObject::new(DeviceConfig::default())?;
118/// device.provision_audit_recipient(BACnetRecipient::Device(recipient))?;
119/// db.add(Box::new(device))?;
120/// let mut reporter = AuditReporterObject::new(1, "Target writes")?;
121/// reporter.set_audit_level(AuditLevel::AUDIT_ALL)?;
122/// let mut operations = AuditOperationFlags::empty();
123/// operations.insert(AuditOperation::WRITE);
124/// reporter.set_auditable_operations(operations)?;
125/// reporter.set_issue_confirmed_notifications(true)?;
126/// db.add(Box::new(reporter))?;
127/// let mut server = BACnetServer::bip_builder().database(db)
128///     .audit_reporters(AuditReportersConfig {
129///         reporters: vec![ObjectIdentifier::new(ObjectType::AUDIT_REPORTER, 1)?],
130///     })
131///     .device_binding(DeviceBinding::local(recipient, [127, 0, 0, 1, 0xBA, 0xC1])?)?
132///     .build().await?;
133/// server.stop().await?;
134/// # Ok(())
135/// # }
136/// ```
137#[derive(Debug, Clone)]
138pub struct AuditReportersConfig {
139    /// Between one and 64 distinct concrete target Reporter identifiers.
140    /// Startup validates and canonicalizes them by instance; this is a local bound.
141    pub reporters: Vec<ObjectIdentifier>,
142}
143
144impl AuditReportersConfig {
145    pub(super) fn canonicalize(&mut self) -> Result<(), Error> {
146        if self.reporters.is_empty()
147            || self.reporters.len() > 64
148            || self.reporters.iter().any(|oid| {
149                oid.object_type() != ObjectType::AUDIT_REPORTER
150                    || oid.instance_number() == ObjectIdentifier::MAX_INSTANCE
151            })
152        {
153            return Err(Error::Encoding(
154                "target Audit requires 1..=64 concrete Reporter identifiers".into(),
155            ));
156        }
157        self.reporters.sort_by_key(|oid| oid.instance_number());
158        if self.reporters.windows(2).any(|pair| pair[0] == pair[1]) {
159            return Err(Error::Encoding(
160                "duplicate target Audit Reporter identifier".into(),
161            ));
162        }
163        Ok(())
164    }
165}
166
167impl<T: TransportPort + 'static> ServerBuilder<T> {
168    /// Enable the narrow target audit profile; see [`AuditReportersConfig`].
169    pub fn audit_reporters(mut self, profile: AuditReportersConfig) -> Self {
170        self.config.audit_reporters = Some(profile);
171        self
172    }
173}
174
175impl BipServerBuilder {
176    /// Enable the narrow target audit profile; see [`AuditReportersConfig`].
177    pub fn audit_reporters(mut self, profile: AuditReportersConfig) -> Self {
178        self.config.audit_reporters = Some(profile);
179        self
180    }
181}
182
183/// Validate the supported direct unicast IPv4 Audit address shape. Link-kind
184/// and interface-specific broadcast checks remain the runtime owner's duty.
185#[doc(hidden)]
186pub fn valid_bip_audit_address(address: &BACnetAddress) -> bool {
187    if address.network_number != 0 || address.mac_address.len() != 6 {
188        return false;
189    }
190    let mac = address.mac_address.as_slice();
191    let ip = std::net::Ipv4Addr::new(mac[0], mac[1], mac[2], mac[3]);
192    !ip.is_unspecified()
193        && !ip.is_multicast()
194        && !ip.is_broadcast()
195        && mac[0] != 0
196        && mac[0] < 240
197        && (mac[4] != 0 || mac[5] != 0)
198}
199
200fn recipient(db: &ObjectDatabase, device: ObjectIdentifier) -> Option<BACnetRecipient> {
201    let PropertyValue::ApplicationData(value) = db
202        .get(&device)?
203        .read_property(PropertyIdentifier::AUDIT_NOTIFICATION_RECIPIENT, None)
204        .ok()?
205    else {
206        return None;
207    };
208    bacnet_encoding::constructed::decode_recipient(&value, 0)
209        .ok()
210        .map(|(value, _)| value)
211}
212
213/// Where a request came from: the peer's link address, its network address
214/// when routed, and a confirmed request's invoke ID.
215#[derive(Clone, Copy)]
216pub(super) struct RequestSource<'s> {
217    pub(super) mac: &'s [u8],
218    pub(super) network: Option<&'s NpduAddress>,
219    pub(super) invoke_id: u8,
220    /// This network's own number as read for the request, the one its
221    /// command origin was matched against too, so a binding routed through
222    /// it names a direct request from its final MAC (#1404).
223    pub(super) local_network: Option<u16>,
224}
225
226pub(super) struct WriteAudit<'a, T: TransportPort> {
227    config: &'a ServerConfig,
228    network: &'a Arc<NetworkLayer<T>>,
229    transactions: &'a Arc<NotificationTransactions>,
230    source: BACnetRecipient,
231    invoke_id: Option<u8>,
232    pending: Option<PendingWrite>,
233}
234
235struct PendingWrite {
236    delay: Option<bacnet_objects::audit::AuditSendDelay>,
237    selection: Option<WriteSelection>,
238    route: Option<Arc<ConfirmedRecipientRoute>>,
239    failure: Option<AuditFailureTicket<Arc<ConfirmedRecipientRoute>>>,
240    completion: bacnet_objects::audit::AuditDeliveryToken,
241    status: Arc<AuditReporterStatus>,
242    confirmed: bool,
243    notification: BACnetAuditNotification,
244}
245
246impl<'a, T: TransportPort + 'static> WriteAudit<'a, T> {
247    pub(super) fn write_source(&self) -> bacnet_objects::device::AuditWriteSource {
248        bacnet_objects::device::AuditWriteSource {
249            device: self.source.clone(),
250            invoke_id: self.invoke_id.expect("network write source"),
251        }
252    }
253
254    pub(super) fn local(
255        config: &'a ServerConfig,
256        network: &'a Arc<NetworkLayer<T>>,
257        transactions: &'a Arc<NotificationTransactions>,
258        db: &ObjectDatabase,
259    ) -> Option<Self> {
260        Some(Self {
261            config,
262            network,
263            transactions,
264            source: BACnetRecipient::Device(db.local_device().identifier()?),
265            invoke_id: None,
266            pending: None,
267        })
268    }
269
270    pub(super) async fn new(
271        config: &'a ServerConfig,
272        network: &'a Arc<NetworkLayer<T>>,
273        transactions: &'a Arc<NotificationTransactions>,
274        bindings: &Arc<RwLock<DeviceBindingTable>>,
275        request: RequestSource<'_>,
276    ) -> Self {
277        let RequestSource {
278            mac,
279            network: source_network,
280            invoke_id,
281            local_network,
282        } = request;
283        let source = Self::requester(
284            config,
285            transactions,
286            bindings,
287            mac,
288            source_network,
289            local_network,
290        )
291        .await;
292        Self::requested_by(config, network, transactions, source, Some(invoke_id))
293    }
294
295    /// The audit of writes `source` asked for, in a request carrying
296    /// `invoke_id`; `None` for an unconfirmed one (Table 19-4).
297    pub(super) fn requested_by(
298        config: &'a ServerConfig,
299        network: &'a Arc<NetworkLayer<T>>,
300        transactions: &'a Arc<NotificationTransactions>,
301        source: BACnetRecipient,
302        invoke_id: Option<u8>,
303    ) -> Self {
304        Self {
305            config,
306            network,
307            transactions,
308            source,
309            invoke_id,
310            pending: None,
311        }
312    }
313
314    /// The device a request from `source_mac`, routed from `source_network`,
315    /// came from, as an Audit record names it: the Device bound to that
316    /// address, while the audit profile is on and one is, otherwise the
317    /// address. `local_network` is this network's number as read for the
318    /// request.
319    pub(super) async fn requester(
320        config: &ServerConfig,
321        transactions: &Arc<NotificationTransactions>,
322        bindings: &Arc<RwLock<DeviceBindingTable>>,
323        source_mac: &[u8],
324        source_network: Option<&NpduAddress>,
325        local_network: Option<u16>,
326    ) -> BACnetRecipient {
327        // Entries were checked against the concrete link at configuration or
328        // observation admission. Correlation needs no caller code under locks.
329        let known_source = if config.audit_reporters.is_some() {
330            bindings
331                .read()
332                .await
333                .source_device(source_mac, source_network, local_network, |mac| {
334                    transactions
335                        .audit_routes
336                        .get()
337                        .is_some_and(|routes| routes.is_group(mac))
338                })
339        } else {
340            None
341        };
342        known_source
343            .map(BACnetRecipient::Device)
344            .unwrap_or_else(|| {
345                BACnetRecipient::Address(BACnetAddress {
346                    network_number: source_network.map_or(0, |source| source.network),
347                    mac_address: source_network.map_or_else(
348                        || MacAddr::from_slice(source_mac),
349                        |source| source.mac_address.clone(),
350                    ),
351                })
352            })
353    }
354}
355
356impl<T: TransportPort + 'static> WriteAudit<'_, T> {
357    pub(super) fn select(
358        &self,
359        target: Option<ObjectIdentifier>,
360        kind: ObjectType,
361    ) -> Option<bacnet_objects::audit::SelectedAuditReporter> {
362        self.transactions
363            .audit_association
364            .get()?
365            .select(target, kind, None)
366    }
367    /// Local attempt-accounting policy for the supported network WRITE family.
368    /// A configured enabled Reporter falls back to itself without adding nominal
369    /// membership. Ordinary targets (including a disabled Reporter) use filters.
370    pub(super) fn select_write(
371        &self,
372        db: &ObjectDatabase,
373        target: ObjectIdentifier,
374    ) -> Option<(bacnet_objects::audit::SelectedAuditReporter, bool)> {
375        let mandatory = db
376            .get(&target)
377            .and_then(|object| object.audit_reporter_internal())
378            .is_some_and(|reporter| reporter.configuration_internal().enabled());
379        let association = self.transactions.audit_association.get()?;
380        let selected = if mandatory {
381            association.select_change(target, None)
382        } else {
383            association.select(Some(target), target.object_type(), None)
384        }?;
385        Some((selected, mandatory))
386    }
387    /// The file handler calls once after execution, while still holding the DB
388    /// guard, and never for decoder rejections or configured budget overload.
389    pub(super) fn file_completed(
390        &mut self,
391        db: &mut ObjectDatabase,
392        target: ObjectIdentifier,
393        result: &Result<(), Error>,
394    ) {
395        self.pending = None;
396        let Some((selected_reporter, mandatory)) = self.select_write(db, target) else {
397            return;
398        };
399        let reporter = &selected_reporter.configuration;
400        let device = db.local_device().identifier();
401        let route = device
402            .and_then(|device| recipient(db, device))
403            .and_then(|value| self.transactions.audit_routes.get()?.resolve(&value));
404        let status = Arc::clone(&selected_reporter.status);
405        status.set_configured(device.is_some() && route.is_some());
406        let Some(device) = device else { return };
407        let policy = db
408            .get(&target)
409            .map(|object| object.audit_object_policy_internal())
410            .unwrap_or_default()
411            .effective_internal(reporter);
412        let selected = if mandatory {
413            true
414        } else {
415            policy.reports(AuditOperation::WRITE, None, None)
416        };
417        if !selected {
418            return;
419        }
420        self.pending = Some(PendingWrite {
421            delay: (!mandatory)
422                .then_some(reporter.maximum_send_delay)
423                .flatten(),
424            selection: None,
425            failure: self.failure_ticket(&status, reporter.confirmed, device, route.clone()),
426            route,
427            completion: status.begin_delivery(),
428            status,
429            confirmed: reporter.confirmed,
430            notification: BACnetAuditNotification {
431                source_timestamp: None,
432                target_timestamp: None,
433                source_device: self.source.clone(),
434                source_object: None,
435                operation: AuditOperation::WRITE,
436                source_comment: None,
437                target_comment: None,
438                invoke_id: self.invoke_id,
439                source_user_id: None,
440                source_user_role: None,
441                target_device: BACnetRecipient::Device(device),
442                target_object: Some(target),
443                target_property: None,
444                target_priority: None,
445                target_value: None,
446                current_value: None,
447                result: None,
448            },
449        });
450        match result {
451            Ok(()) => self.committed(db),
452            Err(error) => self.failed(db, error),
453        }
454    }
455
456    /// List services have no priority. The handler supplies its existing pre-image
457    /// only after all element/framed decoding; an absent object is still a known
458    /// target for a decoded execution failure.
459    pub(super) fn before_list(
460        &mut self,
461        db: &ObjectDatabase,
462        request: &bacnet_services::list_manipulation::ListElementRequest,
463        current: Option<&PropertyValue>,
464    ) {
465        self.pending = None;
466        let Some((selected_reporter, mandatory)) = self.select_write(db, request.object_identifier)
467        else {
468            return;
469        };
470        let reporter = &selected_reporter.configuration;
471        let device = db.local_device().identifier();
472        let route = device
473            .and_then(|device| recipient(db, device))
474            .and_then(|value| self.transactions.audit_routes.get()?.resolve(&value));
475        let status = Arc::clone(&selected_reporter.status);
476        status.set_configured(device.is_some() && route.is_some());
477        let Some(device) = device else { return };
478        let policy = db
479            .get(&request.object_identifier)
480            .map(|object| object.audit_object_policy_internal())
481            .unwrap_or_default()
482            .effective_internal(reporter);
483        let selected = if mandatory {
484            true
485        } else {
486            policy.reports(
487                AuditOperation::WRITE,
488                Some(request.property_identifier),
489                None,
490            )
491        };
492        if !selected {
493            return;
494        }
495        self.pending = Some(PendingWrite {
496            delay: (!mandatory)
497                .then_some(reporter.maximum_send_delay)
498                .flatten(),
499            selection: None,
500            failure: self.failure_ticket(&status, reporter.confirmed, device, route.clone()),
501            route,
502            completion: status.begin_delivery(),
503            status,
504            confirmed: reporter.confirmed,
505            notification: BACnetAuditNotification {
506                source_timestamp: None,
507                target_timestamp: None,
508                source_device: self.source.clone(),
509                source_object: None,
510                operation: AuditOperation::WRITE,
511                source_comment: None,
512                target_comment: None,
513                invoke_id: self.invoke_id,
514                source_user_id: None,
515                source_user_role: None,
516                target_device: BACnetRecipient::Device(device),
517                target_object: Some(request.object_identifier),
518                target_property: Some(AuditPropertyReference {
519                    property_identifier: request.property_identifier,
520                    property_array_index: request.property_array_index.map(u64::from),
521                }),
522                target_priority: None,
523                target_value: (request.list_of_elements.len() <= 32
524                    && bacnet_encoding::constructed::validate_tlv_sequence(
525                        &request.list_of_elements,
526                        "list delta",
527                    )
528                    .is_ok())
529                .then(|| request.list_of_elements.clone()),
530                current_value: current.and_then(small_value).filter(|bytes| {
531                    bacnet_encoding::constructed::validate_tlv_sequence(bytes, "list pre-image")
532                        .is_ok()
533                }),
534                result: None,
535            },
536        });
537    }
538
539    /// Called under the execution database guard: after CREATE's final/candidate
540    /// identity is known, or before DELETE removes the selected Reporter itself.
541    pub(super) fn before_lifecycle(
542        &mut self,
543        db: &ObjectDatabase,
544        operation: AuditOperation,
545        target: Option<ObjectIdentifier>,
546        kind: ObjectType,
547        use_object_policy: bool,
548    ) {
549        self.pending = None;
550        let Some(selected_reporter) = self.select(target, kind) else {
551            return;
552        };
553        let reporter = &selected_reporter.configuration;
554        let device = db.local_device().identifier();
555        let route = device
556            .and_then(|device| recipient(db, device))
557            .and_then(|value| self.transactions.audit_routes.get()?.resolve(&value));
558        let status = Arc::clone(&selected_reporter.status);
559        status.set_configured(device.is_some() && route.is_some());
560        let Some(device) = device else { return };
561        let selected = target.map_or_else(
562            || reporter.monitors_unassigned(kind),
563            |oid| reporter.monitors(oid),
564        );
565        let policy = target
566            .filter(|_| use_object_policy)
567            .and_then(|oid| db.get(&oid))
568            .map(|object| object.audit_object_policy_internal())
569            .unwrap_or_default()
570            .effective_internal(reporter);
571        if !selected || !policy.reports(operation, None, None) {
572            return;
573        }
574        self.pending = Some(PendingWrite {
575            delay: reporter.maximum_send_delay,
576            selection: None,
577            failure: self.failure_ticket(&status, reporter.confirmed, device, route.clone()),
578            route,
579            completion: status.begin_delivery(),
580            status,
581            confirmed: reporter.confirmed,
582            notification: BACnetAuditNotification {
583                source_timestamp: None,
584                target_timestamp: None,
585                source_device: self.source.clone(),
586                source_object: None,
587                operation,
588                source_comment: None,
589                target_comment: None,
590                invoke_id: self.invoke_id,
591                source_user_id: None,
592                source_user_role: None,
593                target_device: BACnetRecipient::Device(device),
594                target_object: target,
595                target_property: None,
596                target_priority: None,
597                target_value: None,
598                current_value: None,
599                result: None,
600            },
601        });
602    }
603
604    pub(super) fn lifecycle_completed(
605        &mut self,
606        db: &mut ObjectDatabase,
607        result: &Result<(), Error>,
608    ) {
609        match result {
610            Ok(()) => self.committed(db),
611            Err(error) => self.failed(db, error),
612        }
613    }
614
615    fn complete(&mut self, db: &mut ObjectDatabase, result: Option<(ErrorClass, ErrorCode)>) {
616        let Some(mut pending) = self.pending.take() else {
617            return;
618        };
619        if pending
620            .selection
621            .as_ref()
622            .is_some_and(|selection| !selection.selected(db, result.is_none()))
623        {
624            return;
625        }
626        if pending.route.is_none() || pending.failure.is_none() {
627            return;
628        }
629        // No await separates execution completion from notification admission.
630        // A later response-send timeout cannot change the recorded outcome.
631        pending.notification.result = result;
632        pending.notification.target_timestamp =
633            Some(super::event_timestamp::sample_event_timestamp(db).timestamp);
634        self.admit(pending);
635    }
636
637    fn admit(&self, pending: PendingWrite) {
638        if let Some(delay) = pending.delay.filter(|delay| delay.seconds() != 0) {
639            let completion = DeliveryCompletion {
640                status: Arc::clone(&pending.status),
641                epoch: pending.completion,
642                finished: false,
643            };
644            if let Some(queue) = self
645                .transactions
646                .audit_batch
647                .get()
648                .and_then(std::sync::Weak::upgrade)
649            {
650                let mut record = BytesMut::new();
651                if bacnet_encoding::constructed::encode_audit_notification(
652                    &pending.notification,
653                    &mut record,
654                )
655                .is_err()
656                {
657                    return;
658                }
659                let queued = super::audit_batch_queue::QueuedAudit::new(
660                    completion,
661                    record,
662                    pending
663                        .notification
664                        .target_timestamp
665                        .clone()
666                        .expect("completed record"),
667                    pending.failure.clone().expect("captured ordinary record"),
668                    delay,
669                );
670                if queue.enqueue(queued).is_err() {
671                    self.resource_drop(&pending);
672                }
673                return;
674            }
675        }
676        let Some(route) = pending.route.clone() else {
677            return;
678        };
679        let completion = DeliveryCompletion {
680            status: Arc::clone(&pending.status),
681            epoch: pending.completion,
682            finished: false,
683        };
684        // Validate encoding and APDU fit before resource admission: those
685        // failures must not become resource-drop counts even under overload.
686        let Some(mut bytes) = encode_notification(
687            &pending.notification,
688            pending.confirmed,
689            self.config.max_apdu_length,
690            0,
691        ) else {
692            return;
693        };
694        let permit = match self.transactions.try_admit_audit() {
695            Ok(permit) => permit,
696            Err(tokio::sync::TryAcquireError::NoPermits) => {
697                self.resource_drop(&pending);
698                return;
699            }
700            Err(tokio::sync::TryAcquireError::Closed) => return,
701        };
702        let confirmed = pending.confirmed;
703        let reserved = if confirmed {
704            match self.transactions.reserve(
705                route.canonical_peer.clone(),
706                ConfirmedServiceChoice::CONFIRMED_AUDIT_NOTIFICATION,
707            ) {
708                Ok(reserved) => Some(reserved),
709                Err(NotificationReserveError::Coordinator(
710                    bacnet_endpoint_core::coordinator::ReserveError::Exhausted,
711                )) => {
712                    self.resource_drop(&pending);
713                    return;
714                }
715                Err(_) => return,
716            }
717        } else {
718            None
719        };
720        if let Some((operation, _)) = &reserved {
721            let Some(encoded) = encode_notification(
722                &pending.notification,
723                confirmed,
724                self.config.max_apdu_length,
725                operation.invoke_id(),
726            ) else {
727                return;
728            };
729            bytes = encoded;
730        }
731        let network = Arc::clone(self.network);
732        // The absolute deadline includes scheduling and transport send, not only ACK wait.
733        let deadline = tokio::time::Instant::now() + DELIVERY_TIMEOUT;
734        let context_pin = pending.failure;
735        self.transactions.spawn(async move {
736            let _permit = permit;
737            let _context_pin = context_pin;
738            let delivered = deliver(&network, &route, &bytes, reserved, deadline).await;
739            completion.finish(delivered);
740        });
741    }
742}
743
744#[path = "audit_delivery.rs"]
745mod delivery;
746pub(super) use delivery::{
747    deliver, deliver_global_broadcast, deliver_observed, encode_notification, DeliveryCompletion,
748};
749
750/// Table 19-4 permits omission above 32 encoded octets. Bound encoding work,
751/// including nested lists, before handing a value to the ordinary encoder.
752pub(super) fn small_value(value: &PropertyValue) -> Option<Vec<u8>> {
753    fn append(value: &PropertyValue, out: &mut BytesMut, remaining: &mut usize) -> Option<()> {
754        if *remaining == 0 || out.len() > 32 {
755            return None;
756        }
757        *remaining -= 1;
758        match value {
759            PropertyValue::List(values) => {
760                if values.len() > 32 {
761                    return None;
762                }
763                for value in values {
764                    append(value, out, remaining)?;
765                }
766            }
767            PropertyValue::CharacterString(value) if value.len() > 32 => return None,
768            PropertyValue::OctetString(value) | PropertyValue::ApplicationData(value)
769                if value.len() > 32 =>
770            {
771                return None
772            }
773            PropertyValue::BitString { data, .. } if data.len() > 32 => return None,
774            _ => encode_property_value(out, value).ok()?,
775        }
776        (out.len() <= 32).then_some(())
777    }
778    let mut bytes = BytesMut::new();
779    append(value, &mut bytes, &mut 64)?;
780    Some(bytes.to_vec())
781}
782
783#[path = "audit_reporter_failure.rs"]
784mod failure;
785pub(super) use failure::record_resource_drop;