mqute_codec/protocol/v5/
disconnect.rs

1//! # Disconnect Packet - MQTT v5
2//!
3//! This module implements the MQTT v5 `Disconnect` packet, which is used to gracefully
4//! terminate a connection between client and server. The `Disconnect` packet can include
5//! a reason code and optional properties to provide additional context for the disconnection.
6
7use crate::Error;
8use crate::codec::util::{decode_byte, decode_variable_integer, encode_variable_integer};
9use crate::codec::{Decode, Encode, RawPacket};
10use crate::protocol::util::len_bytes;
11use crate::protocol::v5::property::{
12    Property, PropertyFrame, property_decode, property_encode, property_len,
13};
14use crate::protocol::v5::reason::ReasonCode;
15use crate::protocol::{FixedHeader, PacketType};
16use bytes::{Buf, BufMut, Bytes, BytesMut};
17use std::time::Duration;
18
19/// Validates reason codes for `Disconnect` packets
20///
21/// MQTT v5 specifies the following valid reason codes:
22/// - 0x00 (Normal Disconnection)
23/// - 0x04 (Disconnect With Will Message)
24/// - 0x80-0x83 (Various error conditions)
25/// - 0x87-0x93 (Protocol and implementation errors)
26/// - 0x97-0xA2 (Administrative and policy violations)
27fn validate_disconnect_reason_code(code: ReasonCode) -> bool {
28    matches!(code.into(), 0 | 4 | 128..=131 | 135 | 137 | 139 | 141..=144 | 147..=162)
29}
30
31/// Represents properties of a `Disconnect` packet
32///
33/// # Example
34///
35/// ```rust
36///
37/// use mqute_codec::protocol::v5::DisconnectProperties;
38///
39/// let properties = DisconnectProperties {
40///     reason_string: Some("Reason string".to_string()),
41///     server_reference: Some("backup.example.com".to_string()),
42///     ..Default::default()
43/// };
44///
45/// ```
46#[derive(Debug, Default, Clone, PartialEq, Eq)]
47pub struct DisconnectProperties {
48    /// Duration in seconds until session expires
49    pub session_expiry_interval: Option<Duration>,
50    /// Human-readable disconnection reason
51    pub reason_string: Option<String>,
52    /// User-defined key-value properties
53    pub user_properties: Vec<(String, String)>,
54    /// Alternative server reference (for redirection)
55    pub server_reference: Option<String>,
56}
57
58impl PropertyFrame for DisconnectProperties {
59    /// Calculates the encoded length of the properties
60    fn encoded_len(&self) -> usize {
61        let mut len = 0usize;
62
63        len += property_len!(&self.session_expiry_interval);
64        len += property_len!(&self.reason_string);
65        len += property_len!(&self.user_properties);
66        len += property_len!(&self.server_reference);
67
68        len
69    }
70
71    /// Encodes the properties into a byte buffer
72    fn encode(&self, buf: &mut BytesMut) {
73        property_encode!(
74            &self.session_expiry_interval,
75            Property::SessionExpiryInterval,
76            buf
77        );
78
79        property_encode!(&self.reason_string, Property::ReasonString, buf);
80        property_encode!(&self.user_properties, Property::UserProp, buf);
81        property_encode!(&self.server_reference, Property::ServerReference, buf);
82    }
83
84    /// Decodes properties from a byte buffer
85    fn decode(buf: &mut Bytes) -> Result<Option<Self>, Error>
86    where
87        Self: Sized,
88    {
89        if buf.is_empty() {
90            return Ok(None);
91        }
92        let mut session_expiry_interval: Option<Duration> = None;
93        let mut reason_string: Option<String> = None;
94        let mut user_properties: Vec<(String, String)> = Vec::new();
95        let mut server_reference: Option<String> = None;
96
97        while buf.has_remaining() {
98            let property: Property = decode_byte(buf)?.try_into()?;
99            match property {
100                Property::SessionExpiryInterval => {
101                    property_decode!(&mut session_expiry_interval, buf);
102                }
103                Property::ReasonString => {
104                    property_decode!(&mut reason_string, buf);
105                }
106                Property::UserProp => {
107                    property_decode!(&mut user_properties, buf);
108                }
109                Property::ServerReference => {
110                    property_decode!(&mut server_reference, buf);
111                }
112                _ => return Err(Error::PropertyMismatch),
113            }
114        }
115
116        Ok(Some(DisconnectProperties {
117            session_expiry_interval,
118            reason_string,
119            user_properties,
120            server_reference,
121        }))
122    }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126struct DisconnectHeader {
127    code: ReasonCode,
128    properties: Option<DisconnectProperties>,
129}
130
131impl DisconnectHeader {
132    pub(crate) fn new(code: ReasonCode, properties: Option<DisconnectProperties>) -> Self {
133        if !validate_disconnect_reason_code(code) {
134            panic!("Invalid reason code {code}")
135        }
136        DisconnectHeader { code, properties }
137    }
138
139    pub(crate) fn encoded_len(&self) -> usize {
140        if self.code == ReasonCode::NormalDisconnection && self.properties.is_none() {
141            return 0;
142        }
143
144        let properties_len = self
145            .properties
146            .as_ref()
147            .map(|properties| properties.encoded_len())
148            .unwrap_or(0);
149
150        1 + len_bytes(properties_len) + properties_len
151    }
152
153    pub(crate) fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
154        // The reason code and property can be omitted if the code is 0x00 and there are no properties
155        if self.code == ReasonCode::NormalDisconnection && self.properties.is_none() {
156            return Ok(());
157        }
158
159        buf.put_u8(self.code.into());
160
161        let properties_len = self
162            .properties
163            .as_ref()
164            .map(|properties| properties.encoded_len())
165            .unwrap_or(0) as u32;
166
167        // Encode properties len
168        encode_variable_integer(buf, properties_len)?;
169
170        // Encode properties
171        if let Some(properties) = self.properties.as_ref() {
172            properties.encode(buf);
173        }
174
175        Ok(())
176    }
177
178    pub(crate) fn decode(payload: &mut Bytes) -> Result<Self, Error> {
179        if payload.is_empty() {
180            return Ok(DisconnectHeader {
181                code: ReasonCode::NormalDisconnection,
182                properties: None,
183            });
184        }
185
186        let code: ReasonCode = decode_byte(payload)?.try_into().map(|code| {
187            if code == ReasonCode::Success {
188                ReasonCode::NormalDisconnection
189            } else {
190                code
191            }
192        })?;
193
194        if !validate_disconnect_reason_code(code) {
195            return Err(Error::InvalidReasonCode(code.into()));
196        }
197
198        let properties_len = decode_variable_integer(payload)? as usize;
199        if payload.len() < properties_len + len_bytes(properties_len) {
200            return Err(Error::MalformedPacket);
201        }
202
203        // Skip variable byte
204        payload.advance(len_bytes(properties_len));
205
206        let mut properties_buf = payload.split_to(properties_len);
207
208        // Deserialize properties
209        let properties = DisconnectProperties::decode(&mut properties_buf)?;
210        Ok(DisconnectHeader { code, properties })
211    }
212}
213
214/// Represents an MQTT v5 `Disconnect` packet.
215///
216/// The `Disconnect` packet is the final MQTT Control Packet sent from the Client
217/// or the Server. It indicates the reason why the Network Connection is being closed
218///
219/// # Example
220///
221/// ```rust
222/// use mqute_codec::protocol::v5::{Disconnect, DisconnectProperties, ReasonCode};
223///
224/// let properties = DisconnectProperties {
225///     reason_string: Some("Reason string".to_string()),
226///     server_reference: Some("backup.example.com".to_string()),
227///     ..Default::default()
228/// };
229///
230/// let disconnect = Disconnect::new(ReasonCode::Success, Some(properties.clone()));
231/// assert_eq!(disconnect.properties(), Some(properties));
232/// ```
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct Disconnect {
235    header: DisconnectHeader,
236}
237
238impl Disconnect {
239    /// Creates a new `Disconnect` packet
240    pub fn new(code: ReasonCode, properties: Option<DisconnectProperties>) -> Self {
241        Disconnect {
242            header: DisconnectHeader::new(code, properties),
243        }
244    }
245
246    /// Returns the reason code
247    pub fn code(&self) -> ReasonCode {
248        self.header.code
249    }
250
251    /// Returns a copy of the properties (if any)
252    pub fn properties(&self) -> Option<DisconnectProperties> {
253        self.header.properties.clone()
254    }
255}
256
257impl Encode for Disconnect {
258    /// Encodes the `Disconnect` packet into a byte buffer
259    fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
260        let header = FixedHeader::new(PacketType::Disconnect, self.payload_len());
261        header.encode(buf)?;
262
263        self.header.encode(buf)
264    }
265
266    /// Calculates the payload length
267    fn payload_len(&self) -> usize {
268        self.header.encoded_len()
269    }
270}
271
272impl Decode for Disconnect {
273    /// Decodes a `Disconnect` packet from raw bytes
274    fn decode(mut packet: RawPacket) -> Result<Self, Error> {
275        if packet.header.packet_type() != PacketType::Disconnect
276            || !packet.header.flags().is_default()
277        {
278            return Err(Error::MalformedPacket);
279        }
280
281        let header = DisconnectHeader::decode(&mut packet.payload)?;
282        Ok(Disconnect { header })
283    }
284}