Skip to main content

bacnet_types/
bitstring.rs

1//! Named BACnet bit-string types (ASHRAE 135-2020 Clause 21).
2//!
3//! Several properties carry a `BitString` whose bits have a fixed, named
4//! meaning defined by the standard. This module models those `BACnetXxx` BIT
5//! STRING productions so a decoded [`crate::primitives::PropertyValue::BitString`]
6//! can be promoted from raw `(unused_bits, data)` bytes to named flags, the same
7//! way [`crate::enums::ResolvedEnum`] promotes an `Enumerated`.
8//!
9//! ## Bit order
10//!
11//! BACnet bit strings are transmitted most-significant-bit-first: bit 0 of the
12//! string is the top bit (`0x80`) of the first content byte, bit 8 is the top
13//! bit of the second byte, and so on. Every type here reads bit *N* of the
14//! string as "feature N", matching how the device object packs these fields
15//! (see `bacnet-objects`'s `compute_object_types_supported`).
16//!
17//! [`crate::primitives::StatusFlags`] predates this module and keeps its own
18//! right-aligned representation; it is re-exported and mapped by the resolver as
19//! well.
20
21#[cfg(not(feature = "std"))]
22use alloc::{format, vec::Vec};
23
24use crate::enums::{AuditOperation, ObjectType, ServiceSupported};
25use crate::error::{Error, Result};
26
27pub use crate::primitives::StatusFlags;
28pub use priority_filter::BACnetPriorityFilter;
29
30mod priority_filter;
31
32/// Read bit `n` of a BACnet bit string (MSB-first) from its content bytes.
33///
34/// `data` is the bit-string payload *without* the leading unused-bits count
35/// (i.e. `PropertyValue::BitString::data`). Bits past the end read as `false`.
36fn wire_bit(data: &[u8], n: usize) -> bool {
37    let mask = 0x80u8 >> (n % 8);
38    data.get(n / 8).is_some_and(|b| b & mask != 0)
39}
40
41/// Pack a bit0-first value into its Clause 20.2.10 wire octet.
42///
43/// Every ≤8-bit BACnet bit string in this stack keeps its internal layout as
44/// `bit0 = first defined bit` (`TO_OFFNORMAL`, `monday`, `low-limit-enable`,
45/// …), while the wire wants the first defined bit in the most significant bit
46/// of the octet. Reversing the byte is that whole conversion: bit 0 lands at
47/// `0x80`, bit 1 at `0x40`, and the result is left-aligned for any width.
48pub fn pack_octet(bits_lsb0: u8) -> u8 {
49    bits_lsb0.reverse_bits()
50}
51
52/// Inverse of [`pack_octet`]: read the first octet of a bit-string payload
53/// back into bit0-first form, masked to the string's `defined_bits`.
54///
55/// Masking (rather than trusting the peer's declared unused-bit count) keeps
56/// nonconformant padding out of the value; an empty payload reads as zero.
57pub fn unpack_octet(data: &[u8], defined_bits: u32) -> u8 {
58    let mask = if defined_bits >= 8 {
59        u8::MAX
60    } else {
61        (1u8 << defined_bits) - 1
62    };
63    data.first().copied().unwrap_or(0).reverse_bits() & mask
64}
65
66/// Decode a `BACnetStatusFlags` bit-string payload (MSB-first) into
67/// [`StatusFlags`], which keeps its right-aligned in-memory layout.
68///
69/// Reads wire bits 0–3 by position, so a peer that declares a wrong string
70/// length still decodes correctly — Clause 20.2.10 puts bit 0 in the most
71/// significant bit of the first octet regardless of length.
72pub fn status_flags_from_bacnet(data: &[u8]) -> StatusFlags {
73    let mut flags = StatusFlags::empty();
74    flags.set(StatusFlags::IN_ALARM, wire_bit(data, 0));
75    flags.set(StatusFlags::FAULT, wire_bit(data, 1));
76    flags.set(StatusFlags::OVERRIDDEN, wire_bit(data, 2));
77    flags.set(StatusFlags::OUT_OF_SERVICE, wire_bit(data, 3));
78    flags
79}
80
81/// Render a bitflags value as its set Clause-21 names (` NAME | NAME `), or `()`
82/// when none are set — for both `Display` and `Debug`, never a raw number.
83macro_rules! impl_named_bit_display {
84    ($t:ty) => {
85        impl core::fmt::Display for $t {
86            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
87                let mut first = true;
88                for (name, _) in self.iter_names() {
89                    if !first {
90                        f.write_str(" | ")?;
91                    }
92                    f.write_str(name)?;
93                    first = false;
94                }
95                if first {
96                    f.write_str("()")?;
97                }
98                Ok(())
99            }
100        }
101
102        impl core::fmt::Debug for $t {
103            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
104                core::fmt::Display::fmt(self, f)
105            }
106        }
107    };
108}
109
110/// Write `Display` items as ` A | B | C ` (or `()` when empty).
111fn write_joined<T: core::fmt::Display>(
112    f: &mut core::fmt::Formatter<'_>,
113    items: impl Iterator<Item = T>,
114) -> core::fmt::Result {
115    let mut first = true;
116    for item in items {
117        if !first {
118            f.write_str(" | ")?;
119        }
120        write!(f, "{item}")?;
121        first = false;
122    }
123    if first {
124        f.write_str("()")?;
125    }
126    Ok(())
127}
128
129bitflags::bitflags! {
130    /// `BACnetEventTransitionBits` — 3-bit string for `event-enable`,
131    /// `acked-transitions`, and `ack-required` (Clause 21).
132    #[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
133    pub struct EventTransitionBits: u8 {
134        /// Transition to an off-normal event state (bit 0).
135        const TO_OFFNORMAL = 1 << 0;
136        /// Transition to the fault event state (bit 1).
137        const TO_FAULT = 1 << 1;
138        /// Transition to the normal event state (bit 2).
139        const TO_NORMAL = 1 << 2;
140    }
141}
142
143impl EventTransitionBits {
144    /// Decode from a BACnet bit-string payload (MSB-first).
145    pub fn from_bacnet(data: &[u8]) -> Self {
146        let mut bits = Self::empty();
147        bits.set(Self::TO_OFFNORMAL, wire_bit(data, 0));
148        bits.set(Self::TO_FAULT, wire_bit(data, 1));
149        bits.set(Self::TO_NORMAL, wire_bit(data, 2));
150        bits
151    }
152
153    /// Encode to the single Clause 20.2.10 wire octet (`unused_bits: 5`):
154    /// `TO_OFFNORMAL` at `0x80`, `TO_FAULT` at `0x40`, `TO_NORMAL` at `0x20`.
155    pub fn to_bacnet(self) -> u8 {
156        pack_octet(self.bits())
157    }
158}
159
160impl_named_bit_display!(EventTransitionBits);
161
162bitflags::bitflags! {
163    /// `BACnetLimitEnable` — 2-bit string for the `limit-enable` property
164    /// (Clause 21).
165    #[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
166    pub struct LimitEnable: u8 {
167        /// Low-limit event detection is enabled (bit 0).
168        const LOW_LIMIT_ENABLE = 1 << 0;
169        /// High-limit event detection is enabled (bit 1).
170        const HIGH_LIMIT_ENABLE = 1 << 1;
171    }
172}
173
174impl LimitEnable {
175    /// Decode from a BACnet bit-string payload (MSB-first).
176    pub fn from_bacnet(data: &[u8]) -> Self {
177        let mut bits = Self::empty();
178        bits.set(Self::LOW_LIMIT_ENABLE, wire_bit(data, 0));
179        bits.set(Self::HIGH_LIMIT_ENABLE, wire_bit(data, 1));
180        bits
181    }
182
183    /// Encode to the single Clause 20.2.10 wire octet (`unused_bits: 6`):
184    /// `LOW_LIMIT_ENABLE` at `0x80`, `HIGH_LIMIT_ENABLE` at `0x40`.
185    pub fn to_bacnet(self) -> u8 {
186        pack_octet(self.bits())
187    }
188}
189
190impl_named_bit_display!(LimitEnable);
191
192/// `BACnetServicesSupported` — the `protocol-services-supported` bit string,
193/// one bit per protocol service (Clause 21).
194///
195/// Stored as a `u64` bitset: bit *N* is set iff [`ServiceSupported`] value *N*
196/// is supported. `protocol-services-supported` is a closed enumeration whose
197/// highest defined bit is 48 (with no vendor-proprietary range), so a `u64`
198/// covers every standard service with headroom. Any nonstandard bit ≥ 64 — a
199/// protocol violation — is ignored on decode. The public API still yields named
200/// [`ServiceSupported`] values, and `Debug` prints names, never a raw number.
201#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
202pub struct ServicesSupported {
203    bits: u64,
204}
205
206impl ServicesSupported {
207    /// Decode a bit-string payload (the `data` of `PropertyValue::BitString`)
208    /// into the bitset of services it advertises.
209    pub fn from_bacnet(data: &[u8]) -> Self {
210        let mut bits = 0u64;
211        for (i, &byte) in data.iter().take(8).enumerate() {
212            bits |= (byte.reverse_bits() as u64) << (i * 8);
213        }
214        Self { bits }
215    }
216
217    /// Whether `service` is marked supported.
218    pub fn contains(&self, service: ServiceSupported) -> bool {
219        let n = service.to_raw() as u32;
220        n < 64 && self.bits & (1u64 << n) != 0
221    }
222
223    /// Iterate the supported services in ascending bit order.
224    pub fn iter(&self) -> impl Iterator<Item = ServiceSupported> + '_ {
225        let bits = self.bits;
226        (0..64)
227            .filter(move |&n| bits & (1u64 << n) != 0)
228            .map(|n| ServiceSupported::from_raw(n as u8))
229    }
230}
231
232impl core::fmt::Display for ServicesSupported {
233    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
234        write_joined(f, self.iter())
235    }
236}
237
238impl core::fmt::Debug for ServicesSupported {
239    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
240        core::fmt::Display::fmt(self, f)
241    }
242}
243
244/// `BACnetObjectTypesSupported` — the `protocol-object-types-supported` bit
245/// string, one bit per object type (Clause 21).
246///
247/// Stored as a `[u64; 16]` bitset (1024 bits): bit *N* is set iff [`ObjectType`]
248/// value *N* is supported. The property covers only standardized object types
249/// (Clause 12.11.15) and the bit-string production is closed, but
250/// `BACnetObjectType` values run to 1023, so decode tolerates the enumeration's
251/// full 10-bit range instead of discarding a nonconformant peer's extra bits —
252/// hence the 16-word array, which is allocation-free and `Copy`. The public API
253/// still yields named
254/// [`ObjectType`] values (reusing that table rather than duplicating it), and
255/// `Debug` prints names, never raw numbers.
256#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
257pub struct ObjectTypesSupported {
258    bits: [u64; 16],
259}
260
261impl ObjectTypesSupported {
262    /// Decode a bit-string payload (the `data` of `PropertyValue::BitString`)
263    /// into the bitset of object types it advertises.
264    pub fn from_bacnet(data: &[u8]) -> Self {
265        let mut bits = [0u64; 16];
266        for (i, &byte) in data.iter().take(128).enumerate() {
267            bits[i / 8] |= (byte.reverse_bits() as u64) << ((i % 8) * 8);
268        }
269        Self { bits }
270    }
271
272    /// Whether `object_type` is marked supported.
273    pub fn contains(&self, object_type: ObjectType) -> bool {
274        let n = object_type.to_raw();
275        n < 1024 && self.bits[(n / 64) as usize] & (1u64 << (n % 64)) != 0
276    }
277
278    /// Iterate the supported object types in ascending bit order.
279    pub fn iter(&self) -> impl Iterator<Item = ObjectType> + '_ {
280        (0..1024u32)
281            .filter(move |&n| self.bits[(n / 64) as usize] & (1u64 << (n % 64)) != 0)
282            .map(ObjectType::from_raw)
283    }
284}
285
286impl core::fmt::Display for ObjectTypesSupported {
287    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
288        write_joined(f, self.iter())
289    }
290}
291
292impl core::fmt::Debug for ObjectTypesSupported {
293    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
294        core::fmt::Display::fmt(self, f)
295    }
296}
297
298/// `BACnetAuditOperationFlags` — a bit string selecting audit operations
299/// (Clause 21).
300///
301/// Bit positions 0 through 15 have the same meanings as the named
302/// [`AuditOperation`] values. Positions 16 through 31 are currently undefined
303/// and reserved for ASHRAE, so they must remain zero. Positions 32 through 63
304/// are retained for proprietary operations.
305#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
306pub struct AuditOperationFlags {
307    bits: u64,
308}
309
310impl AuditOperationFlags {
311    const RESERVED_BITS: u64 = 0x0000_0000_ffff_0000;
312
313    /// An empty set of audit operations.
314    #[inline]
315    pub const fn empty() -> Self {
316        Self { bits: 0 }
317    }
318
319    /// Construct from the bit-position-preserving representation.
320    ///
321    /// Currently undefined ASHRAE-reserved positions 16 through 31 are
322    /// rejected rather than masked.
323    #[inline]
324    pub fn from_bits(bits: u64) -> Result<Self> {
325        if bits & Self::RESERVED_BITS != 0 {
326            return Err(Error::OutOfRange(
327                "BACnetAuditOperationFlags: reserved bits 16-31 must be zero".into(),
328            ));
329        }
330        Ok(Self { bits })
331    }
332
333    /// Return the complete bit-position-preserving representation.
334    #[inline]
335    pub const fn bits(self) -> u64 {
336        self.bits
337    }
338
339    /// Whether no audit operation bit is set.
340    #[inline]
341    pub const fn is_empty(self) -> bool {
342        self.bits == 0
343    }
344
345    /// Whether `operation` is selected.
346    ///
347    /// Raw [`AuditOperation`] values above 63 are outside the production's
348    /// representable range and therefore return `false`.
349    #[inline]
350    pub const fn contains(self, operation: AuditOperation) -> bool {
351        let bit = operation.to_raw();
352        Self::valid_bit(bit) && self.bits & (1u64 << bit) != 0
353    }
354
355    /// Select `operation`, returning `false` without mutation when its raw
356    /// value is outside the representable range 0 through 63.
357    #[inline]
358    pub fn insert(&mut self, operation: AuditOperation) -> bool {
359        let bit = operation.to_raw();
360        if !Self::valid_bit(bit) {
361            return false;
362        }
363        self.bits |= 1u64 << bit;
364        true
365    }
366
367    /// Clear `operation`, returning `false` when its raw value is outside the
368    /// representable range 0 through 63.
369    #[inline]
370    pub fn remove(&mut self, operation: AuditOperation) -> bool {
371        let bit = operation.to_raw();
372        if !Self::valid_bit(bit) {
373            return false;
374        }
375        self.bits &= !(1u64 << bit);
376        true
377    }
378
379    /// Iterate selected operations in ascending bit-position order, including
380    /// proprietary raw values.
381    pub fn iter(self) -> impl Iterator<Item = AuditOperation> {
382        let bits = self.bits;
383        (0..64)
384            .filter(move |&bit| bits & (1u64 << bit) != 0)
385            .map(AuditOperation::from_raw)
386    }
387
388    const fn valid_bit(bit: u32) -> bool {
389        bit <= 15 || (bit >= 32 && bit <= 63)
390    }
391
392    /// Decode a Clause 20.2.10 BIT STRING payload.
393    ///
394    /// `data` excludes the initial unused-bits octet. The decoder rejects
395    /// values wider than the 64 positions allowed by
396    /// `BACnetAuditOperationFlags`, invalid unused-bit counts, and nonzero
397    /// padding in the final octet.
398    pub fn from_bacnet(unused_bits: u8, data: &[u8]) -> Result<Self> {
399        if unused_bits > 7 {
400            return Err(Error::decoding(
401                0,
402                format!("BACnetAuditOperationFlags: unused_bits must be 0-7, got {unused_bits}"),
403            ));
404        }
405        if data.is_empty() {
406            if unused_bits != 0 {
407                return Err(Error::decoding(
408                    0,
409                    "BACnetAuditOperationFlags: an empty bit string must have zero unused bits",
410                ));
411            }
412            return Ok(Self::empty());
413        }
414
415        let meaningful_bits = data.len() * 8 - usize::from(unused_bits);
416        if meaningful_bits > 64 {
417            return Err(Error::decoding(
418                0,
419                format!(
420                    "BACnetAuditOperationFlags: {meaningful_bits} bits exceeds the 64-bit limit"
421                ),
422            ));
423        }
424        if unused_bits != 0 {
425            let padding_mask = (1u8 << unused_bits) - 1;
426            if data.last().is_some_and(|last| last & padding_mask != 0) {
427                return Err(Error::decoding(
428                    0,
429                    "BACnetAuditOperationFlags: final-octet padding bits must be zero",
430                ));
431            }
432        }
433
434        let mut bits = 0u64;
435        for (index, &byte) in data.iter().enumerate() {
436            bits |= u64::from(byte.reverse_bits()) << (index * 8);
437        }
438        if bits & Self::RESERVED_BITS != 0 {
439            return Err(Error::decoding(
440                0,
441                "BACnetAuditOperationFlags: reserved bits 16-31 must be zero",
442            ));
443        }
444        Ok(Self { bits })
445    }
446
447    /// Encode as a shortest-form Clause 20.2.10 BIT STRING payload.
448    ///
449    /// The returned `data` excludes the initial unused-bits octet. Bit 0 is
450    /// placed in the most-significant position of the first data octet.
451    pub fn to_bacnet(self) -> (u8, Vec<u8>) {
452        if self.bits == 0 {
453            return (0, Vec::new());
454        }
455
456        let bit_len = (u64::BITS - self.bits.leading_zeros()) as usize;
457        let byte_len = bit_len.div_ceil(8);
458        let unused_bits = (byte_len * 8 - bit_len) as u8;
459        let mut data = Vec::with_capacity(byte_len);
460        for index in 0..byte_len {
461            data.push(((self.bits >> (index * 8)) as u8).reverse_bits());
462        }
463        (unused_bits, data)
464    }
465}
466
467impl core::fmt::Display for AuditOperationFlags {
468    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
469        write_joined(f, self.iter())
470    }
471}
472
473impl core::fmt::Debug for AuditOperationFlags {
474    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
475        core::fmt::Display::fmt(self, f)
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use crate::primitives::StatusFlags;
483
484    #[test]
485    fn pack_unpack_octet_spec_vectors() {
486        // Clause 20.2.10: first defined bit -> MSB. Deliberately asymmetric
487        // values — round trips alone cannot catch a symmetric encode/decode
488        // inversion, only literal wire bytes can.
489        assert_eq!(pack_octet(0b001), 0x80); // TO_OFFNORMAL / monday
490        assert_eq!(pack_octet(0b100), 0x20); // TO_NORMAL
491        assert_eq!(pack_octet(0b0100_0000), 0x02); // sunday (7-bit valid_days)
492        assert_eq!(unpack_octet(&[0x80], 3), 0b001);
493        assert_eq!(unpack_octet(&[0x20], 3), 0b100);
494        assert_eq!(unpack_octet(&[0xFE], 7), 0x7F); // all seven days
495        assert_eq!(unpack_octet(&[], 3), 0);
496        // Nonconformant junk past the defined bits is masked off.
497        assert_eq!(unpack_octet(&[0xFF], 3), 0b111);
498        // The octet helpers and the typed codecs agree.
499        assert_eq!(
500            EventTransitionBits::from_bacnet(&[pack_octet(0b001)]),
501            EventTransitionBits::TO_OFFNORMAL
502        );
503        assert_eq!(EventTransitionBits::TO_OFFNORMAL.to_bacnet(), 0x80);
504        assert_eq!(LimitEnable::LOW_LIMIT_ENABLE.to_bacnet(), 0x80);
505    }
506
507    #[test]
508    fn event_transition_bits_msb_first() {
509        // Wire byte 0b1010_0000 => bit0 (to-offnormal) and bit2 (to-normal).
510        let bits = EventTransitionBits::from_bacnet(&[0b1010_0000]);
511        assert_eq!(
512            bits,
513            EventTransitionBits::TO_OFFNORMAL | EventTransitionBits::TO_NORMAL
514        );
515        assert_eq!(bits.to_string(), "TO_OFFNORMAL | TO_NORMAL");
516    }
517
518    #[test]
519    fn limit_enable_msb_first() {
520        assert_eq!(
521            LimitEnable::from_bacnet(&[0x80]),
522            LimitEnable::LOW_LIMIT_ENABLE
523        );
524        assert_eq!(
525            LimitEnable::from_bacnet(&[0xC0]),
526            LimitEnable::LOW_LIMIT_ENABLE | LimitEnable::HIGH_LIMIT_ENABLE
527        );
528        assert_eq!(LimitEnable::from_bacnet(&[]).to_string(), "()");
529    }
530
531    #[test]
532    fn object_types_supported_matches_device_layout() {
533        // Byte 8 = 0x80 sets bit 64 => ColorTemperature (per device object test).
534        let mut data = vec![0u8; 9];
535        data[8] = 0x80;
536        let ots = ObjectTypesSupported::from_bacnet(&data);
537        assert!(ots.contains(ObjectType::COLOR_TEMPERATURE));
538        assert!(!ots.contains(ObjectType::ANALOG_INPUT));
539        assert_eq!(
540            ots.iter().collect::<Vec<_>>(),
541            vec![ObjectType::COLOR_TEMPERATURE]
542        );
543
544        // Byte 4 = 0xFD: types 32-37,39 set, 38 (NetworkSecurity) unset.
545        let ss = ObjectTypesSupported::from_bacnet(&[0, 0, 0, 0, 0xFD]);
546        assert!(ss.contains(ObjectType::from_raw(32)));
547        assert!(!ss.contains(ObjectType::NETWORK_SECURITY)); // type 38
548        assert!(ss.contains(ObjectType::from_raw(39)));
549    }
550
551    #[test]
552    fn resolved_value_holds_names_not_bytes() {
553        // The whole point: the decoded value contains named variants, and even
554        // its Debug shows names rather than the raw wire bytes.
555        let ots = ObjectTypesSupported::from_bacnet(&[0b1110_0000]); // types 0,1,2
556        assert_eq!(
557            ots.iter().collect::<Vec<_>>(),
558            [
559                ObjectType::ANALOG_INPUT,
560                ObjectType::ANALOG_OUTPUT,
561                ObjectType::ANALOG_VALUE
562            ]
563        );
564        let dbg = format!("{ots:?}");
565        assert!(
566            dbg.contains("ANALOG_INPUT"),
567            "Debug should name types: {dbg}"
568        );
569        assert!(
570            !dbg.contains("224"),
571            "Debug must not show raw byte 0xE0: {dbg}"
572        );
573    }
574
575    #[test]
576    fn services_supported_follows_clause_21() {
577        // Arbitrary decode vector (historically the device's pre-#192
578        // hardcoded constant — NOT the property the Device object serves).
579        let ss = ServicesSupported::from_bacnet(&[0xA4, 0x0B, 0x80, 0x35, 0x80, 0x00]);
580        assert!(ss.contains(ServiceSupported::ACKNOWLEDGE_ALARM)); // bit 0
581        assert!(ss.contains(ServiceSupported::SUBSCRIBE_COV)); // bit 5
582        assert!(ss.contains(ServiceSupported::READ_PROPERTY)); // bit 12
583        assert!(ss.contains(ServiceSupported::I_AM)); // bit 26
584        assert!(ss.contains(ServiceSupported::TIME_SYNCHRONIZATION)); // bit 32 (NOT who-is)
585        assert!(!ss.contains(ServiceSupported::WHO_IS)); // bit 34
586    }
587
588    #[test]
589    fn status_flags_render_names_not_numbers() {
590        assert_eq!(
591            (StatusFlags::IN_ALARM | StatusFlags::FAULT).to_string(),
592            "IN_ALARM | FAULT"
593        );
594        // Empty renders as "()", and Debug matches Display — never a raw number.
595        assert_eq!(StatusFlags::empty().to_string(), "()");
596        assert_eq!(format!("{:?}", StatusFlags::empty()), "()");
597        assert_eq!(format!("{:?}", StatusFlags::IN_ALARM), "IN_ALARM");
598    }
599
600    #[test]
601    fn small_types_debug_is_named_never_a_number() {
602        // bitflags' derived Debug prints 0x0 for an empty set; ours prints "()".
603        assert_eq!(format!("{:?}", EventTransitionBits::empty()), "()");
604        assert_eq!(format!("{:?}", LimitEnable::empty()), "()");
605        assert_eq!(format!("{:?}", EventTransitionBits::TO_FAULT), "TO_FAULT");
606    }
607
608    #[test]
609    fn audit_operation_flags_empty_and_named_bits_use_msb_first_wire_order() {
610        let empty = AuditOperationFlags::empty();
611        assert!(empty.is_empty());
612        assert_eq!(empty.to_bacnet(), (0, Vec::new()));
613        assert_eq!(AuditOperationFlags::from_bacnet(0, &[]).unwrap(), empty);
614
615        let mut flags = AuditOperationFlags::empty();
616        assert!(flags.insert(AuditOperation::READ));
617        assert!(flags.insert(AuditOperation::GENERAL));
618        assert!(flags.contains(AuditOperation::READ));
619        assert!(flags.contains(AuditOperation::GENERAL));
620        assert_eq!(flags.bits(), (1 << 0) | (1 << 15));
621        assert_eq!(flags.to_bacnet(), (0, vec![0x80, 0x01]));
622        assert_eq!(
623            AuditOperationFlags::from_bacnet(0, &[0x80, 0x01]).unwrap(),
624            flags
625        );
626        assert_eq!(flags.to_string(), "READ | GENERAL");
627    }
628
629    #[test]
630    fn audit_operation_flags_preserve_proprietary_bit_63() {
631        let proprietary_32 = AuditOperation::from_raw(32);
632        let proprietary = AuditOperation::from_raw(63);
633        let mut flags = AuditOperationFlags::from_bits(1u64 << 63).unwrap();
634        assert!(flags.insert(proprietary_32));
635        assert!(flags.contains(proprietary_32));
636        assert!(flags.contains(proprietary));
637        assert_eq!(flags.to_bacnet(), (0, vec![0, 0, 0, 0, 0x80, 0, 0, 1]));
638        assert_eq!(
639            AuditOperationFlags::from_bacnet(0, &[0, 0, 0, 0, 0x80, 0, 0, 1]).unwrap(),
640            flags
641        );
642        assert!(!flags.insert(AuditOperation::from_raw(64)));
643        assert!(!flags.contains(AuditOperation::from_raw(64)));
644        assert!(flags.remove(proprietary_32));
645        assert!(flags.remove(proprietary));
646        assert!(flags.is_empty());
647    }
648
649    #[test]
650    fn audit_operation_flags_reject_overlong_and_nonzero_padding() {
651        assert!(AuditOperationFlags::from_bacnet(7, &[0; 9]).is_err());
652        assert!(AuditOperationFlags::from_bacnet(1, &[0x01]).is_err());
653        assert!(AuditOperationFlags::from_bacnet(1, &[]).is_err());
654        assert!(AuditOperationFlags::from_bacnet(8, &[0]).is_err());
655        assert!(AuditOperationFlags::from_bits(1 << 16).is_err());
656        assert!(AuditOperationFlags::from_bits(1 << 31).is_err());
657        assert!(AuditOperationFlags::from_bacnet(7, &[0, 0, 0x80]).is_err());
658
659        let mut flags = AuditOperationFlags::empty();
660        assert!(!flags.insert(AuditOperation::from_raw(16)));
661        assert!(!flags.insert(AuditOperation::from_raw(31)));
662        assert!(flags.is_empty());
663
664        // Zero padding is accepted; seven unused bits leaves only bit 0.
665        assert_eq!(
666            AuditOperationFlags::from_bacnet(7, &[0x80]).unwrap(),
667            AuditOperationFlags::from_bits(1).unwrap()
668        );
669    }
670}