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}