mqtt5_protocol/protocol/v5/
properties.rs

1use crate::encoding::{
2    decode_binary, decode_string, decode_variable_int, encode_binary, encode_string,
3    encode_variable_int,
4};
5use crate::error::{MqttError, Result};
6use bytes::{Buf, BufMut, Bytes};
7use std::collections::HashMap;
8
9/// MQTT v5.0 Property Identifiers
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum PropertyId {
12    // Byte properties
13    PayloadFormatIndicator = 0x01,
14    RequestProblemInformation = 0x17,
15    RequestResponseInformation = 0x19,
16    MaximumQoS = 0x24,
17    RetainAvailable = 0x25,
18    WildcardSubscriptionAvailable = 0x28,
19    SubscriptionIdentifierAvailable = 0x29,
20    SharedSubscriptionAvailable = 0x2A,
21
22    // Two Byte Integer properties
23    ServerKeepAlive = 0x13,
24    ReceiveMaximum = 0x21,
25    TopicAliasMaximum = 0x22,
26    TopicAlias = 0x23,
27
28    // Four Byte Integer properties
29    MessageExpiryInterval = 0x02,
30    SessionExpiryInterval = 0x11,
31    WillDelayInterval = 0x18,
32    MaximumPacketSize = 0x27,
33
34    // Variable Byte Integer properties
35    SubscriptionIdentifier = 0x0B,
36
37    // UTF-8 Encoded String properties
38    ContentType = 0x03,
39    ResponseTopic = 0x08,
40    AssignedClientIdentifier = 0x12,
41    AuthenticationMethod = 0x15,
42    ResponseInformation = 0x1A,
43    ServerReference = 0x1C,
44    ReasonString = 0x1F,
45
46    // Binary Data properties
47    CorrelationData = 0x09,
48    AuthenticationData = 0x16,
49
50    // UTF-8 String Pair properties
51    UserProperty = 0x26,
52}
53
54impl PropertyId {
55    /// Converts a u8 to `PropertyId`
56    #[must_use]
57    pub fn from_u8(value: u8) -> Option<Self> {
58        match value {
59            0x01 => Some(Self::PayloadFormatIndicator),
60            0x02 => Some(Self::MessageExpiryInterval),
61            0x03 => Some(Self::ContentType),
62            0x08 => Some(Self::ResponseTopic),
63            0x09 => Some(Self::CorrelationData),
64            0x0B => Some(Self::SubscriptionIdentifier),
65            0x11 => Some(Self::SessionExpiryInterval),
66            0x12 => Some(Self::AssignedClientIdentifier),
67            0x13 => Some(Self::ServerKeepAlive),
68            0x15 => Some(Self::AuthenticationMethod),
69            0x16 => Some(Self::AuthenticationData),
70            0x17 => Some(Self::RequestProblemInformation),
71            0x18 => Some(Self::WillDelayInterval),
72            0x19 => Some(Self::RequestResponseInformation),
73            0x1A => Some(Self::ResponseInformation),
74            0x1C => Some(Self::ServerReference),
75            0x1F => Some(Self::ReasonString),
76            0x21 => Some(Self::ReceiveMaximum),
77            0x22 => Some(Self::TopicAliasMaximum),
78            0x23 => Some(Self::TopicAlias),
79            0x24 => Some(Self::MaximumQoS),
80            0x25 => Some(Self::RetainAvailable),
81            0x26 => Some(Self::UserProperty),
82            0x27 => Some(Self::MaximumPacketSize),
83            0x28 => Some(Self::WildcardSubscriptionAvailable),
84            0x29 => Some(Self::SubscriptionIdentifierAvailable),
85            0x2A => Some(Self::SharedSubscriptionAvailable),
86            _ => None,
87        }
88    }
89
90    /// Checks if this property can appear multiple times in a packet
91    #[must_use]
92    pub fn allows_multiple(&self) -> bool {
93        matches!(self, Self::UserProperty | Self::SubscriptionIdentifier)
94    }
95
96    /// Gets the expected value type for this property
97    #[must_use]
98    pub fn value_type(&self) -> PropertyValueType {
99        match self {
100            Self::PayloadFormatIndicator
101            | Self::RequestProblemInformation
102            | Self::RequestResponseInformation
103            | Self::MaximumQoS
104            | Self::RetainAvailable
105            | Self::WildcardSubscriptionAvailable
106            | Self::SubscriptionIdentifierAvailable
107            | Self::SharedSubscriptionAvailable => PropertyValueType::Byte,
108
109            Self::ServerKeepAlive
110            | Self::ReceiveMaximum
111            | Self::TopicAliasMaximum
112            | Self::TopicAlias => PropertyValueType::TwoByteInteger,
113
114            Self::MessageExpiryInterval
115            | Self::SessionExpiryInterval
116            | Self::WillDelayInterval
117            | Self::MaximumPacketSize => PropertyValueType::FourByteInteger,
118
119            Self::SubscriptionIdentifier => PropertyValueType::VariableByteInteger,
120
121            Self::ContentType
122            | Self::ResponseTopic
123            | Self::AssignedClientIdentifier
124            | Self::AuthenticationMethod
125            | Self::ResponseInformation
126            | Self::ServerReference
127            | Self::ReasonString => PropertyValueType::Utf8String,
128
129            Self::CorrelationData | Self::AuthenticationData => PropertyValueType::BinaryData,
130
131            Self::UserProperty => PropertyValueType::Utf8StringPair,
132        }
133    }
134}
135
136/// Property value types
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum PropertyValueType {
139    Byte,
140    TwoByteInteger,
141    FourByteInteger,
142    VariableByteInteger,
143    BinaryData,
144    Utf8String,
145    Utf8StringPair,
146}
147
148/// Property value storage
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub enum PropertyValue {
151    Byte(u8),
152    TwoByteInteger(u16),
153    FourByteInteger(u32),
154    VariableByteInteger(u32),
155    BinaryData(Bytes),
156    Utf8String(String),
157    Utf8StringPair(String, String),
158}
159
160impl PropertyValue {
161    /// Gets the value type
162    #[must_use]
163    pub fn value_type(&self) -> PropertyValueType {
164        match self {
165            Self::Byte(_) => PropertyValueType::Byte,
166            Self::TwoByteInteger(_) => PropertyValueType::TwoByteInteger,
167            Self::FourByteInteger(_) => PropertyValueType::FourByteInteger,
168            Self::VariableByteInteger(_) => PropertyValueType::VariableByteInteger,
169            Self::BinaryData(_) => PropertyValueType::BinaryData,
170            Self::Utf8String(_) => PropertyValueType::Utf8String,
171            Self::Utf8StringPair(_, _) => PropertyValueType::Utf8StringPair,
172        }
173    }
174
175    /// Validates that this value matches the expected type for a property
176    #[must_use]
177    pub fn matches_type(&self, expected: PropertyValueType) -> bool {
178        self.value_type() == expected
179    }
180}
181
182/// Container for MQTT v5.0 properties
183#[derive(Debug, Clone, Default, PartialEq, Eq)]
184pub struct Properties {
185    properties: HashMap<PropertyId, Vec<PropertyValue>>,
186}
187
188impl Properties {
189    /// Creates a new empty properties container
190    #[must_use]
191    pub fn new() -> Self {
192        Self {
193            properties: HashMap::new(),
194        }
195    }
196
197    /// Adds a property value
198    ///
199    /// # Errors
200    ///
201    /// Returns an error if:
202    /// - The value type doesn't match the property's expected type
203    /// - The property doesn't allow multiple values and already exists
204    ///
205    /// # Errors
206    ///
207    /// Returns an error if the operation fails
208    pub fn add(&mut self, id: PropertyId, value: PropertyValue) -> Result<()> {
209        // Validate value type
210        if !value.matches_type(id.value_type()) {
211            return Err(MqttError::ProtocolError(format!(
212                "Property {:?} expects type {:?}, got {:?}",
213                id,
214                id.value_type(),
215                value.value_type()
216            )));
217        }
218
219        // Check if property allows multiple values
220        if !id.allows_multiple() && self.properties.contains_key(&id) {
221            return Err(MqttError::DuplicatePropertyId(id as u8));
222        }
223
224        self.properties.entry(id).or_default().push(value);
225        Ok(())
226    }
227
228    /// Gets a single property value
229    #[must_use]
230    pub fn get(&self, id: PropertyId) -> Option<&PropertyValue> {
231        self.properties.get(&id).and_then(|v| v.first())
232    }
233
234    /// Gets all values for a property (for properties that allow multiple values)
235    #[must_use]
236    pub fn get_all(&self, id: PropertyId) -> Option<&[PropertyValue]> {
237        self.properties.get(&id).map(std::vec::Vec::as_slice)
238    }
239
240    /// Checks if a property is present
241    #[must_use]
242    pub fn contains(&self, id: PropertyId) -> bool {
243        self.properties.contains_key(&id)
244    }
245
246    /// Returns the number of properties (counting multi-value properties as one)
247    #[must_use]
248    pub fn len(&self) -> usize {
249        self.properties.len()
250    }
251
252    /// Checks if there are no properties
253    #[must_use]
254    pub fn is_empty(&self) -> bool {
255        self.properties.is_empty()
256    }
257
258    /// Iterates over all properties and their values
259    pub fn iter(&self) -> impl Iterator<Item = (PropertyId, &PropertyValue)> + '_ {
260        self.properties
261            .iter()
262            .flat_map(|(id, values)| values.iter().map(move |value| (*id, value)))
263    }
264
265    /// Encodes all properties to the buffer
266    ///
267    /// # Errors
268    ///
269    /// Returns an error if encoding fails
270    ///
271    /// # Errors
272    ///
273    /// Returns an error if the operation fails
274    pub fn encode<B: BufMut>(&self, buf: &mut B) -> Result<()> {
275        // First calculate the total properties length
276        let mut props_buf = Vec::new();
277        self.encode_properties(&mut props_buf)?;
278
279        // Write properties length as variable byte integer
280        encode_variable_int(
281            buf,
282            props_buf
283                .len()
284                .try_into()
285                .map_err(|_| MqttError::PacketTooLarge {
286                    size: props_buf.len(),
287                    max: u32::MAX as usize,
288                })?,
289        )?;
290
291        // Write properties data
292        buf.put_slice(&props_buf);
293        Ok(())
294    }
295
296    /// Encodes properties without the length prefix
297    ///
298    /// # Errors
299    ///
300    /// Returns an error if the operation fails
301    fn encode_properties<B: BufMut>(&self, buf: &mut B) -> Result<()> {
302        for (id, values) in &self.properties {
303            for value in values {
304                encode_variable_int(buf, u32::from(*id as u8))?;
305
306                match value {
307                    PropertyValue::Byte(v) => buf.put_u8(*v),
308                    PropertyValue::TwoByteInteger(v) => buf.put_u16(*v),
309                    PropertyValue::FourByteInteger(v) => buf.put_u32(*v),
310                    PropertyValue::VariableByteInteger(v) => encode_variable_int(buf, *v)?,
311                    PropertyValue::BinaryData(v) => encode_binary(buf, v)?,
312                    PropertyValue::Utf8String(v) => encode_string(buf, v)?,
313                    PropertyValue::Utf8StringPair(k, v) => {
314                        encode_string(buf, k)?;
315                        encode_string(buf, v)?;
316                    }
317                }
318            }
319        }
320        Ok(())
321    }
322
323    /// # Errors
324    /// Returns error if encoding fails
325    pub fn encode_direct<B: BufMut>(&self, buf: &mut B) -> Result<()> {
326        let props_len = self.properties_encoded_len();
327        encode_variable_int(
328            buf,
329            props_len
330                .try_into()
331                .map_err(|_| MqttError::PacketTooLarge {
332                    size: props_len,
333                    max: u32::MAX as usize,
334                })?,
335        )?;
336        self.encode_properties(buf)
337    }
338
339    /// Decodes properties from the buffer
340    ///
341    /// # Errors
342    ///
343    /// Returns an error if:
344    /// - Decoding fails
345    /// - Invalid property ID
346    /// - Type mismatch
347    /// - Duplicate property that doesn't allow multiples
348    ///
349    /// # Errors
350    ///
351    /// Returns an error if the operation fails
352    pub fn decode<B: Buf>(buf: &mut B) -> Result<Self> {
353        // Read properties length
354        let props_len = decode_variable_int(buf)? as usize;
355
356        // Create a sub-buffer for properties
357        if buf.remaining() < props_len {
358            return Err(MqttError::MalformedPacket(format!(
359                "Insufficient data for properties: expected {}, got {}",
360                props_len,
361                buf.remaining()
362            )));
363        }
364
365        let mut props_buf = buf.copy_to_bytes(props_len);
366        let mut properties = Self::new();
367
368        while props_buf.has_remaining() {
369            // Read property identifier
370            let id_val = decode_variable_int(&mut props_buf)?;
371            let id_byte = u8::try_from(id_val).map_err(|_| MqttError::InvalidPropertyId(255))?;
372
373            let id = PropertyId::from_u8(id_byte).ok_or(MqttError::InvalidPropertyId(id_byte))?;
374
375            // Read property value based on type
376            let value = match id.value_type() {
377                PropertyValueType::Byte => {
378                    if !props_buf.has_remaining() {
379                        return Err(MqttError::MalformedPacket(
380                            "Insufficient data for byte property".to_string(),
381                        ));
382                    }
383                    PropertyValue::Byte(props_buf.get_u8())
384                }
385                PropertyValueType::TwoByteInteger => {
386                    if props_buf.remaining() < 2 {
387                        return Err(MqttError::MalformedPacket(
388                            "Insufficient data for two-byte integer property".to_string(),
389                        ));
390                    }
391                    PropertyValue::TwoByteInteger(props_buf.get_u16())
392                }
393                PropertyValueType::FourByteInteger => {
394                    if props_buf.remaining() < 4 {
395                        return Err(MqttError::MalformedPacket(
396                            "Insufficient data for four-byte integer property".to_string(),
397                        ));
398                    }
399                    PropertyValue::FourByteInteger(props_buf.get_u32())
400                }
401                PropertyValueType::VariableByteInteger => {
402                    PropertyValue::VariableByteInteger(decode_variable_int(&mut props_buf)?)
403                }
404                PropertyValueType::BinaryData => {
405                    PropertyValue::BinaryData(decode_binary(&mut props_buf)?)
406                }
407                PropertyValueType::Utf8String => {
408                    PropertyValue::Utf8String(decode_string(&mut props_buf)?)
409                }
410                PropertyValueType::Utf8StringPair => {
411                    let key = decode_string(&mut props_buf)?;
412                    let value = decode_string(&mut props_buf)?;
413                    PropertyValue::Utf8StringPair(key, value)
414                }
415            };
416
417            properties.add(id, value)?;
418        }
419
420        Ok(properties)
421    }
422
423    /// Calculates the encoded length of all properties (including length prefix)
424    #[must_use]
425    pub fn encoded_len(&self) -> usize {
426        let props_len = self.properties_encoded_len();
427        crate::encoding::variable_int_len(props_len.try_into().unwrap_or(u32::MAX)) + props_len
428    }
429
430    /// Calculates the encoded length of properties without length prefix
431    #[must_use]
432    fn properties_encoded_len(&self) -> usize {
433        let mut len = 0;
434
435        for (id, values) in &self.properties {
436            for value in values {
437                // Property ID length
438                len += crate::encoding::variable_int_len(u32::from(*id as u8));
439
440                // Property value length
441                len += match value {
442                    PropertyValue::Byte(_) => 1,
443                    PropertyValue::TwoByteInteger(_) => 2,
444                    PropertyValue::FourByteInteger(_) => 4,
445                    PropertyValue::VariableByteInteger(v) => crate::encoding::variable_int_len(*v),
446                    PropertyValue::BinaryData(v) => crate::encoding::binary_len(v),
447                    PropertyValue::Utf8String(v) => crate::encoding::string_len(v),
448                    PropertyValue::Utf8StringPair(k, v) => {
449                        crate::encoding::string_len(k) + crate::encoding::string_len(v)
450                    }
451                };
452            }
453        }
454
455        len
456    }
457
458    // Type-safe property setters that cannot fail
459
460    /// Sets the payload format indicator (0 = unspecified bytes, 1 = UTF-8)
461    pub fn set_payload_format_indicator(&mut self, is_utf8: bool) {
462        self.properties
463            .entry(PropertyId::PayloadFormatIndicator)
464            .or_default()
465            .push(PropertyValue::Byte(u8::from(is_utf8)));
466    }
467
468    /// Sets the message expiry interval in seconds
469    pub fn set_message_expiry_interval(&mut self, seconds: u32) {
470        self.properties
471            .entry(PropertyId::MessageExpiryInterval)
472            .or_default()
473            .push(PropertyValue::FourByteInteger(seconds));
474    }
475
476    #[allow(clippy::must_use_candidate)]
477    pub fn get_message_expiry_interval(&self) -> Option<u32> {
478        self.properties
479            .get(&PropertyId::MessageExpiryInterval)
480            .and_then(|values| values.first())
481            .and_then(|value| {
482                if let PropertyValue::FourByteInteger(v) = value {
483                    Some(*v)
484                } else {
485                    None
486                }
487            })
488    }
489
490    /// Sets the topic alias
491    pub fn set_topic_alias(&mut self, alias: u16) {
492        self.properties
493            .entry(PropertyId::TopicAlias)
494            .or_default()
495            .push(PropertyValue::TwoByteInteger(alias));
496    }
497
498    #[allow(clippy::must_use_candidate)]
499    pub fn get_topic_alias(&self) -> Option<u16> {
500        self.properties
501            .get(&PropertyId::TopicAlias)
502            .and_then(|values| values.first())
503            .and_then(|value| {
504                if let PropertyValue::TwoByteInteger(v) = value {
505                    Some(*v)
506                } else {
507                    None
508                }
509            })
510    }
511
512    /// Sets the response topic
513    pub fn set_response_topic(&mut self, topic: String) {
514        self.properties
515            .entry(PropertyId::ResponseTopic)
516            .or_default()
517            .push(PropertyValue::Utf8String(topic));
518    }
519
520    /// Sets the correlation data
521    pub fn set_correlation_data(&mut self, data: Bytes) {
522        self.properties
523            .entry(PropertyId::CorrelationData)
524            .or_default()
525            .push(PropertyValue::BinaryData(data));
526    }
527
528    /// Adds a user property (can be called multiple times)
529    pub fn add_user_property(&mut self, key: String, value: String) {
530        self.properties
531            .entry(PropertyId::UserProperty)
532            .or_default()
533            .push(PropertyValue::Utf8StringPair(key, value));
534    }
535
536    /// Sets the subscription identifier
537    pub fn set_subscription_identifier(&mut self, id: u32) {
538        self.properties
539            .entry(PropertyId::SubscriptionIdentifier)
540            .or_default()
541            .push(PropertyValue::VariableByteInteger(id));
542    }
543
544    #[must_use]
545    pub fn get_subscription_identifier(&self) -> Option<u32> {
546        self.properties
547            .get(&PropertyId::SubscriptionIdentifier)
548            .and_then(|values| values.first())
549            .and_then(|value| {
550                if let PropertyValue::VariableByteInteger(v) = value {
551                    Some(*v)
552                } else {
553                    None
554                }
555            })
556    }
557
558    /// Sets the session expiry interval
559    pub fn set_session_expiry_interval(&mut self, seconds: u32) {
560        self.properties
561            .entry(PropertyId::SessionExpiryInterval)
562            .or_default()
563            .push(PropertyValue::FourByteInteger(seconds));
564    }
565
566    #[allow(clippy::must_use_candidate)]
567    pub fn get_session_expiry_interval(&self) -> Option<u32> {
568        self.properties
569            .get(&PropertyId::SessionExpiryInterval)
570            .and_then(|values| values.first())
571            .and_then(|value| {
572                if let PropertyValue::FourByteInteger(v) = value {
573                    Some(*v)
574                } else {
575                    None
576                }
577            })
578    }
579
580    /// Sets the assigned client identifier
581    pub fn set_assigned_client_identifier(&mut self, id: String) {
582        self.properties
583            .entry(PropertyId::AssignedClientIdentifier)
584            .or_default()
585            .push(PropertyValue::Utf8String(id));
586    }
587
588    /// Sets the server keep alive
589    pub fn set_server_keep_alive(&mut self, seconds: u16) {
590        self.properties
591            .entry(PropertyId::ServerKeepAlive)
592            .or_default()
593            .push(PropertyValue::TwoByteInteger(seconds));
594    }
595
596    /// Sets the authentication method
597    pub fn set_authentication_method(&mut self, method: String) {
598        self.properties
599            .entry(PropertyId::AuthenticationMethod)
600            .or_default()
601            .push(PropertyValue::Utf8String(method));
602    }
603
604    /// Sets the authentication data
605    pub fn set_authentication_data(&mut self, data: Bytes) {
606        self.properties
607            .entry(PropertyId::AuthenticationData)
608            .or_default()
609            .push(PropertyValue::BinaryData(data));
610    }
611
612    #[allow(clippy::must_use_candidate)]
613    pub fn get_authentication_method(&self) -> Option<&String> {
614        self.properties
615            .get(&PropertyId::AuthenticationMethod)
616            .and_then(|values| values.first())
617            .and_then(|value| {
618                if let PropertyValue::Utf8String(s) = value {
619                    Some(s)
620                } else {
621                    None
622                }
623            })
624    }
625
626    #[allow(clippy::must_use_candidate)]
627    pub fn get_authentication_data(&self) -> Option<&[u8]> {
628        self.properties
629            .get(&PropertyId::AuthenticationData)
630            .and_then(|values| values.first())
631            .and_then(|value| {
632                if let PropertyValue::BinaryData(b) = value {
633                    Some(b.as_ref())
634                } else {
635                    None
636                }
637            })
638    }
639
640    /// Sets request problem information
641    pub fn set_request_problem_information(&mut self, request: bool) {
642        self.properties
643            .entry(PropertyId::RequestProblemInformation)
644            .or_default()
645            .push(PropertyValue::Byte(u8::from(request)));
646    }
647
648    #[allow(clippy::must_use_candidate)]
649    pub fn get_request_problem_information(&self) -> Option<bool> {
650        self.properties
651            .get(&PropertyId::RequestProblemInformation)
652            .and_then(|values| values.first())
653            .and_then(|value| {
654                if let PropertyValue::Byte(v) = value {
655                    Some(*v != 0)
656                } else {
657                    None
658                }
659            })
660    }
661
662    /// Sets the will delay interval
663    pub fn set_will_delay_interval(&mut self, seconds: u32) {
664        self.properties
665            .entry(PropertyId::WillDelayInterval)
666            .or_default()
667            .push(PropertyValue::FourByteInteger(seconds));
668    }
669
670    /// Sets request response information
671    pub fn set_request_response_information(&mut self, request: bool) {
672        self.properties
673            .entry(PropertyId::RequestResponseInformation)
674            .or_default()
675            .push(PropertyValue::Byte(u8::from(request)));
676    }
677
678    #[allow(clippy::must_use_candidate)]
679    pub fn get_request_response_information(&self) -> Option<bool> {
680        self.properties
681            .get(&PropertyId::RequestResponseInformation)
682            .and_then(|values| values.first())
683            .and_then(|value| {
684                if let PropertyValue::Byte(v) = value {
685                    Some(*v != 0)
686                } else {
687                    None
688                }
689            })
690    }
691
692    /// Sets the response information
693    pub fn set_response_information(&mut self, info: String) {
694        self.properties
695            .entry(PropertyId::ResponseInformation)
696            .or_default()
697            .push(PropertyValue::Utf8String(info));
698    }
699
700    /// Sets the server reference
701    pub fn set_server_reference(&mut self, reference: String) {
702        self.properties
703            .entry(PropertyId::ServerReference)
704            .or_default()
705            .push(PropertyValue::Utf8String(reference));
706    }
707
708    /// Sets the reason string
709    pub fn set_reason_string(&mut self, reason: String) {
710        self.properties
711            .entry(PropertyId::ReasonString)
712            .or_default()
713            .push(PropertyValue::Utf8String(reason));
714    }
715
716    /// Sets the receive maximum
717    pub fn set_receive_maximum(&mut self, max: u16) {
718        self.properties
719            .entry(PropertyId::ReceiveMaximum)
720            .or_default()
721            .push(PropertyValue::TwoByteInteger(max));
722    }
723
724    #[allow(clippy::must_use_candidate)]
725    pub fn get_receive_maximum(&self) -> Option<u16> {
726        self.properties
727            .get(&PropertyId::ReceiveMaximum)
728            .and_then(|values| values.first())
729            .and_then(|value| {
730                if let PropertyValue::TwoByteInteger(v) = value {
731                    Some(*v)
732                } else {
733                    None
734                }
735            })
736    }
737
738    /// Sets the topic alias maximum
739    pub fn set_topic_alias_maximum(&mut self, max: u16) {
740        self.properties
741            .entry(PropertyId::TopicAliasMaximum)
742            .or_default()
743            .push(PropertyValue::TwoByteInteger(max));
744    }
745
746    #[allow(clippy::must_use_candidate)]
747    pub fn get_topic_alias_maximum(&self) -> Option<u16> {
748        self.properties
749            .get(&PropertyId::TopicAliasMaximum)
750            .and_then(|values| values.first())
751            .and_then(|value| {
752                if let PropertyValue::TwoByteInteger(v) = value {
753                    Some(*v)
754                } else {
755                    None
756                }
757            })
758    }
759
760    /// Sets the maximum `QoS`
761    pub fn set_maximum_qos(&mut self, qos: u8) {
762        self.properties
763            .entry(PropertyId::MaximumQoS)
764            .or_default()
765            .push(PropertyValue::Byte(qos));
766    }
767
768    /// Sets retain available
769    pub fn set_retain_available(&mut self, available: bool) {
770        self.properties
771            .entry(PropertyId::RetainAvailable)
772            .or_default()
773            .push(PropertyValue::Byte(u8::from(available)));
774    }
775
776    /// Sets the maximum packet size
777    pub fn set_maximum_packet_size(&mut self, size: u32) {
778        self.properties
779            .entry(PropertyId::MaximumPacketSize)
780            .or_default()
781            .push(PropertyValue::FourByteInteger(size));
782    }
783
784    /// Sets wildcard subscription available
785    pub fn set_wildcard_subscription_available(&mut self, available: bool) {
786        self.properties
787            .entry(PropertyId::WildcardSubscriptionAvailable)
788            .or_default()
789            .push(PropertyValue::Byte(u8::from(available)));
790    }
791
792    /// Sets subscription identifier available
793    pub fn set_subscription_identifier_available(&mut self, available: bool) {
794        self.properties
795            .entry(PropertyId::SubscriptionIdentifierAvailable)
796            .or_default()
797            .push(PropertyValue::Byte(u8::from(available)));
798    }
799
800    /// Sets shared subscription available
801    pub fn set_shared_subscription_available(&mut self, available: bool) {
802        self.properties
803            .entry(PropertyId::SharedSubscriptionAvailable)
804            .or_default()
805            .push(PropertyValue::Byte(u8::from(available)));
806    }
807
808    #[allow(clippy::must_use_candidate)]
809    pub fn get_maximum_qos(&self) -> Option<u8> {
810        self.properties
811            .get(&PropertyId::MaximumQoS)
812            .and_then(|values| values.first())
813            .and_then(|value| {
814                if let PropertyValue::Byte(v) = value {
815                    Some(*v)
816                } else {
817                    None
818                }
819            })
820    }
821
822    /// Sets the content type
823    pub fn set_content_type(&mut self, content_type: String) {
824        self.properties
825            .entry(PropertyId::ContentType)
826            .or_default()
827            .push(PropertyValue::Utf8String(content_type));
828    }
829}
830
831#[cfg(test)]
832mod tests {
833    use super::*;
834    use bytes::BytesMut;
835
836    #[test]
837    fn test_property_id_from_u8() {
838        assert_eq!(
839            PropertyId::from_u8(0x01),
840            Some(PropertyId::PayloadFormatIndicator)
841        );
842        assert_eq!(PropertyId::from_u8(0x26), Some(PropertyId::UserProperty));
843        assert_eq!(
844            PropertyId::from_u8(0x2A),
845            Some(PropertyId::SharedSubscriptionAvailable)
846        );
847        assert_eq!(PropertyId::from_u8(0xFF), None);
848        assert_eq!(PropertyId::from_u8(0x00), None);
849    }
850
851    #[test]
852    fn test_property_allows_multiple() {
853        assert!(PropertyId::UserProperty.allows_multiple());
854        assert!(PropertyId::SubscriptionIdentifier.allows_multiple());
855        assert!(!PropertyId::PayloadFormatIndicator.allows_multiple());
856        assert!(!PropertyId::SessionExpiryInterval.allows_multiple());
857    }
858
859    #[test]
860    fn test_property_value_type() {
861        assert_eq!(
862            PropertyId::PayloadFormatIndicator.value_type(),
863            PropertyValueType::Byte
864        );
865        assert_eq!(
866            PropertyId::TopicAlias.value_type(),
867            PropertyValueType::TwoByteInteger
868        );
869        assert_eq!(
870            PropertyId::SessionExpiryInterval.value_type(),
871            PropertyValueType::FourByteInteger
872        );
873        assert_eq!(
874            PropertyId::SubscriptionIdentifier.value_type(),
875            PropertyValueType::VariableByteInteger
876        );
877        assert_eq!(
878            PropertyId::ContentType.value_type(),
879            PropertyValueType::Utf8String
880        );
881        assert_eq!(
882            PropertyId::CorrelationData.value_type(),
883            PropertyValueType::BinaryData
884        );
885        assert_eq!(
886            PropertyId::UserProperty.value_type(),
887            PropertyValueType::Utf8StringPair
888        );
889    }
890
891    #[test]
892    fn test_property_value_matches_type() {
893        let byte_val = PropertyValue::Byte(1);
894        assert!(byte_val.matches_type(PropertyValueType::Byte));
895        assert!(!byte_val.matches_type(PropertyValueType::TwoByteInteger));
896
897        let string_val = PropertyValue::Utf8String("test".to_string());
898        assert!(string_val.matches_type(PropertyValueType::Utf8String));
899        assert!(!string_val.matches_type(PropertyValueType::BinaryData));
900    }
901
902    #[test]
903    fn test_properties_add_valid() {
904        let mut props = Properties::new();
905
906        // Add single-value properties
907        props
908            .add(PropertyId::PayloadFormatIndicator, PropertyValue::Byte(1))
909            .unwrap();
910        props
911            .add(
912                PropertyId::SessionExpiryInterval,
913                PropertyValue::FourByteInteger(3600),
914            )
915            .unwrap();
916        props
917            .add(
918                PropertyId::ContentType,
919                PropertyValue::Utf8String("text/plain".to_string()),
920            )
921            .unwrap();
922
923        // Add multi-value property
924        props
925            .add(
926                PropertyId::UserProperty,
927                PropertyValue::Utf8StringPair("key1".to_string(), "value1".to_string()),
928            )
929            .unwrap();
930        props
931            .add(
932                PropertyId::UserProperty,
933                PropertyValue::Utf8StringPair("key2".to_string(), "value2".to_string()),
934            )
935            .unwrap();
936
937        assert_eq!(props.len(), 4); // 4 unique property IDs
938    }
939
940    #[test]
941    fn test_properties_add_type_mismatch() {
942        let mut props = Properties::new();
943
944        // Try to add wrong type
945        let result = props.add(
946            PropertyId::PayloadFormatIndicator,
947            PropertyValue::FourByteInteger(100),
948        );
949        assert!(result.is_err());
950    }
951
952    #[test]
953    fn test_properties_add_duplicate_single_value() {
954        let mut props = Properties::new();
955
956        // Add first time - should succeed
957        props
958            .add(PropertyId::PayloadFormatIndicator, PropertyValue::Byte(0))
959            .unwrap();
960
961        // Add second time - should fail
962        let result = props.add(PropertyId::PayloadFormatIndicator, PropertyValue::Byte(1));
963        assert!(result.is_err());
964    }
965
966    #[test]
967    fn test_properties_get() {
968        let mut props = Properties::new();
969        props
970            .add(
971                PropertyId::ContentType,
972                PropertyValue::Utf8String("text/html".to_string()),
973            )
974            .unwrap();
975
976        let value = props.get(PropertyId::ContentType).unwrap();
977        match value {
978            PropertyValue::Utf8String(s) => assert_eq!(s, "text/html"),
979            _ => panic!("Wrong value type"),
980        }
981
982        assert!(props.get(PropertyId::ResponseTopic).is_none());
983    }
984
985    #[test]
986    fn test_properties_get_all() {
987        let mut props = Properties::new();
988
989        // Add multiple user properties
990        props
991            .add(
992                PropertyId::UserProperty,
993                PropertyValue::Utf8StringPair("k1".to_string(), "v1".to_string()),
994            )
995            .unwrap();
996        props
997            .add(
998                PropertyId::UserProperty,
999                PropertyValue::Utf8StringPair("k2".to_string(), "v2".to_string()),
1000            )
1001            .unwrap();
1002
1003        let values = props.get_all(PropertyId::UserProperty).unwrap();
1004        assert_eq!(values.len(), 2);
1005    }
1006
1007    #[test]
1008    fn test_properties_encode_decode_empty() {
1009        let props = Properties::new();
1010        let mut buf = BytesMut::new();
1011
1012        props.encode(&mut buf).unwrap();
1013        assert_eq!(buf[0], 0); // Empty properties = 0 length
1014
1015        let decoded = Properties::decode(&mut buf).unwrap();
1016        assert!(decoded.is_empty());
1017    }
1018
1019    #[test]
1020    fn test_properties_encode_decode_single_values() {
1021        let mut props = Properties::new();
1022
1023        // Add various property types
1024        props
1025            .add(PropertyId::PayloadFormatIndicator, PropertyValue::Byte(1))
1026            .unwrap();
1027        props
1028            .add(PropertyId::TopicAlias, PropertyValue::TwoByteInteger(100))
1029            .unwrap();
1030        props
1031            .add(
1032                PropertyId::SessionExpiryInterval,
1033                PropertyValue::FourByteInteger(3600),
1034            )
1035            .unwrap();
1036        props
1037            .add(
1038                PropertyId::SubscriptionIdentifier,
1039                PropertyValue::VariableByteInteger(123),
1040            )
1041            .unwrap();
1042        props
1043            .add(
1044                PropertyId::ContentType,
1045                PropertyValue::Utf8String("text/plain".to_string()),
1046            )
1047            .unwrap();
1048        props
1049            .add(
1050                PropertyId::CorrelationData,
1051                PropertyValue::BinaryData(Bytes::from(vec![1, 2, 3, 4])),
1052            )
1053            .unwrap();
1054        props
1055            .add(
1056                PropertyId::UserProperty,
1057                PropertyValue::Utf8StringPair("key".to_string(), "value".to_string()),
1058            )
1059            .unwrap();
1060
1061        let mut buf = BytesMut::new();
1062        props.encode(&mut buf).unwrap();
1063
1064        let decoded = Properties::decode(&mut buf).unwrap();
1065        assert_eq!(decoded.len(), props.len());
1066
1067        // Verify each property
1068        match decoded.get(PropertyId::PayloadFormatIndicator).unwrap() {
1069            PropertyValue::Byte(v) => assert_eq!(*v, 1),
1070            _ => panic!("Wrong type"),
1071        }
1072
1073        match decoded.get(PropertyId::TopicAlias).unwrap() {
1074            PropertyValue::TwoByteInteger(v) => assert_eq!(*v, 100),
1075            _ => panic!("Wrong type"),
1076        }
1077
1078        match decoded.get(PropertyId::ContentType).unwrap() {
1079            PropertyValue::Utf8String(v) => assert_eq!(v, "text/plain"),
1080            _ => panic!("Wrong type"),
1081        }
1082    }
1083
1084    #[test]
1085    fn test_properties_encode_decode_multiple_values() {
1086        let mut props = Properties::new();
1087
1088        // Add multiple user properties
1089        props
1090            .add(
1091                PropertyId::UserProperty,
1092                PropertyValue::Utf8StringPair("env".to_string(), "prod".to_string()),
1093            )
1094            .unwrap();
1095        props
1096            .add(
1097                PropertyId::UserProperty,
1098                PropertyValue::Utf8StringPair("version".to_string(), "1.0".to_string()),
1099            )
1100            .unwrap();
1101
1102        // Add multiple subscription identifiers
1103        props
1104            .add(
1105                PropertyId::SubscriptionIdentifier,
1106                PropertyValue::VariableByteInteger(10),
1107            )
1108            .unwrap();
1109        props
1110            .add(
1111                PropertyId::SubscriptionIdentifier,
1112                PropertyValue::VariableByteInteger(20),
1113            )
1114            .unwrap();
1115
1116        let mut buf = BytesMut::new();
1117        props.encode(&mut buf).unwrap();
1118
1119        let decoded = Properties::decode(&mut buf).unwrap();
1120
1121        let user_props = decoded.get_all(PropertyId::UserProperty).unwrap();
1122        assert_eq!(user_props.len(), 2);
1123
1124        let sub_ids = decoded.get_all(PropertyId::SubscriptionIdentifier).unwrap();
1125        assert_eq!(sub_ids.len(), 2);
1126    }
1127
1128    #[test]
1129    fn test_properties_decode_invalid_property_id() {
1130        let mut buf = BytesMut::new();
1131        buf.put_u8(1); // Properties length
1132        buf.put_u8(0xFF); // Invalid property ID
1133
1134        let result = Properties::decode(&mut buf);
1135        assert!(result.is_err());
1136    }
1137
1138    #[test]
1139    fn test_properties_decode_insufficient_data() {
1140        let mut buf = BytesMut::new();
1141        buf.put_u8(10); // Claims 10 bytes but buffer is empty
1142
1143        let result = Properties::decode(&mut buf);
1144        assert!(result.is_err());
1145    }
1146
1147    #[test]
1148    fn test_properties_encoded_len() {
1149        let mut props = Properties::new();
1150        props
1151            .add(PropertyId::PayloadFormatIndicator, PropertyValue::Byte(1))
1152            .unwrap();
1153        props
1154            .add(
1155                PropertyId::ContentType,
1156                PropertyValue::Utf8String("test".to_string()),
1157            )
1158            .unwrap();
1159
1160        let mut buf = BytesMut::new();
1161        props.encode(&mut buf).unwrap();
1162
1163        assert_eq!(props.encoded_len(), buf.len());
1164    }
1165
1166    #[test]
1167    fn test_all_property_ids_have_correct_types() {
1168        // Test that all property IDs in from_u8 have matching value types
1169        for id in 0u8..=0x2A {
1170            if let Some(prop_id) = PropertyId::from_u8(id) {
1171                // Just verify it has a value type (no panic)
1172                let _ = prop_id.value_type();
1173            }
1174        }
1175    }
1176}