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//! Every label is a `&'static str`, and each refusal is a [`DecodeError`]
42//! that builds its text only when displayed (#1646). The readers that return
43//! only borrowed octets and numbers need no allocator; those returning a
44//! copy, a `String` or a `PropertyValue` need `alloc`.
45//!
46//! An [`Error::Decoding`] carries the [`DecodingKind`] of the fault, which a
47//! responder turns into a Reject reason (#1446):
48//!
49//! - [`DecodingKind::Missing`]: the data ends where a required member, a
50//!   tag header's remaining octets or a frame's closing tag is due, or the
51//!   tag found there shows the member was left out (see [`misplaced_kind`]);
52//! - [`DecodingKind::InvalidTag`]: any other tag that doesn't fit where it
53//!   stands, a malformed tag header, or nesting deeper than the walk takes;
54//! - [`DecodingKind::Trailing`]: octets after the last member of a value
55//!   that must fill its input ([`expect_end`]) or close its frame
56//!   ([`expect_closing`]), unless they open with a closing tag or a header
57//!   that doesn't decode, which is [`DecodingKind::InvalidTag`];
58//! - [`DecodingKind::OutOfRange`]: an Unsigned or ENUMERATED too large for
59//!   its field;
60//! - [`DecodingKind::Overflow`]: more items than a framed value takes, or a
61//!   tag length past its sanity bound;
62//! - [`DecodingKind::InvalidEncoding`]: contents whose encoding isn't valid
63//!   for their datatype, such as a fixed-size member of the wrong length,
64//!   the kind [`Error::decoding`] makes.
65//!
66//! The helpers another crate needs are public: the peeks for an optional
67//! member, a frame's opening and closing tags and its body, the context
68//! readers for contents, fixed-size contents, Unsigned or ENUMERATED values,
69//! REAL, BOOLEAN, BIT STRING, OCTET STRING, CharacterString and object
70//! identifiers, the optional-member wrapper, the application readers, the
71//! trailing-data check, and the kinds a misplaced tag is ([`misplaced_kind`],
72//! [`misplaced_tag`], [`unclosed_kind`]). The rest stay private to this
73//! crate.
74//!
75//! ```
76//! use bacnet_encoding::constructed::tagged::{decode_ctx_unsigned, expect_end, next_is_context};
77//! use bacnet_types::error::Error;
78//!
79//! // A required `[0]` Unsigned 7 and no optional `[1]` after it.
80//! let data = [0x09, 0x07];
81//! let (value, end) = decode_ctx_unsigned::<u32>(&data, 0, 0, "Example")?;
82//! assert_eq!((value, end), (7, 2));
83//! assert!(!next_is_context(&data, end, 1)?);
84//! expect_end(&data, end, end, "Example")?;
85//!
86//! // The same member announcing two contents octets but holding one.
87//! let cut = decode_ctx_unsigned::<u32>(&[0x0A, 0x07], 0, 0, "Example");
88//! assert!(matches!(cut, Err(Error::BufferTooShort { need: 3, have: 2 })));
89//! # Ok::<(), Error>(())
90//! ```
91
92#[cfg(all(feature = "alloc", not(feature = "std")))]
93use alloc::{string::String, vec::Vec};
94
95use bacnet_types::error::{DecodeError, DecodingKind, Error};
96use bacnet_types::primitives::ObjectIdentifier;
97#[cfg(feature = "alloc")]
98use bacnet_types::primitives::PropertyValue;
99
100#[cfg(feature = "alloc")]
101use super::MAX_FRAMED_ITEMS;
102use crate::primitives;
103use crate::tags::{self, MAX_CONTEXT_NESTING_DEPTH, Tag, TagClass};
104
105// ---------------------------------------------------------------------------
106// Peeking at the next tag
107// ---------------------------------------------------------------------------
108
109/// Whether the tag at `offset` passes `test`; `false` at the end of the data.
110fn next_tag_is(data: &[u8], offset: usize, test: impl FnOnce(&Tag) -> bool) -> Result<bool, Error> {
111    if offset >= data.len() {
112        return Ok(false);
113    }
114    Ok(test(&tags::decode_tag(data, offset)?.0))
115}
116
117/// Whether a primitive context tag `tag` starts at `offset`; `false` at the
118/// end of the data, so an optional member may be the last one. A malformed
119/// tag there is an error.
120pub fn next_is_context(data: &[u8], offset: usize, tag: u8) -> Result<bool, Error> {
121    next_tag_is(data, offset, |t| t.is_context(tag))
122}
123
124/// Whether an opening context tag `tag` starts at `offset`; `false` at the
125/// end of the data. A malformed tag there is an error.
126pub fn next_is_opening(data: &[u8], offset: usize, tag: u8) -> Result<bool, Error> {
127    next_tag_is(data, offset, |t| t.is_opening_tag(tag))
128}
129
130/// Whether an application tag `number` (one of [`tags::app_tag`]) starts at
131/// `offset`; `false` at the end of the data, so an optional
132/// application-tagged member may be the last one. A malformed tag there is an
133/// error.
134pub fn next_is_application(data: &[u8], offset: usize, number: u8) -> Result<bool, Error> {
135    next_tag_is(data, offset, |t| {
136        t.class == TagClass::Application && t.number == number
137    })
138}
139
140/// Whether a closing context tag `tag` starts at `offset`, for walking the
141/// items inside a frame. Unlike [`next_is_context`], running out of data is
142/// an error: the frame never closed.
143#[cfg(feature = "alloc")]
144pub(crate) fn next_is_closing(data: &[u8], offset: usize, tag: u8) -> Result<bool, Error> {
145    Ok(tags::decode_tag(data, offset)?.0.is_closing_tag(tag))
146}
147
148// ---------------------------------------------------------------------------
149// Frames and whole inputs
150// ---------------------------------------------------------------------------
151
152/// The kind of fault the tag `found`, at `at` in `data`, is where a required
153/// member is due.
154///
155/// `expected` is the member's context tag number, or `None` for an
156/// application-tagged member or a CHOICE none of whose alternatives came.
157/// The member was left out, [`DecodingKind::Missing`], when the tag closes
158/// the frame open around it, so the frame ended first, or is a context tag
159/// numbered above `expected`, since members follow in tag order. Any other
160/// tag doesn't fit there, [`DecodingKind::InvalidTag`]: a closing tag that
161/// closes no open frame (one at the top level, or of another number) among
162/// them.
163pub fn misplaced_kind(data: &[u8], at: usize, found: &Tag, expected: Option<u8>) -> DecodingKind {
164    let later = expected.is_some_and(|number| {
165        found.class == TagClass::Context && !found.is_closing && found.number > number
166    });
167    if later || (found.is_closing && closes_open_frame(data, at, found.number)) {
168        DecodingKind::Missing
169    } else {
170        DecodingKind::InvalidTag
171    }
172}
173
174/// Whether a closing tag `number` at `at` closes the innermost frame open
175/// there, found by walking the tags of `data` from its start. A walk that
176/// doesn't land on `at` (contents that aren't tags) says no, and so does one
177/// that finds more than [`MAX_CONTEXT_NESTING_DEPTH`] frames open at once.
178/// The depth counts from the start of `data`, the frames around the member's
179/// production included, so well-formed input nested less than that deep in
180/// its own production can still reach past it. Saying no is the safe
181/// answer: the tag is then [`DecodingKind::InvalidTag`] rather than
182/// [`DecodingKind::Missing`], and the input is refused either way.
183fn closes_open_frame(data: &[u8], at: usize, number: u8) -> bool {
184    let mut open = [0u8; MAX_CONTEXT_NESTING_DEPTH];
185    let mut depth = 0;
186    let mut pos = 0;
187    while pos < at {
188        let Ok((tag, next)) = tags::decode_tag(data, pos) else {
189            return false;
190        };
191        pos = if tag.is_opening {
192            let Some(slot) = open.get_mut(depth) else {
193                return false;
194            };
195            *slot = tag.number;
196            depth += 1;
197            next
198        } else if tag.is_closing {
199            if depth == 0 || open[depth - 1] != tag.number {
200                return false;
201            }
202            depth -= 1;
203            next
204        } else if tag.class == TagClass::Application && tag.number == tags::app_tag::BOOLEAN {
205            next
206        } else {
207            next.saturating_add(tag.length as usize)
208        };
209    }
210    pos == at && depth > 0 && open[depth - 1] == number
211}
212
213/// The error for the tag `found`, at `offset` in `data`, where a required
214/// member is due, of the kind [`misplaced_kind`] gives, described by
215/// `label`. [`misplaced`] gives the same fault as a [`DecodeError`], to
216/// name a context or fill the label's placeholders.
217pub fn misplaced_tag(
218    data: &[u8],
219    found: &Tag,
220    expected: Option<u8>,
221    offset: usize,
222    label: &'static str,
223) -> Error {
224    misplaced(data, found, expected, offset, label).into()
225}
226
227/// [`misplaced_tag`]'s fault as a [`DecodeError`], for a caller that adds a
228/// context or values before returning it.
229pub fn misplaced(
230    data: &[u8],
231    found: &Tag,
232    expected: Option<u8>,
233    offset: usize,
234    label: &'static str,
235) -> DecodeError {
236    DecodeError::new(misplaced_kind(data, offset, found, expected), offset, label)
237}
238
239/// The refusal of the tag `found` at `offset` where `what`'s member is due:
240/// context tag `expected`, or for `None` an application-tagged one. `label`
241/// names the member through its one placeholder, which `named` fills: the
242/// context tag's number, or the application tag's.
243#[cold]
244fn misplaced_member(
245    data: &[u8],
246    found: &Tag,
247    expected: Option<u8>,
248    offset: usize,
249    what: &'static str,
250    (label, named): (&'static str, u8),
251) -> Error {
252    DecodeError::new(misplaced_kind(data, offset, found, expected), offset, label)
253        .within(what)
254        .with_value(named.into())
255        .into()
256}
257
258/// The kind of fault the tag `found` is where a frame's closing tag is due:
259/// another closing tag is [`DecodingKind::InvalidTag`]; any other tag is a
260/// member the frame holds beyond its last one, [`DecodingKind::Trailing`].
261pub fn unclosed_kind(found: &Tag) -> DecodingKind {
262    if found.is_closing {
263        DecodingKind::InvalidTag
264    } else {
265        DecodingKind::Trailing
266    }
267}
268
269/// Require an opening context tag `tag` at `offset`; return the offset of its
270/// content.
271pub fn expect_opening(
272    data: &[u8],
273    offset: usize,
274    tag: u8,
275    what: &'static str,
276) -> Result<usize, Error> {
277    let (t, pos) = tags::decode_tag(data, offset)?;
278    if !t.is_opening_tag(tag) {
279        return Err(misplaced_member(
280            data,
281            &t,
282            Some(tag),
283            offset,
284            what,
285            ("expected opening tag [{}]", tag),
286        ));
287    }
288    Ok(pos)
289}
290
291/// Require a closing context tag `tag` at `offset`; return the offset past it.
292/// Any other tag there is of the kind [`unclosed_kind`] gives.
293pub fn expect_closing(
294    data: &[u8],
295    offset: usize,
296    tag: u8,
297    what: &'static str,
298) -> Result<usize, Error> {
299    let (t, pos) = tags::decode_tag(data, offset)?;
300    if !t.is_closing_tag(tag) {
301        return Err(
302            DecodeError::new(unclosed_kind(&t), offset, "expected closing tag [{}]")
303                .within(what)
304                .with_value(tag.into())
305                .into(),
306        );
307    }
308    Ok(pos)
309}
310
311/// Require constructed context tag `tag` at `offset`; return its body (the
312/// octets between its opening and closing tags, with nested frames balanced)
313/// and the offset past its closing tag.
314///
315/// A member inside the frame whose contents run past the end of the data is
316/// found while the frame is extracted, and is [`Error::BufferTooShort`], as
317/// it would be outside the frame. Once the body is extracted, every member
318/// in it fits.
319pub fn decode_ctx_constructed<'a>(
320    data: &'a [u8],
321    offset: usize,
322    tag: u8,
323    what: &'static str,
324) -> Result<(&'a [u8], usize), Error> {
325    let content = expect_opening(data, offset, tag, what)?;
326    tags::extract_context_value(data, content, tag)
327}
328
329/// Decode an `ABSTRACT-SYNTAX.&Type` value framed in an opening/closing
330/// context tag `tag` pair whose content starts at `content`; returns it and
331/// the offset past the closing tag.
332///
333/// One application element decodes as itself and any other count, none
334/// included, as a [`PropertyValue::List`] (an array written or read whole). A
335/// context-tagged element decodes to [`PropertyValue::ApplicationData`], so
336/// the value re-encodes to the same octets. Only with `alloc`.
337#[cfg(feature = "alloc")]
338pub(crate) fn decode_framed_value(
339    data: &[u8],
340    content: usize,
341    tag: u8,
342    what: &'static str,
343) -> Result<(PropertyValue, usize), Error> {
344    let (inner, end) = tags::extract_context_value(data, content, tag)?;
345    let body = &data[..content + inner.len()];
346    let mut values = Vec::new();
347    let mut offset = content;
348    while offset < body.len() {
349        if values.len() >= MAX_FRAMED_ITEMS {
350            return Err(DecodeError::new(
351                DecodingKind::Overflow,
352                offset,
353                "value exceeds item limit",
354            )
355            .within(what)
356            .into());
357        }
358        let (value, next) = primitives::decode_application_value(body, offset)?;
359        values.push(value);
360        offset = next;
361    }
362    let value = match <[PropertyValue; 1]>::try_from(values) {
363        Ok([value]) => value,
364        Err(values) => PropertyValue::List(values),
365    };
366    Ok((value, end))
367}
368
369/// Require a value that stopped at `end` to have used all of `data`.
370///
371/// Octets left over are [`DecodingKind::Trailing`], arguments past the
372/// value's last member, unless they open with a closing tag, which closes
373/// nothing, or with a tag header that doesn't decode: those are
374/// [`DecodingKind::InvalidTag`].
375///
376/// `at` is the offset the error reports: `end` itself when `data` is the
377/// whole input, or the frame's own offset in the larger input when `data` is
378/// a frame body sliced out of it (offsets into the slice would mislead).
379pub fn expect_end(data: &[u8], end: usize, at: usize, what: &'static str) -> Result<(), Error> {
380    if end == data.len() {
381        return Ok(());
382    }
383    let kind = match tags::decode_tag(data, end) {
384        Ok((tag, _)) if !tag.is_closing => DecodingKind::Trailing,
385        _ => DecodingKind::InvalidTag,
386    };
387    Err(DecodeError::new(kind, at, "{} trailing byte(s)")
388        .within(what)
389        .with_value(data.len().saturating_sub(end) as u64)
390        .into())
391}
392
393// ---------------------------------------------------------------------------
394// Primitive contents
395// ---------------------------------------------------------------------------
396
397/// The `length` contents octets of a primitive that start at `start`, and the
398/// offset past them; contents that run past the end of `data` are
399/// [`Error::BufferTooShort`].
400///
401/// [`tags::decode_tag`] caps a length at 1 MiB and puts `start` inside
402/// `data`, so the end can't overflow; saturating keeps that true for any
403/// caller.
404#[inline]
405pub(crate) fn contents(data: &[u8], start: usize, length: u32) -> Result<(&[u8], usize), Error> {
406    let end = usize::try_from(length).map_or(usize::MAX, |length| start.saturating_add(length));
407    if end > data.len() {
408        return Err(Error::buffer_too_short(end, data.len()));
409    }
410    Ok((&data[start..end], end))
411}
412
413/// Require a primitive context tag `tag` at `offset`; return its header and
414/// the offset of its contents. `misplaced` is the refusal's label, its `{}`
415/// the tag number.
416#[inline]
417fn ctx_header(
418    data: &[u8],
419    offset: usize,
420    tag: u8,
421    misplaced: &'static str,
422    what: &'static str,
423) -> Result<(Tag, usize), Error> {
424    let (t, start) = tags::decode_tag(data, offset)?;
425    if !t.is_context(tag) {
426        return Err(misplaced_member(
427            data,
428            &t,
429            Some(tag),
430            offset,
431            what,
432            (misplaced, tag),
433        ));
434    }
435    Ok((t, start))
436}
437
438/// Require a primitive context tag `tag` at `offset`; return its contents and
439/// the offset past them.
440#[inline]
441pub fn decode_ctx_primitive<'a>(
442    data: &'a [u8],
443    offset: usize,
444    tag: u8,
445    what: &'static str,
446) -> Result<(&'a [u8], usize), Error> {
447    let (t, start) = ctx_header(data, offset, tag, "expected context tag [{}]", what)?;
448    contents(data, start, t.length)
449}
450
451/// A fixed-size primitive type [`decode_ctx_fixed`] reads: how many contents
452/// octets it has, and the labels of its two refusals.
453///
454/// The consts cover the types the shared readers take. A codec with another
455/// one builds its own, its labels in the same form:
456///
457/// ```
458/// use bacnet_encoding::constructed::tagged::{decode_ctx_fixed, Fixed};
459///
460/// const MASK: Fixed = Fixed {
461///     octets: 4,
462///     misplaced: "expected context tag [{}] broadcast mask",
463///     wrong_length: "[{}] broadcast mask has {} contents octets, expected 4",
464/// };
465/// let refused = decode_ctx_fixed(&[0x1B, 255, 255, 0], 0, 1, MASK, "BDT entry").unwrap_err();
466/// assert_eq!(
467///     refused.to_string(),
468///     "decoding error at offset 0: BDT entry: [1] broadcast mask has 3 contents octets, expected 4"
469/// );
470/// ```
471#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
472pub struct Fixed {
473    /// The contents octets every value of the type has.
474    pub octets: u32,
475    /// The label when another tag stands where the member's is due; its `{}`
476    /// is the member's tag number.
477    pub misplaced: &'static str,
478    /// The label when the member's header announces another length; its two
479    /// `{}` are the tag number and the length announced.
480    pub wrong_length: &'static str,
481}
482
483/// [`Fixed`] for the type `$name` of `$octets` octets.
484macro_rules! fixed {
485    ($name:literal, $octets:literal) => {
486        Fixed {
487            octets: $octets,
488            misplaced: concat!("expected context tag [{}] ", $name),
489            wrong_length: concat!(
490                "[{}] ",
491                $name,
492                " has {} contents octets, expected ",
493                $octets
494            ),
495        }
496    };
497}
498
499impl Fixed {
500    /// A REAL: four octets.
501    pub const REAL: Self = fixed!("REAL", 4);
502    /// A Double: eight octets.
503    pub const DOUBLE: Self = fixed!("Double", 8);
504    /// A BOOLEAN: one octet.
505    pub const BOOLEAN: Self = fixed!("BOOLEAN", 1);
506    /// A Date: four octets.
507    pub const DATE: Self = fixed!("Date", 4);
508    /// A Time: four octets.
509    pub const TIME: Self = fixed!("Time", 4);
510    /// An object identifier: four octets.
511    pub const OBJECT_IDENTIFIER: Self = fixed!("object identifier", 4);
512}
513
514/// Require a primitive context tag `tag` at `offset` holding exactly
515/// `fixed.octets` contents octets of that type; return those contents and the
516/// offset past them. A header announcing any other length is
517/// [`Error::Decoding`] even when the data also stops early.
518#[inline]
519pub fn decode_ctx_fixed<'a>(
520    data: &'a [u8],
521    offset: usize,
522    tag: u8,
523    fixed: Fixed,
524    what: &'static str,
525) -> Result<(&'a [u8], usize), Error> {
526    let (t, start) = ctx_header(data, offset, tag, fixed.misplaced, what)?;
527    if t.length != fixed.octets {
528        return Err(ctx_length_error(offset, tag, fixed, what, t.length));
529    }
530    contents(data, start, fixed.octets)
531}
532
533/// Report a wrong fixed length before attempting to read its contents.
534#[cold]
535fn ctx_length_error(
536    offset: usize,
537    tag: u8,
538    fixed: Fixed,
539    what: &'static str,
540    length: u32,
541) -> Error {
542    DecodeError::new(DecodingKind::InvalidEncoding, offset, fixed.wrong_length)
543        .within(what)
544        .with_values(tag.into(), length.into())
545        .into()
546}
547
548/// Require a primitive context tag `tag` at `offset` holding a REAL.
549pub fn decode_ctx_real(
550    data: &[u8],
551    offset: usize,
552    tag: u8,
553    what: &'static str,
554) -> Result<(f32, usize), Error> {
555    let (octets, end) = decode_ctx_fixed(data, offset, tag, Fixed::REAL, what)?;
556    Ok((primitives::decode_real(octets)?, end))
557}
558
559/// Require a primitive context tag `tag` at `offset` holding a BOOLEAN: one
560/// contents octet, 0 or 1 (Clause 20.2.3).
561pub fn decode_ctx_boolean(
562    data: &[u8],
563    offset: usize,
564    tag: u8,
565    what: &'static str,
566) -> Result<(bool, usize), Error> {
567    let (octets, end) = decode_ctx_fixed(data, offset, tag, Fixed::BOOLEAN, what)?;
568    match octets[0] {
569        0 => Ok((false, end)),
570        1 => Ok((true, end)),
571        _ => Err(DecodeError::new(
572            DecodingKind::InvalidEncoding,
573            end - 1,
574            "[{}] BOOLEAN contents must be 0 or 1",
575        )
576        .within(what)
577        .with_value(tag.into())
578        .into()),
579    }
580}
581
582/// Require a primitive context tag `tag` at `offset` holding an object
583/// identifier, which is always four octets.
584#[inline]
585pub fn decode_ctx_object_id(
586    data: &[u8],
587    offset: usize,
588    tag: u8,
589    what: &'static str,
590) -> Result<(ObjectIdentifier, usize), Error> {
591    let (octets, end) = decode_ctx_fixed(data, offset, tag, Fixed::OBJECT_IDENTIFIER, what)?;
592    Ok((ObjectIdentifier::decode(octets)?, end))
593}
594
595/// Require a primitive context tag `tag` at `offset` holding a BIT STRING;
596/// returns its unused-bit count and a copy of its data octets. Only with
597/// `alloc`; [`decode_ctx_bit_string_ref`] borrows the octets.
598#[cfg(feature = "alloc")]
599pub fn decode_ctx_bit_string(
600    data: &[u8],
601    offset: usize,
602    tag: u8,
603    what: &'static str,
604) -> Result<((u8, Vec<u8>), usize), Error> {
605    let ((unused, bits), end) = decode_ctx_bit_string_ref(data, offset, tag, what)?;
606    Ok(((unused, bits.to_vec()), end))
607}
608
609/// Require a primitive context tag `tag` at `offset` holding a BIT STRING;
610/// returns its unused-bit count and data octets, borrowed from `data`.
611pub fn decode_ctx_bit_string_ref<'a>(
612    data: &'a [u8],
613    offset: usize,
614    tag: u8,
615    what: &'static str,
616) -> Result<((u8, &'a [u8]), usize), Error> {
617    let (t, start) = ctx_header(
618        data,
619        offset,
620        tag,
621        "expected context tag [{}] BIT STRING",
622        what,
623    )?;
624    let (octets, end) = contents(data, start, t.length)?;
625    Ok((primitives::decode_bit_string_ref(octets)?, end))
626}
627
628/// Require a primitive context tag `tag` at `offset` holding an OCTET STRING;
629/// returns a copy of its octets. Only with `alloc`;
630/// [`decode_ctx_octet_string_ref`] borrows them.
631#[cfg(feature = "alloc")]
632pub fn decode_ctx_octet_string(
633    data: &[u8],
634    offset: usize,
635    tag: u8,
636    what: &'static str,
637) -> Result<(Vec<u8>, usize), Error> {
638    let (octets, end) = decode_ctx_octet_string_ref(data, offset, tag, what)?;
639    Ok((octets.to_vec(), end))
640}
641
642/// Require a primitive context tag `tag` at `offset` holding an OCTET STRING;
643/// returns its octets, borrowed from `data`.
644pub fn decode_ctx_octet_string_ref<'a>(
645    data: &'a [u8],
646    offset: usize,
647    tag: u8,
648    what: &'static str,
649) -> Result<(&'a [u8], usize), Error> {
650    let (t, start) = ctx_header(
651        data,
652        offset,
653        tag,
654        "expected context tag [{}] OCTET STRING",
655        what,
656    )?;
657    contents(data, start, t.length)
658}
659
660/// Require a primitive context tag `tag` at `offset` holding a
661/// CharacterString. Only with `alloc`; [`decode_ctx_character_string_ref`]
662/// reads it without converting it.
663#[cfg(feature = "alloc")]
664pub fn decode_ctx_character_string(
665    data: &[u8],
666    offset: usize,
667    tag: u8,
668    what: &'static str,
669) -> Result<(String, usize), Error> {
670    let (octets, end) = decode_ctx_primitive(data, offset, tag, what)?;
671    Ok((primitives::decode_character_string(octets)?, end))
672}
673
674/// Require a primitive context tag `tag` at `offset` holding a
675/// CharacterString; returns it borrowed from `data`, its character set
676/// checked (see [`primitives::CharacterStringRef::decode`]).
677pub fn decode_ctx_character_string_ref<'a>(
678    data: &'a [u8],
679    offset: usize,
680    tag: u8,
681    what: &'static str,
682) -> Result<(primitives::CharacterStringRef<'a>, usize), Error> {
683    let (octets, end) = decode_ctx_primitive(data, offset, tag, what)?;
684    Ok((primitives::CharacterStringRef::decode(octets)?, end))
685}
686
687/// Read the optional member under primitive context tag `tag` with `decode`
688/// when that tag comes next; otherwise `None` with the offset unchanged.
689/// What `decode` returns may borrow from `data`, so [`decode_ctx_primitive`]
690/// yields the member's contents.
691pub fn decode_optional_ctx<'a, T>(
692    data: &'a [u8],
693    offset: usize,
694    tag: u8,
695    what: &'static str,
696    decode: impl FnOnce(&'a [u8], usize, u8, &'static str) -> Result<(T, usize), Error>,
697) -> Result<(Option<T>, usize), Error> {
698    if !next_is_context(data, offset, tag)? {
699        return Ok((None, offset));
700    }
701    let (value, end) = decode(data, offset, tag, what)?;
702    Ok((Some(value), end))
703}
704
705// ---------------------------------------------------------------------------
706// Unsigned and ENUMERATED
707// ---------------------------------------------------------------------------
708
709/// An unsigned integer type a decoded Unsigned or ENUMERATED is narrowed to:
710/// `u8`, `u16`, `u32` or `u64`. Sealed, so no other type implements it.
711pub trait UnsignedWidth: sealed::Sealed + TryFrom<u64> {
712    /// The type's name, for the error when a value doesn't fit it.
713    const NAME: &'static str;
714    /// The label when a context-tagged value doesn't fit: its two `{}` are
715    /// the tag number and the value.
716    const CONTEXT_EXCEEDS: &'static str;
717    /// The label when an application-tagged Unsigned doesn't fit.
718    const UNSIGNED_EXCEEDS: &'static str;
719    /// The label when an application-tagged ENUMERATED doesn't fit.
720    const ENUMERATED_EXCEEDS: &'static str;
721}
722
723/// [`UnsignedWidth`] for `$ty`.
724macro_rules! width {
725    ($ty:ident) => {
726        impl UnsignedWidth for $ty {
727            const NAME: &'static str = stringify!($ty);
728            const CONTEXT_EXCEEDS: &'static str =
729                concat!("[{}] value {} exceeds ", stringify!($ty));
730            const UNSIGNED_EXCEEDS: &'static str = concat!("Unsigned exceeds ", stringify!($ty));
731            const ENUMERATED_EXCEEDS: &'static str =
732                concat!("ENUMERATED exceeds ", stringify!($ty));
733        }
734    };
735}
736
737mod sealed {
738    /// Keeps [`UnsignedWidth`](super::UnsignedWidth) to the widths this
739    /// module implements it for.
740    pub trait Sealed {}
741
742    impl Sealed for u8 {}
743    impl Sealed for u16 {}
744    impl Sealed for u32 {}
745    impl Sealed for u64 {}
746}
747
748width!(u8);
749width!(u16);
750width!(u32);
751width!(u64);
752
753/// Narrow `value`, read from context tag `tag` at `offset`, to `T`.
754fn narrow<T: UnsignedWidth>(
755    value: u64,
756    offset: usize,
757    tag: u8,
758    what: &'static str,
759) -> Result<T, Error> {
760    T::try_from(value).map_err(|_| {
761        DecodeError::new(DecodingKind::OutOfRange, offset, T::CONTEXT_EXCEEDS)
762            .within(what)
763            .with_values(tag.into(), value)
764            .into()
765    })
766}
767
768/// Require a primitive context tag `tag` at `offset` holding an Unsigned
769/// that fits `T`. An ENUMERATED's contents are encoded the same way, so this
770/// reads those too. Leading zero octets are accepted, as
771/// [`primitives::decode_unsigned`] accepts them.
772#[inline]
773pub fn decode_ctx_unsigned<T: UnsignedWidth>(
774    data: &[u8],
775    offset: usize,
776    tag: u8,
777    what: &'static str,
778) -> Result<(T, usize), Error> {
779    let (octets, end) = decode_ctx_primitive(data, offset, tag, what)?;
780    let value = primitives::decode_unsigned(octets)?;
781    Ok((narrow(value, offset, tag, what)?, end))
782}
783
784/// [`decode_ctx_unsigned`] for a codec that must re-encode what it read octet
785/// for octet: the contents must also be the shortest encoding (see
786/// [`decode_canonical_unsigned`]).
787pub fn decode_ctx_canonical_unsigned<T: UnsignedWidth>(
788    data: &[u8],
789    offset: usize,
790    tag: u8,
791    what: &'static str,
792) -> Result<(T, usize), Error> {
793    let (octets, end) = decode_ctx_primitive(data, offset, tag, what)?;
794    let value = decode_canonical_unsigned(octets, offset, what)?;
795    Ok((narrow(value, offset, tag, what)?, end))
796}
797
798/// The value of Unsigned or ENUMERATED contents in their shortest encoding:
799/// one to eight octets, and no leading zero octet unless it is the only one.
800/// Errors report `offset`, the field's tag.
801pub fn decode_canonical_unsigned(
802    octets: &[u8],
803    offset: usize,
804    what: &'static str,
805) -> Result<u64, Error> {
806    if octets.is_empty() || octets.len() > 8 {
807        return Err(DecodeError::new(
808            DecodingKind::InvalidEncoding,
809            offset,
810            "has {} contents octets, expected one to eight",
811        )
812        .within(what)
813        .with_value(octets.len() as u64)
814        .into());
815    }
816    if octets.len() > 1 && octets[0] == 0 {
817        return Err(DecodeError::new(
818            DecodingKind::InvalidEncoding,
819            offset,
820            "must use the shortest Unsigned/Enumerated encoding",
821        )
822        .within(what)
823        .into());
824    }
825    primitives::decode_unsigned(octets)
826}
827
828// ---------------------------------------------------------------------------
829// Application-tagged items
830// ---------------------------------------------------------------------------
831
832/// Require an application tag `number` (one of [`tags::app_tag`]) at
833/// `offset`; return its contents and the offset past them.
834///
835/// A BOOLEAN is refused with [`Error::Decoding`] whatever the data holds: its
836/// value is the tag's length field, so it has no contents to return (see
837/// [`Tag::is_boolean_true`]).
838///
839/// The label is static, so a read that succeeds builds none (#1645), and a
840/// refusal builds its text only when displayed (#1646).
841///
842/// ```
843/// use bacnet_encoding::constructed::tagged::decode_app_primitive;
844/// use bacnet_encoding::tags::app_tag;
845///
846/// // A record that is an Unsigned rather than an OCTET STRING.
847/// let error = decode_app_primitive(&[0x21, 0x01], 0, app_tag::OCTET_STRING, "record").unwrap_err();
848/// assert_eq!(
849///     error.to_string(),
850///     "decoding error at offset 0: record: expected application-tagged OCTET STRING",
851/// );
852/// ```
853pub fn decode_app_primitive<'a>(
854    data: &'a [u8],
855    offset: usize,
856    number: u8,
857    what: &'static str,
858) -> Result<(&'a [u8], usize), Error> {
859    let (t, start) = app_header(data, offset, number, what)?;
860    contents(data, start, t.length)
861}
862
863/// Require an application tag `number` at `offset`; return its header and
864/// the offset of its contents. A BOOLEAN has none (see
865/// [`decode_app_primitive`]).
866fn app_header(
867    data: &[u8],
868    offset: usize,
869    number: u8,
870    what: &'static str,
871) -> Result<(Tag, usize), Error> {
872    if number == tags::app_tag::BOOLEAN {
873        return Err(DecodeError::new(
874            DecodingKind::InvalidEncoding,
875            offset,
876            "an application-tagged BOOLEAN has no contents to read",
877        )
878        .within(what)
879        .into());
880    }
881    let (t, start) = tags::decode_tag(data, offset)?;
882    if t.class != TagClass::Application || t.number != number {
883        return Err(misplaced_member(
884            data,
885            &t,
886            None,
887            offset,
888            what,
889            ("expected application-tagged {t}", number),
890        ));
891    }
892    Ok((t, start))
893}
894
895/// Require an application tag `number` at `offset` holding exactly `octets`
896/// contents octets; return them and the offset past them. As with
897/// [`decode_ctx_fixed`], a header announcing any other length is
898/// [`Error::Decoding`] even when the data also stops early.
899pub fn decode_app_fixed<'a>(
900    data: &'a [u8],
901    offset: usize,
902    number: u8,
903    octets: u32,
904    what: &'static str,
905) -> Result<(&'a [u8], usize), Error> {
906    let (t, start) = app_header(data, offset, number, what)?;
907    if t.length != octets {
908        let refused = match primitives::fixed_length_label(number) {
909            Some((label, _)) => DecodeError::new(DecodingKind::InvalidEncoding, offset, label)
910                .with_value(t.length.into()),
911            None => DecodeError::new(
912                DecodingKind::InvalidEncoding,
913                offset,
914                "application value has {} contents octets, expected {}",
915            )
916            .with_values(t.length.into(), octets.into()),
917        };
918        return Err(refused.within(what).into());
919    }
920    contents(data, start, octets)
921}
922
923/// Require an application-tagged object identifier, four contents octets,
924/// at `offset`.
925pub fn decode_app_object_id(
926    data: &[u8],
927    offset: usize,
928    what: &'static str,
929) -> Result<(ObjectIdentifier, usize), Error> {
930    let (octets, end) = decode_app_fixed(data, offset, tags::app_tag::OBJECT_IDENTIFIER, 4, what)?;
931    Ok((ObjectIdentifier::decode(octets)?, end))
932}
933
934/// Decode one application-tagged Unsigned that fits `T`. Leading zero octets
935/// are accepted.
936pub fn decode_app_unsigned<T: UnsignedWidth>(
937    data: &[u8],
938    offset: usize,
939    what: &'static str,
940) -> Result<(T, usize), Error> {
941    let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::UNSIGNED, what)?;
942    let value = primitives::decode_unsigned(octets)?;
943    Ok((
944        narrow_app(value, end - octets.len(), T::UNSIGNED_EXCEEDS, what)?,
945        end,
946    ))
947}
948
949/// Decode one application-tagged BIT STRING (`SEQUENCE OF BIT STRING` item).
950/// Only with `alloc`: it copies the data octets.
951#[cfg(feature = "alloc")]
952pub fn decode_app_bit_string(
953    data: &[u8],
954    offset: usize,
955    what: &'static str,
956) -> Result<((u8, Vec<u8>), usize), Error> {
957    let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::BIT_STRING, what)?;
958    Ok((primitives::decode_bit_string(octets)?, end))
959}
960
961/// Decode one application-tagged ENUMERATED (a `SEQUENCE OF` item, or an
962/// error class or code) that fits `T`. Leading zero octets are accepted.
963pub fn decode_app_enumerated<T: UnsignedWidth>(
964    data: &[u8],
965    offset: usize,
966    what: &'static str,
967) -> Result<(T, usize), Error> {
968    let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::ENUMERATED, what)?;
969    let value = primitives::decode_unsigned(octets)?;
970    Ok((
971        narrow_app(value, end - octets.len(), T::ENUMERATED_EXCEEDS, what)?,
972        end,
973    ))
974}
975
976/// [`decode_app_enumerated`] for a codec that must re-encode what it read
977/// octet for octet: the contents must also be the shortest encoding (see
978/// [`decode_canonical_unsigned`]).
979#[cfg(feature = "alloc")]
980pub(crate) fn decode_app_canonical_enumerated<T: UnsignedWidth>(
981    data: &[u8],
982    offset: usize,
983    what: &'static str,
984) -> Result<(T, usize), Error> {
985    let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::ENUMERATED, what)?;
986    let value = decode_canonical_unsigned(octets, offset, what)?;
987    Ok((
988        narrow_app(value, end - octets.len(), T::ENUMERATED_EXCEEDS, what)?,
989        end,
990    ))
991}
992
993/// Narrow an application-tagged value whose contents start at `contents` to
994/// `T`, `label` naming the refusal.
995fn narrow_app<T: UnsignedWidth>(
996    value: u64,
997    contents: usize,
998    label: &'static str,
999    what: &'static str,
1000) -> Result<T, Error> {
1001    T::try_from(value).map_err(|_| {
1002        DecodeError::new(DecodingKind::OutOfRange, contents, label)
1003            .within(what)
1004            .into()
1005    })
1006}
1007
1008/// Decode one application-tagged CharacterString. Only with `alloc`;
1009/// [`decode_app_character_string_ref`] reads it without converting it.
1010#[cfg(feature = "alloc")]
1011pub fn decode_app_character_string(
1012    data: &[u8],
1013    offset: usize,
1014    what: &'static str,
1015) -> Result<(String, usize), Error> {
1016    let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::CHARACTER_STRING, what)?;
1017    Ok((primitives::decode_character_string(octets)?, end))
1018}
1019
1020/// Decode one application-tagged CharacterString, borrowed from `data`, its
1021/// character set checked (see [`primitives::CharacterStringRef::decode`]).
1022pub fn decode_app_character_string_ref<'a>(
1023    data: &'a [u8],
1024    offset: usize,
1025    what: &'static str,
1026) -> Result<(primitives::CharacterStringRef<'a>, usize), Error> {
1027    let (octets, end) = decode_app_primitive(data, offset, tags::app_tag::CHARACTER_STRING, what)?;
1028    Ok((primitives::CharacterStringRef::decode(octets)?, end))
1029}
1030
1031#[cfg(all(test, feature = "alloc"))]
1032#[path = "tagged_frame_tests.rs"]
1033mod frame_tests;