mqtt5_protocol/packet/
disconnect.rs1use 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#[derive(Debug, Clone)]
10pub struct DisconnectPacket {
11 pub reason_code: ReasonCode,
13 pub properties: Properties,
15}
16
17impl DisconnectPacket {
18 #[must_use]
20 pub fn new(reason_code: ReasonCode) -> Self {
21 Self {
22 reason_code,
23 properties: Properties::default(),
24 }
25 }
26
27 #[must_use]
29 pub fn normal() -> Self {
30 Self::new(NORMAL_DISCONNECTION)
31 }
32
33 #[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 #[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 #[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 #[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 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 if self.reason_code != NORMAL_DISCONNECTION || !self.properties.is_empty() {
109 buf.put_u8(u8::from(self.reason_code));
110
111 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 if !buf.has_remaining() {
123 return Ok(Self::normal());
125 }
126
127 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 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 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}