mqute_codec/protocol/v5/
unsuback.rs

1//! # Unsubscribe Acknowledgment (UnsubAck) Packet - MQTT v5
2//!
3//! This module implements the MQTT v5 `UnsubAck` packet, which is sent by the server
4//! to acknowledge receipt and processing of an UNSUBSCRIBE packet. The `UnsubAck` packet
5//! contains return codes indicating the result of each unsubscription request.
6
7use crate::Error;
8use crate::codec::{Decode, Encode, RawPacket};
9use crate::protocol::v5::reason::ReasonCode;
10use crate::protocol::v5::util::{ack_properties, ack_properties_frame_impl, id_header};
11use crate::protocol::{Codes, FixedHeader, PacketType};
12use bytes::{Buf, Bytes, BytesMut};
13
14// Defines properties specific to `UnsubAck` packets
15ack_properties!(UnsubAckProperties);
16
17// Implements the PropertyFrame trait for UnsubAckProperties
18ack_properties_frame_impl!(UnsubAckProperties);
19
20/// Validates reason codes for `UnsubAck` packets
21///
22/// MQTT v5 specifies the following valid reason codes for `UnsubAck`:
23/// - 0x00 (Success) - Unsubscription successful
24/// - 0x11 (No subscription existed) - No matching subscription found
25/// - 0x80 (Unspecified error) - Unspecified error condition
26/// - 0x83 (Implementation specific error) - Implementation-specific error
27/// - 0x87 (Not authorized) - Client not authorized
28/// - 0x8F (Topic Filter invalid) - Invalid topic filter format
29/// - 0x91 (Packet Identifier in use) - Duplicate packet ID
30fn validate_unsuback_reason_code(code: ReasonCode) -> bool {
31    matches!(code.into(), 0 | 17 | 128 | 131 | 135 | 143 | 145)
32}
33
34// Internal header structure for `UnsubAck` packets
35id_header!(UnsubAckHeader, UnsubAckProperties);
36
37/// Represents an MQTT v5 `UnsubAck` packet
38///
39/// The `UnsubAck` packet is sent by the server to acknowledge receipt and processing
40/// of an UNSUBSCRIBE packet. It contains:
41/// - Packet Identifier matching the UNSUBSCRIBE packet
42/// - List of return codes indicating unsubscription results
43/// - Optional properties (v5 only)
44///
45/// # Example
46///
47/// ```rust
48/// use mqute_codec::protocol::Codes;
49/// use mqute_codec::protocol::v5::{UnsubAck, ReasonCode};
50///
51/// // Successful unsubscription
52/// let unsuback = UnsubAck::new(
53///     1234,
54///     None,
55///     vec![ReasonCode::Success, ReasonCode::Success]
56/// );
57///
58/// assert_eq!(unsuback.packet_id(), 1234u16);
59///
60/// // Mixed results unsubscription
61/// let unsuback = UnsubAck::new(
62///     5678,
63///     None,
64///     vec![
65///         ReasonCode::Success,
66///         ReasonCode::NoSubscriptionExisted
67///     ]
68/// );
69///
70/// let codes = Codes::new(vec![ReasonCode::Success, ReasonCode::NoSubscriptionExisted]);
71/// assert_eq!(unsuback.codes(), codes);
72/// ```
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct UnsubAck {
75    header: UnsubAckHeader,
76    codes: Codes<ReasonCode>,
77}
78
79impl UnsubAck {
80    /// Creates a new `UnsubAck` packet
81    ///
82    /// # Panics
83    /// - If no reason codes are provided
84    /// - If any reason code is invalid for `UnsubAck`
85    pub fn new<T>(packet_id: u16, properties: Option<UnsubAckProperties>, codes: T) -> Self
86    where
87        T: IntoIterator<Item = ReasonCode>,
88    {
89        let codes: Vec<ReasonCode> = codes.into_iter().collect();
90
91        if codes.is_empty() {
92            panic!("At least one reason code is required");
93        }
94
95        if !codes
96            .iter()
97            .all(|&code| validate_unsuback_reason_code(code))
98        {
99            panic!("Invalid reason code");
100        }
101
102        let header = UnsubAckHeader::new(packet_id, properties);
103
104        UnsubAck {
105            header,
106            codes: codes.into(),
107        }
108    }
109
110    /// Returns the packet identifier
111    pub fn packet_id(&self) -> u16 {
112        self.header.packet_id
113    }
114
115    /// Returns the list of reason codes
116    pub fn codes(&self) -> Codes<ReasonCode> {
117        self.codes.clone()
118    }
119
120    /// Returns a copy of the properties (if any)
121    pub fn properties(&self) -> Option<UnsubAckProperties> {
122        self.header.properties.clone()
123    }
124}
125
126impl Encode for UnsubAck {
127    /// Encodes the `UnsubAck` packet into a byte buffer
128    fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
129        let header = FixedHeader::new(PacketType::UnsubAck, self.payload_len());
130        header.encode(buf)?;
131
132        self.header.encode(buf)?;
133        self.codes.encode(buf);
134        Ok(())
135    }
136
137    /// Calculates the total packet length
138    fn payload_len(&self) -> usize {
139        self.header.encoded_len() + self.codes.len()
140    }
141}
142
143impl Decode for UnsubAck {
144    /// Decodes an `UnsubAck` packet from raw bytes
145    fn decode(mut packet: RawPacket) -> Result<Self, Error> {
146        if packet.header.packet_type() != PacketType::UnsubAck
147            || !packet.header.flags().is_default()
148        {
149            return Err(Error::MalformedPacket);
150        }
151
152        let header = UnsubAckHeader::decode(&mut packet.payload)?;
153        let codes = Codes::decode(&mut packet.payload)?;
154
155        let codes: Vec<ReasonCode> = codes.into();
156
157        if !codes
158            .iter()
159            .all(|&code| validate_unsuback_reason_code(code))
160        {
161            return Err(Error::MalformedPacket);
162        }
163
164        Ok(UnsubAck {
165            header,
166            codes: codes.into(),
167        })
168    }
169}