use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PropertyId {
PayloadFormatIndicator,
MessageExpiryInterval,
ContentType,
ResponseTopic,
CorrelationData,
SubscriptionIdentifier,
SessionExpiryInterval,
AssignedClientIdentifier,
ServerKeepAlive,
AuthenticationMethod,
AuthenticationData,
ReceiveMaximum,
TopicAliasMaximum,
TopicAlias,
UserProperty,
Other(u32),
}
impl PropertyId {
#[must_use]
pub const fn to_value(self) -> u32 {
match self {
Self::PayloadFormatIndicator => 1,
Self::MessageExpiryInterval => 2,
Self::ContentType => 3,
Self::ResponseTopic => 8,
Self::CorrelationData => 9,
Self::SubscriptionIdentifier => 11,
Self::SessionExpiryInterval => 17,
Self::AssignedClientIdentifier => 18,
Self::ServerKeepAlive => 19,
Self::AuthenticationMethod => 21,
Self::AuthenticationData => 22,
Self::ReceiveMaximum => 33,
Self::TopicAliasMaximum => 34,
Self::TopicAlias => 35,
Self::UserProperty => 38,
Self::Other(v) => v,
}
}
#[must_use]
pub const fn from_value(v: u32) -> Self {
match v {
1 => Self::PayloadFormatIndicator,
2 => Self::MessageExpiryInterval,
3 => Self::ContentType,
8 => Self::ResponseTopic,
9 => Self::CorrelationData,
11 => Self::SubscriptionIdentifier,
17 => Self::SessionExpiryInterval,
18 => Self::AssignedClientIdentifier,
19 => Self::ServerKeepAlive,
21 => Self::AuthenticationMethod,
22 => Self::AuthenticationData,
33 => Self::ReceiveMaximum,
34 => Self::TopicAliasMaximum,
35 => Self::TopicAlias,
38 => Self::UserProperty,
other => Self::Other(other),
}
}
#[must_use]
pub const fn value_kind(self) -> PropertyValueKind {
match self {
Self::PayloadFormatIndicator => PropertyValueKind::Byte,
Self::MessageExpiryInterval => PropertyValueKind::FourByteInt,
Self::ContentType => PropertyValueKind::Utf8String,
Self::ResponseTopic => PropertyValueKind::Utf8String,
Self::CorrelationData => PropertyValueKind::BinaryData,
Self::SubscriptionIdentifier => PropertyValueKind::VariableByteInt,
Self::SessionExpiryInterval => PropertyValueKind::FourByteInt,
Self::AssignedClientIdentifier => PropertyValueKind::Utf8String,
Self::ServerKeepAlive => PropertyValueKind::TwoByteInt,
Self::AuthenticationMethod => PropertyValueKind::Utf8String,
Self::AuthenticationData => PropertyValueKind::BinaryData,
Self::ReceiveMaximum => PropertyValueKind::TwoByteInt,
Self::TopicAliasMaximum => PropertyValueKind::TwoByteInt,
Self::TopicAlias => PropertyValueKind::TwoByteInt,
Self::UserProperty => PropertyValueKind::Utf8StringPair,
Self::Other(_) => PropertyValueKind::BinaryData,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PropertyValueKind {
Byte,
TwoByteInt,
FourByteInt,
VariableByteInt,
Utf8String,
Utf8StringPair,
BinaryData,
}
impl PropertyValueKind {
#[allow(clippy::result_unit_err)]
pub fn decode_two_byte_int(bytes: &[u8]) -> Result<u16, ()> {
if bytes.len() < 2 {
return Err(());
}
Ok(u16::from_be_bytes([bytes[0], bytes[1]]))
}
#[allow(clippy::result_unit_err)]
pub fn decode_four_byte_int(bytes: &[u8]) -> Result<u32, ()> {
if bytes.len() < 4 {
return Err(());
}
Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
#[allow(clippy::result_unit_err)]
pub fn decode_utf8_string(bytes: &[u8]) -> Result<&str, ()> {
if bytes.len() < 2 {
return Err(());
}
let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
if bytes.len() < 2 + len {
return Err(());
}
core::str::from_utf8(&bytes[2..2 + len]).map_err(|_| ())
}
#[allow(clippy::result_unit_err)]
pub fn decode_binary_data(bytes: &[u8]) -> Result<&[u8], ()> {
if bytes.len() < 2 {
return Err(());
}
let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
if bytes.len() < 2 + len {
return Err(());
}
Ok(&bytes[2..2 + len])
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Property {
pub id: PropertyId,
pub value: Vec<u8>,
}
impl Property {
#[must_use]
pub const fn new(id: PropertyId, value: Vec<u8>) -> Self {
Self { id, value }
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn property_id_round_trip_for_all_named() {
for id in [
PropertyId::PayloadFormatIndicator,
PropertyId::MessageExpiryInterval,
PropertyId::ContentType,
PropertyId::ResponseTopic,
PropertyId::CorrelationData,
PropertyId::SubscriptionIdentifier,
PropertyId::SessionExpiryInterval,
PropertyId::AssignedClientIdentifier,
PropertyId::ServerKeepAlive,
PropertyId::AuthenticationMethod,
PropertyId::AuthenticationData,
PropertyId::ReceiveMaximum,
PropertyId::TopicAliasMaximum,
PropertyId::TopicAlias,
PropertyId::UserProperty,
] {
assert_eq!(PropertyId::from_value(id.to_value()), id);
}
}
#[test]
fn property_id_unknown_yields_other_variant() {
assert_eq!(PropertyId::from_value(99), PropertyId::Other(99));
}
#[test]
fn well_known_property_ids_match_spec_values() {
assert_eq!(PropertyId::PayloadFormatIndicator.to_value(), 1);
assert_eq!(PropertyId::ContentType.to_value(), 3);
assert_eq!(PropertyId::SubscriptionIdentifier.to_value(), 11);
assert_eq!(PropertyId::SessionExpiryInterval.to_value(), 17);
assert_eq!(PropertyId::TopicAlias.to_value(), 35);
assert_eq!(PropertyId::UserProperty.to_value(), 38);
}
#[test]
fn value_kind_matches_table_2_4() {
assert_eq!(
PropertyId::PayloadFormatIndicator.value_kind(),
PropertyValueKind::Byte
);
assert_eq!(
PropertyId::MessageExpiryInterval.value_kind(),
PropertyValueKind::FourByteInt
);
assert_eq!(
PropertyId::ContentType.value_kind(),
PropertyValueKind::Utf8String
);
assert_eq!(
PropertyId::CorrelationData.value_kind(),
PropertyValueKind::BinaryData
);
assert_eq!(
PropertyId::SubscriptionIdentifier.value_kind(),
PropertyValueKind::VariableByteInt
);
assert_eq!(
PropertyId::ReceiveMaximum.value_kind(),
PropertyValueKind::TwoByteInt
);
assert_eq!(
PropertyId::UserProperty.value_kind(),
PropertyValueKind::Utf8StringPair
);
}
#[test]
fn decode_two_byte_int_round_trip() {
let bytes = 0xCAFEu16.to_be_bytes();
assert_eq!(
PropertyValueKind::decode_two_byte_int(&bytes).expect("ok"),
0xCAFE
);
}
#[test]
fn decode_two_byte_int_truncated_rejected() {
assert!(PropertyValueKind::decode_two_byte_int(&[0x01]).is_err());
}
#[test]
fn decode_four_byte_int_round_trip() {
let bytes = 0xDEADBEEFu32.to_be_bytes();
assert_eq!(
PropertyValueKind::decode_four_byte_int(&bytes).expect("ok"),
0xDEADBEEF
);
}
#[test]
fn decode_utf8_string_round_trip() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&5u16.to_be_bytes());
bytes.extend_from_slice(b"hello");
assert_eq!(
PropertyValueKind::decode_utf8_string(&bytes).expect("ok"),
"hello"
);
}
#[test]
fn decode_utf8_string_invalid_utf8_rejected() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&1u16.to_be_bytes());
bytes.push(0xff);
assert!(PropertyValueKind::decode_utf8_string(&bytes).is_err());
}
#[test]
fn decode_utf8_string_truncated_rejected() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&10u16.to_be_bytes());
bytes.extend_from_slice(b"hi"); assert!(PropertyValueKind::decode_utf8_string(&bytes).is_err());
}
#[test]
fn decode_binary_data_round_trip() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&3u16.to_be_bytes());
bytes.extend_from_slice(&[0x01, 0x02, 0x03]);
assert_eq!(
PropertyValueKind::decode_binary_data(&bytes).expect("ok"),
&[0x01, 0x02, 0x03]
);
}
}