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