mqute_codec/protocol/util.rs
1//! # MQTT Protocol Utilities
2//!
3//! This module provides utility functions for MQTT protocol handling, including:
4//! - Variable byte integer length calculation
5//! - Topic name and filter validation
6//! - System topic detection
7//!
8//! These utilities are used throughout the codec for validating MQTT-specific
9//! data structures and ensuring protocol compliance.
10//!
11
12/// Calculates the number of bytes required to encode a length value using MQTT's
13/// variable byte integer encoding format.
14///
15/// MQTT uses a variable-length encoding scheme for remaining length values where
16/// each byte encodes 7 bits of data with the most significant bit indicating
17/// continuation. This function determines how many bytes are needed to encode
18/// a given length value.
19///
20/// # Panics
21///
22/// Panics if the length exceeds the maximum allowed by MQTT specification
23/// (268,435,455 bytes or ~256 MB).
24///
25/// # MQTT Specification Reference
26///
27/// This implements the variable byte integer encoding from MQTT specification
28/// section 1.5.5.
29///
30/// # Example
31///
32/// ```
33/// use mqute_codec::protocol::util;
34///
35/// assert_eq!(util::len_bytes(127), 1); // Fits in 1 byte
36/// assert_eq!(util::len_bytes(128), 2); // Requires 2 bytes
37/// assert_eq!(util::len_bytes(16383), 2); // Maximum for 2 bytes
38/// assert_eq!(util::len_bytes(16384), 3); // Requires 3 bytes
39/// ```
40#[inline]
41pub fn len_bytes(len: usize) -> usize {
42 if len < 128 {
43 1
44 } else if len < 16_384 {
45 2
46 } else if len < 2_097_152 {
47 3
48 } else if len < 268_435_456 {
49 4
50 } else {
51 panic!("Length of remaining bytes must be less than 28 bits")
52 }
53}
54
55/// Validates whether a string is a valid MQTT topic name.
56///
57/// MQTT topic names must follow specific rules:
58/// - Must not be empty
59/// - Maximum length of 65,535 UTF-8 encoded bytes
60/// - Must not contain null characters
61/// - Must not contain wildcards (`+` or `#`)
62/// - Can contain any other UTF-8 characters including `/` for hierarchy
63///
64/// # MQTT Specification Reference
65///
66/// Follows MQTT specification rules for topic names (section 4.7).
67///
68/// # Example
69///
70/// ```
71/// use mqute_codec::protocol::util;
72///
73/// assert!(util::is_valid_topic_name("sensors/temperature"));
74/// assert!(util::is_valid_topic_name("$SYS/monitor"));
75/// assert!(!util::is_valid_topic_name("sensors/+")); // Contains wildcard
76/// assert!(!util::is_valid_topic_name("")); // Empty
77/// ```
78pub fn is_valid_topic_name<T: AsRef<str>>(name: T) -> bool {
79 let name = name.as_ref();
80
81 // Check minimum length and UTF-8 encoding length
82 if name.is_empty() || name.len() > 65_535 {
83 return false;
84 }
85
86 // Check for null character and wildcards (not allowed in topic names)
87 if name.contains('\0') || name.contains('#') || name.contains('+') {
88 return false;
89 }
90
91 true
92}
93
94/// Validates whether a string is a valid MQTT topic filter.
95///
96/// MQTT topic filters are used in subscriptions and can include wildcards:
97/// - `+` (single-level wildcard) - matches one hierarchy level
98/// - `#` (multi-level wildcard) - matches zero or more hierarchy levels
99///
100/// Validation rules:
101/// - Must not be empty
102/// - Maximum length of 65,535 UTF-8 encoded bytes
103/// - Must not contain null characters
104/// - Multi-level wildcard (`#`) must be the last character if present
105/// - Multi-level wildcard must be preceded by `/` unless it's the only character
106/// - Single-level wildcard (`+`) must occupy entire hierarchy levels
107///
108/// # MQTT Specification Reference
109///
110/// Follows MQTT specification rules for topic filters (section 4.7).
111///
112/// # Example
113///
114/// ```
115/// use mqute_codec::protocol::util;
116///
117/// assert!(util::is_valid_topic_filter("sensors/+/temperature"));
118/// assert!(util::is_valid_topic_filter("sensors/#"));
119/// assert!(util::is_valid_topic_filter("sensors/+/temperature/#"));
120/// assert!(!util::is_valid_topic_filter("sensors/temperature/#/ranking"));
121/// assert!(!util::is_valid_topic_filter("sensors+"));
122/// ```
123pub fn is_valid_topic_filter<T: AsRef<str>>(filter: T) -> bool {
124 let filter = filter.as_ref();
125
126 // Check minimum length and UTF-8 encoding length
127 if filter.is_empty() || filter.len() > 65_535 {
128 return false;
129 }
130
131 // Check for null character
132 if filter.contains('\0') {
133 return false;
134 }
135
136 // Multi-level wildcard validation
137 if let Some(pos) = filter.find('#') {
138 // Multi-level wildcard must be last character
139 if pos != filter.len() - 1 {
140 return false;
141 }
142
143 // Multi-level wildcard must be preceded by separator or be alone
144 if filter.len() > 1 {
145 let preceding_char = filter.chars().nth(pos - 1).unwrap();
146 if preceding_char != '/' {
147 return false;
148 }
149 }
150
151 // Check if # appears anywhere else
152 if filter.matches('#').count() > 1 {
153 return false;
154 }
155 }
156
157 // Single-level wildcard validation
158 if filter.contains('+') {
159 // Split by levels to check each segment
160 let levels: Vec<&str> = filter.split('/').collect();
161 for level in levels {
162 if level.contains('+') && level != "+" {
163 return false;
164 }
165 }
166 }
167
168 true
169}
170
171/// Determines if a topic name represents a system topic.
172///
173/// MQTT system topics are reserved for broker-specific functionality and
174/// typically start with the `$` character. Clients should generally avoid
175/// publishing to system topics unless specifically documented by the broker.
176///
177/// # Example
178///
179/// ```
180/// use mqute_codec::protocol::util;
181///
182/// assert!(util::is_system_topic("$SYS/monitor"));
183/// assert!(!util::is_system_topic("sensors/temperature"));
184/// ```
185pub fn is_system_topic<T: AsRef<str>>(topic: T) -> bool {
186 topic.as_ref().starts_with('$')
187}
188
189#[cfg(test)]
190mod tests {
191 use crate::protocol::util;
192
193 #[test]
194 fn test_valid_topic_names() {
195 let topic_names = vec![
196 "sport/tennis/player1",
197 "sport/tennis/player1/ranking",
198 "sport/tennis/player1/score/wimbledon",
199 "sport",
200 "sport/",
201 "/",
202 "Accounts payable",
203 "/finance",
204 "$SYS/monitor/Clients",
205 ];
206
207 for name in topic_names {
208 assert!(util::is_valid_topic_name(name));
209 }
210 }
211
212 #[test]
213 fn test_invalid_topic_names() {
214 let topic_names = vec![
215 "",
216 "sport/\0/tennis",
217 "sport/tennis/player1/#",
218 "sport+",
219 "#",
220 "sport/tennis#",
221 "sport/tennis/#/ranking",
222 ];
223
224 for name in topic_names {
225 assert!(!util::is_valid_topic_name(name));
226 }
227 }
228
229 #[test]
230 fn test_valid_topic_filters() {
231 let filters = vec![
232 "sport/tennis/player1/#",
233 "sport/#",
234 "#",
235 "sport/tennis/#",
236 "+",
237 "+/tennis/#",
238 "sport/+/player1",
239 "/finance",
240 "$SYS/#",
241 "$SYS/monitor/+",
242 ];
243
244 for filter in filters {
245 assert!(util::is_valid_topic_filter(filter));
246 }
247 }
248
249 #[test]
250 fn test_invalid_topic_filters() {
251 let filters = vec!["sport/tennis#", "sport/tennis/#/ranking", "sport+", ""];
252
253 for filter in filters {
254 assert!(!util::is_valid_topic_filter(filter));
255 }
256 }
257}