Skip to main content

bacnet_encoding/primitives/
view.rs

1//! Borrowed views of CharacterString contents and application-tagged values,
2//! which read the octets in place instead of copying them (#1614).
3
4use bacnet_types::error::{DecodeError, DecodingKind, Error};
5use bacnet_types::primitives::{Date, ObjectIdentifier, Time};
6
7use super::{
8    charset, decode_bit_string_ref, decode_double, decode_real, decode_signed, decode_unsigned,
9    fixed_length_label, invalid_utf8, unknown_charset,
10};
11use crate::tags::{self, Tag, TagClass, app_tag};
12
13/// A CharacterString's contents, borrowed from the octets they were decoded
14/// from: the character-set octet, then the string in that character set
15/// (Clause 20.2.9).
16///
17/// [`decode`](Self::decode) checks the contents' structure as every decoder
18/// of a CharacterString does (#1778): a character set the clause defines,
19/// valid UTF-8, a DBCS string's two-octet code page, and whole UCS-2 and
20/// UCS-4 characters that are Unicode scalar values. So
21/// [`as_str`](Self::as_str) borrows UTF-8 contents as a `&str`; contents in
22/// another character set are carried as they are, for the caller to convert.
23///
24/// ```
25/// use bacnet_encoding::primitives::{charset, CharacterStringRef};
26///
27/// let text = CharacterStringRef::decode(&[charset::UTF8, b'A', b'V'])?;
28/// assert_eq!((text.charset(), text.as_str()), (charset::UTF8, Some("AV")));
29///
30/// let latin1 = CharacterStringRef::decode(&[charset::ISO_8859_1, 0xE9])?;
31/// assert_eq!((latin1.as_str(), latin1.bytes()), (None, &[0xE9][..]));
32/// # Ok::<(), bacnet_types::error::Error>(())
33/// ```
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub struct CharacterStringRef<'a> {
36    /// The character-set octet and the string after it; never empty once
37    /// decoded.
38    contents: &'a [u8],
39}
40
41impl<'a> CharacterStringRef<'a> {
42    /// Read CharacterString contents, refusing the malformed structures the
43    /// type's docs list, each [`DecodingKind::InvalidEncoding`] at an offset
44    /// into `contents`: no or a reserved character-set octet, invalid UTF-8,
45    /// a DBCS string without its code page, and broken UCS-2 or UCS-4.
46    pub fn decode(contents: &'a [u8]) -> Result<Self, Error> {
47        check_character_string(contents)?;
48        Ok(Self { contents })
49    }
50
51    /// Contents taken as they are, for a decoder that checks them itself.
52    pub(super) fn unchecked(contents: &'a [u8]) -> Self {
53        Self { contents }
54    }
55
56    /// The character set, one of [`charset`].
57    pub fn charset(&self) -> u8 {
58        self.contents.first().copied().unwrap_or(charset::UTF8)
59    }
60
61    /// The string's octets in its character set, after the character-set
62    /// octet.
63    pub fn bytes(&self) -> &'a [u8] {
64        self.contents.get(1..).unwrap_or_default()
65    }
66
67    /// The complete contents: the character-set octet and the string.
68    pub fn contents(&self) -> &'a [u8] {
69        self.contents
70    }
71
72    /// The string, when it is UTF-8; `None` for any other character set.
73    pub fn as_str(&self) -> Option<&'a str> {
74        if self.charset() != charset::UTF8 {
75            return None;
76        }
77        core::str::from_utf8(self.bytes()).ok()
78    }
79}
80
81/// Check CharacterString `contents`, the character-set octet and the string
82/// after it (Clause 20.2.9). The one check the borrowed view and the owned
83/// validator share, so they can't drift apart (#1778). It refuses, each as
84/// [`DecodingKind::InvalidEncoding`] at an offset into `contents`:
85///
86/// - no character-set octet, or one the clause doesn't define;
87/// - UTF-8 contents that aren't valid UTF-8;
88/// - an IBM/Microsoft DBCS string without its two-octet code page;
89/// - UCS-2 or UCS-4 contents that aren't whole two- or four-octet
90///   characters, or hold one that isn't a Unicode scalar value.
91///
92/// JIS X 0208 and ISO 8859-1 contents are taken as they are.
93pub(super) fn check_character_string(contents: &[u8]) -> Result<(), Error> {
94    let Some((&charset_id, payload)) = contents.split_first() else {
95        return Err(Error::decoding(
96            0,
97            "CharacterString requires at least 1 byte for charset",
98        ));
99    };
100    match charset_id {
101        charset::UTF8 => {
102            core::str::from_utf8(payload).map_err(invalid_utf8)?;
103        }
104        charset::IBM_MICROSOFT_DBCS if payload.len() < 2 => {
105            return Err(Error::decoding(
106                1,
107                "DBCS CharacterString requires a two-octet code page",
108            ));
109        }
110        charset::UCS2 => {
111            let (characters, rest) = payload.as_chunks::<2>();
112            if !rest.is_empty() {
113                return Err(Error::decoding(1, "UCS-2 data must have even length"));
114            }
115            let not_scalar = |c: &[u8; 2]| char::from_u32(u16::from_be_bytes(*c).into()).is_none();
116            if let Some(index) = characters.iter().position(not_scalar) {
117                return Err(Error::decoding(1 + index * 2, "invalid UCS-2 code point"));
118            }
119        }
120        charset::UCS4 => {
121            let (characters, rest) = payload.as_chunks::<4>();
122            if !rest.is_empty() {
123                return Err(Error::decoding(
124                    1,
125                    "UCS-4 CharacterString length must be a multiple of four",
126                ));
127            }
128            let not_scalar = |c: &[u8; 4]| char::from_u32(u32::from_be_bytes(*c)).is_none();
129            if let Some(index) = characters.iter().position(not_scalar) {
130                return Err(Error::decoding(1 + index * 4, "invalid UCS-4 code point"));
131            }
132        }
133        charset::IBM_MICROSOFT_DBCS | charset::JIS_X_0208 | charset::ISO_8859_1 => {}
134        other => return Err(unknown_charset(other)),
135    }
136    Ok(())
137}
138
139/// One application-tagged value of a primitive datatype (Clause 20.2.2 to
140/// 20.2.14), borrowed from the octets it was decoded from.
141///
142/// [`decode_application_value_ref`] reads one; strings, octet strings and bit
143/// strings stay in place.
144#[derive(Debug, Clone, Copy, PartialEq)]
145pub enum ApplicationValueRef<'a> {
146    /// Null.
147    Null,
148    /// Boolean.
149    Boolean(bool),
150    /// Unsigned integer, up to 64 bits.
151    Unsigned(u64),
152    /// Signed integer.
153    Signed(i32),
154    /// IEEE 754 single-precision float.
155    Real(f32),
156    /// IEEE 754 double-precision float.
157    Double(f64),
158    /// Octet string.
159    OctetString(&'a [u8]),
160    /// Character string.
161    CharacterString(CharacterStringRef<'a>),
162    /// Bit string.
163    BitString {
164        /// How many trailing bits of the final data octet carry nothing.
165        unused_bits: u8,
166        /// The bit data octets.
167        data: &'a [u8],
168    },
169    /// Enumerated value.
170    Enumerated(u32),
171    /// Date.
172    Date(Date),
173    /// Time.
174    Time(Time),
175    /// Object identifier.
176    ObjectIdentifier(ObjectIdentifier),
177}
178
179/// Decode one application-tagged primitive value from `data` at `offset`,
180/// without copying it; returns the value and the offset past it.
181///
182/// It refuses what the owned decoder, `decode_application_value`, refuses
183/// in an application-tagged value: a fixed-size value whose header gives
184/// another length is [`Error::Decoding`], contents that run past the end of
185/// `data` are [`Error::BufferTooShort`]. Every offset it reports indexes
186/// `data`, a fault inside the contents included, where the owned decoder
187/// reports one of those from the contents' start. It also refuses a
188/// context-tagged element, which only the owned decoder keeps (as its raw
189/// octets), with [`DecodingKind::InvalidTag`]. A CharacterString is checked
190/// as [`CharacterStringRef::decode`] checks it, so one in a character set the
191/// owned decoder can't convert still reads here.
192///
193/// ```
194/// use bacnet_encoding::primitives::{decode_application_value_ref, ApplicationValueRef};
195///
196/// // A REAL 72.5, then a CharacterString "AV".
197/// let data = [0x44, 0x42, 0x91, 0x00, 0x00, 0x73, 0x00, b'A', b'V'];
198/// let (real, next) = decode_application_value_ref(&data, 0)?;
199/// assert_eq!(real, ApplicationValueRef::Real(72.5));
200/// let (text, end) = decode_application_value_ref(&data, next)?;
201/// let ApplicationValueRef::CharacterString(text) = text else { unreachable!() };
202/// assert_eq!((text.as_str(), end), (Some("AV"), data.len()));
203/// # Ok::<(), bacnet_types::error::Error>(())
204/// ```
205pub fn decode_application_value_ref(
206    data: &[u8],
207    offset: usize,
208) -> Result<(ApplicationValueRef<'_>, usize), Error> {
209    let (tag, content_start) = tags::decode_tag(data, offset)?;
210    if tag.class == TagClass::Context {
211        return Err(Error::invalid_tag(
212            offset,
213            "expected an application-tagged value, found a context tag",
214        ));
215    }
216    let (value, end) = decode_application_primitive::<true>(data, offset, tag, content_start)?;
217    if let ApplicationValueRef::CharacterString(text) = value {
218        let checked =
219            CharacterStringRef::decode(text.contents()).map_err(rebased(content_start))?;
220        return Ok((ApplicationValueRef::CharacterString(checked), end));
221    }
222    Ok((value, end))
223}
224
225/// A refusal of contents that start at `base` in the data, its offset moved
226/// from the contents' start to the data's.
227pub(super) fn rebased(base: usize) -> impl Fn(Error) -> Error {
228    move |error| match error {
229        Error::Decoding(mut refused) => {
230            refused.offset = refused.offset.saturating_add(base);
231            refused.into()
232        }
233        other => other,
234    }
235}
236
237/// Decode the application-tagged value whose header `tag`, at `offset`, has
238/// its contents at `content_start`. A CharacterString comes back unchecked:
239/// the owned decoder converts it, and the view checks it. A fault inside the
240/// contents is reported at its offset in `data` when `IN_DATA`, and from the
241/// contents' start otherwise, as the owned decoder always has. A const
242/// parameter, so the owned decoder's code carries no offset arithmetic.
243pub(super) fn decode_application_primitive<const IN_DATA: bool>(
244    data: &[u8],
245    offset: usize,
246    tag: Tag,
247    content_start: usize,
248) -> Result<(ApplicationValueRef<'_>, usize), Error> {
249    if tag.is_opening || tag.is_closing {
250        return Err(Error::invalid_tag(offset, "unexpected opening/closing tag"));
251    }
252    if tag.number == app_tag::NULL && tag.length != 0 {
253        return Err(Error::decoding(
254            offset,
255            "application NULL must have no contents",
256        ));
257    }
258    if tag.number == app_tag::BOOLEAN && tag.length > 1 {
259        return Err(Error::decoding(
260            offset,
261            "application BOOLEAN L/V/T must be 0 or 1",
262        ));
263    }
264    let content_end = content_start
265        .checked_add(tag.length as usize)
266        .ok_or_else(|| Error::decoding(content_start, "length overflow"))?;
267    if tag.number == app_tag::BOOLEAN {
268        return Ok((ApplicationValueRef::Boolean(tag.length != 0), content_start));
269    }
270    // A fixed-size value's length is judged before its contents are read, so
271    // a wrong length is reported as such even when the data also stops early.
272    if let Some((label, octets)) = fixed_length_label(tag.number)
273        && tag.length != octets
274    {
275        return Err(
276            DecodeError::new(DecodingKind::InvalidEncoding, offset, label)
277                .with_value(tag.length.into())
278                .into(),
279        );
280    }
281    if data.len() < content_end {
282        return Err(Error::buffer_too_short(content_end, data.len()));
283    }
284    let content = &data[content_start..content_end];
285    let at = |error| {
286        if IN_DATA {
287            rebased(content_start)(error)
288        } else {
289            error
290        }
291    };
292    let value = match tag.number {
293        app_tag::NULL => ApplicationValueRef::Null,
294        app_tag::UNSIGNED => ApplicationValueRef::Unsigned(decode_unsigned(content).map_err(at)?),
295        app_tag::SIGNED => ApplicationValueRef::Signed(decode_signed(content).map_err(at)?),
296        app_tag::REAL => ApplicationValueRef::Real(decode_real(content).map_err(at)?),
297        app_tag::DOUBLE => ApplicationValueRef::Double(decode_double(content).map_err(at)?),
298        app_tag::OCTET_STRING => ApplicationValueRef::OctetString(content),
299        app_tag::CHARACTER_STRING => {
300            ApplicationValueRef::CharacterString(CharacterStringRef::unchecked(content))
301        }
302        app_tag::BIT_STRING => {
303            let (unused_bits, data) = decode_bit_string_ref(content).map_err(at)?;
304            ApplicationValueRef::BitString { unused_bits, data }
305        }
306        app_tag::ENUMERATED => {
307            let value = u32::try_from(decode_unsigned(content).map_err(at)?)
308                .map_err(|_| Error::out_of_range(content_start, "ENUMERATED exceeds u32"))?;
309            ApplicationValueRef::Enumerated(value)
310        }
311        app_tag::DATE => ApplicationValueRef::Date(Date::decode(content).map_err(at)?),
312        app_tag::TIME => ApplicationValueRef::Time(Time::decode(content).map_err(at)?),
313        app_tag::OBJECT_IDENTIFIER => {
314            ApplicationValueRef::ObjectIdentifier(ObjectIdentifier::decode(content).map_err(at)?)
315        }
316        other => {
317            return Err(DecodeError::new(
318                DecodingKind::InvalidTag,
319                offset,
320                "unknown application tag number {}",
321            )
322            .with_value(other.into())
323            .into());
324        }
325    };
326    Ok((value, content_end))
327}
328
329#[cfg(test)]
330mod tests;