1use const_fn::const_fn;
2use heapless::Vec;
3
4use crate::{
5 client::options::{RetainHandling, SubscriptionOptions},
6 eio::Write,
7 fmt::const_debug_assert,
8 io::{
9 err::WriteError,
10 write::{Writable, wlen},
11 },
12 types::MqttString,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
23#[cfg_attr(feature = "defmt", derive(defmt::Format))]
24pub struct TopicName<'t>(MqttString<'t>);
25
26impl<'t> TopicName<'t> {
27 const fn is_valid(s: &MqttString) -> bool {
28 let s = s.as_str().as_bytes();
29
30 if s.is_empty() {
33 return false;
34 }
35
36 let mut i = 0;
37
38 while i < s.len() {
39 let b = s[i];
40
41 if b == b'+' || b == b'#' {
48 return false;
49 }
50
51 i += 1;
52 }
53
54 true
55 }
56
57 #[const_fn(cfg(not(feature = "alloc")))]
59 #[must_use]
60 pub fn new(string: MqttString<'t>) -> Option<Self> {
61 if Self::is_valid(&string) {
62 Some(Self(string))
63 } else {
64 None
65 }
66 }
67
68 #[must_use]
76 pub const fn new_unchecked(string: MqttString<'t>) -> Self {
77 const_debug_assert!(
78 Self::is_valid(&string),
79 "the provided string is not valid TopicName syntax"
80 );
81
82 Self(string)
83 }
84
85 #[inline]
87 #[must_use]
88 pub const fn as_borrowed(&'t self) -> Self {
89 Self(self.0.as_borrowed())
90 }
91}
92
93impl<'t> AsRef<MqttString<'t>> for TopicName<'t> {
94 fn as_ref(&self) -> &MqttString<'t> {
95 &self.0
96 }
97}
98impl<'t> From<TopicName<'t>> for MqttString<'t> {
99 fn from(value: TopicName<'t>) -> Self {
100 value.0
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
111#[cfg_attr(feature = "defmt", derive(defmt::Format))]
112pub struct TopicFilter<'t>(MqttString<'t>);
113
114impl<'t> TopicFilter<'t> {
115 const fn is_valid(s: &MqttString) -> bool {
116 let s = s.as_str().as_bytes();
117
118 if s.is_empty() {
121 return false;
122 }
123
124 let mut i = 0;
125 let mut level_len = 0;
126 let mut level_contains_wildcard = false;
127
128 while i < s.len() {
129 let b = s[i];
130
131 if b == b'#' {
136 if i == s.len() - 1 {
140 level_contains_wildcard = true;
141 } else {
142 return false;
143 }
144 }
145
146 if b == b'+' {
147 level_contains_wildcard = true;
148 }
149
150 if b == b'/' {
151 level_len = 0;
152 level_contains_wildcard = false;
153 } else {
154 level_len += 1;
155
156 if level_len > 1 && level_contains_wildcard {
161 return false;
162 }
163 }
164
165 i += 1;
166 }
167
168 true
169 }
170
171 #[const_fn(cfg(not(feature = "alloc")))]
173 #[must_use]
174 pub fn new(string: MqttString<'t>) -> Option<Self> {
175 if Self::is_valid(&string) {
176 Some(Self(string))
177 } else {
178 None
179 }
180 }
181
182 #[must_use]
190 pub const fn new_unchecked(string: MqttString<'t>) -> Self {
191 const_debug_assert!(
192 Self::is_valid(&string),
193 "the provided string is not valid TopicFilter syntax"
194 );
195
196 Self(string)
197 }
198
199 #[inline]
201 #[must_use]
202 pub const fn as_borrowed(&'t self) -> Self {
203 Self(self.0.as_borrowed())
204 }
205}
206
207impl<'t> AsRef<MqttString<'t>> for TopicFilter<'t> {
208 fn as_ref(&self) -> &MqttString<'t> {
209 &self.0
210 }
211}
212impl<'t> From<TopicFilter<'t>> for MqttString<'t> {
213 fn from(value: TopicFilter<'t>) -> Self {
214 value.0
215 }
216}
217impl<'t> From<TopicName<'t>> for TopicFilter<'t> {
218 fn from(value: TopicName<'t>) -> Self {
219 Self(value.0)
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
224#[cfg_attr(feature = "defmt", derive(defmt::Format))]
225pub struct SubscriptionFilter<'t> {
226 topic: TopicFilter<'t>,
227 subscription_options: u8,
228}
229
230impl<const MAX_TOPIC_FILTERS: usize> Writable for Vec<SubscriptionFilter<'_>, MAX_TOPIC_FILTERS> {
231 fn written_len(&self) -> usize {
232 self.iter()
233 .map(|t| &t.topic)
234 .map(|t| t.written_len() + wlen!(u8))
235 .sum()
236 }
237
238 async fn write<W: Write>(&self, write: &mut W) -> Result<(), WriteError<W::Error>> {
239 for t in self {
240 t.topic.write(write).await?;
241 t.subscription_options.write(write).await?;
242 }
243
244 Ok(())
245 }
246}
247
248impl<'t> SubscriptionFilter<'t> {
249 pub const fn new(topic: TopicFilter<'t>, options: &SubscriptionOptions) -> Self {
250 let retain_handling_bits = match options.retain_handling {
251 RetainHandling::AlwaysSend => 0x00,
252 RetainHandling::SendIfNotSubscribedBefore => 0x10,
253 RetainHandling::NeverSend => 0x20,
254 };
255
256 let retain_as_published_bit = match options.retain_as_published {
257 true => 0x08,
258 false => 0x00,
259 };
260
261 let no_local_bit = match options.no_local {
262 true => 0x04,
263 false => 0x00,
264 };
265
266 let qos_bits = options.qos.into_bits(0);
267
268 let subscribe_options_bits =
269 retain_handling_bits | retain_as_published_bit | no_local_bit | qos_bits;
270
271 Self {
272 topic,
273 subscription_options: subscribe_options_bits,
274 }
275 }
276}
277
278#[cfg(test)]
279mod unit {
280 use tokio_test::assert_ok;
281
282 use crate::types::{MqttString, TopicFilter, TopicName};
283
284 macro_rules! assert_valid {
285 ($t:ty, $l:literal) => {
286 let s = assert_ok!(MqttString::from_str($l));
287 assert!(<$t>::new(s).is_some())
288 };
289 }
290 macro_rules! assert_invalid {
291 ($t:ty, $l:literal) => {
292 match MqttString::from_str($l) {
293 Ok(s) => assert!(<$t>::new(s).is_none()),
294 Err(_) => {}
295 }
296 };
297 }
298
299 #[test]
300 fn topic_name_zero_characters() {
301 assert_invalid!(TopicName, "");
302 }
303
304 #[test]
305 fn topic_name_null_character() {
306 assert_invalid!(TopicName, "he\0/yo");
307 }
308
309 #[test]
310 fn topic_name_with_wildcard() {
311 assert_invalid!(TopicName, "+wrong");
312 assert_invalid!(TopicName, "wro#ng");
313 assert_invalid!(TopicName, "w/r/o/n/g+");
314 assert_invalid!(TopicName, "w/r/o/+/g");
315 assert_invalid!(TopicName, "wrong/#/path");
316 assert_invalid!(TopicName, "wrong/+/path");
317 assert_invalid!(TopicName, "wrong/path/#");
318 assert_invalid!(TopicName, "#");
319 assert_invalid!(TopicName, "+");
320 }
321
322 #[test]
323 fn topic_name_valid() {
324 assert_valid!(TopicName, "/");
325 assert_valid!(TopicName, "r");
326 assert_valid!(TopicName, "right");
327 assert_valid!(TopicName, "sport/tennis/player1");
328 assert_valid!(TopicName, "sport/tennis/player1/ranking");
329 assert_valid!(TopicName, "sport/tennis/player1/score/wimbledon");
330 }
331
332 #[test]
333 fn topic_filter_zero_characters() {
334 assert_invalid!(TopicFilter, "");
335 }
336
337 #[test]
338 fn topic_filter_null_character() {
339 assert_invalid!(TopicFilter, "he\0/yo");
340 }
341
342 #[test]
343 fn topic_filter_with_invalid_wildcard() {
344 assert_invalid!(TopicFilter, "++/");
345 assert_invalid!(TopicFilter, "/++");
346
347 assert_invalid!(TopicFilter, "a+/");
348 assert_invalid!(TopicFilter, "+a/");
349 assert_invalid!(TopicFilter, "/a+/");
350 assert_invalid!(TopicFilter, "/+a/");
351 assert_invalid!(TopicFilter, "/a+");
352
353 assert_invalid!(TopicFilter, "##");
354 assert_invalid!(TopicFilter, "a#");
355 assert_invalid!(TopicFilter, "#a");
356
357 assert_invalid!(TopicFilter, "a#/");
358 assert_invalid!(TopicFilter, "#a/");
359 assert_invalid!(TopicFilter, "/a#/");
360 assert_invalid!(TopicFilter, "/#a/");
361 assert_invalid!(TopicFilter, "/a#");
362 assert_invalid!(TopicFilter, "/#a");
363
364 assert_invalid!(TopicFilter, "+wrong");
365 assert_invalid!(TopicFilter, "wro#ng");
366 assert_invalid!(TopicFilter, "w/r/o/n/g+");
367 assert_invalid!(TopicFilter, "wrong/#/path");
368 }
369
370 #[test]
371 fn topic_filter_valid() {
372 assert_valid!(TopicFilter, "#");
373 assert_valid!(TopicFilter, "/#");
374 assert_valid!(TopicFilter, "a/#");
375
376 assert_valid!(TopicFilter, "+");
377 assert_valid!(TopicFilter, "/+");
378 assert_valid!(TopicFilter, "+/");
379 assert_valid!(TopicFilter, "a/+");
380 assert_valid!(TopicFilter, "+/a");
381
382 assert_valid!(TopicFilter, "/");
383 assert_valid!(TopicFilter, "//");
384 assert_valid!(TopicFilter, "r");
385
386 assert_valid!(TopicFilter, "r/i/g/+/t");
387 assert_valid!(TopicFilter, "correct/+/path");
388 assert_valid!(TopicFilter, "right/path/#");
389 assert_valid!(TopicFilter, "right");
390 assert_valid!(TopicFilter, "sport/tennis/player1");
391 assert_valid!(TopicFilter, "sport/tennis/player1/ranking");
392 assert_valid!(TopicFilter, "sport/tennis/player1/score/wimbledon");
393 }
394}