use byteorder::{BigEndian, WriteBytesExt};
use std::hash::{Hash, Hasher};
use std::io::Write;
use crate::QoS;
use crate::{ByteArray, DecodeError, DecodePacket, EncodeError, EncodePacket};
#[derive(Debug, Default, Clone, Eq, PartialOrd, Ord)]
pub struct Topic {
topic: String,
parts: Vec<TopicPart>,
}
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, PartialEq, Eq)]
pub enum TopicError {
EmptyTopic,
TooManyData,
InvalidChar,
ContainsWildChar,
}
impl PartialEq for Topic {
fn eq(&self, other: &Self) -> bool {
self.topic.eq(&other.topic)
}
}
impl Hash for Topic {
fn hash<H: Hasher>(&self, state: &mut H) {
self.topic.hash(state);
}
}
impl Topic {
pub fn parse(s: &str) -> Result<Self, TopicError> {
let parts = Self::parse_parts(s)?;
Ok(Self {
topic: s.to_string(),
parts,
})
}
fn parse_parts(s: &str) -> Result<Vec<TopicPart>, TopicError> {
s.split('/').map(TopicPart::parse).collect()
}
#[must_use]
pub fn is_match(&self, s: &str) -> bool {
for (index, part) in s.split('/').into_iter().enumerate() {
match self.parts.get(index) {
None | Some(TopicPart::Empty) => return false,
Some(TopicPart::Normal(ref s_part) | TopicPart::Internal(ref s_part)) => {
if s_part != part {
return false;
}
}
Some(TopicPart::SingleWildcard) => {
}
Some(TopicPart::MultiWildcard) => return true,
}
}
true
}
#[must_use]
pub const fn topic(&self) -> &String {
&self.topic
}
#[must_use]
pub fn len(&self) -> usize {
self.topic.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.topic.is_empty()
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
self.topic.as_bytes()
}
}
#[allow(clippy::module_name_repetitions)]
pub fn validate_sub_topic(topic: &str) -> Result<(), TopicError> {
if topic.is_empty() {
return Err(TopicError::EmptyTopic);
}
if topic == "#" {
return Ok(());
}
let bytes = topic.as_bytes();
for (index, b) in bytes.iter().enumerate() {
if b == &b'#' {
if index > 0 && bytes[index - 1] != b'/' {
return Err(TopicError::InvalidChar);
}
if index != bytes.len() - 1 {
return Err(TopicError::InvalidChar);
}
} else if b == &b'+' {
if index > 0 && bytes[index - 1] != b'/' {
return Err(TopicError::InvalidChar);
}
}
}
Ok(())
}
#[allow(clippy::module_name_repetitions)]
pub fn validate_pub_topic(topic: &str) -> Result<(), TopicError> {
if topic.is_empty() {
return Err(TopicError::EmptyTopic);
}
if topic.len() > u16::MAX as usize {
return Err(TopicError::TooManyData);
}
if topic.as_bytes().iter().any(|c| c == &b'+' || c == &b'#') {
Err(TopicError::InvalidChar)
} else {
Ok(())
}
}
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TopicPart {
Internal(String),
Normal(String),
#[default]
Empty,
MultiWildcard,
SingleWildcard,
}
impl TopicPart {
fn has_wildcard(s: &str) -> bool {
s.contains(|c| c == '#' || c == '+')
}
#[must_use]
fn is_internal(s: &str) -> bool {
s.starts_with('$')
}
fn parse(s: &str) -> Result<Self, TopicError> {
match s {
"" => Ok(Self::Empty),
"+" => Ok(Self::SingleWildcard),
"#" => Ok(Self::MultiWildcard),
_ => {
if Self::has_wildcard(s) {
Err(TopicError::ContainsWildChar)
} else if Self::is_internal(s) {
Ok(Self::Internal(s.to_string()))
} else {
Ok(Self::Normal(s.to_string()))
}
}
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SubscribePattern {
topic: Topic,
qos: QoS,
}
impl SubscribePattern {
pub fn parse(topic: &str, qos: QoS) -> Result<Self, TopicError> {
let topic = Topic::parse(topic)?;
Ok(Self { topic, qos })
}
#[must_use]
#[inline]
pub const fn new(topic: Topic, qos: QoS) -> Self {
Self { topic, qos }
}
#[must_use]
#[inline]
pub const fn topic(&self) -> &Topic {
&self.topic
}
#[must_use]
#[inline]
pub const fn qos(&self) -> QoS {
self.qos
}
}
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PubTopic(String);
impl PubTopic {
pub fn new(topic: &str) -> Result<Self, TopicError> {
validate_pub_topic(topic)?;
Ok(Self(topic.to_string()))
}
#[must_use]
pub fn bytes(&self) -> usize {
2 + self.0.len()
}
}
impl AsRef<str> for PubTopic {
fn as_ref(&self) -> &str {
&self.0
}
}
impl DecodePacket for PubTopic {
fn decode(ba: &mut ByteArray) -> Result<Self, DecodeError> {
let len = ba.read_u16()?;
let s = ba.read_string(len as usize)?;
validate_pub_topic(&s)?;
Ok(Self(s))
}
}
impl EncodePacket for PubTopic {
fn encode(&self, buf: &mut Vec<u8>) -> Result<usize, EncodeError> {
#[allow(clippy::cast_possible_truncation)]
let len = self.0.len() as u16;
buf.write_u16::<BigEndian>(len)?;
buf.write_all(self.0.as_bytes())?;
Ok(self.bytes())
}
}
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SubTopic(String);
impl SubTopic {
pub fn new(topic: &str) -> Result<Self, TopicError> {
validate_sub_topic(topic)?;
Ok(Self(topic.to_string()))
}
#[must_use]
pub fn bytes(&self) -> usize {
2 + self.0.len()
}
}
impl AsRef<str> for SubTopic {
fn as_ref(&self) -> &str {
&self.0
}
}
impl DecodePacket for SubTopic {
fn decode(ba: &mut ByteArray) -> Result<Self, DecodeError> {
let len = ba.read_u16()?;
let s = ba.read_string(len as usize)?;
validate_sub_topic(&s)?;
Ok(Self(s))
}
}
impl EncodePacket for SubTopic {
fn encode(&self, buf: &mut Vec<u8>) -> Result<usize, EncodeError> {
#[allow(clippy::cast_possible_truncation)]
let len = self.0.len() as u16;
buf.write_u16::<BigEndian>(len)?;
buf.write_all(self.0.as_bytes())?;
Ok(self.bytes())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse() {
let t_sys = Topic::parse("$SYS/uptime");
assert!(t_sys.is_ok());
}
#[test]
fn test_topic_match() {
let t_sys = Topic::parse("$SYS");
assert!(t_sys.is_ok());
let t_dev = Topic::parse("dev/#").unwrap();
assert!(t_dev.is_match("dev/cpu/0"));
}
}