mqtt5_protocol/packet/
disconnect.rs

1use crate::error::{MqttError, Result};
2use crate::packet::{FixedHeader, MqttPacket, PacketType};
3use crate::protocol::v5::properties::Properties;
4use crate::protocol::v5::reason_codes::NORMAL_DISCONNECTION;
5use crate::types::ReasonCode;
6use bytes::{Buf, BufMut};
7
8/// MQTT DISCONNECT packet
9#[derive(Debug, Clone)]
10pub struct DisconnectPacket {
11    /// Disconnect reason code
12    pub reason_code: ReasonCode,
13    /// DISCONNECT properties (v5.0 only)
14    pub properties: Properties,
15}
16
17impl DisconnectPacket {
18    /// Creates a new DISCONNECT packet
19    #[must_use]
20    pub fn new(reason_code: ReasonCode) -> Self {
21        Self {
22            reason_code,
23            properties: Properties::default(),
24        }
25    }
26
27    /// Creates a normal disconnection packet
28    #[must_use]
29    pub fn normal() -> Self {
30        Self::new(NORMAL_DISCONNECTION)
31    }
32
33    /// Sets the session expiry interval
34    #[must_use]
35    pub fn with_session_expiry_interval(mut self, seconds: u32) -> Self {
36        self.properties.set_session_expiry_interval(seconds);
37        self
38    }
39
40    /// Sets the reason string
41    #[must_use]
42    pub fn with_reason_string(mut self, reason: String) -> Self {
43        self.properties.set_reason_string(reason);
44        self
45    }
46
47    /// Sets the server reference for redirect
48    #[must_use]
49    pub fn with_server_reference(mut self, reference: String) -> Self {
50        self.properties.set_server_reference(reference);
51        self
52    }
53
54    /// Adds a user property
55    #[must_use]
56    pub fn with_user_property(mut self, key: String, value: String) -> Self {
57        self.properties.add_user_property(key, value);
58        self
59    }
60
61    /// Validates the reason code for DISCONNECT
62    fn is_valid_disconnect_reason_code(code: ReasonCode) -> bool {
63        matches!(
64            code,
65            NORMAL_DISCONNECTION
66                | ReasonCode::DisconnectWithWillMessage
67                | ReasonCode::UnspecifiedError
68                | ReasonCode::MalformedPacket
69                | ReasonCode::ProtocolError
70                | ReasonCode::ImplementationSpecificError
71                | ReasonCode::NotAuthorized
72                | ReasonCode::ServerBusy
73                | ReasonCode::ServerShuttingDown
74                | ReasonCode::KeepAliveTimeout
75                | ReasonCode::SessionTakenOver
76                | ReasonCode::TopicFilterInvalid
77                | ReasonCode::TopicNameInvalid
78                | ReasonCode::ReceiveMaximumExceeded
79                | ReasonCode::TopicAliasInvalid
80                | ReasonCode::PacketTooLarge
81                | ReasonCode::MessageRateTooHigh
82                | ReasonCode::QuotaExceeded
83                | ReasonCode::AdministrativeAction
84                | ReasonCode::PayloadFormatInvalid
85                | ReasonCode::RetainNotSupported
86                | ReasonCode::QoSNotSupported
87                | ReasonCode::UseAnotherServer
88                | ReasonCode::ServerMoved
89                | ReasonCode::SharedSubscriptionsNotSupported
90                | ReasonCode::ConnectionRateExceeded
91                | ReasonCode::MaximumConnectTime
92                | ReasonCode::SubscriptionIdentifiersNotSupported
93                | ReasonCode::WildcardSubscriptionsNotSupported
94        )
95    }
96}
97
98impl MqttPacket for DisconnectPacket {
99    fn packet_type(&self) -> PacketType {
100        PacketType::Disconnect
101    }
102
103    fn encode_body<B: BufMut>(&self, buf: &mut B) -> Result<()> {
104        // For v5.0, encode reason code and properties
105        // For v3.1.1, DISCONNECT has no variable header or payload
106
107        // Only encode if not normal disconnection or has properties
108        if self.reason_code != NORMAL_DISCONNECTION || !self.properties.is_empty() {
109            buf.put_u8(u8::from(self.reason_code));
110
111            // Only encode properties if present
112            if !self.properties.is_empty() {
113                self.properties.encode(buf)?;
114            }
115        }
116
117        Ok(())
118    }
119
120    fn decode_body<B: Buf>(buf: &mut B, _fixed_header: &FixedHeader) -> Result<Self> {
121        // Check if we have any data
122        if !buf.has_remaining() {
123            // v3.1.1 style or v5.0 normal disconnection with no properties
124            return Ok(Self::normal());
125        }
126
127        // Read reason code
128        let reason_byte = buf.get_u8();
129        let reason_code = ReasonCode::from_u8(reason_byte).ok_or_else(|| {
130            MqttError::MalformedPacket(format!("Invalid DISCONNECT reason code: {reason_byte}"))
131        })?;
132
133        if !Self::is_valid_disconnect_reason_code(reason_code) {
134            return Err(MqttError::MalformedPacket(format!(
135                "Invalid DISCONNECT reason code: {reason_code:?}"
136            )));
137        }
138
139        // Properties (if present)
140        let properties = if buf.has_remaining() {
141            Properties::decode(buf)?
142        } else {
143            Properties::default()
144        };
145
146        Ok(Self {
147            reason_code,
148            properties,
149        })
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use crate::protocol::v5::properties::PropertyId;
157    use bytes::BytesMut;
158
159    #[test]
160    fn test_disconnect_normal() {
161        let packet = DisconnectPacket::normal();
162        assert_eq!(packet.reason_code, NORMAL_DISCONNECTION);
163        assert!(packet.properties.is_empty());
164    }
165
166    #[test]
167    fn test_disconnect_with_reason() {
168        let packet = DisconnectPacket::new(ReasonCode::ServerShuttingDown)
169            .with_reason_string("Maintenance mode".to_string());
170
171        assert_eq!(packet.reason_code, ReasonCode::ServerShuttingDown);
172        assert!(packet.properties.contains(PropertyId::ReasonString));
173    }
174
175    #[test]
176    fn test_disconnect_encode_decode_normal() {
177        let packet = DisconnectPacket::normal();
178
179        let mut buf = BytesMut::new();
180        packet.encode(&mut buf).unwrap();
181
182        let fixed_header = FixedHeader::decode(&mut buf).unwrap();
183        assert_eq!(fixed_header.packet_type, PacketType::Disconnect);
184
185        let decoded = DisconnectPacket::decode_body(&mut buf, &fixed_header).unwrap();
186        assert_eq!(decoded.reason_code, NORMAL_DISCONNECTION);
187    }
188
189    #[test]
190    fn test_disconnect_encode_decode_with_properties() {
191        let packet = DisconnectPacket::new(ReasonCode::SessionTakenOver)
192            .with_session_expiry_interval(0)
193            .with_reason_string("Another client connected".to_string());
194
195        let mut buf = BytesMut::new();
196        packet.encode(&mut buf).unwrap();
197
198        let fixed_header = FixedHeader::decode(&mut buf).unwrap();
199        let decoded = DisconnectPacket::decode_body(&mut buf, &fixed_header).unwrap();
200
201        assert_eq!(decoded.reason_code, ReasonCode::SessionTakenOver);
202        assert!(decoded
203            .properties
204            .contains(PropertyId::SessionExpiryInterval));
205        assert!(decoded.properties.contains(PropertyId::ReasonString));
206    }
207
208    #[test]
209    fn test_disconnect_v311_style() {
210        // Empty body should decode as normal disconnection
211        let mut buf = BytesMut::new();
212        let fixed_header = FixedHeader::new(PacketType::Disconnect, 0, 0);
213
214        let decoded = DisconnectPacket::decode_body(&mut buf, &fixed_header).unwrap();
215        assert_eq!(decoded.reason_code, NORMAL_DISCONNECTION);
216        assert!(decoded.properties.is_empty());
217    }
218}