mqtt5_protocol/packet/
pubcomp.rs

1use crate::error::{MqttError, Result};
2use crate::packet::{FixedHeader, MqttPacket, PacketType};
3use crate::protocol::v5::properties::Properties;
4use crate::types::ReasonCode;
5use bytes::{Buf, BufMut};
6
7/// MQTT PUBCOMP packet (`QoS` 2 publish complete, part 3)
8#[derive(Debug, Clone)]
9pub struct PubCompPacket {
10    /// Packet identifier
11    pub packet_id: u16,
12    /// Reason code
13    pub reason_code: ReasonCode,
14    /// PUBCOMP properties (v5.0 only)
15    pub properties: Properties,
16}
17
18impl PubCompPacket {
19    /// Creates a new PUBCOMP packet
20    #[must_use]
21    pub fn new(packet_id: u16) -> Self {
22        Self {
23            packet_id,
24            reason_code: ReasonCode::Success,
25            properties: Properties::default(),
26        }
27    }
28
29    /// Creates a new PUBCOMP packet with a reason code
30    #[must_use]
31    pub fn new_with_reason(packet_id: u16, reason_code: ReasonCode) -> Self {
32        Self {
33            packet_id,
34            reason_code,
35            properties: Properties::default(),
36        }
37    }
38
39    /// Sets the reason string
40    #[must_use]
41    pub fn with_reason_string(mut self, reason: String) -> Self {
42        self.properties.set_reason_string(reason);
43        self
44    }
45
46    /// Adds a user property
47    #[must_use]
48    pub fn with_user_property(mut self, key: String, value: String) -> Self {
49        self.properties.add_user_property(key, value);
50        self
51    }
52
53    /// Validates the reason code for PUBCOMP
54    fn is_valid_pubcomp_reason_code(code: ReasonCode) -> bool {
55        matches!(
56            code,
57            ReasonCode::Success | ReasonCode::PacketIdentifierNotFound
58        )
59    }
60}
61
62impl MqttPacket for PubCompPacket {
63    fn packet_type(&self) -> PacketType {
64        PacketType::PubComp
65    }
66
67    fn encode_body<B: BufMut>(&self, buf: &mut B) -> Result<()> {
68        // Variable header
69        buf.put_u16(self.packet_id);
70
71        // For v5.0, always encode reason code and properties
72        // For v3.1.1, we would skip these if reason_code is Success and no properties
73        if self.reason_code != ReasonCode::Success || !self.properties.is_empty() {
74            buf.put_u8(u8::from(self.reason_code));
75            self.properties.encode(buf)?;
76        }
77
78        Ok(())
79    }
80
81    fn decode_body<B: Buf>(buf: &mut B, _fixed_header: &FixedHeader) -> Result<Self> {
82        // Packet identifier
83        if buf.remaining() < 2 {
84            return Err(MqttError::MalformedPacket(
85                "PUBCOMP missing packet identifier".to_string(),
86            ));
87        }
88        let packet_id = buf.get_u16();
89
90        // Reason code and properties (optional in v3.1.1, always present in v5.0)
91        let (reason_code, properties) = if buf.has_remaining() {
92            // Read reason code
93            let reason_byte = buf.get_u8();
94            let code = ReasonCode::from_u8(reason_byte).ok_or_else(|| {
95                MqttError::MalformedPacket(format!("Invalid PUBCOMP reason code: {reason_byte}"))
96            })?;
97
98            if !Self::is_valid_pubcomp_reason_code(code) {
99                return Err(MqttError::MalformedPacket(format!(
100                    "Invalid PUBCOMP reason code: {code:?}"
101                )));
102            }
103
104            // Properties (if present)
105            let props = if buf.has_remaining() {
106                Properties::decode(buf)?
107            } else {
108                Properties::default()
109            };
110
111            (code, props)
112        } else {
113            // v3.1.1 style - no reason code or properties means success
114            (ReasonCode::Success, Properties::default())
115        };
116
117        Ok(Self {
118            packet_id,
119            reason_code,
120            properties,
121        })
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::protocol::v5::properties::PropertyId;
129    use bytes::BytesMut;
130
131    #[test]
132    fn test_pubcomp_basic() {
133        let packet = PubCompPacket::new(123);
134
135        assert_eq!(packet.packet_id, 123);
136        assert_eq!(packet.reason_code, ReasonCode::Success);
137        assert!(packet.properties.is_empty());
138    }
139
140    #[test]
141    fn test_pubcomp_with_reason() {
142        let packet = PubCompPacket::new_with_reason(456, ReasonCode::PacketIdentifierNotFound)
143            .with_reason_string("Packet ID not found".to_string());
144
145        assert_eq!(packet.packet_id, 456);
146        assert_eq!(packet.reason_code, ReasonCode::PacketIdentifierNotFound);
147        assert!(packet.properties.contains(PropertyId::ReasonString));
148    }
149
150    #[test]
151    fn test_pubcomp_encode_decode() {
152        let packet = PubCompPacket::new(789);
153
154        let mut buf = BytesMut::new();
155        packet.encode(&mut buf).unwrap();
156
157        let fixed_header = FixedHeader::decode(&mut buf).unwrap();
158        assert_eq!(fixed_header.packet_type, PacketType::PubComp);
159
160        let decoded = PubCompPacket::decode_body(&mut buf, &fixed_header).unwrap();
161        assert_eq!(decoded.packet_id, 789);
162        assert_eq!(decoded.reason_code, ReasonCode::Success);
163    }
164
165    #[test]
166    fn test_pubcomp_encode_decode_with_properties() {
167        let packet = PubCompPacket::new(999)
168            .with_user_property("status".to_string(), "completed".to_string());
169
170        let mut buf = BytesMut::new();
171        packet.encode(&mut buf).unwrap();
172
173        let fixed_header = FixedHeader::decode(&mut buf).unwrap();
174        let decoded = PubCompPacket::decode_body(&mut buf, &fixed_header).unwrap();
175
176        assert_eq!(decoded.packet_id, 999);
177        assert!(decoded.properties.contains(PropertyId::UserProperty));
178    }
179}