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, with `alloc`, the `PropertyValue` sum type.
5
6#[cfg(all(feature = "alloc", not(feature = "std")))]
7use alloc::{string::String, vec::Vec};
8
9use crate::enums::ObjectType;
10use crate::error::{DecodeError, DecodingKind, Error, RangeError};
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 [`Error::Range`] if the object type exceeds 1023 or
67    /// `instance_number` exceeds [`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(RangeError::new("instance number {} exceeds max 4194303")
71                .with_value(u64::from(instance_number))
72                .into());
73        }
74        if object_type.to_raw() > 0x3FF {
75            return Err(RangeError::new("object type {} exceeds max 1023")
76                .with_value(u64::from(object_type.to_raw()))
77                .into());
78        }
79        Ok(Self {
80            object_type,
81            instance_number,
82        })
83    }
84
85    /// Create a new ObjectIdentifier for a concrete addressable object.
86    ///
87    /// This rejects [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE), while
88    /// [`ObjectIdentifier::new`] keeps accepting that value for wire-level
89    /// round trips and BACnet service contexts where it is meaningful.
90    ///
91    /// # Errors
92    /// Returns [`Error::Range`] if the object type exceeds 1023, or
93    /// `instance_number` exceeds [`MAX_INSTANCE`](Self::MAX_INSTANCE) or
94    /// equals [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE).
95    pub fn new_addressable(object_type: ObjectType, instance_number: u32) -> Result<Self, Error> {
96        let identifier = Self::new(object_type, instance_number)?;
97        if instance_number == Self::WILDCARD_INSTANCE {
98            return Err(RangeError::new(
99                "instance number 4194303 is reserved as the wildcard value; \
100                 maximum addressable instance is 4194302",
101            )
102            .into());
103        }
104        Ok(identifier)
105    }
106
107    /// The object type.
108    pub const fn object_type(&self) -> ObjectType {
109        self.object_type
110    }
111
112    /// The instance number (0 to 4,194,303).
113    ///
114    /// Values returned from wire decoding may equal
115    /// [`WILDCARD_INSTANCE`](Self::WILDCARD_INSTANCE). Treat that value as a
116    /// concrete addressable object only after the relevant service context has
117    /// resolved its wildcard semantics.
118    pub const fn instance_number(&self) -> u32 {
119        self.instance_number
120    }
121
122    /// Encode to the 4-byte BACnet wire format (big-endian).
123    pub fn encode(&self) -> [u8; 4] {
124        // `new` and `decode`, the only constructors, keep both fields in
125        // range, so these hold by construction; their messages are static
126        // so no build links formatting for them (#1777).
127        debug_assert!(
128            self.object_type.to_raw() <= 0x3FF,
129            "ObjectType exceeds the 10-bit field"
130        );
131        debug_assert!(
132            self.instance_number <= Self::MAX_INSTANCE,
133            "instance exceeds MAX_INSTANCE"
134        );
135        let value = (self.object_type.to_raw() << 22) | self.instance_number;
136        value.to_be_bytes()
137    }
138
139    /// Decode from the 4-byte BACnet wire format (big-endian).
140    pub fn decode(data: &[u8]) -> Result<Self, Error> {
141        if data.len() != 4 {
142            return Err(wrong_length(
143                "ObjectIdentifier expects exactly 4 bytes, got {}",
144                data,
145            ));
146        }
147        let value = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
148        let type_raw = (value >> 22) & 0x3FF;
149        let instance = value & Self::MAX_INSTANCE;
150        Ok(Self {
151            object_type: ObjectType::from_raw(type_raw),
152            instance_number: instance,
153        })
154    }
155}
156
157/// The error for fixed-size contents of another length than `label` names.
158#[cold]
159fn wrong_length(label: &'static str, data: &[u8]) -> Error {
160    DecodeError::new(DecodingKind::InvalidEncoding, 0, label)
161        .with_value(data.len() as u64)
162        .into()
163}
164
165impl core::fmt::Debug for ObjectIdentifier {
166    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
167        write!(
168            f,
169            "ObjectIdentifier({:?}, {})",
170            self.object_type, self.instance_number
171        )
172    }
173}
174
175impl core::fmt::Display for ObjectIdentifier {
176    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
177        write!(f, "{},{}", self.object_type, self.instance_number)
178    }
179}
180
181// ---------------------------------------------------------------------------
182// Date (Clause 20.2.12)
183// ---------------------------------------------------------------------------
184
185/// BACnet Date: year, month, day, day-of-week.
186///
187/// - Year: 0-254 relative to 1900 (0xFF = unspecified)
188/// - Month: 1-14 (13=odd, 14=even, 0xFF=unspecified)
189/// - Day: 1-34 (32=last, 33=odd, 34=even, 0xFF=unspecified)
190/// - Day of week: 1=Monday..7=Sunday (0xFF=unspecified)
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
192pub struct Date {
193    /// Year minus 1900 (0-254, or 0xFF for unspecified).
194    pub year: u8,
195    /// Month (1-14, or 0xFF for unspecified).
196    pub month: u8,
197    /// Day of month (1-34, or 0xFF for unspecified).
198    pub day: u8,
199    /// Day of week (1=Monday..7=Sunday, or 0xFF for unspecified).
200    pub day_of_week: u8,
201}
202
203impl Date {
204    /// Value indicating "unspecified" for any date field.
205    pub const UNSPECIFIED: u8 = 0xFF;
206
207    /// Encode to 4 bytes.
208    pub fn encode(&self) -> [u8; 4] {
209        [self.year, self.month, self.day, self.day_of_week]
210    }
211
212    /// Decode from 4 bytes.
213    pub fn decode(data: &[u8]) -> Result<Self, Error> {
214        if data.len() != 4 {
215            return Err(wrong_length("Date expects exactly 4 bytes, got {}", data));
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(wrong_length("Time expects exactly 4 bytes, got {}", data));
267        }
268        Ok(Self {
269            hour: data[0],
270            minute: data[1],
271            second: data[2],
272            hundredths: data[3],
273        })
274    }
275}
276
277// ---------------------------------------------------------------------------
278// BACnetTimeStamp (Clause 20.2.1.5)
279// ---------------------------------------------------------------------------
280
281/// BACnet timestamp -- a CHOICE of Time, sequence number, or DateTime.
282#[derive(Debug, Clone, PartialEq)]
283pub enum BACnetTimeStamp {
284    /// Context tag 0: Time
285    Time(Time),
286    /// Context tag 1: Unsigned sequence number in the required 0..=65535 range.
287    SequenceNumber(u16),
288    /// Context tag 2: BACnetDateTime (Date + Time)
289    DateTime {
290        /// Calendar date component.
291        date: Date,
292        /// Time-of-day component.
293        time: Time,
294    },
295}
296
297// ---------------------------------------------------------------------------
298// StatusFlags (Clause 12.X -- used by many object types)
299// ---------------------------------------------------------------------------
300
301bitflags::bitflags! {
302    /// BACnet StatusFlags -- 4-bit bitstring present on most objects.
303    #[derive(Clone, Copy, PartialEq, Eq, Hash)]
304    pub struct StatusFlags: u8 {
305        /// Event_State is anything other than NORMAL.
306        const IN_ALARM = 0b1000;
307        /// Reliability is present and is not NO_FAULT_DETECTED.
308        const FAULT = 0b0100;
309        /// The value is being supplied by a local override rather than the physical input.
310        const OVERRIDDEN = 0b0010;
311        /// Out_Of_Service is TRUE.
312        const OUT_OF_SERVICE = 0b0001;
313    }
314}
315
316impl core::fmt::Display for StatusFlags {
317    /// Renders the set flags by their Clause-21 names (`IN_ALARM | FAULT`), or
318    /// `()` when none are set — never a raw number.
319    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
320        let mut first = true;
321        for (name, _) in self.iter_names() {
322            if !first {
323                f.write_str(" | ")?;
324            }
325            f.write_str(name)?;
326            first = false;
327        }
328        if first {
329            f.write_str("()")?;
330        }
331        Ok(())
332    }
333}
334
335impl core::fmt::Debug for StatusFlags {
336    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
337        core::fmt::Display::fmt(self, f)
338    }
339}
340
341// ---------------------------------------------------------------------------
342// PropertyValue -- sum type for BACnet property values
343// ---------------------------------------------------------------------------
344
345/// A BACnet application-layer value.
346///
347/// This enum covers all primitive value types that can appear as property
348/// values in BACnet objects. Constructed types (lists, sequences) are
349/// represented as nested structures. Only with `alloc`: it owns its strings
350/// and lists.
351#[cfg(feature = "alloc")]
352#[derive(Debug, Clone, PartialEq)]
353pub enum PropertyValue {
354    /// Null value.
355    Null,
356    /// Boolean value.
357    Boolean(bool),
358    /// Unsigned integer (up to 64-bit for BACnet Unsigned64).
359    Unsigned(u64),
360    /// Signed integer.
361    Signed(i32),
362    /// IEEE 754 single-precision float.
363    Real(f32),
364    /// IEEE 754 double-precision float.
365    Double(f64),
366    /// Octet string (raw bytes).
367    OctetString(Vec<u8>),
368    /// Character string (UTF-8).
369    CharacterString(String),
370    /// Bit string (variable length).
371    BitString {
372        /// Number of unused bits in the last byte.
373        unused_bits: u8,
374        /// The bit data bytes.
375        data: Vec<u8>,
376    },
377    /// Enumerated value.
378    Enumerated(u32),
379    /// Date value.
380    Date(Date),
381    /// Time value.
382    Time(Time),
383    /// Object identifier.
384    ObjectIdentifier(ObjectIdentifier),
385    /// A sequence (array) of property values.
386    ///
387    /// Used when reading an entire array property with `arrayIndex` absent
388    /// (Clause 15.5.1). Each element is encoded as its own application-tagged
389    /// value, concatenated in order.
390    List(Vec<PropertyValue>),
391    /// Raw, already-encoded application-layer bytes for a property whose wire
392    /// form is a context-tagged (CHOICE/SEQUENCE) production that the flat
393    /// application-tagged model above cannot express — e.g. a framed
394    /// `BACnetEventParameter` or a `Recipient_List` of `BACnetDestination`.
395    ///
396    /// The bytes are emitted verbatim by
397    /// `bacnet_encoding::primitives::encode_property_value` and are produced
398    /// by `decode_application_value` when it encounters a context-tagged
399    /// element; they always span exactly one complete tagged element,
400    /// including its context tag header(s). Objects that serve structured
401    /// properties decode these with the matching framed codec in
402    /// `bacnet-encoding`; a property writer that is not expecting this variant
403    /// should reject it as `INVALID_DATA_TYPE`.
404    ApplicationData(Vec<u8>),
405}
406
407#[cfg(feature = "alloc")]
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::Range(_))
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::Range(_))
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::Range(_))
668                ));
669            }
670        }
671    }
672
673    /// The static labels read as the formatted messages did (#1646).
674    #[test]
675    fn object_identifier_refusals_name_the_value() {
676        let text = |result: Result<ObjectIdentifier, Error>| result.unwrap_err().to_string();
677        assert_eq!(
678            text(ObjectIdentifier::new(ObjectType::DEVICE, 4_194_304)),
679            "value out of range: instance number 4194304 exceeds max 4194303"
680        );
681        assert_eq!(
682            text(ObjectIdentifier::new(ObjectType::from_raw(1024), 0)),
683            "value out of range: object type 1024 exceeds max 1023"
684        );
685        assert_eq!(
686            text(ObjectIdentifier::new_addressable(
687                ObjectType::DEVICE,
688                ObjectIdentifier::WILDCARD_INSTANCE
689            )),
690            "value out of range: instance number 4194303 is reserved as the wildcard value; \
691             maximum addressable instance is 4194302"
692        );
693        assert_eq!(
694            ObjectIdentifier::decode(&[0; 5]).unwrap_err().to_string(),
695            "decoding error at offset 0: ObjectIdentifier expects exactly 4 bytes, got 5"
696        );
697        assert_eq!(
698            Date::decode(&[0; 3]).unwrap_err().to_string(),
699            "decoding error at offset 0: Date expects exactly 4 bytes, got 3"
700        );
701        assert_eq!(
702            Time::decode(&[]).unwrap_err().to_string(),
703            "decoding error at offset 0: Time expects exactly 4 bytes, got 0"
704        );
705    }
706
707    #[test]
708    fn object_identifier_wire_boundaries_preserve_identity() {
709        for (raw_type, instance, wire) in [
710            (0, 0, [0, 0, 0, 0]),
711            (1023, 0, [0xff, 0xc0, 0, 0]),
712            (
713                1023,
714                ObjectIdentifier::MAX_ADDRESSABLE_INSTANCE,
715                [0xff, 0xff, 0xff, 0xfe],
716            ),
717            (
718                1023,
719                ObjectIdentifier::MAX_INSTANCE,
720                [0xff, 0xff, 0xff, 0xff],
721            ),
722        ] {
723            let object_type = ObjectType::from_raw(raw_type);
724            let oid = ObjectIdentifier::new(object_type, instance).unwrap();
725            assert_eq!(oid.encode(), wire);
726            assert_eq!(ObjectIdentifier::decode(&wire).unwrap(), oid);
727            if instance == ObjectIdentifier::WILDCARD_INSTANCE {
728                assert!(ObjectIdentifier::new_addressable(object_type, instance).is_err());
729            } else {
730                assert_eq!(
731                    ObjectIdentifier::new_addressable(object_type, instance).unwrap(),
732                    oid
733                );
734            }
735        }
736        // Raw ObjectType remains a generic selector outside ObjectIdentifier.
737        assert_eq!(ObjectType::from_raw(u32::MAX).to_raw(), u32::MAX);
738    }
739
740    #[cfg(feature = "alloc")]
741    #[test]
742    fn property_value_variants() {
743        let null = PropertyValue::Null;
744        let boolean = PropertyValue::Boolean(true);
745        let real = PropertyValue::Real(72.5);
746        let string = PropertyValue::CharacterString("test".into());
747
748        assert_eq!(null, PropertyValue::Null);
749        assert_eq!(boolean, PropertyValue::Boolean(true));
750        assert_ne!(real, PropertyValue::Real(73.0));
751        assert_eq!(string, PropertyValue::CharacterString("test".into()));
752    }
753}