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