Skip to main content

bacnet_types/
primitives.rs

1//! BACnet primitive data types per ASHRAE 135-2020 Clause 20.2.
2//!
3//! Core types used throughout the protocol: [`ObjectIdentifier`], [`Date`],
4//! [`Time`], and the [`PropertyValue`] sum type.
5
6#[cfg(not(feature = "std"))]
7use alloc::{string::String, vec::Vec};
8
9use crate::enums::ObjectType;
10use crate::error::Error;
11
12// ---------------------------------------------------------------------------
13// ObjectIdentifier (Clause 20.2.14)
14// ---------------------------------------------------------------------------
15
16/// BACnet Object Identifier: 10-bit type + 22-bit instance number.
17///
18/// Uniquely identifies a BACnet object within a device. Encoded as a
19/// 4-byte big-endian value: `(object_type << 22) | instance_number`.
20/// Checked construction and wire decoding preserve both field widths, so
21/// encoding cannot silently truncate an identifier.
22///
23/// The all-ones instance value [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE)
24/// is still a valid 22-bit wire value, and some service contexts give it
25/// wildcard meaning. Use [`ObjectIdentifier::new`] or [`ObjectIdentifier::decode`]
26/// when preserving wire-level values. Use [`ObjectIdentifier::new_addressable`]
27/// when constructing identifiers for concrete objects that can be stored,
28/// addressed, or compared as actual object instances.
29#[derive(Clone, Copy, PartialEq, Eq, Hash)]
30pub struct ObjectIdentifier {
31    object_type: ObjectType,
32    instance_number: u32,
33}
34
35impl ObjectIdentifier {
36    /// Maximum encodable instance number (2^22 - 1 = 4,194,303).
37    ///
38    /// This is also [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE). It is
39    /// accepted by [`ObjectIdentifier::new`] and [`ObjectIdentifier::decode`]
40    /// for wire compatibility, but callers that need a concrete addressable
41    /// object should use [`MAX_ADDRESSABLE_INSTANCE`](Self::MAX_ADDRESSABLE_INSTANCE)
42    /// or [`ObjectIdentifier::new_addressable`].
43    pub const MAX_INSTANCE: u32 = 0x3F_FFFF;
44
45    /// The reserved wildcard instance number (4,194,303).
46    ///
47    /// BACnet service clauses assign this value special "any/local instance"
48    /// meaning in selected contexts, for example Who-Is/Who-Am-I discovery and
49    /// Device/Network Port object access. It is not a concrete object instance
50    /// for addressable-object storage.
51    pub const WILDCARD_INSTANCE: u32 = Self::MAX_INSTANCE;
52
53    /// Maximum instance number for a concrete addressable object (4,194,302).
54    ///
55    /// This excludes [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE).
56    pub const MAX_ADDRESSABLE_INSTANCE: u32 = Self::WILDCARD_INSTANCE - 1;
57
58    /// Create an ObjectIdentifier from a 10-bit type and 22-bit wire instance.
59    ///
60    /// This constructor preserves [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE)
61    /// because it is a valid encoded value in BACnet service payloads. Use
62    /// [`ObjectIdentifier::new_addressable`] when the identifier must name a
63    /// concrete object instance.
64    ///
65    /// # Errors
66    /// Returns `Err` if the object type exceeds 1023 or `instance_number` exceeds
67    /// [`MAX_INSTANCE`](Self::MAX_INSTANCE).
68    pub fn new(object_type: ObjectType, instance_number: u32) -> Result<Self, Error> {
69        if instance_number > Self::MAX_INSTANCE {
70            return Err(Error::OutOfRange(format!(
71                "instance number {} exceeds max {}",
72                instance_number,
73                Self::MAX_INSTANCE
74            )));
75        }
76        if object_type.to_raw() > 0x3FF {
77            return Err(Error::OutOfRange(format!(
78                "object type {} exceeds max 1023",
79                object_type.to_raw()
80            )));
81        }
82        Ok(Self {
83            object_type,
84            instance_number,
85        })
86    }
87
88    /// Create a new ObjectIdentifier for a concrete addressable object.
89    ///
90    /// This rejects [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE), while
91    /// [`ObjectIdentifier::new`] keeps accepting that value for wire-level
92    /// round trips and BACnet service contexts where it is meaningful.
93    ///
94    /// # Errors
95    /// Returns `Err` if the object type exceeds 1023, or `instance_number` exceeds
96    /// [`MAX_INSTANCE`](Self::MAX_INSTANCE) or equals
97    /// [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE).
98    pub fn new_addressable(object_type: ObjectType, instance_number: u32) -> Result<Self, Error> {
99        let identifier = Self::new(object_type, instance_number)?;
100        if instance_number == Self::WILDCARD_INSTANCE {
101            return Err(Error::OutOfRange(format!(
102                "instance number {} is reserved as the wildcard value; maximum addressable instance is {}",
103                instance_number,
104                Self::MAX_ADDRESSABLE_INSTANCE
105            )));
106        }
107        Ok(identifier)
108    }
109
110    /// The object type.
111    pub const fn object_type(&self) -> ObjectType {
112        self.object_type
113    }
114
115    /// The instance number (0 to 4,194,303).
116    ///
117    /// Values returned from wire decoding may equal
118    /// [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE). Treat that value as a
119    /// concrete addressable object only after the relevant service context has
120    /// resolved its wildcard semantics.
121    pub const fn instance_number(&self) -> u32 {
122        self.instance_number
123    }
124
125    /// Encode to the 4-byte BACnet wire format (big-endian).
126    pub fn encode(&self) -> [u8; 4] {
127        assert!(
128            self.object_type.to_raw() <= 0x3FF,
129            "ObjectType {} exceeds 10-bit field",
130            self.object_type.to_raw()
131        );
132        assert!(
133            self.instance_number <= Self::MAX_INSTANCE,
134            "Instance {} exceeds MAX_INSTANCE",
135            self.instance_number
136        );
137        let value = (self.object_type.to_raw() << 22) | self.instance_number;
138        value.to_be_bytes()
139    }
140
141    /// Decode from the 4-byte BACnet wire format (big-endian).
142    pub fn decode(data: &[u8]) -> Result<Self, Error> {
143        if data.len() != 4 {
144            return Err(Error::decoding(
145                0,
146                format!(
147                    "ObjectIdentifier expects exactly 4 bytes, got {}",
148                    data.len()
149                ),
150            ));
151        }
152        let value = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
153        let type_raw = (value >> 22) & 0x3FF;
154        let instance = value & Self::MAX_INSTANCE;
155        Ok(Self {
156            object_type: ObjectType::from_raw(type_raw),
157            instance_number: instance,
158        })
159    }
160}
161
162impl core::fmt::Debug for ObjectIdentifier {
163    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
164        write!(
165            f,
166            "ObjectIdentifier({:?}, {})",
167            self.object_type, self.instance_number
168        )
169    }
170}
171
172impl core::fmt::Display for ObjectIdentifier {
173    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
174        write!(f, "{},{}", self.object_type, self.instance_number)
175    }
176}
177
178// ---------------------------------------------------------------------------
179// Date (Clause 20.2.12)
180// ---------------------------------------------------------------------------
181
182/// BACnet Date: year, month, day, day-of-week.
183///
184/// - Year: 0-254 relative to 1900 (0xFF = unspecified)
185/// - Month: 1-14 (13=odd, 14=even, 0xFF=unspecified)
186/// - Day: 1-34 (32=last, 33=odd, 34=even, 0xFF=unspecified)
187/// - Day of week: 1=Monday..7=Sunday (0xFF=unspecified)
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
189pub struct Date {
190    /// Year minus 1900 (0-254, or 0xFF for unspecified).
191    pub year: u8,
192    /// Month (1-14, or 0xFF for unspecified).
193    pub month: u8,
194    /// Day of month (1-34, or 0xFF for unspecified).
195    pub day: u8,
196    /// Day of week (1=Monday..7=Sunday, or 0xFF for unspecified).
197    pub day_of_week: u8,
198}
199
200impl Date {
201    /// Value indicating "unspecified" for any date field.
202    pub const UNSPECIFIED: u8 = 0xFF;
203
204    /// Encode to 4 bytes.
205    pub fn encode(&self) -> [u8; 4] {
206        [self.year, self.month, self.day, self.day_of_week]
207    }
208
209    /// Decode from 4 bytes.
210    pub fn decode(data: &[u8]) -> Result<Self, Error> {
211        if data.len() != 4 {
212            return Err(Error::decoding(
213                0,
214                format!("Date expects exactly 4 bytes, got {}", data.len()),
215            ));
216        }
217        Ok(Self {
218            year: data[0],
219            month: data[1],
220            day: data[2],
221            day_of_week: data[3],
222        })
223    }
224
225    /// Get the actual year (1900 + year field), or None if unspecified.
226    pub fn actual_year(&self) -> Option<u16> {
227        if self.year == Self::UNSPECIFIED {
228            None
229        } else {
230            Some(1900 + self.year as u16)
231        }
232    }
233}
234
235// ---------------------------------------------------------------------------
236// Time (Clause 20.2.13)
237// ---------------------------------------------------------------------------
238
239/// BACnet Time: hour, minute, second, hundredths.
240///
241/// Each field can be 0xFF for "unspecified".
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
243pub struct Time {
244    /// Hour (0-23, or 0xFF for unspecified).
245    pub hour: u8,
246    /// Minute (0-59, or 0xFF for unspecified).
247    pub minute: u8,
248    /// Second (0-59, or 0xFF for unspecified).
249    pub second: u8,
250    /// Hundredths of a second (0-99, or 0xFF for unspecified).
251    pub hundredths: u8,
252}
253
254impl Time {
255    /// Value indicating "unspecified" for any time field.
256    pub const UNSPECIFIED: u8 = 0xFF;
257
258    /// Encode to 4 bytes.
259    pub fn encode(&self) -> [u8; 4] {
260        [self.hour, self.minute, self.second, self.hundredths]
261    }
262
263    /// Decode from 4 bytes.
264    pub fn decode(data: &[u8]) -> Result<Self, Error> {
265        if data.len() != 4 {
266            return Err(Error::decoding(
267                0,
268                format!("Time expects exactly 4 bytes, got {}", data.len()),
269            ));
270        }
271        Ok(Self {
272            hour: data[0],
273            minute: data[1],
274            second: data[2],
275            hundredths: data[3],
276        })
277    }
278}
279
280// ---------------------------------------------------------------------------
281// BACnetTimeStamp (Clause 20.2.1.5)
282// ---------------------------------------------------------------------------
283
284/// BACnet timestamp -- a CHOICE of Time, sequence number, or DateTime.
285#[derive(Debug, Clone, PartialEq)]
286pub enum BACnetTimeStamp {
287    /// Context tag 0: Time
288    Time(Time),
289    /// Context tag 1: Unsigned sequence number in the required 0..=65535 range.
290    SequenceNumber(u16),
291    /// Context tag 2: BACnetDateTime (Date + Time)
292    DateTime {
293        /// Calendar date component.
294        date: Date,
295        /// Time-of-day component.
296        time: Time,
297    },
298}
299
300// ---------------------------------------------------------------------------
301// StatusFlags (Clause 12.X -- used by many object types)
302// ---------------------------------------------------------------------------
303
304bitflags::bitflags! {
305    /// BACnet StatusFlags -- 4-bit bitstring present on most objects.
306    #[derive(Clone, Copy, PartialEq, Eq, Hash)]
307    pub struct StatusFlags: u8 {
308        /// Event_State is anything other than NORMAL.
309        const IN_ALARM = 0b1000;
310        /// Reliability is present and is not NO_FAULT_DETECTED.
311        const FAULT = 0b0100;
312        /// The value is being supplied by a local override rather than the physical input.
313        const OVERRIDDEN = 0b0010;
314        /// Out_Of_Service is TRUE.
315        const OUT_OF_SERVICE = 0b0001;
316    }
317}
318
319impl core::fmt::Display for StatusFlags {
320    /// Renders the set flags by their Clause-21 names (`IN_ALARM | FAULT`), or
321    /// `()` when none are set — never a raw number.
322    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
323        let mut first = true;
324        for (name, _) in self.iter_names() {
325            if !first {
326                f.write_str(" | ")?;
327            }
328            f.write_str(name)?;
329            first = false;
330        }
331        if first {
332            f.write_str("()")?;
333        }
334        Ok(())
335    }
336}
337
338impl core::fmt::Debug for StatusFlags {
339    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
340        core::fmt::Display::fmt(self, f)
341    }
342}
343
344// ---------------------------------------------------------------------------
345// PropertyValue -- sum type for BACnet property values
346// ---------------------------------------------------------------------------
347
348/// A BACnet application-layer value.
349///
350/// This enum covers all primitive value types that can appear as property
351/// values in BACnet objects. Constructed types (lists, sequences) are
352/// represented as nested structures.
353#[derive(Debug, Clone, PartialEq)]
354pub enum PropertyValue {
355    /// Null value.
356    Null,
357    /// Boolean value.
358    Boolean(bool),
359    /// Unsigned integer (up to 64-bit for BACnet Unsigned64).
360    Unsigned(u64),
361    /// Signed integer.
362    Signed(i32),
363    /// IEEE 754 single-precision float.
364    Real(f32),
365    /// IEEE 754 double-precision float.
366    Double(f64),
367    /// Octet string (raw bytes).
368    OctetString(Vec<u8>),
369    /// Character string (UTF-8).
370    CharacterString(String),
371    /// Bit string (variable length).
372    BitString {
373        /// Number of unused bits in the last byte.
374        unused_bits: u8,
375        /// The bit data bytes.
376        data: Vec<u8>,
377    },
378    /// Enumerated value.
379    Enumerated(u32),
380    /// Date value.
381    Date(Date),
382    /// Time value.
383    Time(Time),
384    /// Object identifier.
385    ObjectIdentifier(ObjectIdentifier),
386    /// A sequence (array) of property values.
387    ///
388    /// Used when reading an entire array property with `arrayIndex` absent
389    /// (Clause 15.5.1). Each element is encoded as its own application-tagged
390    /// value, concatenated in order.
391    List(Vec<PropertyValue>),
392    /// Raw, already-encoded application-layer bytes for a property whose wire
393    /// form is a context-tagged (CHOICE/SEQUENCE) production that the flat
394    /// application-tagged model above cannot express — e.g. a framed
395    /// `BACnetEventParameter` or a `Recipient_List` of `BACnetDestination`.
396    ///
397    /// The bytes are emitted verbatim by
398    /// `bacnet_encoding::primitives::encode_property_value` and are produced
399    /// by `decode_application_value` when it encounters a context-tagged
400    /// element; they always span exactly one complete tagged element,
401    /// including its context tag header(s). Objects that serve structured
402    /// properties decode these with the matching framed codec in
403    /// `bacnet-encoding`; a property writer that is not expecting this variant
404    /// should reject it as `INVALID_DATA_TYPE`.
405    ApplicationData(Vec<u8>),
406}
407
408impl PropertyValue {
409    /// Whether this is a value of a primitive datatype, one application tag
410    /// on the wire: every variant except `List` and `ApplicationData`.
411    pub fn is_primitive(&self) -> bool {
412        match self {
413            Self::Null
414            | Self::Boolean(_)
415            | Self::Unsigned(_)
416            | Self::Signed(_)
417            | Self::Real(_)
418            | Self::Double(_)
419            | Self::OctetString(_)
420            | Self::CharacterString(_)
421            | Self::BitString { .. }
422            | Self::Enumerated(_)
423            | Self::Date(_)
424            | Self::Time(_)
425            | Self::ObjectIdentifier(_) => true,
426            Self::List(_) | Self::ApplicationData(_) => false,
427        }
428    }
429}
430
431// ---------------------------------------------------------------------------
432// Tests
433// ---------------------------------------------------------------------------
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    #[test]
440    fn object_identifier_encode_decode_round_trip() {
441        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
442        let bytes = oid.encode();
443        let decoded = ObjectIdentifier::decode(&bytes).unwrap();
444        assert_eq!(oid, decoded);
445    }
446
447    #[test]
448    fn object_identifier_wire_format() {
449        // AnalogInput (type=0) instance=1: (0 << 22) | 1 = 0x00000001
450        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
451        assert_eq!(oid.encode(), [0x00, 0x00, 0x00, 0x01]);
452
453        // Device (type=8) instance=1234: (8 << 22) | 1234 = 0x020004D2
454        let oid = ObjectIdentifier::new(ObjectType::DEVICE, 1234).unwrap();
455        let expected = ((8u32 << 22) | 1234u32).to_be_bytes();
456        assert_eq!(oid.encode(), expected);
457    }
458
459    #[test]
460    fn object_identifier_max_instance() {
461        let oid =
462            ObjectIdentifier::new(ObjectType::DEVICE, ObjectIdentifier::MAX_INSTANCE).unwrap();
463        assert_eq!(oid.instance_number(), 0x3F_FFFF);
464        let bytes = oid.encode();
465        let decoded = ObjectIdentifier::decode(&bytes).unwrap();
466        assert_eq!(decoded.instance_number(), 0x3F_FFFF);
467    }
468
469    #[test]
470    fn object_identifier_invalid_instance() {
471        let result = ObjectIdentifier::new(ObjectType::DEVICE, ObjectIdentifier::MAX_INSTANCE + 1);
472        assert!(result.is_err());
473    }
474
475    #[test]
476    fn object_identifier_buffer_too_short() {
477        let result = ObjectIdentifier::decode(&[0x00, 0x00]);
478        assert!(result.is_err());
479    }
480
481    #[test]
482    fn object_identifier_overlong_errors() {
483        assert!(ObjectIdentifier::decode(&[0x00, 0x00, 0x00, 0x01, 0xFF]).is_err());
484    }
485
486    #[test]
487    fn date_encode_decode_round_trip() {
488        let date = Date {
489            year: 124, // 2024
490            month: 6,
491            day: 15,
492            day_of_week: 6, // Saturday
493        };
494        let bytes = date.encode();
495        let decoded = Date::decode(&bytes).unwrap();
496        assert_eq!(date, decoded);
497        assert_eq!(decoded.actual_year(), Some(2024));
498    }
499
500    #[test]
501    fn date_unspecified_year() {
502        let date = Date {
503            year: Date::UNSPECIFIED,
504            month: 1,
505            day: 1,
506            day_of_week: Date::UNSPECIFIED,
507        };
508        assert_eq!(date.actual_year(), None);
509    }
510
511    #[test]
512    fn date_overlong_errors() {
513        assert!(Date::decode(&[124, 6, 15, 6, 0]).is_err());
514    }
515
516    #[test]
517    fn time_encode_decode_round_trip() {
518        let time = Time {
519            hour: 14,
520            minute: 30,
521            second: 45,
522            hundredths: 50,
523        };
524        let bytes = time.encode();
525        let decoded = Time::decode(&bytes).unwrap();
526        assert_eq!(time, decoded);
527    }
528
529    #[test]
530    fn time_overlong_errors() {
531        assert!(Time::decode(&[14, 30, 45, 50, 0]).is_err());
532    }
533
534    #[test]
535    fn status_flags_operations() {
536        let flags = StatusFlags::IN_ALARM | StatusFlags::OUT_OF_SERVICE;
537        assert!(flags.contains(StatusFlags::IN_ALARM));
538        assert!(flags.contains(StatusFlags::OUT_OF_SERVICE));
539        assert!(!flags.contains(StatusFlags::FAULT));
540        assert!(!flags.contains(StatusFlags::OVERRIDDEN));
541    }
542
543    // --- OID edge cases ---
544
545    #[test]
546    fn object_identifier_instance_zero() {
547        // Instance 0 is valid and commonly used (e.g., Device,0)
548        let oid = ObjectIdentifier::new(ObjectType::DEVICE, 0).unwrap();
549        assert_eq!(oid.instance_number(), 0);
550        let bytes = oid.encode();
551        let decoded = ObjectIdentifier::decode(&bytes).unwrap();
552        assert_eq!(decoded.instance_number(), 0);
553        assert_eq!(decoded.object_type(), ObjectType::DEVICE);
554    }
555
556    #[test]
557    fn object_identifier_all_types_instance_zero() {
558        // Instance 0 for each common type should round-trip correctly
559        for type_raw in [0u32, 1, 2, 3, 4, 5, 6, 8, 10, 13, 14, 17, 19] {
560            let obj_type = ObjectType::from_raw(type_raw);
561            let oid = ObjectIdentifier::new(obj_type, 0).unwrap();
562            let bytes = oid.encode();
563            let decoded = ObjectIdentifier::decode(&bytes).unwrap();
564            assert_eq!(decoded.object_type(), obj_type, "type {type_raw} failed");
565            assert_eq!(
566                decoded.instance_number(),
567                0,
568                "type {type_raw} instance failed"
569            );
570        }
571    }
572
573    #[test]
574    fn object_identifier_wildcard_instance() {
575        let oid =
576            ObjectIdentifier::new(ObjectType::DEVICE, ObjectIdentifier::WILDCARD_INSTANCE).unwrap();
577        assert_eq!(oid.instance_number(), ObjectIdentifier::MAX_INSTANCE);
578        let bytes = oid.encode();
579        let decoded = ObjectIdentifier::decode(&bytes).unwrap();
580        assert_eq!(decoded.instance_number(), ObjectIdentifier::MAX_INSTANCE);
581
582        let raw_wire_value = ((ObjectType::DEVICE.to_raw() << 22)
583            | ObjectIdentifier::WILDCARD_INSTANCE)
584            .to_be_bytes();
585        let decoded = ObjectIdentifier::decode(&raw_wire_value).unwrap();
586        assert_eq!(decoded.object_type(), ObjectType::DEVICE);
587        assert_eq!(
588            decoded.instance_number(),
589            ObjectIdentifier::WILDCARD_INSTANCE
590        );
591    }
592
593    #[test]
594    fn object_identifier_new_addressable_rejects_wildcard() {
595        assert_eq!(
596            ObjectIdentifier::MAX_ADDRESSABLE_INSTANCE,
597            ObjectIdentifier::WILDCARD_INSTANCE - 1
598        );
599
600        let oid = ObjectIdentifier::new_addressable(
601            ObjectType::DEVICE,
602            ObjectIdentifier::MAX_ADDRESSABLE_INSTANCE,
603        )
604        .unwrap();
605        assert_eq!(
606            oid.instance_number(),
607            ObjectIdentifier::MAX_ADDRESSABLE_INSTANCE
608        );
609
610        let analog_input = ObjectIdentifier::new_addressable(
611            ObjectType::ANALOG_INPUT,
612            ObjectIdentifier::MAX_ADDRESSABLE_INSTANCE,
613        )
614        .unwrap();
615        assert_eq!(analog_input.object_type(), ObjectType::ANALOG_INPUT);
616        assert_eq!(
617            analog_input.instance_number(),
618            ObjectIdentifier::MAX_ADDRESSABLE_INSTANCE
619        );
620
621        let wildcard =
622            ObjectIdentifier::new_addressable(ObjectType::DEVICE, ObjectIdentifier::MAX_INSTANCE)
623                .unwrap_err();
624        assert!(wildcard.to_string().contains("wildcard"));
625
626        let too_large = ObjectIdentifier::new_addressable(
627            ObjectType::DEVICE,
628            ObjectIdentifier::MAX_INSTANCE + 1,
629        );
630        assert!(too_large.is_err());
631    }
632
633    #[test]
634    fn object_identifier_decode_extra_bytes_errors() {
635        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 42).unwrap();
636        let mut bytes = oid.encode().to_vec();
637        bytes.extend_from_slice(&[0xFF, 0xFF]);
638        assert!(ObjectIdentifier::decode(&bytes).is_err());
639    }
640
641    #[test]
642    fn object_identifier_new_rejects_type_overflow() {
643        for raw in [1024, u32::MAX] {
644            assert!(matches!(
645                ObjectIdentifier::new(ObjectType::from_raw(raw), 0),
646                Err(Error::OutOfRange(_))
647            ));
648        }
649    }
650
651    #[test]
652    fn object_identifier_addressable_rejects_type_overflow() {
653        for raw in [1024, u32::MAX] {
654            assert!(matches!(
655                ObjectIdentifier::new_addressable(ObjectType::from_raw(raw), 0),
656                Err(Error::OutOfRange(_))
657            ));
658        }
659    }
660
661    #[test]
662    fn object_identifier_checked_instance_boundaries() {
663        for instance in [ObjectIdentifier::MAX_INSTANCE + 1, u32::MAX] {
664            for constructor in [ObjectIdentifier::new, ObjectIdentifier::new_addressable] {
665                assert!(matches!(
666                    constructor(ObjectType::from_raw(1023), instance),
667                    Err(Error::OutOfRange(_))
668                ));
669            }
670        }
671    }
672
673    #[test]
674    fn object_identifier_wire_boundaries_preserve_identity() {
675        for (raw_type, instance, wire) in [
676            (0, 0, [0, 0, 0, 0]),
677            (1023, 0, [0xff, 0xc0, 0, 0]),
678            (
679                1023,
680                ObjectIdentifier::MAX_ADDRESSABLE_INSTANCE,
681                [0xff, 0xff, 0xff, 0xfe],
682            ),
683            (
684                1023,
685                ObjectIdentifier::MAX_INSTANCE,
686                [0xff, 0xff, 0xff, 0xff],
687            ),
688        ] {
689            let object_type = ObjectType::from_raw(raw_type);
690            let oid = ObjectIdentifier::new(object_type, instance).unwrap();
691            assert_eq!(oid.encode(), wire);
692            assert_eq!(ObjectIdentifier::decode(&wire).unwrap(), oid);
693            if instance == ObjectIdentifier::WILDCARD_INSTANCE {
694                assert!(ObjectIdentifier::new_addressable(object_type, instance).is_err());
695            } else {
696                assert_eq!(
697                    ObjectIdentifier::new_addressable(object_type, instance).unwrap(),
698                    oid
699                );
700            }
701        }
702        // Raw ObjectType remains a generic selector outside ObjectIdentifier.
703        assert_eq!(ObjectType::from_raw(u32::MAX).to_raw(), u32::MAX);
704    }
705
706    #[test]
707    fn property_value_variants() {
708        let null = PropertyValue::Null;
709        let boolean = PropertyValue::Boolean(true);
710        let real = PropertyValue::Real(72.5);
711        let string = PropertyValue::CharacterString("test".into());
712
713        assert_eq!(null, PropertyValue::Null);
714        assert_eq!(boolean, PropertyValue::Boolean(true));
715        assert_ne!(real, PropertyValue::Real(73.0));
716        assert_eq!(string, PropertyValue::CharacterString("test".into()));
717    }
718}