mqtt5_protocol/packet/
suback.rs

1use crate::error::{MqttError, Result};
2use crate::packet::{FixedHeader, MqttPacket, PacketType};
3use crate::protocol::v5::properties::Properties;
4use crate::QoS;
5use bytes::{Buf, BufMut};
6
7/// MQTT SUBACK packet
8#[derive(Debug, Clone)]
9pub struct SubAckPacket {
10    /// Packet identifier
11    pub packet_id: u16,
12    /// Reason codes for each subscription
13    pub reason_codes: Vec<SubAckReasonCode>,
14    /// SUBACK properties (v5.0 only)
15    pub properties: Properties,
16}
17
18/// SUBACK reason codes
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[repr(u8)]
21pub enum SubAckReasonCode {
22    /// Maximum `QoS` 0
23    GrantedQoS0 = 0x00,
24    /// Maximum `QoS` 1
25    GrantedQoS1 = 0x01,
26    /// Maximum `QoS` 2
27    GrantedQoS2 = 0x02,
28    /// Unspecified error
29    UnspecifiedError = 0x80,
30    /// Implementation specific error
31    ImplementationSpecificError = 0x83,
32    /// Not authorized
33    NotAuthorized = 0x87,
34    /// Topic filter invalid
35    TopicFilterInvalid = 0x8F,
36    /// Packet identifier in use
37    PacketIdentifierInUse = 0x91,
38    /// Quota exceeded
39    QuotaExceeded = 0x97,
40    /// Shared subscriptions not supported
41    SharedSubscriptionsNotSupported = 0x9E,
42    /// Subscription identifiers not supported
43    SubscriptionIdentifiersNotSupported = 0xA1,
44    /// Wildcard subscriptions not supported
45    WildcardSubscriptionsNotSupported = 0xA2,
46}
47
48impl SubAckReasonCode {
49    /// Creates a reason code from a granted `QoS` level
50    #[must_use]
51    pub fn from_qos(qos: QoS) -> Self {
52        match qos {
53            QoS::AtMostOnce => Self::GrantedQoS0,
54            QoS::AtLeastOnce => Self::GrantedQoS1,
55            QoS::ExactlyOnce => Self::GrantedQoS2,
56        }
57    }
58
59    /// Converts a u8 to a `SubAckReasonCode`
60    #[must_use]
61    pub fn from_u8(value: u8) -> Option<Self> {
62        match value {
63            0x00 => Some(Self::GrantedQoS0),
64            0x01 => Some(Self::GrantedQoS1),
65            0x02 => Some(Self::GrantedQoS2),
66            0x80 => Some(Self::UnspecifiedError),
67            0x83 => Some(Self::ImplementationSpecificError),
68            0x87 => Some(Self::NotAuthorized),
69            0x8F => Some(Self::TopicFilterInvalid),
70            0x91 => Some(Self::PacketIdentifierInUse),
71            0x97 => Some(Self::QuotaExceeded),
72            0x9E => Some(Self::SharedSubscriptionsNotSupported),
73            0xA1 => Some(Self::SubscriptionIdentifiersNotSupported),
74            0xA2 => Some(Self::WildcardSubscriptionsNotSupported),
75            _ => None,
76        }
77    }
78
79    /// Returns true if this is a success code
80    #[must_use]
81    pub fn is_success(&self) -> bool {
82        matches!(
83            self,
84            Self::GrantedQoS0 | Self::GrantedQoS1 | Self::GrantedQoS2
85        )
86    }
87
88    /// Returns the granted `QoS` level if this is a success code
89    #[must_use]
90    pub fn granted_qos(&self) -> Option<QoS> {
91        match self {
92            Self::GrantedQoS0 => Some(QoS::AtMostOnce),
93            Self::GrantedQoS1 => Some(QoS::AtLeastOnce),
94            Self::GrantedQoS2 => Some(QoS::ExactlyOnce),
95            _ => None,
96        }
97    }
98}
99
100impl SubAckPacket {
101    /// Creates a new SUBACK packet
102    #[must_use]
103    pub fn new(packet_id: u16) -> Self {
104        Self {
105            packet_id,
106            reason_codes: Vec::new(),
107            properties: Properties::default(),
108        }
109    }
110
111    /// Adds a reason code
112    #[must_use]
113    pub fn add_reason_code(mut self, code: SubAckReasonCode) -> Self {
114        self.reason_codes.push(code);
115        self
116    }
117
118    /// Adds a reason code for a granted `QoS`
119    #[must_use]
120    pub fn add_granted_qos(mut self, qos: QoS) -> Self {
121        self.reason_codes.push(SubAckReasonCode::from_qos(qos));
122        self
123    }
124
125    /// Sets the reason string
126    #[must_use]
127    pub fn with_reason_string(mut self, reason: String) -> Self {
128        self.properties.set_reason_string(reason);
129        self
130    }
131
132    /// Adds a user property
133    #[must_use]
134    pub fn with_user_property(mut self, key: String, value: String) -> Self {
135        self.properties.add_user_property(key, value);
136        self
137    }
138}
139
140impl MqttPacket for SubAckPacket {
141    fn packet_type(&self) -> PacketType {
142        PacketType::SubAck
143    }
144
145    fn encode_body<B: BufMut>(&self, buf: &mut B) -> Result<()> {
146        // Variable header
147        buf.put_u16(self.packet_id);
148
149        // Properties (v5.0)
150        self.properties.encode(buf)?;
151
152        // Payload - reason codes
153        if self.reason_codes.is_empty() {
154            return Err(MqttError::MalformedPacket(
155                "SUBACK packet must contain at least one reason code".to_string(),
156            ));
157        }
158
159        for code in &self.reason_codes {
160            buf.put_u8(*code as u8);
161        }
162
163        Ok(())
164    }
165
166    fn decode_body<B: Buf>(buf: &mut B, _fixed_header: &FixedHeader) -> Result<Self> {
167        // Packet identifier
168        if buf.remaining() < 2 {
169            return Err(MqttError::MalformedPacket(
170                "SUBACK missing packet identifier".to_string(),
171            ));
172        }
173        let packet_id = buf.get_u16();
174
175        // Properties (v5.0)
176        let properties = Properties::decode(buf)?;
177
178        // Payload - reason codes
179        let mut reason_codes = Vec::new();
180
181        if !buf.has_remaining() {
182            return Err(MqttError::MalformedPacket(
183                "SUBACK packet must contain at least one reason code".to_string(),
184            ));
185        }
186
187        while buf.has_remaining() {
188            let code_byte = buf.get_u8();
189            let code = SubAckReasonCode::from_u8(code_byte).ok_or_else(|| {
190                MqttError::MalformedPacket(format!("Invalid SUBACK reason code: 0x{code_byte:02X}"))
191            })?;
192            reason_codes.push(code);
193        }
194
195        Ok(Self {
196            packet_id,
197            reason_codes,
198            properties,
199        })
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::protocol::v5::properties::PropertyId;
207    use bytes::BytesMut;
208
209    #[test]
210    fn test_suback_reason_code_from_qos() {
211        assert_eq!(
212            SubAckReasonCode::from_qos(QoS::AtMostOnce),
213            SubAckReasonCode::GrantedQoS0
214        );
215        assert_eq!(
216            SubAckReasonCode::from_qos(QoS::AtLeastOnce),
217            SubAckReasonCode::GrantedQoS1
218        );
219        assert_eq!(
220            SubAckReasonCode::from_qos(QoS::ExactlyOnce),
221            SubAckReasonCode::GrantedQoS2
222        );
223    }
224
225    #[test]
226    fn test_suback_reason_code_is_success() {
227        assert!(SubAckReasonCode::GrantedQoS0.is_success());
228        assert!(SubAckReasonCode::GrantedQoS1.is_success());
229        assert!(SubAckReasonCode::GrantedQoS2.is_success());
230        assert!(!SubAckReasonCode::NotAuthorized.is_success());
231        assert!(!SubAckReasonCode::TopicFilterInvalid.is_success());
232    }
233
234    #[test]
235    fn test_suback_basic() {
236        let packet = SubAckPacket::new(123)
237            .add_granted_qos(QoS::AtLeastOnce)
238            .add_granted_qos(QoS::ExactlyOnce)
239            .add_reason_code(SubAckReasonCode::NotAuthorized);
240
241        assert_eq!(packet.packet_id, 123);
242        assert_eq!(packet.reason_codes.len(), 3);
243        assert_eq!(packet.reason_codes[0], SubAckReasonCode::GrantedQoS1);
244        assert_eq!(packet.reason_codes[1], SubAckReasonCode::GrantedQoS2);
245        assert_eq!(packet.reason_codes[2], SubAckReasonCode::NotAuthorized);
246    }
247
248    #[test]
249    fn test_suback_encode_decode() {
250        let packet = SubAckPacket::new(789)
251            .add_granted_qos(QoS::AtMostOnce)
252            .add_granted_qos(QoS::AtLeastOnce)
253            .add_reason_code(SubAckReasonCode::TopicFilterInvalid)
254            .with_reason_string("Invalid wildcard usage".to_string());
255
256        let mut buf = BytesMut::new();
257        packet.encode(&mut buf).unwrap();
258
259        let fixed_header = FixedHeader::decode(&mut buf).unwrap();
260        assert_eq!(fixed_header.packet_type, PacketType::SubAck);
261
262        let decoded = SubAckPacket::decode_body(&mut buf, &fixed_header).unwrap();
263        assert_eq!(decoded.packet_id, 789);
264        assert_eq!(decoded.reason_codes.len(), 3);
265        assert_eq!(decoded.reason_codes[0], SubAckReasonCode::GrantedQoS0);
266        assert_eq!(decoded.reason_codes[1], SubAckReasonCode::GrantedQoS1);
267        assert_eq!(
268            decoded.reason_codes[2],
269            SubAckReasonCode::TopicFilterInvalid
270        );
271
272        let reason_str = decoded.properties.get(PropertyId::ReasonString);
273        assert!(reason_str.is_some());
274    }
275
276    #[test]
277    fn test_suback_empty_reason_codes() {
278        let packet = SubAckPacket::new(123);
279
280        let mut buf = BytesMut::new();
281        let result = packet.encode(&mut buf);
282        assert!(result.is_err());
283    }
284}