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