#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(feature = "alloc")]
extern crate alloc;
mod addr;
#[cfg(feature = "alloc")]
mod advertise;
#[cfg(feature = "alloc")]
mod broker;
#[cfg(feature = "alloc")]
mod instance;
mod log;
#[cfg(feature = "alloc")]
mod search;
#[cfg(feature = "alloc")]
mod txt;
pub use addr::{AddrScope, ScopedAddr};
pub use ebus_mqtt::Instant;
#[cfg(feature = "alloc")]
pub use addr::candidate_addresses;
#[cfg(feature = "alloc")]
pub use advertise::{
Advertisement, BrokerService, HttpService, Identity, MAX_DEFAULT_INSTANCE_OCTETS,
MAX_RENAME_SUFFIX, ServiceRecord,
};
#[cfg(feature = "alloc")]
pub use broker::{BrokerEndpoint, UrlError, Wanted, match_configured, rank_brokers, select_broker};
#[cfg(feature = "alloc")]
pub use instance::{Instance, InstanceKey, InstanceTable, Sighting, Slot};
#[cfg(feature = "alloc")]
pub use search::{BrokerConfig, BrokerConfigError, BrokerSearch};
#[cfg(feature = "alloc")]
pub use txt::{
EbusTxt, TXT_STRING_MAX, TXT_STRING_WARN, TXT_TOTAL_WARN, TxtError, TxtRecord, TxtReport,
check_txt, parse_ebus_txt,
};
pub const EBUS_SERVICE: &str = "_ebus._tcp";
pub const DEVICE_INFO_SERVICE: &str = "_device-info._tcp";
pub const HTTP_SERVICE: &str = "_http._tcp";
pub const HTTPS_SERVICE: &str = "_https._tcp";
pub const EBUS_VERSION: &str = "0.9";
pub const TXTVERS: &str = "1";
pub const ROLE_DEVICE: &str = "device";
pub const ROLE_CONTROLLER: &str = "controller";
pub const ROLE_BROKER_HOST: &str = "broker-host";
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum BrokerType {
SecureMqtt,
MqttWss,
MqttWs,
Mqtt,
}
pub const BROKER_TYPES: &[BrokerType] = &[
BrokerType::SecureMqtt,
BrokerType::MqttWss,
BrokerType::MqttWs,
BrokerType::Mqtt,
];
pub const TCP_BROKER_TYPES: &[BrokerType] = &[BrokerType::SecureMqtt, BrokerType::Mqtt];
impl BrokerType {
pub const fn as_str(self) -> &'static str {
match self {
Self::SecureMqtt => "_secure-mqtt._tcp",
Self::MqttWss => "_mqtt-wss._tcp",
Self::MqttWs => "_mqtt-ws._tcp",
Self::Mqtt => "_mqtt._tcp",
}
}
pub fn parse(service_type: &str) -> Option<Self> {
let t = service_type.strip_suffix('.').unwrap_or(service_type);
let t = match t.len().checked_sub(".local".len()) {
Some(at) if t.is_char_boundary(at) && t[at..].eq_ignore_ascii_case(".local") => {
&t[..at]
}
_ => t,
};
BROKER_TYPES
.iter()
.copied()
.find(|b| b.as_str().eq_ignore_ascii_case(t))
}
pub const fn default_port(self) -> u16 {
match self {
Self::SecureMqtt => 8883,
Self::MqttWss => 9002,
Self::MqttWs => 9001,
Self::Mqtt => 1883,
}
}
pub const fn uses_tls(self) -> bool {
matches!(self, Self::SecureMqtt | Self::MqttWss)
}
pub const fn scheme(self) -> &'static str {
match self {
Self::SecureMqtt => "mqtts",
Self::MqttWss => "wss",
Self::MqttWs => "ws",
Self::Mqtt => "mqtt",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum BrokerMode {
ConfiguredOnly,
DiscoveryWithFallback,
DiscoveryOnly,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ParseModeError;
impl core::fmt::Display for ParseModeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(
"unknown broker mode; expected configured-only, discovery-with-fallback or discovery-only",
)
}
}
impl core::error::Error for ParseModeError {}
impl BrokerMode {
pub const ALL: [BrokerMode; 3] = [
Self::ConfiguredOnly,
Self::DiscoveryWithFallback,
Self::DiscoveryOnly,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::ConfiguredOnly => "configured-only",
Self::DiscoveryWithFallback => "discovery-with-fallback",
Self::DiscoveryOnly => "discovery-only",
}
}
pub fn parse(text: &str) -> Result<Self, ParseModeError> {
let text = text.trim();
Self::ALL
.into_iter()
.find(|m| {
let want = m.as_str().as_bytes();
want.len() == text.len()
&& want.iter().zip(text.bytes()).all(|(&w, t)| {
let t = t.to_ascii_lowercase();
w == if t == b'_' { b'-' } else { t }
})
})
.ok_or(ParseModeError)
}
}
impl core::str::FromStr for BrokerMode {
type Err = ParseModeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
pub fn strip_dot(name: &str) -> &str {
name.strip_suffix('.').unwrap_or(name)
}
pub fn same_host(a: &str, b: &str) -> bool {
let (a, b) = (strip_dot(a), strip_dot(b));
a.chars()
.flat_map(char::to_lowercase)
.eq(b.chars().flat_map(char::to_lowercase))
}
pub fn is_local_name(host: &str) -> bool {
let h = strip_dot(host).as_bytes();
h.len() > ".local".len() && h[h.len() - ".local".len()..].eq_ignore_ascii_case(b".local")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn broker_type_order_is_preference() {
let mut v = [
BrokerType::Mqtt,
BrokerType::MqttWs,
BrokerType::SecureMqtt,
BrokerType::MqttWss,
];
v.sort();
assert_eq!(v, BROKER_TYPES);
}
#[test]
fn broker_type_parse() {
for &t in BROKER_TYPES {
assert_eq!(BrokerType::parse(t.as_str()), Some(t));
}
assert_eq!(
BrokerType::parse("_Secure-MQTT._tcp.local."),
Some(BrokerType::SecureMqtt)
);
assert_eq!(
BrokerType::parse("_mqtt._tcp.local"),
Some(BrokerType::Mqtt)
);
assert_eq!(BrokerType::parse("_mqtt._tcp."), Some(BrokerType::Mqtt));
assert_eq!(BrokerType::parse("_ebus._tcp"), None);
assert_eq!(BrokerType::parse("_mqtt._udp"), None);
assert_eq!(BrokerType::parse(""), None);
assert_eq!(BrokerType::parse("é.local"), None);
}
#[test]
fn broker_type_properties() {
let ports = BROKER_TYPES.iter().map(|t| t.default_port());
assert!(ports.eq([8883, 9002, 9001, 1883]));
let tls = BROKER_TYPES.iter().map(|t| t.uses_tls());
assert!(tls.eq([true, true, false, false]));
}
#[test]
fn mode_parse() {
assert_eq!(
BrokerMode::parse(" Discovery_Only "),
Ok(BrokerMode::DiscoveryOnly)
);
assert_eq!("configured-only".parse(), Ok(BrokerMode::ConfiguredOnly));
assert_eq!(BrokerMode::parse(""), Err(ParseModeError));
assert_eq!(BrokerMode::parse("discovery"), Err(ParseModeError));
}
#[test]
fn host_names() {
assert!(same_host("Host.Local.", "host.local"));
assert!(!same_host("host.local..", "host.local"));
assert!(is_local_name("broker.LOCAL."));
assert!(!is_local_name("local"));
assert!(!is_local_name("broker.example.com"));
assert_eq!(strip_dot("a.local."), "a.local");
assert_eq!(strip_dot("a.local.."), "a.local.");
}
}
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;