use core::fmt;
use std::convert::TryFrom;
use crate::{
checks::application::mqtt::{
decode_remaining_length, parse_packet_type, validate_fixed_header_flags,
validate_mqtt_header_available, validate_mqtt_min_length,
validate_remaining_length_available, variable_header_len,
},
errors::application::mqtt::MqttError,
};
#[cfg_attr(all(doc, feature = "doc-diagrams"), aquamarine::aquamarine)]
#[derive(Debug)]
pub struct MqttPacket {
pub fixed_header: MqttFixedHeader,
pub variable_header: Vec<u8>,
pub payload: Vec<u8>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct MqttFixedHeader {
pub packet_type: MqttPacketType,
pub remaining_length: u32,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum MqttPacketType {
Connect = 1,
Connack,
Publish,
Puback,
Pubrec,
Pubrel,
Pubcomp,
Subscribe,
Suback,
Unsubscribe,
Unsuback,
Pingreq,
Pingresp,
Disconnect,
}
impl fmt::Display for MqttPacketType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
MqttPacketType::Connect => "CONNECT",
MqttPacketType::Connack => "CONNACK",
MqttPacketType::Publish => "PUBLISH",
MqttPacketType::Puback => "PUBACK",
MqttPacketType::Pubrec => "PUBREC",
MqttPacketType::Pubrel => "PUBREL",
MqttPacketType::Pubcomp => "PUBCOMP",
MqttPacketType::Subscribe => "SUBSCRIBE",
MqttPacketType::Suback => "SUBACK",
MqttPacketType::Unsubscribe => "UNSUBSCRIBE",
MqttPacketType::Unsuback => "UNSUBACK",
MqttPacketType::Pingreq => "PINGREQ",
MqttPacketType::Pingresp => "PINGRESP",
MqttPacketType::Disconnect => "DISCONNECT",
};
write!(f, "{s}")
}
}
impl fmt::Display for MqttFixedHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"packet_type={}, remaining_length={}",
self.packet_type, self.remaining_length
)
}
}
impl fmt::Display for MqttPacket {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"MQTT Packet: fixed_header={}, variable_header={:02X?}, payload={:02X?}",
self.fixed_header, self.variable_header, self.payload
)
}
}
impl TryFrom<&[u8]> for MqttPacket {
type Error = MqttError;
fn try_from(packet: &[u8]) -> Result<Self, Self::Error> {
validate_mqtt_min_length(packet)?;
let first = packet[0];
let packet_type = parse_packet_type(first)?;
validate_fixed_header_flags(packet_type, first)?;
let (remaining_length, rl_bytes) = decode_remaining_length(&packet[1..])?;
let header_len = 1 + rl_bytes;
validate_mqtt_header_available(packet.len(), header_len)?;
let available = packet.len() - header_len;
validate_remaining_length_available(remaining_length, available)?;
let body = &packet[header_len..header_len + remaining_length as usize];
let vh_len = variable_header_len(packet_type, body)?;
let (vh, pl) = body.split_at(vh_len);
Ok(MqttPacket {
fixed_header: MqttFixedHeader {
packet_type,
remaining_length,
},
variable_header: vh.to_vec(),
payload: pl.to_vec(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_connect_packet() {
let packet: &[u8] = &[
0x10, 0x0E, 0x00, 0x04, b'M', b'Q', b'T', b'T', 0x04, 0x02, 0x00, 0x3C, 0x00, 0x02, b'i', b'd', ];
let mqtt = MqttPacket::try_from(packet).expect("CONNECT valide");
assert_eq!(mqtt.fixed_header.packet_type, MqttPacketType::Connect);
assert_eq!(mqtt.fixed_header.remaining_length, 14);
assert_eq!(mqtt.variable_header.len(), 10);
assert_eq!(mqtt.payload, vec![0x00, 0x02, b'i', b'd']);
}
#[test]
fn test_parse_connack_packet() {
let packet: &[u8] = &[0x20, 0x02, 0x00, 0x00];
let mqtt = MqttPacket::try_from(packet).expect("CONNACK valide");
assert_eq!(mqtt.fixed_header.packet_type, MqttPacketType::Connack);
assert_eq!(mqtt.variable_header, vec![0x00, 0x00]);
assert!(mqtt.payload.is_empty());
}
#[test]
fn test_parse_publish_packet_with_flags() {
let packet: &[u8] = &[
0x3B, 0x09, 0x00, 0x03, b'a', b'/', b'b', 0x00, 0x01, 0xDE, 0xAD, ];
let mqtt = MqttPacket::try_from(packet).expect("PUBLISH valide");
assert_eq!(mqtt.fixed_header.packet_type, MqttPacketType::Publish);
assert_eq!(mqtt.variable_header, vec![0x00, 0x03, b'a', b'/', b'b']);
assert_eq!(mqtt.payload, vec![0x00, 0x01, 0xDE, 0xAD]);
}
#[test]
fn test_parse_pingreq_packet() {
let packet: &[u8] = &[0xC0, 0x00];
let mqtt = MqttPacket::try_from(packet).expect("PINGREQ valide");
assert_eq!(mqtt.fixed_header.packet_type, MqttPacketType::Pingreq);
assert_eq!(mqtt.fixed_header.remaining_length, 0);
}
#[test]
fn test_parse_subscribe_requires_flags_0010() {
let bad: &[u8] = &[0x80, 0x05, 0x00, 0x01, 0x00, 0x01, b'a'];
assert!(matches!(
MqttPacket::try_from(bad),
Err(MqttError::InvalidHeaderFlags { .. })
));
let good: &[u8] = &[0x82, 0x06, 0x00, 0x01, 0x00, 0x01, b'a', 0x00];
assert!(MqttPacket::try_from(good).is_ok());
}
#[test]
fn test_packet_too_short() {
assert!(matches!(
MqttPacket::try_from(&[0x10][..]),
Err(MqttError::PacketTooShort { .. })
));
}
#[test]
fn test_invalid_packet_type() {
assert!(matches!(
MqttPacket::try_from(&[0x00, 0x00][..]),
Err(MqttError::InvalidPacketType { raw: 0 })
));
assert!(matches!(
MqttPacket::try_from(&[0xF0, 0x00][..]),
Err(MqttError::InvalidPacketType { raw: 15 })
));
}
#[test]
fn test_remaining_length_overflow() {
assert!(matches!(
MqttPacket::try_from(&[0xC0, 0xFF, 0xFF, 0xFF, 0xFF][..]),
Err(MqttError::RemainingLengthOverflow)
));
}
#[test]
fn test_remaining_length_exceeds_buffer() {
assert!(matches!(
MqttPacket::try_from(&[0xC0, 0x05][..]),
Err(MqttError::RemainingLengthExceedsBuffer { .. })
));
}
#[test]
fn test_connect_variable_header_too_short() {
assert!(matches!(
MqttPacket::try_from(&[0x10, 0x02, 0x00, 0x04][..]),
Err(MqttError::VariableHeaderTooShort { .. })
));
}
#[test]
fn test_publish_invalid_topic_length() {
assert!(matches!(
MqttPacket::try_from(&[0x30, 0x03, 0x00, 0x0A, b'a'][..]),
Err(MqttError::InvalidTopicLength { .. })
));
}
#[test]
fn test_display_packet_types() {
let cases: &[(MqttPacketType, &str)] = &[
(MqttPacketType::Connect, "CONNECT"),
(MqttPacketType::Connack, "CONNACK"),
(MqttPacketType::Publish, "PUBLISH"),
(MqttPacketType::Puback, "PUBACK"),
(MqttPacketType::Pubrec, "PUBREC"),
(MqttPacketType::Pubrel, "PUBREL"),
(MqttPacketType::Pubcomp, "PUBCOMP"),
(MqttPacketType::Subscribe, "SUBSCRIBE"),
(MqttPacketType::Suback, "SUBACK"),
(MqttPacketType::Unsubscribe, "UNSUBSCRIBE"),
(MqttPacketType::Unsuback, "UNSUBACK"),
(MqttPacketType::Pingreq, "PINGREQ"),
(MqttPacketType::Pingresp, "PINGRESP"),
(MqttPacketType::Disconnect, "DISCONNECT"),
];
for (packet_type, expected) in cases {
assert_eq!(packet_type.to_string(), *expected);
}
}
#[test]
fn test_display_packet_and_header() {
let packet: &[u8] = &[0x20, 0x02, 0x00, 0x00];
let mqtt = MqttPacket::try_from(packet).unwrap();
assert_eq!(
mqtt.fixed_header.to_string(),
"packet_type=CONNACK, remaining_length=2"
);
let rendered = mqtt.to_string();
assert!(rendered.contains("MQTT Packet:"));
assert!(rendered.contains("CONNACK"));
}
}