bacnet-objects 0.13.0

BACnet object model: traits, database, and standard object types
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
//! Checking a credential against the Access Rights it is assigned (Clause
//! 12.34.9; #1331).
//!
//! The access-control objects store and serve the rules: an Access Rights
//! object holds Positive_Access_Rules and Negative_Access_Rules, an Access
//! Credential names its rights in Assigned_Access_Rights, and an Access Zone
//! names its Entry_Points. [`evaluate_access_rights`] reads them and tells the
//! application whether the rules let a credential through the Access Point
//! where it was presented, and if not, which Access_Event value the failure
//! carries.
//!
//! The evaluator is pure. It takes a shared borrow of the database, reads
//! properties the way a peer would see them (`read_property`), and does no
//! I/O. It takes no lock and changes nothing: it doesn't set Access_Event or
//! any other property, and acting on the decision is up to the application.
//! A server application calls it under the database read guard, as in
//! `evaluate_access_rights(&*server.database().read().await, credential,
//! point)`, and drops the guard before acting on the decision.
//!
//! [`evaluate_access_rights`] documents each step. Where Clause 12.34.9
//! leaves the reading to the device, this evaluator takes these:
//!
//! - A time range reads an Enumerated by the enumeration of the property it
//!   names, as `ResolvedEnum::from_property` knows it, since the value alone
//!   doesn't say. A BACnetBinaryPV, or any property whose type isn't known
//!   to be another enumeration (the Present_Value of any object among them),
//!   reads ACTIVE as TRUE and any other number as FALSE.
//! - A time range whose type can never read TRUE under that reading is
//!   FALSE at every moment: REAL, Double, strings and the other types the
//!   clause doesn't name, an enumeration known to be another one
//!   (Event_State, Reliability and the rest), and a whole array read
//!   without an index.
//! - Only a value of a type that can read TRUE, read here and found FALSE,
//!   puts a rule outside its time range. A time range that is FALSE at every
//!   moment (the types above, no reference or an unspecified one, a missing
//!   object or property, a failed read, NULL, index 0, or another device)
//!   gives a positive rule covering the point no access there at any time:
//!   DENIED_NO_ACCESS_RIGHTS, not DENIED_OUT_OF_TIME_RANGE.
//! - A negative rule whose location is ALL denies with
//!   DENIED_POINT_NO_ACCESS_RIGHTS: it bars this point as much as a rule
//!   naming the point does, and the clause names a value only for the point
//!   and zone cases.
//! - A reference names this device only when it has no device member or
//!   names the database's own Device (`LocalDevice::is_local`, as the rest
//!   of the stack reads references). The wildcard Device instance 4194303
//!   names no device in particular, so a reference carrying it is never
//!   read, and neither is one naming another device. Such a reference never
//!   grants access. A negative rule whose location or time range names one
//!   doesn't hold, so it bars no one: Clause 12.34.9.1 has a reference that
//!   is unspecified or can't be retrieved evaluate to FALSE.
//! - Clause 12.35.18 has the device ignore an assignment naming a missing
//!   object or an object that isn't Access Rights. Leaving out an assignment
//!   naming another device or the wildcard Device as well is this
//!   evaluator's own policy. All of them are listed as unresolved, so the
//!   application can still deny.

use std::collections::HashMap;

use bacnet_encoding::constructed::{decode_access_rule, decode_assigned_access_rights};
use bacnet_types::constructed::{
    BACnetAccessRule, BACnetAssignedAccessRights, BACnetDeviceObjectPropertyReference,
    BACnetDeviceObjectReference,
};
use bacnet_types::enums::{
    AccessEvent, AccessRuleLocationSpecifier, AccessRuleTimeRangeSpecifier, AuthorizationExemption,
    BinaryPV, ErrorClass, ErrorCode, ObjectType, PropertyIdentifier, ResolvedEnum,
};
use bacnet_types::error::Error;
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};

use super::rights::unspecified;
use crate::common;
use crate::database::{LocalDevice, ObjectDatabase};
use crate::device_reference::decode_references;
use crate::traits::BACnetObject;

/// What [`evaluate_access_rights`] found for one credential at one Access
/// Point.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct AccessRightsEvaluation {
    /// The outcome of the Access Rights check.
    pub decision: AccessRightsDecision,
    /// The enabled Assigned_Access_Rights elements that gave no rules
    /// because their Access Rights object couldn't be read here, in array
    /// order. The decision leaves them out. Clause 12.35.18 asks for that
    /// with a missing object or one that isn't Access Rights; for an object
    /// in another device, or behind the wildcard Device, it is this
    /// evaluator's policy, since it reads nothing remotely. An application
    /// that would rather deny while part of a credential's rights is out of
    /// sight checks that this is empty. Always empty for
    /// [`AccessRightsDecision::Exempt`], which reads no rights.
    pub unresolved: Vec<UnresolvedAccessRights>,
}

/// The outcome of the Access Rights check (Clause 12.34.9.2).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AccessRightsDecision {
    /// The credential's Authorization_Exemptions lists ACCESS_RIGHTS, so the
    /// check didn't run and counts as passed (Clause 12.35.25).
    Exempt,
    /// No negative rule held, and this positive rule did: the first in
    /// gathering and array order.
    #[non_exhaustive]
    Granted {
        /// The positive rule that granted access.
        rule: AccessRulePosition,
    },
    /// The check failed.
    #[non_exhaustive]
    Denied {
        /// The Access_Event value Clause 12.34.9.2 gives this failure:
        /// DENIED_POINT_NO_ACCESS_RIGHTS or DENIED_ZONE_NO_ACCESS_RIGHTS
        /// for a negative rule, DENIED_OUT_OF_TIME_RANGE, or
        /// DENIED_NO_ACCESS_RIGHTS. The evaluator doesn't write it.
        access_event: AccessEvent,
        /// The rule behind the denial: the negative rule that held, or, for
        /// DENIED_OUT_OF_TIME_RANGE, the first positive rule that covered
        /// the point while a value read for its time range was FALSE. `None`
        /// for DENIED_NO_ACCESS_RIGHTS.
        rule: Option<AccessRulePosition>,
    },
}

impl AccessRightsDecision {
    /// Whether the check let the credential through: granted or exempt.
    pub fn allows(&self) -> bool {
        matches!(self, Self::Exempt | Self::Granted { .. })
    }

    /// The Access_Event value of a denial, `None` otherwise.
    pub fn access_event(&self) -> Option<AccessEvent> {
        match self {
            Self::Denied { access_event, .. } => Some(*access_event),
            _ => None,
        }
    }

    /// The rule the decision names: the granting rule, or a denial's
    /// rule when it has one.
    pub fn rule(&self) -> Option<AccessRulePosition> {
        match self {
            Self::Granted { rule } => Some(*rule),
            Self::Denied { rule, .. } => *rule,
            Self::Exempt => None,
        }
    }
}

/// Where an access rule sits: its Access Rights object, its array and its
/// place in that array.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct AccessRulePosition {
    /// The Access Rights object holding the rule.
    pub access_rights: ObjectIdentifier,
    /// Which of the object's two rule arrays holds it.
    pub kind: AccessRuleKind,
    /// The rule's one-based array index, the index a ReadProperty of
    /// [`kind.property()`](AccessRuleKind::property) names it by.
    pub index: u32,
}

/// Which rule array of an Access Rights object a rule comes from.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AccessRuleKind {
    /// Positive_Access_Rules, the rules that grant access.
    Positive,
    /// Negative_Access_Rules, the rules that deny it.
    Negative,
}

impl AccessRuleKind {
    /// The property holding the rules of this kind.
    pub fn property(self) -> PropertyIdentifier {
        match self {
            Self::Positive => PropertyIdentifier::POSITIVE_ACCESS_RULES,
            Self::Negative => PropertyIdentifier::NEGATIVE_ACCESS_RULES,
        }
    }
}

/// An enabled Assigned_Access_Rights element that gave no rules.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UnresolvedAccessRights {
    /// The element's one-based index in Assigned_Access_Rights.
    pub index: u32,
    /// The reference the element holds.
    pub reference: BACnetDeviceObjectReference,
    /// Why it gave no rules.
    pub reason: UnresolvedReason,
}

/// Why an Assigned_Access_Rights element gave no rules.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum UnresolvedReason {
    /// It names an object in another device, which this evaluator doesn't
    /// read.
    Remote,
    /// Its device member is the wildcard Device instance 4194303, which
    /// names no device in particular, so there is no device to ask.
    WildcardDevice,
    /// It names an object type other than Access Rights.
    NotAccessRights,
    /// The database holds no object with that identifier.
    Missing,
    /// The Access Rights object's Enable or rule arrays didn't read as their
    /// datatypes. The built-in object always serves them; this covers an
    /// application's own object.
    Unreadable,
}

/// Check the Access Rights rules of `credential`, presented at `point`, as
/// Clause 12.34.9.2 describes.
///
/// # Steps
///
/// 1. **Exemption.** When the credential's Authorization_Exemptions lists
///    ACCESS_RIGHTS, the check doesn't run and counts as passed (Clause
///    12.35.25): the decision is [`AccessRightsDecision::Exempt`], and no
///    rule is read. A credential that doesn't serve the optional row is not
///    exempt.
/// 2. **Gathering.** Assigned_Access_Rights is walked in array order.
///    - An element whose own enable flag is FALSE is skipped, and so is the
///      unused marker, instance 4194303 in the object and in the device when
///      it names one (Clause 12.35.18).
///    - An element naming a missing object or an object that isn't Access
///      Rights gives no rules: Clause 12.35.18 has the device ignore it.
///      Neither does one naming another device or the wildcard Device;
///      that is this evaluator's policy, since it reads nothing remotely.
///      Each such element is listed
///      in [`AccessRightsEvaluation::unresolved`], so that an application can
///      deny when part of a credential's rights is out of its sight.
///    - An Access Rights object whose Enable is FALSE gives no rules either
///      (Clause 12.34.8), and isn't listed: it was found.
/// 3. **Negative rules first.** Each enabled negative rule of each gathered
///    object is tried, objects in gathering order and rules in array order,
///    before any positive rule is (Clause 12.34.9.2). The first one that
///    holds denies access, with the Access_Event its location gives (see
///    below).
/// 4. **Positive rules.** Otherwise the first enabled positive rule that
///    holds, in the same order, grants access.
/// 5. **No rule held.** When some enabled positive rule covered this point
///    and a value read here for its time range was FALSE, the credential
///    could pass here at another time, so the denial is
///    DENIED_OUT_OF_TIME_RANGE. Otherwise it is DENIED_NO_ACCESS_RIGHTS,
///    including when every rule covering the point has a time range that is
///    FALSE at every moment (see below).
///
/// A rule holds when its enable flag is TRUE, its location covers the point
/// and its time range is TRUE (Clause 12.34.9.1).
///
/// # Locations
///
/// - ALL covers every point, whatever the location reference holds.
/// - SPECIFIED with an Access Point covers that point only.
/// - SPECIFIED with an Access Zone covers the point when the zone's
///   Entry_Points names it in this device. Each zone's list is read once
///   per evaluation.
/// - Anything else covers nothing: no reference, an unspecified one, one to
///   another device or the wildcard Device, a missing zone, another object
///   type, or a specifier outside the two named values.
///
/// A negative rule that holds denies with DENIED_POINT_NO_ACCESS_RIGHTS when
/// its location names the point and DENIED_ZONE_NO_ACCESS_RIGHTS when it
/// names a zone. Clause 12.34.9.2 names no value for a negative rule covering
/// every location; such a rule bars passage through this point as much as
/// one naming it, so this evaluator reports DENIED_POINT_NO_ACCESS_RIGHTS.
///
/// # Time ranges
///
/// ALWAYS is TRUE. SPECIFIED reads the referenced property from this
/// database, honouring the reference's array index, and turns the value into
/// TRUE or FALSE:
///
/// - BOOLEAN: its value.
/// - Unsigned: TRUE unless zero.
/// - INTEGER: TRUE above zero.
/// - Enumerated: read by the property's enumeration
///   ([`ResolvedEnum::from_property`]), since the value alone doesn't name
///   one. For a BACnetBinaryPV, or any property whose type isn't known to
///   be another enumeration (the Present_Value of any object among them),
///   ACTIVE (1) is TRUE and any other number FALSE. Clause 12.34.9.1 leaves
///   types other than the four it names to the device, so reading these as
///   BACnetBinaryPV is a local choice.
///
/// A FALSE from those values is read here, so a positive rule with one is
/// outside its time range now and may hold at another time. A time range
/// that can't read TRUE at any moment is FALSE at every moment instead:
///
/// - a type that never reads TRUE under these rules: REAL, Double, strings
///   and the other types the clause doesn't name, an enumeration known to
///   be another one (Event_State, Reliability and the rest), and a whole
///   array read without an index (the same local choice);
/// - a specifier outside the two named values, SPECIFIED with no reference,
///   an unspecified reference, or one naming another device or the wildcard
///   Device;
/// - a missing object or a property it doesn't serve, an index on a
///   property that isn't an array, index 0 (an array's size is no
///   time-range value) or one past the end, any other failed read, and NULL.
///
/// Such a rule never makes a denial DENIED_OUT_OF_TIME_RANGE.
///
/// # Which device a reference names
///
/// A reference names this device when it has no device member or names the
/// Device the database speaks for ([`ObjectDatabase::local_device`]), the
/// reading the rest of the stack takes. Anything else, the wildcard Device
/// instance 4194303 included, names no device this evaluator can read: such
/// a time range is FALSE at every moment, such a location covers nothing,
/// and such an Assigned_Access_Rights element is listed as unresolved. So
/// such a reference never grants access. A negative rule naming one doesn't
/// hold either, so it bars no one: Clause 12.34.9.1 has a reference that is
/// unspecified or can't be retrieved evaluate to FALSE, whichever kind of
/// rule holds it. An application that would rather deny can act on the
/// unresolved list. A database holding no Device has no Device of its own,
/// so then only a reference with no device member is local.
///
/// # Not covered
///
/// The Access Rights check is one of several authorization checks. This
/// evaluator doesn't judge Accompaniment (Clause 12.34.11), the credential's
/// status, validity window or threat authority, the point's authorization
/// mode, occupancy or passback, and it doesn't record the outcome on the
/// point.
///
/// # Errors
///
/// OBJECT / UNKNOWN_OBJECT, before any rule is read, when
/// `credential` names no Access Credential in `db` or `point` no Access
/// Point. An error reading or decoding the credential's
/// Assigned_Access_Rights is passed on; the built-in credential always
/// serves it whole.
///
/// # Example
///
/// ```
/// use bacnet_objects::access_control::{
///     evaluate_access_rights, AccessCredentialObject, AccessPointObject, AccessRightsObject,
/// };
/// use bacnet_objects::database::ObjectDatabase;
/// use bacnet_types::constructed::{BACnetAccessRule, BACnetAssignedAccessRights};
/// use bacnet_types::enums::{AccessEvent, ObjectType};
/// use bacnet_types::primitives::ObjectIdentifier;
///
/// let door = ObjectIdentifier::new(ObjectType::ACCESS_POINT, 1)?;
/// let staff = ObjectIdentifier::new(ObjectType::ACCESS_RIGHTS, 1)?;
/// let card = ObjectIdentifier::new(ObjectType::ACCESS_CREDENTIAL, 1)?;
///
/// // Staff may pass Point 1 at any time.
/// let mut rights = AccessRightsObject::new(1, "STAFF")?;
/// rights.set_positive_access_rules([BACnetAccessRule::new(None, Some(door.into()), true)])?;
/// let mut credential = AccessCredentialObject::new(1, "CARD-1")?;
/// credential.set_assigned_access_rights(vec![BACnetAssignedAccessRights {
///     assigned_access_rights: staff.into(),
///     enable: true,
/// }])?;
///
/// let mut db = ObjectDatabase::new();
/// db.add(Box::new(AccessPointObject::new(1, "DOOR-1")?))?;
/// db.add(Box::new(AccessPointObject::new(2, "DOOR-2")?))?;
/// db.add(Box::new(rights))?;
/// db.add(Box::new(credential))?;
///
/// assert!(evaluate_access_rights(&db, card, door)?.decision.allows());
/// let elsewhere = ObjectIdentifier::new(ObjectType::ACCESS_POINT, 2)?;
/// let evaluation = evaluate_access_rights(&db, card, elsewhere)?;
/// assert_eq!(
///     evaluation.decision.access_event(),
///     Some(AccessEvent::DENIED_NO_ACCESS_RIGHTS)
/// );
/// # Ok::<(), bacnet_types::error::Error>(())
/// ```
pub fn evaluate_access_rights(
    db: &ObjectDatabase,
    credential: ObjectIdentifier,
    point: ObjectIdentifier,
) -> Result<AccessRightsEvaluation, Error> {
    let holder = object_of_type(db, credential, ObjectType::ACCESS_CREDENTIAL)?;
    object_of_type(db, point, ObjectType::ACCESS_POINT)?;
    if exempt(holder) {
        return Ok(AccessRightsEvaluation {
            decision: AccessRightsDecision::Exempt,
            unresolved: Vec::new(),
        });
    }
    let mut scope = Scope {
        db,
        local: db.local_device(),
        point,
        zones: HashMap::new(),
    };
    let (gathered, unresolved) = scope.gather(&assigned_access_rights(holder)?);
    Ok(AccessRightsEvaluation {
        decision: scope.decide(&gathered),
        unresolved,
    })
}

/// The object `oid` names in `db`, when it is of `object_type`.
fn object_of_type(
    db: &ObjectDatabase,
    oid: ObjectIdentifier,
    object_type: ObjectType,
) -> Result<&dyn BACnetObject, Error> {
    db.get(&oid)
        .filter(|_| oid.object_type() == object_type)
        .ok_or_else(|| common::protocol_error(ErrorClass::OBJECT, ErrorCode::UNKNOWN_OBJECT))
}

/// Whether the credential serves an Authorization_Exemptions list holding
/// ACCESS_RIGHTS.
fn exempt(credential: &dyn BACnetObject) -> bool {
    let access_rights = PropertyValue::Enumerated(AuthorizationExemption::ACCESS_RIGHTS.to_raw());
    matches!(
        credential.read_property(PropertyIdentifier::AUTHORIZATION_EXEMPTIONS, None),
        Ok(PropertyValue::List(exemptions)) if exemptions.contains(&access_rights)
    )
}

/// The credential's Assigned_Access_Rights, decoded from the value it serves.
fn assigned_access_rights(
    credential: &dyn BACnetObject,
) -> Result<Vec<BACnetAssignedAccessRights>, Error> {
    let value = credential.read_property(PropertyIdentifier::ASSIGNED_ACCESS_RIGHTS, None)?;
    // Each element opens with the reference's frame, context tag [0].
    common::decode_elements(
        &value,
        |tag| tag.is_opening_tag(0),
        decode_assigned_access_rights,
    )
}

/// The enabled rules of one gathered Access Rights object.
struct Gathered {
    oid: ObjectIdentifier,
    negative: Vec<BACnetAccessRule>,
    positive: Vec<BACnetAccessRule>,
}

impl Gathered {
    fn rules(&self, kind: AccessRuleKind) -> &[BACnetAccessRule] {
        match kind {
            AccessRuleKind::Positive => &self.positive,
            AccessRuleKind::Negative => &self.negative,
        }
    }
}

/// Every enabled rule of `kind`, objects in gathering order and rules in
/// array order, with its position.
fn enabled_rules(
    gathered: &[Gathered],
    kind: AccessRuleKind,
) -> impl Iterator<Item = (AccessRulePosition, &BACnetAccessRule)> {
    gathered.iter().flat_map(move |rights| {
        (1..)
            .zip(rights.rules(kind))
            .filter(|(_, rule)| rule.enable)
            .map(move |(index, rule)| {
                let position = AccessRulePosition {
                    access_rights: rights.oid,
                    kind,
                    index,
                };
                (position, rule)
            })
    })
}

/// What a rule's location matched: the kind of place decides the
/// Access_Event a negative rule denies with.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Place {
    /// Location-Specifier ALL.
    Everywhere,
    /// The Access Point itself.
    Point,
    /// An Access Zone the point enters.
    Zone,
}

impl Place {
    /// The Access_Event of a negative rule that held here (Clause
    /// 12.34.9.2). A rule for every location bars this point as well, so it
    /// is reported as the point's denial: a local reading, since the clause
    /// names only the point and zone cases.
    fn denial(self) -> AccessEvent {
        match self {
            Place::Zone => AccessEvent::DENIED_ZONE_NO_ACCESS_RIGHTS,
            Place::Point | Place::Everywhere => AccessEvent::DENIED_POINT_NO_ACCESS_RIGHTS,
        }
    }
}

/// A rule's time range, as far as this database can tell (Clause
/// 12.34.9.1).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TimeRange {
    /// ALWAYS, or a value read here as TRUE.
    True,
    /// A value read here as FALSE: the rule may hold at another time.
    False,
    /// FALSE at every moment: no usable reference, a failed read, NULL,
    /// index 0, or a type that never reads TRUE (see
    /// [`evaluate_access_rights`]).
    Never,
}

/// One evaluation: the database, its own Device, the point where the
/// credential was presented, and what each zone's Entry_Points said about
/// that point, read once per evaluation however many rules name the zone.
struct Scope<'a> {
    db: &'a ObjectDatabase,
    local: LocalDevice,
    point: ObjectIdentifier,
    zones: HashMap<ObjectIdentifier, bool>,
}

impl Scope<'_> {
    /// Whether a reference with this device member names this device: no
    /// member, or the database's own Device. The wildcard Device names none
    /// in particular, so it isn't local.
    fn is_local(&self, device: Option<ObjectIdentifier>) -> bool {
        self.local.is_local(device)
    }

    /// The enabled Access Rights objects the enabled elements of
    /// `assigned` name, in order, and the elements that gave no rules.
    fn gather(
        &self,
        assigned: &[BACnetAssignedAccessRights],
    ) -> (Vec<Gathered>, Vec<UnresolvedAccessRights>) {
        let mut gathered = Vec::new();
        let mut unresolved = Vec::new();
        for (index, element) in (1..).zip(assigned) {
            let reference = &element.assigned_access_rights;
            if !element.enable
                || unspecified(reference.object_identifier, reference.device_identifier)
            {
                continue;
            }
            match self.resolve(reference) {
                Ok(Some(rights)) => gathered.push(rights),
                // An Access Rights object with Enable FALSE.
                Ok(None) => {}
                Err(reason) => unresolved.push(UnresolvedAccessRights {
                    index,
                    reference: reference.clone(),
                    reason,
                }),
            }
        }
        (gathered, unresolved)
    }

    /// The rules of the Access Rights object `reference` names: `None`
    /// while its Enable is FALSE, or why it gives none.
    fn resolve(
        &self,
        reference: &BACnetDeviceObjectReference,
    ) -> Result<Option<Gathered>, UnresolvedReason> {
        let oid = reference.object_identifier;
        if reference.device_identifier.is_some_and(is_wildcard_device) {
            return Err(UnresolvedReason::WildcardDevice);
        }
        if !self.is_local(reference.device_identifier) {
            return Err(UnresolvedReason::Remote);
        }
        if oid.object_type() != ObjectType::ACCESS_RIGHTS {
            return Err(UnresolvedReason::NotAccessRights);
        }
        let object = self.db.get(&oid).ok_or(UnresolvedReason::Missing)?;
        // Table 12-39's Enable is property 133, LOG_ENABLE in the enum.
        match object.read_property(PropertyIdentifier::LOG_ENABLE, None) {
            Ok(PropertyValue::Boolean(true)) => {}
            Ok(PropertyValue::Boolean(false)) => return Ok(None),
            _ => return Err(UnresolvedReason::Unreadable),
        }
        let rules = |kind: AccessRuleKind| {
            object
                .read_property(kind.property(), None)
                .and_then(|value| {
                    // A rule opens with its time-range specifier, context tag [0].
                    common::decode_elements(&value, |tag| tag.is_context(0), decode_access_rule)
                })
                .map_err(|_| UnresolvedReason::Unreadable)
        };
        Ok(Some(Gathered {
            oid,
            negative: rules(AccessRuleKind::Negative)?,
            positive: rules(AccessRuleKind::Positive)?,
        }))
    }

    /// The decision over the gathered rules (Clause 12.34.9.2).
    fn decide(&mut self, gathered: &[Gathered]) -> AccessRightsDecision {
        for (position, rule) in enabled_rules(gathered, AccessRuleKind::Negative) {
            if let Some(place) = self.location(rule) {
                if self.time_range(rule) == TimeRange::True {
                    return AccessRightsDecision::Denied {
                        access_event: place.denial(),
                        rule: Some(position),
                    };
                }
            }
        }
        let mut out_of_time = None;
        for (position, rule) in enabled_rules(gathered, AccessRuleKind::Positive) {
            if self.location(rule).is_some() {
                match self.time_range(rule) {
                    TimeRange::True => return AccessRightsDecision::Granted { rule: position },
                    TimeRange::False => {
                        out_of_time.get_or_insert(position);
                    }
                    TimeRange::Never => {}
                }
            }
        }
        match out_of_time {
            Some(position) => AccessRightsDecision::Denied {
                access_event: AccessEvent::DENIED_OUT_OF_TIME_RANGE,
                rule: Some(position),
            },
            None => AccessRightsDecision::Denied {
                access_event: AccessEvent::DENIED_NO_ACCESS_RIGHTS,
                rule: None,
            },
        }
    }

    /// What the rule's location matched at this point, if anything.
    fn location(&mut self, rule: &BACnetAccessRule) -> Option<Place> {
        match rule.location_specifier {
            AccessRuleLocationSpecifier::ALL => Some(Place::Everywhere),
            AccessRuleLocationSpecifier::SPECIFIED => {
                let reference = rule.location.as_ref()?;
                let oid = reference.object_identifier;
                if unspecified(oid, reference.device_identifier)
                    || !self.is_local(reference.device_identifier)
                {
                    return None;
                }
                match oid.object_type() {
                    ObjectType::ACCESS_POINT => (oid == self.point).then_some(Place::Point),
                    ObjectType::ACCESS_ZONE => self.enters(oid).then_some(Place::Zone),
                    _ => None,
                }
            }
            _ => None,
        }
    }

    /// Whether the zone `zone` names the point among its Entry_Points,
    /// read once per evaluation.
    fn enters(&mut self, zone: ObjectIdentifier) -> bool {
        if let Some(&enters) = self.zones.get(&zone) {
            return enters;
        }
        let enters = self.read_entry_points(zone);
        self.zones.insert(zone, enters);
        enters
    }

    /// Whether the zone's Entry_Points, as it serves them now, names the
    /// point in this device.
    fn read_entry_points(&self, zone: ObjectIdentifier) -> bool {
        let Some(zone) = self.db.get(&zone) else {
            return false;
        };
        let Ok(value) = zone.read_property(PropertyIdentifier::ENTRY_POINTS, None) else {
            return false;
        };
        decode_references::<BACnetDeviceObjectReference>(&value).is_ok_and(|entries| {
            entries.iter().any(|entry| {
                entry.object_identifier == self.point && self.is_local(entry.device_identifier)
            })
        })
    }

    /// The rule's time range (Clause 12.34.9.1).
    fn time_range(&self, rule: &BACnetAccessRule) -> TimeRange {
        match rule.time_range_specifier {
            AccessRuleTimeRangeSpecifier::ALWAYS => TimeRange::True,
            AccessRuleTimeRangeSpecifier::SPECIFIED => rule
                .time_range
                .as_ref()
                .map_or(TimeRange::Never, |reference| {
                    self.read_time_range(reference)
                }),
            _ => TimeRange::Never,
        }
    }

    /// The time range the property `reference` names, read here.
    fn read_time_range(&self, reference: &BACnetDeviceObjectPropertyReference) -> TimeRange {
        let oid = reference.object_identifier;
        if unspecified(oid, reference.device_identifier)
            || !self.is_local(reference.device_identifier)
        {
            return TimeRange::Never;
        }
        let Some(object) = self.db.get(&oid) else {
            return TimeRange::Never;
        };
        let property = PropertyIdentifier::from_raw(reference.property_identifier);
        let index = reference.property_array_index;
        // ReadProperty's gate: only an array takes an index. Index 0 reads
        // the array's size, which is no time-range value.
        if index == Some(0) || (index.is_some() && !object.is_array_property(property)) {
            return TimeRange::Never;
        }
        match object.read_property(property, index) {
            Ok(value) => time_range_value(property, &value),
            Err(_) => TimeRange::Never,
        }
    }
}

/// A time-range value read from `property` (Clause 12.34.9.1, and the local
/// choices in the module documentation).
fn time_range_value(property: PropertyIdentifier, value: &PropertyValue) -> TimeRange {
    let holds = match *value {
        PropertyValue::Boolean(value) => value,
        PropertyValue::Unsigned(value) => value != 0,
        PropertyValue::Signed(value) => value > 0,
        PropertyValue::Enumerated(raw) => match ResolvedEnum::from_property(property, raw) {
            ResolvedEnum::BinaryPV(state) => state == BinaryPV::ACTIVE,
            // A property whose type isn't known to be another enumeration,
            // the Present_Value of any object among them, reads as one.
            ResolvedEnum::Unknown(raw) => raw == BinaryPV::ACTIVE.to_raw(),
            // Another enumeration never reads TRUE.
            _ => return TimeRange::Never,
        },
        // NULL, and the types that never read TRUE: REAL, Double, strings,
        // a whole array and the rest.
        _ => return TimeRange::Never,
    };
    if holds {
        TimeRange::True
    } else {
        TimeRange::False
    }
}

/// Whether a device member is the wildcard Device instance 4194303.
fn is_wildcard_device(device: ObjectIdentifier) -> bool {
    device.object_type() == ObjectType::DEVICE
        && device.instance_number() == ObjectIdentifier::WILDCARD_INSTANCE
}

#[cfg(test)]
#[path = "rights_evaluation_tests.rs"]
mod tests;

#[cfg(test)]
#[path = "rights_evaluation_term_tests.rs"]
mod term_tests;

#[cfg(test)]
#[path = "rights_evaluation_order_tests.rs"]
mod order_tests;

#[cfg(test)]
#[path = "rights_evaluation_scenario_tests.rs"]
mod scenario_tests;