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;