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;