mqtt5_protocol/packet/
unsuback.rs

1use crate::error::{MqttError, Result};
2use crate::packet::{FixedHeader, MqttPacket, PacketType};
3use crate::protocol::v5::properties::Properties;
4use bytes::{Buf, BufMut};
5
6/// MQTT UNSUBACK packet
7#[derive(Debug, Clone)]
8pub struct UnsubAckPacket {
9    /// Packet identifier
10    pub packet_id: u16,
11    /// Reason codes for each unsubscription
12    pub reason_codes: Vec<UnsubAckReasonCode>,
13    /// UNSUBACK properties (v5.0 only)
14    pub properties: Properties,
15    /// Protocol version (4 = v3.1.1, 5 = v5.0)
16    pub protocol_version: u8,
17}
18
19/// UNSUBACK reason codes
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[repr(u8)]
22pub enum UnsubAckReasonCode {
23    /// Success
24    Success = 0x00,
25    /// No subscription existed
26    NoSubscriptionExisted = 0x11,
27    /// Unspecified error
28    UnspecifiedError = 0x80,
29    /// Implementation specific error
30    ImplementationSpecificError = 0x83,
31    /// Not authorized
32    NotAuthorized = 0x87,
33    /// Topic filter invalid
34    TopicFilterInvalid = 0x8F,
35    /// Packet identifier in use
36    PacketIdentifierInUse = 0x91,
37}
38
39impl UnsubAckReasonCode {
40    /// Converts a u8 to an `UnsubAckReasonCode`
41    #[must_use]
42    pub fn from_u8(value: u8) -> Option<Self> {
43        match value {
44            0x00 => Some(Self::Success),
45            0x11 => Some(Self::NoSubscriptionExisted),
46            0x80 => Some(Self::UnspecifiedError),
47            0x83 => Some(Self::ImplementationSpecificError),
48            0x87 => Some(Self::NotAuthorized),
49            0x8F => Some(Self::TopicFilterInvalid),
50            0x91 => Some(Self::PacketIdentifierInUse),
51            _ => None,
52        }
53    }
54
55    /// Returns true if this is a success code
56    #[must_use]
57    pub fn is_success(&self) -> bool {
58        matches!(self, Self::Success)
59    }
60}
61
62impl UnsubAckPacket {
63    /// Creates a new UNSUBACK packet (v5.0)
64    #[must_use]
65    pub fn new(packet_id: u16) -> Self {
66        Self {
67            packet_id,
68            reason_codes: Vec::new(),
69            properties: Properties::default(),
70            protocol_version: 5,
71        }
72    }
73
74    /// Creates a new UNSUBACK packet for v3.1.1
75    #[must_use]
76    pub fn new_v311(packet_id: u16) -> Self {
77        Self {
78            packet_id,
79            reason_codes: Vec::new(),
80            properties: Properties::default(),
81            protocol_version: 4,
82        }
83    }
84
85    /// Adds a reason code
86    #[must_use]
87    pub fn add_reason_code(mut self, code: UnsubAckReasonCode) -> Self {
88        self.reason_codes.push(code);
89        self
90    }
91
92    /// Adds a success reason code
93    #[must_use]
94    pub fn add_success(mut self) -> Self {
95        self.reason_codes.push(UnsubAckReasonCode::Success);
96        self
97    }
98
99    /// Sets the reason string
100    #[must_use]
101    pub fn with_reason_string(mut self, reason: String) -> Self {
102        self.properties.set_reason_string(reason);
103        self
104    }
105
106    /// Adds a user property
107    #[must_use]
108    pub fn with_user_property(mut self, key: String, value: String) -> Self {
109        self.properties.add_user_property(key, value);
110        self
111    }
112}
113
114impl MqttPacket for UnsubAckPacket {
115    fn packet_type(&self) -> PacketType {
116        PacketType::UnsubAck
117    }
118
119    fn encode_body<B: BufMut>(&self, buf: &mut B) -> Result<()> {
120        buf.put_u16(self.packet_id);
121
122        if self.protocol_version == 5 {
123            self.properties.encode(buf)?;
124
125            if self.reason_codes.is_empty() {
126                return Err(MqttError::MalformedPacket(
127                    "UNSUBACK packet must contain at least one reason code".to_string(),
128                ));
129            }
130
131            for code in &self.reason_codes {
132                buf.put_u8(*code as u8);
133            }
134        }
135
136        Ok(())
137    }
138
139    fn decode_body<B: Buf>(buf: &mut B, _fixed_header: &FixedHeader) -> Result<Self> {
140        // Packet identifier
141        if buf.remaining() < 2 {
142            return Err(MqttError::MalformedPacket(
143                "UNSUBACK missing packet identifier".to_string(),
144            ));
145        }
146        let packet_id = buf.get_u16();
147
148        // Properties (v5.0)
149        let properties = Properties::decode(buf)?;
150
151        // Payload - reason codes
152        let mut reason_codes = Vec::new();
153
154        if !buf.has_remaining() {
155            return Err(MqttError::MalformedPacket(
156                "UNSUBACK packet must contain at least one reason code".to_string(),
157            ));
158        }
159
160        while buf.has_remaining() {
161            let code_byte = buf.get_u8();
162            let code = UnsubAckReasonCode::from_u8(code_byte).ok_or_else(|| {
163                MqttError::MalformedPacket(format!(
164                    "Invalid UNSUBACK reason code: 0x{code_byte:02X}"
165                ))
166            })?;
167            reason_codes.push(code);
168        }
169
170        Ok(Self {
171            packet_id,
172            reason_codes,
173            properties,
174            protocol_version: 5,
175        })
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::protocol::v5::properties::PropertyId;
183    use bytes::BytesMut;
184
185    #[test]
186    fn test_unsuback_reason_code_is_success() {
187        assert!(UnsubAckReasonCode::Success.is_success());
188        assert!(!UnsubAckReasonCode::NoSubscriptionExisted.is_success());
189        assert!(!UnsubAckReasonCode::NotAuthorized.is_success());
190    }
191
192    #[test]
193    fn test_unsuback_basic() {
194        let packet = UnsubAckPacket::new(123)
195            .add_success()
196            .add_success()
197            .add_reason_code(UnsubAckReasonCode::NoSubscriptionExisted);
198
199        assert_eq!(packet.packet_id, 123);
200        assert_eq!(packet.reason_codes.len(), 3);
201        assert_eq!(packet.reason_codes[0], UnsubAckReasonCode::Success);
202        assert_eq!(packet.reason_codes[1], UnsubAckReasonCode::Success);
203        assert_eq!(
204            packet.reason_codes[2],
205            UnsubAckReasonCode::NoSubscriptionExisted
206        );
207    }
208
209    #[test]
210    fn test_unsuback_encode_decode() {
211        let packet = UnsubAckPacket::new(789)
212            .add_success()
213            .add_reason_code(UnsubAckReasonCode::NotAuthorized)
214            .add_reason_code(UnsubAckReasonCode::TopicFilterInvalid)
215            .with_reason_string("Invalid topic filter".to_string());
216
217        let mut buf = BytesMut::new();
218        packet.encode(&mut buf).unwrap();
219
220        let fixed_header = FixedHeader::decode(&mut buf).unwrap();
221        assert_eq!(fixed_header.packet_type, PacketType::UnsubAck);
222
223        let decoded = UnsubAckPacket::decode_body(&mut buf, &fixed_header).unwrap();
224        assert_eq!(decoded.packet_id, 789);
225        assert_eq!(decoded.reason_codes.len(), 3);
226        assert_eq!(decoded.reason_codes[0], UnsubAckReasonCode::Success);
227        assert_eq!(decoded.reason_codes[1], UnsubAckReasonCode::NotAuthorized);
228        assert_eq!(
229            decoded.reason_codes[2],
230            UnsubAckReasonCode::TopicFilterInvalid
231        );
232
233        let reason_str = decoded.properties.get(PropertyId::ReasonString);
234        assert!(reason_str.is_some());
235    }
236
237    #[test]
238    fn test_unsuback_empty_reason_codes() {
239        let packet = UnsubAckPacket::new(123);
240
241        let mut buf = BytesMut::new();
242        let result = packet.encode(&mut buf);
243        assert!(result.is_err());
244    }
245}