Skip to main content

bacnet_encoding/primitives/
strings.rs

1//! Sink encoders for the variable-length primitives: CharacterString (UTF-8),
2//! OCTET STRING and BIT STRING (#1614).
3//!
4//! Each stages its tag header and leading octet in a few bytes on the stack
5//! and appends them with the caller's payload in one
6//! [`EncodeSink::write_parts`] call, so a refusal leaves the sink as it was.
7//! The octets are those of the `BytesMut` encoders: the header of Clause
8//! 20.2.1, then for a CharacterString the character-set octet X'00' and the
9//! UTF-8 octets (Clause 20.2.9), for a BIT STRING the unused-bit count and the
10//! data octets (Clause 20.2.10), and for an OCTET STRING the octets alone
11//! (Clause 20.2.8).
12//!
13//! A value whose contents are longer than a header's 32-bit length can say is
14//! refused with [`SinkFull`], whose `needed` is its encoded length and
15//! `available` the sink's room, or `u32::MAX` for a sink that advertises
16//! none. The CharacterString `BytesMut` encoders, and the contents-only BIT
17//! STRING one, delegate to these, a too-long CharacterString being a static
18//! [`Error::Encode`](bacnet_types::error::Error::Encode). The tagged OCTET
19//! and BIT STRING `BytesMut` encoders keep their own code: they write such a
20//! value with its length saturated instead.
21//! Only a host with more than 4 GiB in one value can meet either.
22
23use super::charset;
24use crate::sink::{EncodeSink, SinkFull};
25use crate::tags::{TagClass, app_tag, encode_tag_to_sink};
26
27/// The longest tag header [`encode_tag_to_sink`] writes.
28const MAX_HEADER: usize = 7;
29
30/// Append the header for `tag`/`class`, then `lead` and `payload`, as one
31/// value.
32fn write_tagged<S: EncodeSink + ?Sized>(
33    sink: &mut S,
34    tag: u8,
35    class: TagClass,
36    lead: &[u8],
37    payload: &[u8],
38) -> Result<usize, SinkFull> {
39    let contents = lead.len().saturating_add(payload.len());
40    let length = header_length(sink, contents)?;
41    let mut header = [0; MAX_HEADER];
42    let mut tail = &mut header[..];
43    let header_len = encode_tag_to_sink(&mut tail, tag, class, length)
44        .expect("seven octets hold any tag header");
45    sink.write_parts(&[&header[..header_len], lead, payload])?;
46    Ok(header_len + contents)
47}
48
49/// The header length for a value of `contents` octets bound for `sink`, or
50/// the refusal of one too long for a 32-bit length. It takes the length
51/// alone, so a test can reach the refusal without a 4 GiB value.
52fn header_length<S: EncodeSink + ?Sized>(sink: &S, contents: usize) -> Result<u32, SinkFull> {
53    u32::try_from(contents).map_err(|_| SinkFull {
54        needed: contents.saturating_add(MAX_HEADER),
55        available: sink.remaining_capacity().unwrap_or(u32::MAX as usize),
56    })
57}
58
59/// Append `lead` and `payload` as one value, without a tag.
60fn write_contents<S: EncodeSink + ?Sized>(
61    sink: &mut S,
62    lead: u8,
63    payload: &[u8],
64) -> Result<usize, SinkFull> {
65    sink.write_parts(&[&[lead], payload])?;
66    Ok(1 + payload.len())
67}
68
69/// Append a UTF-8 CharacterString's contents, without a tag: the
70/// character-set octet X'00', then the string's octets. Returns the octets
71/// appended.
72pub fn encode_character_string_to_sink<S: EncodeSink + ?Sized>(
73    sink: &mut S,
74    value: &str,
75) -> Result<usize, SinkFull> {
76    write_contents(sink, charset::UTF8, value.as_bytes())
77}
78
79/// Append an application-tagged UTF-8 CharacterString. Returns the octets
80/// appended.
81///
82/// ```
83/// use bacnet_encoding::primitives::encode_app_character_string_to_sink;
84///
85/// let mut storage = [0; 5];
86/// let mut tail = &mut storage[..];
87/// assert_eq!(encode_app_character_string_to_sink(&mut tail, "AV")?, 4);
88/// // Four more octets don't fit the one left: nothing is written.
89/// assert!(encode_app_character_string_to_sink(&mut tail, "AV").is_err());
90/// assert_eq!(tail.len(), 1);
91/// assert_eq!(storage, [0x73, 0x00, b'A', b'V', 0]);
92/// # Ok::<(), bacnet_encoding::sink::SinkFull>(())
93/// ```
94pub fn encode_app_character_string_to_sink<S: EncodeSink + ?Sized>(
95    sink: &mut S,
96    value: &str,
97) -> Result<usize, SinkFull> {
98    write_tagged(
99        sink,
100        app_tag::CHARACTER_STRING,
101        TagClass::Application,
102        &[charset::UTF8],
103        value.as_bytes(),
104    )
105}
106
107/// Append a UTF-8 CharacterString under context tag `tag`. Returns the
108/// octets appended.
109pub fn encode_ctx_character_string_to_sink<S: EncodeSink + ?Sized>(
110    sink: &mut S,
111    tag: u8,
112    value: &str,
113) -> Result<usize, SinkFull> {
114    write_tagged(
115        sink,
116        tag,
117        TagClass::Context,
118        &[charset::UTF8],
119        value.as_bytes(),
120    )
121}
122
123/// Append an application-tagged OCTET STRING. Returns the octets appended.
124pub fn encode_app_octet_string_to_sink<S: EncodeSink + ?Sized>(
125    sink: &mut S,
126    data: &[u8],
127) -> Result<usize, SinkFull> {
128    write_tagged(
129        sink,
130        app_tag::OCTET_STRING,
131        TagClass::Application,
132        &[],
133        data,
134    )
135}
136
137/// Append an OCTET STRING under context tag `tag`. Returns the octets
138/// appended.
139pub fn encode_ctx_octet_string_to_sink<S: EncodeSink + ?Sized>(
140    sink: &mut S,
141    tag: u8,
142    data: &[u8],
143) -> Result<usize, SinkFull> {
144    write_tagged(sink, tag, TagClass::Context, &[], data)
145}
146
147/// Append a BIT STRING's contents, without a tag: the unused-bit count, then
148/// the data octets. Returns the octets appended. Like the `BytesMut`
149/// encoder, it doesn't check the count.
150pub fn encode_bit_string_to_sink<S: EncodeSink + ?Sized>(
151    sink: &mut S,
152    unused_bits: u8,
153    data: &[u8],
154) -> Result<usize, SinkFull> {
155    write_contents(sink, unused_bits, data)
156}
157
158/// Append an application-tagged BIT STRING. Returns the octets appended.
159pub fn encode_app_bit_string_to_sink<S: EncodeSink + ?Sized>(
160    sink: &mut S,
161    unused_bits: u8,
162    data: &[u8],
163) -> Result<usize, SinkFull> {
164    write_tagged(
165        sink,
166        app_tag::BIT_STRING,
167        TagClass::Application,
168        &[unused_bits],
169        data,
170    )
171}
172
173/// Append a BIT STRING under context tag `tag`. Returns the octets appended.
174pub fn encode_ctx_bit_string_to_sink<S: EncodeSink + ?Sized>(
175    sink: &mut S,
176    tag: u8,
177    unused_bits: u8,
178    data: &[u8],
179) -> Result<usize, SinkFull> {
180    write_tagged(sink, tag, TagClass::Context, &[unused_bits], data)
181}
182
183#[cfg(test)]
184mod tests;