rust_mqtt/types/string.rs
1use core::str::{Utf8Error, from_utf8, from_utf8_unchecked};
2
3use const_fn::const_fn;
4
5use crate::{
6 fmt::const_debug_assert,
7 types::{MqttBinary, TooLargeToEncode},
8};
9
10/// Error returned when creating [`MqttString`] failed.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum MqttStringError {
13 /// The passed data is not valid UTF-8.
14 Utf8Error(Utf8Error),
15
16 /// The passed data contains at least one null character.
17 NullCharacter,
18
19 /// The passed data exceeds the max length of [`MqttString::MAX_LENGTH`].
20 TooLargeToEncode,
21}
22
23#[cfg(feature = "defmt")]
24impl defmt::Format for MqttStringError {
25 fn format(&self, fmt: defmt::Formatter) {
26 match self {
27 Self::Utf8Error(e) => defmt::write!(
28 fmt,
29 "Utf8Error(Utf8Error {{ valid_up_to: {:?}, error_len: {:?} }})",
30 e.valid_up_to(),
31 e.error_len()
32 ),
33 Self::NullCharacter => defmt::write!(fmt, "NullCharacter"),
34 Self::TooLargeToEncode => defmt::write!(fmt, "TooLargeToEncode"),
35 }
36 }
37}
38impl From<Utf8Error> for MqttStringError {
39 fn from(e: Utf8Error) -> Self {
40 Self::Utf8Error(e)
41 }
42}
43impl From<TooLargeToEncode> for MqttStringError {
44 fn from(_: TooLargeToEncode) -> Self {
45 Self::TooLargeToEncode
46 }
47}
48
49/// Arbitrary UTF-8 encoded string with a length in bytes less than or equal to
50/// [`MqttString::MAX_LENGTH`] ([`u16::MAX`]) and no null characters.
51/// Exceeding this size ultimately leads to malformed packets.
52///
53/// # Examples
54///
55/// ```rust
56/// use rust_mqtt::types::{MqttBinary, MqttString, MqttStringError};
57///
58/// let bytes = [b'a'; MqttString::MAX_LENGTH];
59/// let too_long = [b'a'; MqttString::MAX_LENGTH + 1];
60/// let null_character = "hi\0there";
61///
62/// let slice = core::str::from_utf8(&bytes)?;
63/// let too_long = core::str::from_utf8(&too_long)?;
64///
65/// let b = MqttBinary::from_slice(&bytes)?;
66/// let s = MqttString::from_utf8_binary(b)?;
67/// assert_eq!(s.as_str(), slice);
68/// let b = MqttBinary::from_slice(null_character.as_bytes())?;
69/// assert_eq!(MqttString::from_utf8_binary(b).unwrap_err(), MqttStringError::NullCharacter);
70///
71/// let s = MqttString::from_str(slice)?;
72/// assert_eq!(s.as_str(), slice);
73/// assert_eq!(MqttString::from_str(too_long).unwrap_err(), MqttStringError::TooLargeToEncode);
74/// assert_eq!(MqttString::from_str(&null_character).unwrap_err(), MqttStringError::NullCharacter);
75///
76/// let s = MqttString::from_str_unchecked(slice);
77/// assert_eq!(s.as_str(), slice);
78///
79/// let b = MqttBinary::from_slice_unchecked(slice.as_bytes());
80/// let s = unsafe { MqttString::from_utf8_binary_unchecked(b) };
81/// assert_eq!(s.as_str(), slice);
82///
83/// # Ok::<(), MqttStringError>(())
84/// ```
85#[derive(Default, Clone, PartialEq, Eq)]
86pub struct MqttString<'s>(pub(crate) MqttBinary<'s>);
87
88impl core::fmt::Debug for MqttString<'_> {
89 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
90 f.debug_tuple("MqttString").field(&self.as_ref()).finish()
91 }
92}
93
94#[cfg(feature = "defmt")]
95impl<'a> defmt::Format for MqttString<'a> {
96 fn format(&self, fmt: defmt::Formatter) {
97 defmt::write!(fmt, "MqttString({:?})", self.as_ref());
98 }
99}
100
101impl<'s> TryFrom<MqttBinary<'s>> for MqttString<'s> {
102 type Error = MqttStringError;
103
104 fn try_from(value: MqttBinary<'s>) -> Result<Self, Self::Error> {
105 Self::from_utf8_binary(value)
106 }
107}
108impl<'s> TryFrom<&'s str> for MqttString<'s> {
109 type Error = MqttStringError;
110
111 fn try_from(value: &'s str) -> Result<Self, Self::Error> {
112 Self::from_str(value)
113 }
114}
115
116impl AsRef<str> for MqttString<'_> {
117 fn as_ref(&self) -> &str {
118 // Safety: MqttString contains valid UTF-8
119 unsafe { from_utf8_unchecked(self.0.as_ref()) }
120 }
121}
122
123impl<'s> MqttString<'s> {
124 /// The maximum length of a string in bytes so that it can be encoded.
125 /// This value is limited by the 2-byte length field.
126 pub const MAX_LENGTH: usize = MqttBinary::MAX_LENGTH;
127
128 /// Converts [`MqttBinary`] into [`MqttString`] by checking for null characters and valid UTF-8.
129 /// Valid length is guaranteed by [`MqttBinary`]'s invariant.
130 ///
131 /// # Errors
132 ///
133 /// * [`MqttStringError::Utf8Error`] if `b` is not valid UTF-8.
134 /// * [`MqttStringError::NullCharacter`] if `b` contains an ASCII `\0` character.
135 /// * [`MqttStringError::TooLargeToEncode`] if `b`'s length exceeds [`MqttString::MAX_LENGTH`].
136 #[const_fn(cfg(not(feature = "alloc")))]
137 pub const fn from_utf8_binary(b: MqttBinary<'s>) -> Result<Self, MqttStringError> {
138 let mut i = 0;
139 while i < b.as_bytes().len() {
140 if b.as_bytes()[i] == 0 {
141 return Err(MqttStringError::NullCharacter);
142 }
143 i += 1;
144 }
145
146 match from_utf8(b.as_bytes()) {
147 Ok(_) => Ok(Self(b)),
148 Err(e) => Err(MqttStringError::Utf8Error(e)),
149 }
150 }
151
152 /// Converts [`MqttBinary`] into [`MqttString`] without checking for null characters or valid UTF-8.
153 /// Valid length is guaranteed by [`MqttBinary`]'s invariant.
154 ///
155 /// # Safety
156 ///
157 /// The binary passed in must be valid UTF-8.
158 ///
159 /// # Invariants
160 ///
161 /// The binary data does not contain any null characters.
162 ///
163 /// # Panics
164 ///
165 /// In debug builds, this function will panic if the binary contains a null character or is not
166 /// valid UTF-8.
167 #[must_use]
168 pub const unsafe fn from_utf8_binary_unchecked(b: MqttBinary<'s>) -> Self {
169 if cfg!(debug_assertions) {
170 let mut i = 0;
171 while i < b.as_bytes().len() {
172 const_debug_assert!(b.as_bytes()[i] != 0);
173 i += 1;
174 }
175 }
176 const_debug_assert!(from_utf8(b.as_bytes()).is_ok());
177
178 Self(b)
179 }
180
181 /// Converts a string slice into [`MqttString`] by checking for null characters and the max
182 /// length of [`MqttString::MAX_LENGTH`].
183 ///
184 /// # Errors
185 ///
186 /// * [`MqttStringError::NullCharacter`] if `s` contains an ASCII `\0` character.
187 /// * [`MqttStringError::TooLargeToEncode`] if `s`' length exceeds [`MqttString::MAX_LENGTH`].
188 pub const fn from_str(s: &'s str) -> Result<Self, MqttStringError> {
189 let mut i = 0;
190 while i < s.len() {
191 if s.as_bytes()[i] == 0 {
192 return Err(MqttStringError::NullCharacter);
193 }
194 i += 1;
195 }
196
197 match s.len() {
198 ..=Self::MAX_LENGTH => Ok(Self(MqttBinary::from_slice_unchecked(s.as_bytes()))),
199 _ => Err(MqttStringError::TooLargeToEncode),
200 }
201 }
202
203 /// Converts a string slice into [`MqttString`] without checking for null characters or the max
204 /// length of [`MqttString::MAX_LENGTH`].
205 ///
206 /// # Invariants
207 ///
208 /// The length of the string slice must be less than or equal to [`MqttString::MAX_LENGTH`]. The
209 /// string must not contain any null characters.
210 ///
211 /// # Panics
212 ///
213 /// In debug builds, this function will panic if the slice contains a null character or its length is greater
214 /// than [`MqttString::MAX_LENGTH`].
215 #[must_use]
216 pub const fn from_str_unchecked(s: &'s str) -> Self {
217 if cfg!(debug_assertions) {
218 let mut i = 0;
219 while i < s.len() {
220 const_debug_assert!(s.as_bytes()[i] != 0);
221 i += 1;
222 }
223 }
224
225 Self(MqttBinary::from_slice_unchecked(s.as_bytes()))
226 }
227
228 /// Returns the length of the underlying data in bytes.
229 #[inline]
230 #[must_use]
231 pub const fn len(&self) -> u16 {
232 self.0.len()
233 }
234
235 /// Returns whether the underlying data is empty.
236 #[inline]
237 #[must_use]
238 pub const fn is_empty(&self) -> bool {
239 self.0.is_empty()
240 }
241
242 /// Returns the underlying string as `&str`
243 #[inline]
244 #[must_use]
245 pub const fn as_str(&self) -> &str {
246 // Safety: MqttString contains valid UTF-8
247 unsafe { from_utf8_unchecked(self.0.as_bytes()) }
248 }
249
250 /// Delegates to [`crate::Bytes::as_borrowed`].
251 #[inline]
252 #[must_use]
253 pub const fn as_borrowed(&'s self) -> Self {
254 Self(self.0.as_borrowed())
255 }
256}