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}