1#![cfg_attr(not(feature = "std"), no_std)]
56#![cfg_attr(docsrs, feature(doc_cfg))]
57
58#[cfg(feature = "alloc")]
59extern crate alloc;
60
61mod addr;
62#[cfg(feature = "alloc")]
63mod advertise;
64#[cfg(feature = "alloc")]
65mod broker;
66#[cfg(feature = "alloc")]
67mod instance;
68mod log;
69#[cfg(feature = "alloc")]
70mod search;
71#[cfg(feature = "alloc")]
72mod txt;
73
74pub use addr::{AddrScope, ScopedAddr};
75pub use ebus_mqtt::Instant;
76
77#[cfg(feature = "alloc")]
78pub use addr::candidate_addresses;
79#[cfg(feature = "alloc")]
80pub use advertise::{
81 Advertisement, BrokerService, HttpService, Identity, MAX_DEFAULT_INSTANCE_OCTETS,
82 MAX_RENAME_SUFFIX, ServiceRecord,
83};
84#[cfg(feature = "alloc")]
85pub use broker::{BrokerEndpoint, UrlError, Wanted, match_configured, rank_brokers, select_broker};
86#[cfg(feature = "alloc")]
87pub use instance::{Instance, InstanceKey, InstanceTable, Sighting, Slot};
88#[cfg(feature = "alloc")]
89pub use search::{BrokerConfig, BrokerConfigError, BrokerSearch};
90#[cfg(feature = "alloc")]
91pub use txt::{
92 EbusTxt, TXT_STRING_MAX, TXT_STRING_WARN, TXT_TOTAL_WARN, TxtError, TxtRecord, TxtReport,
93 check_txt, parse_ebus_txt,
94};
95
96pub const EBUS_SERVICE: &str = "_ebus._tcp";
98pub const DEVICE_INFO_SERVICE: &str = "_device-info._tcp";
100pub const HTTP_SERVICE: &str = "_http._tcp";
102pub const HTTPS_SERVICE: &str = "_https._tcp";
104
105pub const EBUS_VERSION: &str = "0.9";
108
109pub const TXTVERS: &str = "1";
111
112pub const ROLE_DEVICE: &str = "device";
114pub const ROLE_CONTROLLER: &str = "controller";
116pub const ROLE_BROKER_HOST: &str = "broker-host";
118
119#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
123#[non_exhaustive]
124pub enum BrokerType {
125 SecureMqtt,
127 MqttWss,
129 MqttWs,
131 Mqtt,
133}
134
135pub const BROKER_TYPES: &[BrokerType] = &[
137 BrokerType::SecureMqtt,
138 BrokerType::MqttWss,
139 BrokerType::MqttWs,
140 BrokerType::Mqtt,
141];
142
143pub const TCP_BROKER_TYPES: &[BrokerType] = &[BrokerType::SecureMqtt, BrokerType::Mqtt];
146
147impl BrokerType {
148 pub const fn as_str(self) -> &'static str {
150 match self {
151 Self::SecureMqtt => "_secure-mqtt._tcp",
152 Self::MqttWss => "_mqtt-wss._tcp",
153 Self::MqttWs => "_mqtt-ws._tcp",
154 Self::Mqtt => "_mqtt._tcp",
155 }
156 }
157
158 pub fn parse(service_type: &str) -> Option<Self> {
161 let t = service_type.strip_suffix('.').unwrap_or(service_type);
162 let t = match t.len().checked_sub(".local".len()) {
163 Some(at) if t.is_char_boundary(at) && t[at..].eq_ignore_ascii_case(".local") => {
164 &t[..at]
165 }
166 _ => t,
167 };
168 BROKER_TYPES
169 .iter()
170 .copied()
171 .find(|b| b.as_str().eq_ignore_ascii_case(t))
172 }
173
174 pub const fn default_port(self) -> u16 {
176 match self {
177 Self::SecureMqtt => 8883,
178 Self::MqttWss => 9002,
179 Self::MqttWs => 9001,
180 Self::Mqtt => 1883,
181 }
182 }
183
184 pub const fn uses_tls(self) -> bool {
186 matches!(self, Self::SecureMqtt | Self::MqttWss)
187 }
188
189 pub const fn scheme(self) -> &'static str {
191 match self {
192 Self::SecureMqtt => "mqtts",
193 Self::MqttWss => "wss",
194 Self::MqttWs => "ws",
195 Self::Mqtt => "mqtt",
196 }
197 }
198}
199
200#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
202pub enum BrokerMode {
203 ConfiguredOnly,
205 DiscoveryWithFallback,
207 DiscoveryOnly,
209}
210
211#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
213pub struct ParseModeError;
214
215impl core::fmt::Display for ParseModeError {
216 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
217 f.write_str(
218 "unknown broker mode; expected configured-only, discovery-with-fallback or discovery-only",
219 )
220 }
221}
222
223impl core::error::Error for ParseModeError {}
224
225impl BrokerMode {
226 pub const ALL: [BrokerMode; 3] = [
228 Self::ConfiguredOnly,
229 Self::DiscoveryWithFallback,
230 Self::DiscoveryOnly,
231 ];
232
233 pub const fn as_str(self) -> &'static str {
235 match self {
236 Self::ConfiguredOnly => "configured-only",
237 Self::DiscoveryWithFallback => "discovery-with-fallback",
238 Self::DiscoveryOnly => "discovery-only",
239 }
240 }
241
242 pub fn parse(text: &str) -> Result<Self, ParseModeError> {
247 let text = text.trim();
248 Self::ALL
249 .into_iter()
250 .find(|m| {
251 let want = m.as_str().as_bytes();
252 want.len() == text.len()
253 && want.iter().zip(text.bytes()).all(|(&w, t)| {
254 let t = t.to_ascii_lowercase();
255 w == if t == b'_' { b'-' } else { t }
256 })
257 })
258 .ok_or(ParseModeError)
259 }
260}
261
262impl core::str::FromStr for BrokerMode {
263 type Err = ParseModeError;
264 fn from_str(s: &str) -> Result<Self, Self::Err> {
265 Self::parse(s)
266 }
267}
268
269pub fn strip_dot(name: &str) -> &str {
271 name.strip_suffix('.').unwrap_or(name)
272}
273
274pub fn same_host(a: &str, b: &str) -> bool {
277 let (a, b) = (strip_dot(a), strip_dot(b));
278 a.chars()
279 .flat_map(char::to_lowercase)
280 .eq(b.chars().flat_map(char::to_lowercase))
281}
282
283pub fn is_local_name(host: &str) -> bool {
286 let h = strip_dot(host).as_bytes();
287 h.len() > ".local".len() && h[h.len() - ".local".len()..].eq_ignore_ascii_case(b".local")
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn broker_type_order_is_preference() {
296 let mut v = [
297 BrokerType::Mqtt,
298 BrokerType::MqttWs,
299 BrokerType::SecureMqtt,
300 BrokerType::MqttWss,
301 ];
302 v.sort();
303 assert_eq!(v, BROKER_TYPES);
304 }
305
306 #[test]
307 fn broker_type_parse() {
308 for &t in BROKER_TYPES {
309 assert_eq!(BrokerType::parse(t.as_str()), Some(t));
310 }
311 assert_eq!(
312 BrokerType::parse("_Secure-MQTT._tcp.local."),
313 Some(BrokerType::SecureMqtt)
314 );
315 assert_eq!(
316 BrokerType::parse("_mqtt._tcp.local"),
317 Some(BrokerType::Mqtt)
318 );
319 assert_eq!(BrokerType::parse("_mqtt._tcp."), Some(BrokerType::Mqtt));
320 assert_eq!(BrokerType::parse("_ebus._tcp"), None);
321 assert_eq!(BrokerType::parse("_mqtt._udp"), None);
322 assert_eq!(BrokerType::parse(""), None);
323 assert_eq!(BrokerType::parse("é.local"), None);
324 }
325
326 #[test]
327 fn broker_type_properties() {
328 let ports = BROKER_TYPES.iter().map(|t| t.default_port());
329 assert!(ports.eq([8883, 9002, 9001, 1883]));
330 let tls = BROKER_TYPES.iter().map(|t| t.uses_tls());
331 assert!(tls.eq([true, true, false, false]));
332 }
333
334 #[test]
335 fn mode_parse() {
336 assert_eq!(
337 BrokerMode::parse(" Discovery_Only "),
338 Ok(BrokerMode::DiscoveryOnly)
339 );
340 assert_eq!("configured-only".parse(), Ok(BrokerMode::ConfiguredOnly));
341 assert_eq!(BrokerMode::parse(""), Err(ParseModeError));
342 assert_eq!(BrokerMode::parse("discovery"), Err(ParseModeError));
343 }
344
345 #[test]
346 fn host_names() {
347 assert!(same_host("Host.Local.", "host.local"));
348 assert!(!same_host("host.local..", "host.local"));
349 assert!(is_local_name("broker.LOCAL."));
350 assert!(!is_local_name("local"));
351 assert!(!is_local_name("broker.example.com"));
352 assert_eq!(strip_dot("a.local."), "a.local");
353 assert_eq!(strip_dot("a.local.."), "a.local.");
354 }
355}
356
357#[cfg(doctest)]
358#[doc = include_str!("../README.md")]
359struct ReadmeDoctests;