Skip to main content

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}