mqtt5_protocol/protocol/v5/properties/
mod.rs

1mod accessors;
2mod codec;
3
4use crate::error::{MqttError, Result};
5use crate::prelude::{format, HashMap, String, Vec};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum PropertyId {
9    PayloadFormatIndicator = 0x01,
10    RequestProblemInformation = 0x17,
11    RequestResponseInformation = 0x19,
12    MaximumQoS = 0x24,
13    RetainAvailable = 0x25,
14    WildcardSubscriptionAvailable = 0x28,
15    SubscriptionIdentifierAvailable = 0x29,
16    SharedSubscriptionAvailable = 0x2A,
17
18    ServerKeepAlive = 0x13,
19    ReceiveMaximum = 0x21,
20    TopicAliasMaximum = 0x22,
21    TopicAlias = 0x23,
22
23    MessageExpiryInterval = 0x02,
24    SessionExpiryInterval = 0x11,
25    WillDelayInterval = 0x18,
26    MaximumPacketSize = 0x27,
27
28    SubscriptionIdentifier = 0x0B,
29
30    ContentType = 0x03,
31    ResponseTopic = 0x08,
32    AssignedClientIdentifier = 0x12,
33    AuthenticationMethod = 0x15,
34    ResponseInformation = 0x1A,
35    ServerReference = 0x1C,
36    ReasonString = 0x1F,
37
38    CorrelationData = 0x09,
39    AuthenticationData = 0x16,
40
41    UserProperty = 0x26,
42}
43
44impl PropertyId {
45    #[must_use]
46    pub fn from_u8(value: u8) -> Option<Self> {
47        match value {
48            0x01 => Some(Self::PayloadFormatIndicator),
49            0x02 => Some(Self::MessageExpiryInterval),
50            0x03 => Some(Self::ContentType),
51            0x08 => Some(Self::ResponseTopic),
52            0x09 => Some(Self::CorrelationData),
53            0x0B => Some(Self::SubscriptionIdentifier),
54            0x11 => Some(Self::SessionExpiryInterval),
55            0x12 => Some(Self::AssignedClientIdentifier),
56            0x13 => Some(Self::ServerKeepAlive),
57            0x15 => Some(Self::AuthenticationMethod),
58            0x16 => Some(Self::AuthenticationData),
59            0x17 => Some(Self::RequestProblemInformation),
60            0x18 => Some(Self::WillDelayInterval),
61            0x19 => Some(Self::RequestResponseInformation),
62            0x1A => Some(Self::ResponseInformation),
63            0x1C => Some(Self::ServerReference),
64            0x1F => Some(Self::ReasonString),
65            0x21 => Some(Self::ReceiveMaximum),
66            0x22 => Some(Self::TopicAliasMaximum),
67            0x23 => Some(Self::TopicAlias),
68            0x24 => Some(Self::MaximumQoS),
69            0x25 => Some(Self::RetainAvailable),
70            0x26 => Some(Self::UserProperty),
71            0x27 => Some(Self::MaximumPacketSize),
72            0x28 => Some(Self::WildcardSubscriptionAvailable),
73            0x29 => Some(Self::SubscriptionIdentifierAvailable),
74            0x2A => Some(Self::SharedSubscriptionAvailable),
75            _ => None,
76        }
77    }
78
79    #[must_use]
80    pub fn allows_multiple(&self) -> bool {
81        matches!(self, Self::UserProperty | Self::SubscriptionIdentifier)
82    }
83
84    #[must_use]
85    pub fn value_type(&self) -> PropertyValueType {
86        match self {
87            Self::PayloadFormatIndicator
88            | Self::RequestProblemInformation
89            | Self::RequestResponseInformation
90            | Self::MaximumQoS
91            | Self::RetainAvailable
92            | Self::WildcardSubscriptionAvailable
93            | Self::SubscriptionIdentifierAvailable
94            | Self::SharedSubscriptionAvailable => PropertyValueType::Byte,
95
96            Self::ServerKeepAlive
97            | Self::ReceiveMaximum
98            | Self::TopicAliasMaximum
99            | Self::TopicAlias => PropertyValueType::TwoByteInteger,
100
101            Self::MessageExpiryInterval
102            | Self::SessionExpiryInterval
103            | Self::WillDelayInterval
104            | Self::MaximumPacketSize => PropertyValueType::FourByteInteger,
105
106            Self::SubscriptionIdentifier => PropertyValueType::VariableByteInteger,
107
108            Self::ContentType
109            | Self::ResponseTopic
110            | Self::AssignedClientIdentifier
111            | Self::AuthenticationMethod
112            | Self::ResponseInformation
113            | Self::ServerReference
114            | Self::ReasonString => PropertyValueType::Utf8String,
115
116            Self::CorrelationData | Self::AuthenticationData => PropertyValueType::BinaryData,
117
118            Self::UserProperty => PropertyValueType::Utf8StringPair,
119        }
120    }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum PropertyValueType {
125    Byte,
126    TwoByteInteger,
127    FourByteInteger,
128    VariableByteInteger,
129    BinaryData,
130    Utf8String,
131    Utf8StringPair,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub enum PropertyValue {
136    Byte(u8),
137    TwoByteInteger(u16),
138    FourByteInteger(u32),
139    VariableByteInteger(u32),
140    BinaryData(bytes::Bytes),
141    Utf8String(String),
142    Utf8StringPair(String, String),
143}
144
145impl PropertyValue {
146    #[must_use]
147    pub fn value_type(&self) -> PropertyValueType {
148        match self {
149            Self::Byte(_) => PropertyValueType::Byte,
150            Self::TwoByteInteger(_) => PropertyValueType::TwoByteInteger,
151            Self::FourByteInteger(_) => PropertyValueType::FourByteInteger,
152            Self::VariableByteInteger(_) => PropertyValueType::VariableByteInteger,
153            Self::BinaryData(_) => PropertyValueType::BinaryData,
154            Self::Utf8String(_) => PropertyValueType::Utf8String,
155            Self::Utf8StringPair(_, _) => PropertyValueType::Utf8StringPair,
156        }
157    }
158
159    #[must_use]
160    pub fn matches_type(&self, expected: PropertyValueType) -> bool {
161        self.value_type() == expected
162    }
163}
164
165#[derive(Debug, Clone, Default, PartialEq, Eq)]
166pub struct Properties {
167    pub(crate) properties: HashMap<PropertyId, Vec<PropertyValue>>,
168}
169
170impl Properties {
171    #[must_use]
172    pub fn new() -> Self {
173        Self {
174            properties: HashMap::new(),
175        }
176    }
177
178    /// # Errors
179    /// Returns error if value type doesn't match property's expected type
180    /// or if property doesn't allow multiple values and already exists.
181    pub fn add(&mut self, id: PropertyId, value: PropertyValue) -> Result<()> {
182        if !value.matches_type(id.value_type()) {
183            return Err(MqttError::ProtocolError(format!(
184                "Property {:?} expects type {:?}, got {:?}",
185                id,
186                id.value_type(),
187                value.value_type()
188            )));
189        }
190
191        if !id.allows_multiple() && self.properties.contains_key(&id) {
192            return Err(MqttError::DuplicatePropertyId(id as u8));
193        }
194
195        self.properties.entry(id).or_default().push(value);
196        Ok(())
197    }
198
199    #[must_use]
200    pub fn get(&self, id: PropertyId) -> Option<&PropertyValue> {
201        self.properties.get(&id).and_then(|v| v.first())
202    }
203
204    #[must_use]
205    pub fn get_all(&self, id: PropertyId) -> Option<&[PropertyValue]> {
206        self.properties.get(&id).map(Vec::as_slice)
207    }
208
209    #[must_use]
210    pub fn contains(&self, id: PropertyId) -> bool {
211        self.properties.contains_key(&id)
212    }
213
214    #[must_use]
215    pub fn len(&self) -> usize {
216        self.properties.len()
217    }
218
219    #[must_use]
220    pub fn is_empty(&self) -> bool {
221        self.properties.is_empty()
222    }
223
224    pub fn iter(&self) -> impl Iterator<Item = (PropertyId, &PropertyValue)> + '_ {
225        self.properties
226            .iter()
227            .flat_map(|(id, values)| values.iter().map(move |value| (*id, value)))
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use crate::prelude::ToString;
235    use bytes::{Bytes, BytesMut};
236
237    #[test]
238    fn test_property_id_from_u8() {
239        assert_eq!(
240            PropertyId::from_u8(0x01),
241            Some(PropertyId::PayloadFormatIndicator)
242        );
243        assert_eq!(PropertyId::from_u8(0x26), Some(PropertyId::UserProperty));
244        assert_eq!(
245            PropertyId::from_u8(0x2A),
246            Some(PropertyId::SharedSubscriptionAvailable)
247        );
248        assert_eq!(PropertyId::from_u8(0xFF), None);
249        assert_eq!(PropertyId::from_u8(0x00), None);
250    }
251
252    #[test]
253    fn test_property_allows_multiple() {
254        assert!(PropertyId::UserProperty.allows_multiple());
255        assert!(PropertyId::SubscriptionIdentifier.allows_multiple());
256        assert!(!PropertyId::PayloadFormatIndicator.allows_multiple());
257        assert!(!PropertyId::SessionExpiryInterval.allows_multiple());
258    }
259
260    #[test]
261    fn test_property_value_type() {
262        assert_eq!(
263            PropertyId::PayloadFormatIndicator.value_type(),
264            PropertyValueType::Byte
265        );
266        assert_eq!(
267            PropertyId::TopicAlias.value_type(),
268            PropertyValueType::TwoByteInteger
269        );
270        assert_eq!(
271            PropertyId::SessionExpiryInterval.value_type(),
272            PropertyValueType::FourByteInteger
273        );
274        assert_eq!(
275            PropertyId::SubscriptionIdentifier.value_type(),
276            PropertyValueType::VariableByteInteger
277        );
278        assert_eq!(
279            PropertyId::ContentType.value_type(),
280            PropertyValueType::Utf8String
281        );
282        assert_eq!(
283            PropertyId::CorrelationData.value_type(),
284            PropertyValueType::BinaryData
285        );
286        assert_eq!(
287            PropertyId::UserProperty.value_type(),
288            PropertyValueType::Utf8StringPair
289        );
290    }
291
292    #[test]
293    fn test_property_value_matches_type() {
294        let byte_val = PropertyValue::Byte(1);
295        assert!(byte_val.matches_type(PropertyValueType::Byte));
296        assert!(!byte_val.matches_type(PropertyValueType::TwoByteInteger));
297
298        let string_val = PropertyValue::Utf8String("test".to_string());
299        assert!(string_val.matches_type(PropertyValueType::Utf8String));
300        assert!(!string_val.matches_type(PropertyValueType::BinaryData));
301    }
302
303    #[test]
304    fn test_properties_add_valid() {
305        let mut props = Properties::new();
306
307        props
308            .add(PropertyId::PayloadFormatIndicator, PropertyValue::Byte(1))
309            .unwrap();
310        props
311            .add(
312                PropertyId::SessionExpiryInterval,
313                PropertyValue::FourByteInteger(3600),
314            )
315            .unwrap();
316        props
317            .add(
318                PropertyId::ContentType,
319                PropertyValue::Utf8String("text/plain".to_string()),
320            )
321            .unwrap();
322
323        props
324            .add(
325                PropertyId::UserProperty,
326                PropertyValue::Utf8StringPair("key1".to_string(), "value1".to_string()),
327            )
328            .unwrap();
329        props
330            .add(
331                PropertyId::UserProperty,
332                PropertyValue::Utf8StringPair("key2".to_string(), "value2".to_string()),
333            )
334            .unwrap();
335
336        assert_eq!(props.len(), 4);
337    }
338
339    #[test]
340    fn test_properties_add_type_mismatch() {
341        let mut props = Properties::new();
342
343        let result = props.add(
344            PropertyId::PayloadFormatIndicator,
345            PropertyValue::FourByteInteger(100),
346        );
347        assert!(result.is_err());
348    }
349
350    #[test]
351    fn test_properties_add_duplicate_single_value() {
352        let mut props = Properties::new();
353
354        props
355            .add(PropertyId::PayloadFormatIndicator, PropertyValue::Byte(0))
356            .unwrap();
357
358        let result = props.add(PropertyId::PayloadFormatIndicator, PropertyValue::Byte(1));
359        assert!(result.is_err());
360    }
361
362    #[test]
363    fn test_properties_get() {
364        let mut props = Properties::new();
365        props
366            .add(
367                PropertyId::ContentType,
368                PropertyValue::Utf8String("text/html".to_string()),
369            )
370            .unwrap();
371
372        let value = props.get(PropertyId::ContentType).unwrap();
373        match value {
374            PropertyValue::Utf8String(s) => assert_eq!(s, "text/html"),
375            _ => panic!("Wrong value type"),
376        }
377
378        assert!(props.get(PropertyId::ResponseTopic).is_none());
379    }
380
381    #[test]
382    fn test_properties_get_all() {
383        let mut props = Properties::new();
384
385        props
386            .add(
387                PropertyId::UserProperty,
388                PropertyValue::Utf8StringPair("k1".to_string(), "v1".to_string()),
389            )
390            .unwrap();
391        props
392            .add(
393                PropertyId::UserProperty,
394                PropertyValue::Utf8StringPair("k2".to_string(), "v2".to_string()),
395            )
396            .unwrap();
397
398        let values = props.get_all(PropertyId::UserProperty).unwrap();
399        assert_eq!(values.len(), 2);
400    }
401
402    #[test]
403    fn test_properties_encode_decode_empty() {
404        let props = Properties::new();
405        let mut buf = BytesMut::new();
406
407        props.encode(&mut buf).unwrap();
408        assert_eq!(buf[0], 0);
409
410        let decoded = Properties::decode(&mut buf).unwrap();
411        assert!(decoded.is_empty());
412    }
413
414    #[test]
415    fn test_properties_encode_decode_single_values() {
416        let mut props = Properties::new();
417
418        props
419            .add(PropertyId::PayloadFormatIndicator, PropertyValue::Byte(1))
420            .unwrap();
421        props
422            .add(PropertyId::TopicAlias, PropertyValue::TwoByteInteger(100))
423            .unwrap();
424        props
425            .add(
426                PropertyId::SessionExpiryInterval,
427                PropertyValue::FourByteInteger(3600),
428            )
429            .unwrap();
430        props
431            .add(
432                PropertyId::SubscriptionIdentifier,
433                PropertyValue::VariableByteInteger(123),
434            )
435            .unwrap();
436        props
437            .add(
438                PropertyId::ContentType,
439                PropertyValue::Utf8String("text/plain".to_string()),
440            )
441            .unwrap();
442        props
443            .add(
444                PropertyId::CorrelationData,
445                PropertyValue::BinaryData(Bytes::from(vec![1, 2, 3, 4])),
446            )
447            .unwrap();
448        props
449            .add(
450                PropertyId::UserProperty,
451                PropertyValue::Utf8StringPair("key".to_string(), "value".to_string()),
452            )
453            .unwrap();
454
455        let mut buf = BytesMut::new();
456        props.encode(&mut buf).unwrap();
457
458        let decoded = Properties::decode(&mut buf).unwrap();
459        assert_eq!(decoded.len(), props.len());
460
461        match decoded.get(PropertyId::PayloadFormatIndicator).unwrap() {
462            PropertyValue::Byte(v) => assert_eq!(*v, 1),
463            _ => panic!("Wrong type"),
464        }
465
466        match decoded.get(PropertyId::TopicAlias).unwrap() {
467            PropertyValue::TwoByteInteger(v) => assert_eq!(*v, 100),
468            _ => panic!("Wrong type"),
469        }
470
471        match decoded.get(PropertyId::ContentType).unwrap() {
472            PropertyValue::Utf8String(v) => assert_eq!(v, "text/plain"),
473            _ => panic!("Wrong type"),
474        }
475    }
476
477    #[test]
478    fn test_properties_encode_decode_multiple_values() {
479        let mut props = Properties::new();
480
481        props
482            .add(
483                PropertyId::UserProperty,
484                PropertyValue::Utf8StringPair("env".to_string(), "prod".to_string()),
485            )
486            .unwrap();
487        props
488            .add(
489                PropertyId::UserProperty,
490                PropertyValue::Utf8StringPair("version".to_string(), "1.0".to_string()),
491            )
492            .unwrap();
493
494        props
495            .add(
496                PropertyId::SubscriptionIdentifier,
497                PropertyValue::VariableByteInteger(10),
498            )
499            .unwrap();
500        props
501            .add(
502                PropertyId::SubscriptionIdentifier,
503                PropertyValue::VariableByteInteger(20),
504            )
505            .unwrap();
506
507        let mut buf = BytesMut::new();
508        props.encode(&mut buf).unwrap();
509
510        let decoded = Properties::decode(&mut buf).unwrap();
511
512        let user_props = decoded.get_all(PropertyId::UserProperty).unwrap();
513        assert_eq!(user_props.len(), 2);
514
515        let sub_ids = decoded.get_all(PropertyId::SubscriptionIdentifier).unwrap();
516        assert_eq!(sub_ids.len(), 2);
517    }
518
519    #[test]
520    fn test_properties_decode_invalid_property_id() {
521        use bytes::BufMut;
522        let mut buf = BytesMut::new();
523        buf.put_u8(1);
524        buf.put_u8(0xFF);
525
526        let result = Properties::decode(&mut buf);
527        assert!(result.is_err());
528    }
529
530    #[test]
531    fn test_properties_decode_insufficient_data() {
532        use bytes::BufMut;
533        let mut buf = BytesMut::new();
534        buf.put_u8(10);
535
536        let result = Properties::decode(&mut buf);
537        assert!(result.is_err());
538    }
539
540    #[test]
541    fn test_properties_encoded_len() {
542        let mut props = Properties::new();
543        props
544            .add(PropertyId::PayloadFormatIndicator, PropertyValue::Byte(1))
545            .unwrap();
546        props
547            .add(
548                PropertyId::ContentType,
549                PropertyValue::Utf8String("test".to_string()),
550            )
551            .unwrap();
552
553        let mut buf = BytesMut::new();
554        props.encode(&mut buf).unwrap();
555
556        assert_eq!(props.encoded_len(), buf.len());
557    }
558
559    #[test]
560    fn test_all_property_ids_have_correct_types() {
561        for id in 0u8..=0x2A {
562            if let Some(prop_id) = PropertyId::from_u8(id) {
563                let _ = prop_id.value_type();
564            }
565        }
566    }
567}