Skip to main content

bacnet_encoding/constructed/
tagged.rs

1//! Decode helpers for tagged fields, shared by the constructed codecs, the
2//! formal Error bodies in `apdu/` and the service parameters bacnet-services
3//! decodes.
4//!
5//! Each helper reads one field at an offset and returns what it read with the
6//! offset just past it. Each takes the `what` label its codec passes (the
7//! production, sometimes narrowed to one alternative or member), so every
8//! codec words a refusal the same way:
9//!
10//! - a tag of the wrong number, class or form: `{what}: expected context tag
11//!   [n]`, `{what}: expected opening tag [n]`, `{what}: expected closing tag
12//!   [n]` or `{what}: expected application-tagged Unsigned`, reported at the
13//!   offending tag;
14//! - a fixed-size primitive of the wrong length: `{what}: [n] REAL has 5
15//!   contents octets, expected 4`;
16//! - an Unsigned or ENUMERATED too large for its field: `{what}: [n] value
17//!   70000 exceeds u16`, or `{what}: Unsigned exceeds u16` for an
18//!   application-tagged one;
19//! - bytes left after a value that must fill its input: `{what}: 2 trailing
20//!   byte(s)`.
21//!
22//! One error-kind rule holds across the crate's decoders (#1333):
23//!
24//! - Contents that run past the end of the data fail with
25//!   [`Error::BufferTooShort`], wherever the member stands: at the top level,
26//!   or inside a constructed frame, where [`tags::extract_context_value`]
27//!   finds it while [`decode_ctx_constructed`] or `decode_framed_value`
28//!   extracts the frame.
29//! - A fixed-size member has its length checked against its header before
30//!   its contents are read, so a wrong length is [`Error::Decoding`] even
31//!   when the data also stops early. That covers the context-tagged ones (an
32//!   object identifier, REAL, BOOLEAN, or any type read with
33//!   [`decode_ctx_fixed`]), the application-tagged ones read with
34//!   [`decode_app_fixed`] or [`decode_app_object_id`], and the REAL, Double,
35//!   Date, Time and object identifier that
36//!   [`primitives::decode_application_value`] reads.
37//! - Every other refusal is [`Error::Decoding`], including a tag header cut
38//!   short, which [`tags::decode_tag`] refuses, and a frame whose closing tag
39//!   never comes.
40//!
41//! An [`Error::Decoding`] carries the [`DecodingKind`] of the fault, which a
42//! responder turns into a Reject reason (#1446):
43//!
44//! - [`DecodingKind::Missing`]: the data ends where a required member, a
45//!   tag header's remaining octets or a frame's closing tag is due, or the
46//!   tag found there shows the member was left out (see [`misplaced_kind`]);
47//! - [`DecodingKind::InvalidTag`]: any other tag that doesn't fit where it
48//!   stands, a malformed tag header, or nesting deeper than the walk takes;
49//! - [`DecodingKind::Trailing`]: octets after the last member of a value
50//!   that must fill its input ([`expect_end`]) or close its frame
51//!   ([`expect_closing`]), unless they open with a closing tag or a header
52//!   that doesn't decode, which is [`DecodingKind::InvalidTag`];
53//! - [`DecodingKind::OutOfRange`]: an Unsigned or ENUMERATED too large for
54//!   its field;
55//! - [`DecodingKind::Overflow`]: more items than a framed value takes, or a
56//!   tag length past its sanity bound;
57//! - [`DecodingKind::InvalidEncoding`]: contents whose encoding isn't valid
58//!   for their datatype, such as a fixed-size member of the wrong length,
59//!   the kind [`Error::decoding`] makes.
60//!
61//! The helpers another crate needs are public: the peeks for an optional
62//! member, a frame's opening and closing tags and its body, the context
63//! readers for contents, fixed-size contents, Unsigned or ENUMERATED values,
64//! REAL, BOOLEAN, BIT STRING, OCTET STRING, CharacterString and object
65//! identifiers, the optional-member wrapper, the application readers, the
66//! trailing-data check, and the kinds a misplaced tag is ([`misplaced_kind`],
67//! [`misplaced_tag`], [`unclosed_kind`]). The rest stay private to this
68//! crate.
69//!
70//! ```
71//! use bacnet_encoding::constructed::tagged::{decode_ctx_unsigned, expect_end, next_is_context};
72//! use bacnet_types::error::Error;
73//!
74//! // A required `[0]` Unsigned 7 and no optional `[1]` after it.
75//! let data = [0x09, 0x07];
76//! let (value, end) = decode_ctx_unsigned::<u32>(&data, 0, 0, "Example")?;
77//! assert_eq!((value, end), (7, 2));
78//! assert!(!next_is_context(&data, end, 1)?);
79//! expect_end(&data, end, end, "Example")?;
80//!
81//! // The same member announcing two contents octets but holding one.
82//! let cut = decode_ctx_unsigned::<u32>(&[0x0A, 0x07], 0, 0, "Example");
83//! assert!(matches!(cut, Err(Error::BufferTooShort { need: 3, have: 2 })));
84//! # Ok::<(), Error>(())
85//! ```
86
87#[cfg(not(feature = "std"))]
88use alloc::{string::String, vec::Vec};
89use core::fmt;
90
91use bacnet_types::error::{DecodingKind, Error};
92use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
93
94use super::MAX_FRAMED_ITEMS;
95use crate::primitives;
96use crate::tags::{self, Tag, TagClass};
97
98// ---------------------------------------------------------------------------
99// Peeking at the next tag
100// ---------------------------------------------------------------------------
101
102/// Whether the tag at `offset` passes `test`; `false` at the end of the data.
103fn next_tag_is(data: &[u8], offset: usize, test: impl FnOnce(&Tag) -> bool) -> Result<bool, Error> {
104    if offset >= data.len() {
105        return Ok(false);
106    }
107    Ok(test(&tags::decode_tag(data, offset)?.0))
108}
109
110/// Whether a primitive context tag `tag` starts at `offset`; `false` at the
111/// end of the data, so an optional member may be the last one. A malformed
112/// tag there is an error.
113pub fn next_is_context(data: &[u8], offset: usize, tag: u8) -> Result<bool, Error> {
114    next_tag_is(data, offset, |t| t.is_context(tag))
115}
116
117/// Whether an opening context tag `tag` starts at `offset`; `false` at the
118/// end of the data. A malformed tag there is an error.
119pub fn next_is_opening(data: &[u8], offset: usize, tag: u8) -> Result<bool, Error> {
120    next_tag_is(data, offset, |t| t.is_opening_tag(tag))
121}
122
123/// Whether an application tag `number` (one of [`tags::app_tag`]) starts at
124/// `offset`; `false` at the end of the data, so an optional
125/// application-tagged member may be the last one. A malformed tag there is an
126/// error.
127pub fn next_is_application(data: &[u8], offset: usize, number: u8) -> Result<bool, Error> {
128    next_tag_is(data, offset, |t| {
129        t.class == TagClass::Application && t.number == number
130    })
131}
132
133/// Whether a closing context tag `tag` starts at `offset`, for walking the
134/// items inside a frame. Unlike [`next_is_context`], running out of data is
135/// an error: the frame never closed.
136pub(crate) fn next_is_closing(data: &[u8], offset: usize, tag: u8) -> Result<bool, Error> {
137    Ok(tags::decode_tag(data, offset)?.0.is_closing_tag(tag))
138}
139
140// ---------------------------------------------------------------------------
141// Frames and whole inputs
142// ---------------------------------------------------------------------------
143
144/// The kind of fault the tag `found`, at `at` in `data`, is where a required
145/// member is due.
146///
147/// `expected` is the member's context tag number, or `None` for an
148/// application-tagged member or a CHOICE none of whose alternatives came.
149/// The member was left out, [`DecodingKind::Missing`], when the tag closes
150/// the frame open around it, so the frame ended first, or is a context tag
151/// numbered above `expected`, since members follow in tag order. Any other
152/// tag doesn't fit there, [`DecodingKind::InvalidTag`]: a closing tag that
153/// closes no open frame (one at the top level, or of another number) among
154/// them.
155pub fn misplaced_kind(data: &[u8], at: usize, found: &Tag, expected: Option<u8>) -> DecodingKind {
156    let later = expected.is_some_and(|number| {
157        found.class == TagClass::Context && !found.is_closing && found.number > number
158    });
159    if later || (found.is_closing && closes_open_frame(data, at, found.number)) {
160        DecodingKind::Missing
161    } else {
162        DecodingKind::InvalidTag
163    }
164}
165
166/// Whether a closing tag `number` at `at` closes the innermost frame open
167/// there, found by walking the tags of `data` from its start. A walk that
168/// doesn't land on `at` (contents that aren't tags) says no.
169fn closes_open_frame(data: &[u8], at: usize, number: u8) -> bool {
170    let mut open = Vec::new();
171    let mut pos = 0;
172    while pos < at {
173        let Ok((tag, next)) = tags::decode_tag(data, pos) else {
174            return false;
175        };
176        pos = if tag.is_opening {
177            open.push(tag.number);
178            next
179        } else if tag.is_closing {
180            if open.pop() != Some(tag.number) {
181                return false;
182            }
183            next
184        } else if tag.class == TagClass::Application && tag.number == tags::app_tag::BOOLEAN {
185            next
186        } else {
187            next.saturating_add(tag.length as usize)
188        };
189    }
190    pos == at && open.last() == Some(&number)
191}
192
193/// The error for the tag `found`, at `offset` in `data`, where a required
194/// member is due, of the kind [`misplaced_kind`] gives.
195pub fn misplaced_tag(
196    data: &[u8],
197    found: &Tag,
198    expected: Option<u8>,
199    offset: usize,
200    message: impl Into<String>,
201) -> Error {
202    Error::decoding_kind(
203        misplaced_kind(data, offset, found, expected),
204        offset,
205        message,
206    )
207}
208
209/// The kind of fault the tag `found` is where a frame's closing tag is due:
210/// another closing tag is [`DecodingKind::InvalidTag`]; any other tag is a
211/// member the frame holds beyond its last one, [`DecodingKind::Trailing`].
212pub fn unclosed_kind(found: &Tag) -> DecodingKind {
213    if found.is_closing {
214        DecodingKind::InvalidTag
215    } else {
216        DecodingKind::Trailing
217    }
218}
219
220/// Require an opening context tag `tag` at `offset`; return the offset of its
221/// content.
222pub fn expect_opening(data: &[u8], offset: usize, tag: u8, what: &str) -> Result<usize, Error> {
223    let (t, pos) = tags::decode_tag(data, offset)?;
224    if !t.is_opening_tag(tag) {
225        return Err(misplaced_tag(
226            data,
227            &t,
228            Some(tag),
229            offset,
230            format!("{what}: expected opening tag [{tag}]"),
231        ));
232    }
233    Ok(pos)
234}
235
236/// Require a closing context tag `tag` at `offset`; return the offset past it.
237/// Any other tag there is of the kind [`unclosed_kind`] gives.
238pub fn expect_closing(data: &[u8], offset: usize, tag: u8, what: &str) -> Result<usize, Error> {
239    let (t, pos) = tags::decode_tag(data, offset)?;
240    if !t.is_closing_tag(tag) {
241        return Err(Error::decoding_kind(
242            unclosed_kind(&t),
243            offset,
244            format!("{what}: expected closing tag [{tag}]"),
245        ));
246    }
247    Ok(pos)
248}
249
250/// Require constructed context tag `tag` at `offset`; return its body (the
251/// octets between its opening and closing tags, with nested frames balanced)
252/// and the offset past its closing tag.
253///
254/// A member inside the frame whose contents run past the end of the data is
255/// found while the frame is extracted, and is [`Error::BufferTooShort`], as
256/// it would be outside the frame. Once the body is extracted, every member
257/// in it fits.
258pub fn decode_ctx_constructed<'a>(
259    data: &'a [u8],
260    offset: usize,
261    tag: u8,
262    what: &str,
263) -> Result<(&'a [u8], usize), Error> {
264    let content = expect_opening(data, offset, tag, what)?;
265    tags::extract_context_value(data, content, tag)
266}
267
268/// Decode an `ABSTRACT-SYNTAX.&Type` value framed in an opening/closing
269/// context tag `tag` pair whose content starts at `content`; returns it and
270/// the offset past the closing tag.
271///
272/// One application element decodes as itself and any other count, none
273/// included, as a [`PropertyValue::List`] (an array written or read whole). A
274/// context-tagged element decodes to [`PropertyValue::ApplicationData`], so
275/// the value re-encodes to the same octets.
276pub(crate) fn decode_framed_value(
277    data: &[u8],
278    content: usize,
279    tag: u8,
280    what: &str,
281) -> Result<(PropertyValue, usize), Error> {
282    let (inner, end) = tags::extract_context_value(data, content, tag)?;
283    let body = &data[..content + inner.len()];
284    let mut values = Vec::new();
285    let mut offset = content;
286    while offset < body.len() {
287        if values.len() >= MAX_FRAMED_ITEMS {
288            return Err(Error::overflow(
289                offset,
290                format!("{what}: value exceeds item limit"),
291            ));
292        }
293        let (value, next) = primitives::decode_application_value(body, offset)?;
294        values.push(value);
295        offset = next;
296    }
297    let value = match <[PropertyValue; 1]>::try_from(values) {
298        Ok([value]) => value,
299        Err(values) => PropertyValue::List(values),
300    };
301    Ok((value, end))
302}
303
304/// Require a value that stopped at `end` to have used all of `data`.
305///
306/// Octets left over are [`DecodingKind::Trailing`], arguments past the
307/// value's last member, unless they open with a closing tag, which closes
308/// nothing, or with a tag header that doesn't decode: those are
309/// [`DecodingKind::InvalidTag`].
310///
311/// `at` is the offset the error reports: `end` itself when `data` is the
312/// whole input, or the frame's own offset in the larger input when `data` is
313/// a frame body sliced out of it (offsets into the slice would mislead).
314pub fn expect_end(data: &[u8], end: usize, at: usize, what: &str) -> Result<(), Error> {
315    if end == data.len() {
316        return Ok(());
317    }
318    let kind = match tags::decode_tag(data, end) {
319        Ok((tag, _)) if !tag.is_closing => DecodingKind::Trailing,
320        _ => DecodingKind::InvalidTag,
321    };
322    Err(Error::decoding_kind(
323        kind,
324        at,
325        format!(
326            "{what}: {} trailing byte(s)",
327            data.len().saturating_sub(end)
328        ),
329    ))
330}
331
332// ---------------------------------------------------------------------------
333// Primitive contents
334// ---------------------------------------------------------------------------
335
336/// The `length` contents octets of a primitive that start at `start`, and the
337/// offset past them; contents that run past the end of `data` are
338/// [`Error::BufferTooShort`].
339///
340/// [`tags::decode_tag`] caps a length at 1 MiB and puts `start` inside
341/// `data`, so the end can't overflow; saturating keeps that true for any
342/// caller.
343pub(crate) fn contents(data: &[u8], start: usize, length: u32) -> Result<(&[u8], usize), Error> {
344    let end = usize::try_from(length).map_or(usize::MAX, |length| start.saturating_add(length));
345    if end > data.len() {
346        return Err(Error::buffer_too_short(end, data.len()));
347    }
348    Ok((&data[start..end], end))
349}
350
351/// Require a primitive context tag `tag` at `offset`; return its header and
352/// the offset of its contents. `kind`, unless empty, names the expected type
353/// in the error.
354fn ctx_header(
355    data: &[u8],
356    offset: usize,
357    tag: u8,
358    kind: &str,
359    what: &str,
360) -> Result<(Tag, usize), Error> {
361    let (t, start) = tags::decode_tag(data, offset)?;
362    if !t.is_context(tag) {
363        let space = if kind.is_empty() { "" } else { " " };
364        return Err(misplaced_tag(
365            data,
366            &t,
367            Some(tag),
368            offset,
369            format!("{what}: expected context tag [{tag}]{space}{kind}"),
370        ));
371    }
372    Ok((t, start))
373}
374
375/// Require a primitive context tag `tag` at `offset`; return its contents and
376/// the offset past them.
377pub fn decode_ctx_primitive<'a>(
378    data: &'a [u8],
379    offset: usize,
380    tag: u8,
381    what: &str,
382) -> Result<(&'a [u8], usize), Error> {
383    let (t, start) = ctx_header(data, offset, tag, "", what)?;
384    contents(data, start, t.length)
385}
386
387/// Require a primitive context tag `tag` at `offset` holding exactly `octets`
388/// contents octets of the type `kind` names (`"Time"`, say, for the error);
389/// return those contents and the offset past them. A header announcing any
390/// other length is [`Error::Decoding`] even when the data also stops early.
391pub fn decode_ctx_fixed<'a>(
392    data: &'a [u8],
393    offset: usize,
394    tag: u8,
395    octets: u32,
396    kind: &str,
397    what: &str,
398) -> Result<(&'a [u8], usize), Error> {
399    let (t, start) = ctx_header(data, offset, tag, kind, what)?;
400    if t.length != octets {
401        return Err(Error::decoding(
402            offset,
403            format!(
404                "{what}: [{tag}] {kind} has {} contents octets, expected {octets}",
405                t.length
406            ),
407        ));
408    }
409    contents(data, start, octets)
410}
411
412/// Require a primitive context tag `tag` at `offset` holding a REAL.
413pub fn decode_ctx_real(
414    data: &[u8],
415    offset: usize,
416    tag: u8,
417    what: &str,
418) -> Result<(f32, usize), Error> {
419    let (octets, end) = decode_ctx_fixed(data, offset, tag, 4, "REAL", what)?;
420    Ok((primitives::decode_real(octets)?, end))
421}
422
423/// Require a primitive context tag `tag` at `offset` holding a BOOLEAN: one
424/// contents octet, 0 or 1 (Clause 20.2.3).
425pub fn decode_ctx_boolean(
426    data: &[u8],
427    offset: usize,
428    tag: u8,
429    what: &str,
430) -> Result<(bool, usize), Error> {
431    let (octets, end) = decode_ctx_fixed(data, offset, tag, 1, "BOOLEAN", what)?;
432    match octets[0] {
433        0 => Ok((false, end)),
434        1 => Ok((true, end)),
435        _ => Err(Error::decoding(
436            end - 1,
437            format!("{what}: [{tag}] BOOLEAN contents must be 0 or 1"),
438        )),
439    }
440}
441
442/// Require a primitive context tag `tag` at `offset` holding an object
443/// identifier, which is always four octets.
444pub fn decode_ctx_object_id(
445    data: &[u8],
446    offset: usize,
447    tag: u8,
448    what: &str,
449) -> Result<(ObjectIdentifier, usize), Error> {
450    let (octets, end) = decode_ctx_fixed(data, offset, tag, 4, "object identifier", what)?;
451    Ok((ObjectIdentifier::decode(octets)?, end))
452}
453
454/// Require a primitive context tag `tag` at `offset` holding a BIT STRING;
455/// returns its unused-bit count and data octets.
456pub fn decode_ctx_bit_string(
457    data: &[u8],
458    offset: usize,
459    tag: u8,
460    what: &str,
461) -> Result<((u8, Vec<u8>), usize), Error> {
462    let (t, start) = ctx_header(data, offset, tag, "BIT STRING", what)?;
463    let (octets, end) = contents(data, start, t.length)?;
464    Ok((primitives::decode_bit_string(octets)?, end))
465}
466
467/// Require a primitive context tag `tag` at `offset` holding an OCTET STRING;
468/// returns a copy of its octets.
469pub fn decode_ctx_octet_string(
470    data: &[u8],
471    offset: usize,
472    tag: u8,
473    what: &str,
474) -> Result<(Vec<u8>, usize), Error> {
475    let (t, start) = ctx_header(data, offset, tag, "OCTET STRING", what)?;
476    let (octets, end) = contents(data, start, t.length)?;
477    Ok((octets.to_vec(), end))
478}
479
480/// Require a primitive context tag `tag` at `offset` holding a
481/// CharacterString.
482pub fn decode_ctx_character_string(
483    data: &[u8],
484    offset: usize,
485    tag: u8,
486    what: &str,
487) -> Result<(String, usize), Error> {
488    let (octets, end) = decode_ctx_primitive(data, offset, tag, what)?;
489    Ok((primitives::decode_character_string(octets)?, end))
490}
491
492/// Read the optional member under primitive context tag `tag` with `decode`
493/// when that tag comes next; otherwise `None` with the offset unchanged.
494/// What `decode` returns may borrow from `data`, so [`decode_ctx_primitive`]
495/// yields the member's contents.
496pub fn decode_optional_ctx<'a, T>(
497    data: &'a [u8],
498    offset: usize,
499    tag: u8,
500    what: &str,
501    decode: impl FnOnce(&'a [u8], usize, u8, &str) -> Result<(T, usize), Error>,
502) -> Result<(Option<T>, usize), Error> {
503    if !next_is_context(data, offset, tag)? {
504        return Ok((None, offset));
505    }
506    let (value, end) = decode(data, offset, tag, what)?;
507    Ok((Some(value), end))
508}
509
510// ---------------------------------------------------------------------------
511// Unsigned and ENUMERATED
512// ---------------------------------------------------------------------------
513
514/// An unsigned integer type a decoded Unsigned or ENUMERATED is narrowed to:
515/// `u8`, `u16`, `u32` or `u64`. Sealed, so no other type implements it.
516pub trait UnsignedWidth: sealed::Sealed + TryFrom<u64> {
517    /// The type's name, for the error when a value doesn't fit it.
518    const NAME: &'static str;
519}
520
521mod sealed {
522    /// Keeps [`UnsignedWidth`](super::UnsignedWidth) to the widths this
523    /// module implements it for.
524    pub trait Sealed {}
525
526    impl Sealed for u8 {}
527    impl Sealed for u16 {}
528    impl Sealed for u32 {}
529    impl Sealed for u64 {}
530}
531
532impl UnsignedWidth for u8 {
533    const NAME: &'static str = "u8";
534}
535
536impl UnsignedWidth for u16 {
537    const NAME: &'static str = "u16";
538}
539
540impl UnsignedWidth for u32 {
541    const NAME: &'static str = "u32";
542}
543
544impl UnsignedWidth for u64 {
545    const NAME: &'static str = "u64";
546}
547
548/// Narrow `value`, read from context tag `tag` at `offset`, to `T`.
549fn narrow<T: UnsignedWidth>(value: u64, offset: usize, tag: u8, what: &str) -> Result<T, Error> {
550    T::try_from(value).map_err(|_| {
551        Error::out_of_range(
552            offset,
553            format!("{what}: [{tag}] value {value} exceeds {}", T::NAME),
554        )
555    })
556}
557
558/// Require a primitive context tag `tag` at `offset` holding an Unsigned
559/// that fits `T`. An ENUMERATED's contents are encoded the same way, so this
560/// reads those too. Leading zero octets are accepted, as
561/// [`primitives::decode_unsigned`] accepts them.
562pub fn decode_ctx_unsigned<T: UnsignedWidth>(
563    data: &[u8],
564    offset: usize,
565    tag: u8,
566    what: &str,
567) -> Result<(T, usize), Error> {
568    let (octets, end) = decode_ctx_primitive(data, offset, tag, what)?;
569    let value = primitives::decode_unsigned(octets)?;
570    Ok((narrow(value, offset, tag, what)?, end))
571}
572
573/// [`decode_ctx_unsigned`] for a codec that must re-encode what it read octet
574/// for octet: the contents must also be the shortest encoding (see
575/// [`decode_canonical_unsigned`]).
576pub fn decode_ctx_canonical_unsigned<T: UnsignedWidth>(
577    data: &[u8],
578    offset: usize,
579    tag: u8,
580    what: &str,
581) -> Result<(T, usize), Error> {
582    let (octets, end) = decode_ctx_primitive(data, offset, tag, what)?;
583    let value = decode_canonical_unsigned(octets, offset, what)?;
584    Ok((narrow(value, offset, tag, what)?, end))
585}
586
587/// The value of Unsigned or ENUMERATED contents in their shortest encoding:
588/// one to eight octets, and no leading zero octet unless it is the only one.
589/// Errors report `offset`, the field's tag.
590pub fn decode_canonical_unsigned(octets: &[u8], offset: usize, what: &str) -> Result<u64, Error> {
591    if octets.is_empty() || octets.len() > 8 {
592        return Err(Error::decoding(
593            offset,
594            format!(
595                "{what}: has {} contents octets, expected one to eight",
596                octets.len()
597            ),
598        ));
599    }
600    if octets.len() > 1 && octets[0] == 0 {
601        return Err(Error::decoding(
602            offset,
603            format!("{what}: must use the shortest Unsigned/Enumerated encoding"),
604        ));
605    }
606    primitives::decode_unsigned(octets)
607}
608
609// ---------------------------------------------------------------------------
610// Application-tagged items
611// ---------------------------------------------------------------------------
612
613/// The name an error gives the type of application tag `number`.
614fn app_kind(number: u8) -> &'static str {
615    match number {
616        tags::app_tag::NULL => "NULL",
617        tags::app_tag::BOOLEAN => "BOOLEAN",
618        tags::app_tag::UNSIGNED => "Unsigned",
619        tags::app_tag::SIGNED => "INTEGER",
620        tags::app_tag::REAL => "REAL",
621        tags::app_tag::DOUBLE => "Double",
622        tags::app_tag::OCTET_STRING => "OCTET STRING",
623        tags::app_tag::CHARACTER_STRING => "CharacterString",
624        tags::app_tag::BIT_STRING => "BIT STRING",
625        tags::app_tag::ENUMERATED => "ENUMERATED",
626        tags::app_tag::DATE => "Date",
627        tags::app_tag::TIME => "Time",
628        tags::app_tag::OBJECT_IDENTIFIER => "BACnetObjectIdentifier",
629        _ => "value of a reserved type",
630    }
631}
632
633/// Require an application tag `number` (one of [`tags::app_tag`]) at
634/// `offset`; return its contents and the offset past them.
635///
636/// A BOOLEAN is refused with [`Error::Decoding`] whatever the data holds: its
637/// value is the tag's length field, so it has no contents to return (see
638/// [`Tag::is_boolean_true`]).
639///
640/// `what` may be any [`Display`](fmt::Display), not only a `&str`, for a
641/// label that carries a value, such as a list element's index: pass
642/// [`format_args!`], and the label is formatted only if an error names it,
643/// so a read that succeeds builds none (#1645).
644///
645/// ```
646/// use bacnet_encoding::constructed::tagged::decode_app_primitive;
647/// use bacnet_encoding::tags::app_tag;
648///
649/// // Two records, the second an Unsigned rather than an OCTET STRING.
650/// let data = [0x61, 0xAB, 0x21, 0x01];
651/// let mut offset = 0;
652/// let mut error = None;
653/// for i in 0..2 {
654///     match decode_app_primitive(&data, offset, app_tag::OCTET_STRING, format_args!("record {i}")) {
655///         Ok((_, end)) => offset = end,
656///         Err(e) => error = Some(e.to_string()),
657///     }
658/// }
659/// assert_eq!(
660///     error.as_deref(),
661///     Some("decoding error at offset 2: record 1: expected application-tagged OCTET STRING"),
662/// );
663/// ```
664pub fn decode_app_primitive(
665    data: &[u8],
666    offset: usize,
667    number: u8,
668    what: impl fmt::Display,
669) -> Result<(&[u8], usize), Error> {
670    let (t, start) = app_header(data, offset, number, what)?;
671    contents(data, start, t.length)
672}
673
674/// Require an application tag `number` at `offset`; return its header and
675/// the offset of its contents. A BOOLEAN has none (see
676/// [`decode_app_primitive`]).
677fn app_header(
678    data: &[u8],
679    offset: usize,
680    number: u8,
681    what: impl fmt::Display,
682) -> Result<(Tag, usize), Error> {
683    if number == tags::app_tag::BOOLEAN {
684        return Err(Error::decoding(
685            offset,
686            format!("{what}: an application-tagged BOOLEAN has no contents to read"),
687        ));
688    }
689    let (t, start) = tags::decode_tag(data, offset)?;
690    if t.class != TagClass::Application || t.number != number {
691        return Err(misplaced_tag(
692            data,
693            &t,
694            None,
695            offset,
696            format!("{what}: expected application-tagged {}", app_kind(number)),
697        ));
698    }
699    Ok((t, start))
700}
701
702/// Require an application tag `number` at `offset` holding exactly `octets`
703/// contents octets; return them and the offset past them. As with
704/// [`decode_ctx_fixed`], a header announcing any other length is
705/// [`Error::Decoding`] even when the data also stops early.
706pub fn decode_app_fixed<'a>(
707    data: &'a [u8],
708    offset: usize,
709    number: u8,
710    octets: u32,
711    what: &str,
712) -> Result<(&'a [u8], usize), Error> {
713    let (t, start) = app_header(data, offset, number, what)?;
714    if t.length != octets {
715        return Err(Error::decoding(
716            offset,
717            format!(
718                "{what}: {} has {} contents octets, expected {octets}",
719                app_kind(number),
720                t.length
721            ),
722        ));
723    }
724    contents(data, start, octets)
725}
726
727/// Require an application-tagged object identifier, four contents octets,
728/// at `offset`.
729pub fn decode_app_object_id(
730    data: &[u8],
731    offset: usize,
732    what: &str,
733) -> Result<(ObjectIdentifier, usize), Error> {
734    let (octets, end) = decode_app_fixed(data, offset, tags::app_tag::OBJECT_IDENTIFIER, 4, what)?;
735    Ok((ObjectIdentifier::decode(octets)?, end))
736}
737
738/// Decode one application-tagged Unsigned that fits `T`. Leading zero octets
739/// are accepted.
740pub fn decode_app_unsigned<T: UnsignedWidth>(
741    data: &[u8],
742    offset: usize,
743    what: &str,
744) -> Result<(T, usize), Error> {
745    let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::UNSIGNED, what)?;
746    let value = primitives::decode_unsigned(octets)?;
747    Ok((
748        narrow_app(value, end - octets.len(), "Unsigned", what)?,
749        end,
750    ))
751}
752
753/// Decode one application-tagged BIT STRING (`SEQUENCE OF BIT STRING` item).
754pub fn decode_app_bit_string(
755    data: &[u8],
756    offset: usize,
757    what: &str,
758) -> Result<((u8, Vec<u8>), usize), Error> {
759    let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::BIT_STRING, what)?;
760    Ok((primitives::decode_bit_string(octets)?, end))
761}
762
763/// Decode one application-tagged ENUMERATED (a `SEQUENCE OF` item, or an
764/// error class or code) that fits `T`. Leading zero octets are accepted.
765pub fn decode_app_enumerated<T: UnsignedWidth>(
766    data: &[u8],
767    offset: usize,
768    what: &str,
769) -> Result<(T, usize), Error> {
770    let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::ENUMERATED, what)?;
771    let value = primitives::decode_unsigned(octets)?;
772    Ok((
773        narrow_app(value, end - octets.len(), "ENUMERATED", what)?,
774        end,
775    ))
776}
777
778/// [`decode_app_enumerated`] for a codec that must re-encode what it read
779/// octet for octet: the contents must also be the shortest encoding (see
780/// [`decode_canonical_unsigned`]).
781pub(crate) fn decode_app_canonical_enumerated<T: UnsignedWidth>(
782    data: &[u8],
783    offset: usize,
784    what: &str,
785) -> Result<(T, usize), Error> {
786    let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::ENUMERATED, what)?;
787    let value = decode_canonical_unsigned(octets, offset, what)?;
788    Ok((
789        narrow_app(value, end - octets.len(), "ENUMERATED", what)?,
790        end,
791    ))
792}
793
794/// Narrow an application-tagged `kind` value whose contents start at
795/// `contents` to `T`.
796fn narrow_app<T: UnsignedWidth>(
797    value: u64,
798    contents: usize,
799    kind: &str,
800    what: &str,
801) -> Result<T, Error> {
802    T::try_from(value)
803        .map_err(|_| Error::out_of_range(contents, format!("{what}: {kind} exceeds {}", T::NAME)))
804}
805
806/// Decode one application-tagged CharacterString.
807pub fn decode_app_character_string(
808    data: &[u8],
809    offset: usize,
810    what: &str,
811) -> Result<(String, usize), Error> {
812    let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::CHARACTER_STRING, what)?;
813    Ok((primitives::decode_character_string(octets)?, end))
814}
815
816#[cfg(test)]
817#[path = "tagged_frame_tests.rs"]
818mod frame_tests;