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 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}