mqtt_codec_kit/common/
topic_name.rs

1//! Topic name
2
3use std::{
4    borrow::{Borrow, BorrowMut},
5    io::{self, Read, Write},
6    ops::{Deref, DerefMut},
7    str::FromStr,
8};
9
10use crate::common::{Decodable, Encodable};
11
12#[inline]
13fn is_invalid_topic_name(topic_name: &str) -> bool {
14    topic_name.is_empty()
15        || topic_name.as_bytes().len() > 65535
16        || topic_name.chars().any(|ch| ch == '#' || ch == '+')
17}
18
19/// Topic name
20///
21/// [MQTT v3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718106)
22/// [MQTT v5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901241)
23#[derive(Debug, Eq, PartialEq, Clone, Hash, Ord, PartialOrd)]
24pub struct TopicName(String);
25
26impl TopicName {
27    /// Creates a new topic name from string
28    /// Return error if the string is not a valid topic name
29    pub fn new<S: Into<String>>(topic_name: S) -> Result<TopicName, TopicNameError> {
30        let topic_name = topic_name.into();
31        if is_invalid_topic_name(&topic_name) {
32            Err(TopicNameError(topic_name))
33        } else {
34            Ok(TopicName(topic_name))
35        }
36    }
37
38    /// Creates a new topic name from string without validation
39    ///
40    /// # Safety
41    ///
42    /// Topic names' syntax is defined in
43    /// [MQTT v3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718106)
44    /// [MQTT v5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901241)
45    /// Creating a name from raw string may cause errors
46    pub unsafe fn new_unchecked(topic_name: String) -> TopicName {
47        TopicName(topic_name)
48    }
49}
50
51impl From<TopicName> for String {
52    fn from(topic_name: TopicName) -> String {
53        topic_name.0
54    }
55}
56
57impl FromStr for TopicName {
58    type Err = TopicNameError;
59
60    fn from_str(s: &str) -> Result<Self, Self::Err> {
61        TopicName::new(s)
62    }
63}
64
65impl Deref for TopicName {
66    type Target = TopicNameRef;
67
68    fn deref(&self) -> &TopicNameRef {
69        unsafe { TopicNameRef::new_unchecked(&self.0) }
70    }
71}
72
73impl DerefMut for TopicName {
74    fn deref_mut(&mut self) -> &mut Self::Target {
75        unsafe { TopicNameRef::new_mut_unchecked(&mut self.0) }
76    }
77}
78
79impl Borrow<TopicNameRef> for TopicName {
80    fn borrow(&self) -> &TopicNameRef {
81        Deref::deref(self)
82    }
83}
84
85impl BorrowMut<TopicNameRef> for TopicName {
86    fn borrow_mut(&mut self) -> &mut TopicNameRef {
87        DerefMut::deref_mut(self)
88    }
89}
90
91impl Encodable for TopicName {
92    fn encode<W: Write>(&self, writer: &mut W) -> Result<(), io::Error> {
93        (&self.0[..]).encode(writer)
94    }
95
96    fn encoded_length(&self) -> u32 {
97        (&self.0[..]).encoded_length()
98    }
99}
100
101impl Decodable for TopicName {
102    type Error = TopicNameDecodeError;
103    type Cond = ();
104
105    fn decode_with<R: Read>(reader: &mut R, _rest: ()) -> Result<TopicName, TopicNameDecodeError> {
106        let topic_name = String::decode(reader)?;
107        Ok(TopicName::new(topic_name)?)
108    }
109}
110
111#[derive(Debug, thiserror::Error)]
112#[error("invalid topic filter ({0})")]
113pub struct TopicNameError(pub String);
114
115/// Errors while parsing topic names
116#[derive(Debug, thiserror::Error)]
117#[error(transparent)]
118pub enum TopicNameDecodeError {
119    IoError(#[from] io::Error),
120    InvalidTopicName(#[from] TopicNameError),
121}
122
123/// Reference to a topic name
124#[derive(Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
125#[repr(transparent)]
126pub struct TopicNameRef(str);
127
128impl TopicNameRef {
129    /// Creates a new topic name from string
130    /// Return error if the string is not a valid topic name
131    pub fn new<S: AsRef<str> + ?Sized>(topic_name: &S) -> Result<&TopicNameRef, TopicNameError> {
132        let topic_name = topic_name.as_ref();
133        if is_invalid_topic_name(topic_name) {
134            Err(TopicNameError(topic_name.to_owned()))
135        } else {
136            Ok(unsafe { &*(topic_name as *const str as *const TopicNameRef) })
137        }
138    }
139
140    /// Creates a new topic name from string
141    /// Return error if the string is not a valid topic name
142    pub fn new_mut<S: AsMut<str> + ?Sized>(
143        topic_name: &mut S,
144    ) -> Result<&mut TopicNameRef, TopicNameError> {
145        let topic_name = topic_name.as_mut();
146        if is_invalid_topic_name(topic_name) {
147            Err(TopicNameError(topic_name.to_owned()))
148        } else {
149            Ok(unsafe { &mut *(topic_name as *mut str as *mut TopicNameRef) })
150        }
151    }
152
153    /// Creates a new topic name from string without validation
154    ///
155    /// # Safety
156    ///
157    /// Topic names' syntax is defined in
158    /// [MQTT v3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718106)
159    /// [MQTT v5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901241)
160    /// Creating a name from raw string may cause errors
161    pub unsafe fn new_unchecked<S: AsRef<str> + ?Sized>(topic_name: &S) -> &TopicNameRef {
162        let topic_name = topic_name.as_ref();
163        &*(topic_name as *const str as *const TopicNameRef)
164    }
165
166    /// Creates a new topic name from string without validation
167    ///
168    /// # Safety
169    ///
170    /// Topic names' syntax is defined in
171    /// [MQTT v3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718106)
172    /// [MQTT v5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901241)
173    /// Creating a name from raw string may cause errors
174    pub unsafe fn new_mut_unchecked<S: AsMut<str> + ?Sized>(
175        topic_name: &mut S,
176    ) -> &mut TopicNameRef {
177        let topic_name = topic_name.as_mut();
178        &mut *(topic_name as *mut str as *mut TopicNameRef)
179    }
180
181    /// Check if this topic name is only for server.
182    ///
183    /// Topic names that beginning with a '$' character are reserved for servers
184    pub fn is_server_specific(&self) -> bool {
185        self.0.starts_with('$')
186    }
187}
188
189impl Deref for TopicNameRef {
190    type Target = str;
191
192    fn deref(&self) -> &str {
193        &self.0
194    }
195}
196
197impl ToOwned for TopicNameRef {
198    type Owned = TopicName;
199
200    fn to_owned(&self) -> Self::Owned {
201        TopicName(self.0.to_owned())
202    }
203}
204
205impl Encodable for TopicNameRef {
206    fn encode<W: Write>(&self, writer: &mut W) -> Result<(), io::Error> {
207        (&self.0[..]).encode(writer)
208    }
209
210    fn encoded_length(&self) -> u32 {
211        (&self.0[..]).encoded_length()
212    }
213}
214
215/// Topic name wrapper
216#[derive(Debug, Eq, PartialEq, Clone)]
217pub struct TopicNameHeader(TopicName);
218
219impl TopicNameHeader {
220    pub fn new(topic_name: String) -> Result<TopicNameHeader, TopicNameDecodeError> {
221        match TopicName::new(topic_name) {
222            Ok(h) => Ok(TopicNameHeader(h)),
223            Err(err) => Err(TopicNameDecodeError::InvalidTopicName(err)),
224        }
225    }
226}
227
228impl From<TopicNameHeader> for TopicName {
229    fn from(hdr: TopicNameHeader) -> Self {
230        hdr.0
231    }
232}
233
234impl Encodable for TopicNameHeader {
235    fn encode<W: Write>(&self, writer: &mut W) -> Result<(), io::Error> {
236        (&self.0[..]).encode(writer)
237    }
238
239    fn encoded_length(&self) -> u32 {
240        (&self.0[..]).encoded_length()
241    }
242}
243
244impl Decodable for TopicNameHeader {
245    type Error = TopicNameDecodeError;
246    type Cond = ();
247
248    fn decode_with<R: Read>(
249        reader: &mut R,
250        _rest: (),
251    ) -> Result<TopicNameHeader, TopicNameDecodeError> {
252        TopicNameHeader::new(Decodable::decode(reader)?)
253    }
254}
255
256#[cfg(test)]
257mod test {
258    use super::*;
259
260    #[test]
261    fn topic_name_sys() {
262        let topic_name = "$SYS".to_owned();
263        TopicName::new(topic_name).unwrap();
264
265        let topic_name = "$SYS/broker/connection/test.cosm-energy/state".to_owned();
266        TopicName::new(topic_name).unwrap();
267    }
268
269    #[test]
270    fn topic_name_slash() {
271        TopicName::new("/").unwrap();
272    }
273
274    #[test]
275    fn topic_name_basic() {
276        TopicName::new("/finance").unwrap();
277        TopicName::new("/finance//def").unwrap();
278    }
279}