1use alloc::borrow::Cow;
4use alloc::string::{String, ToString};
5use alloc::vec::Vec;
6use core::fmt;
7
8pub const TXT_STRING_MAX: usize = 255;
10pub const TXT_STRING_WARN: usize = 200;
12pub const TXT_TOTAL_WARN: usize = 1300;
15
16const EBUS_REQUIRED_KEYS: [&str; 4] = ["txtvers", "ebus_version", "roles", "device_id"];
18
19#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
24pub struct TxtRecord {
25 entries: Vec<(String, Option<Vec<u8>>)>,
26}
27
28pub(crate) fn key_eq(a: &str, b: &str) -> bool {
29 a.chars()
30 .flat_map(char::to_lowercase)
31 .eq(b.chars().flat_map(char::to_lowercase))
32}
33
34impl TxtRecord {
35 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn from_strings<I, S>(strings: I) -> Self
45 where
46 I: IntoIterator<Item = S>,
47 S: AsRef<[u8]>,
48 {
49 let mut txt = Self::new();
50 for s in strings {
51 let s = s.as_ref();
52 let (key, value) = match s.iter().position(|&b| b == b'=') {
53 Some(at) => (&s[..at], Some(s[at + 1..].to_vec())),
54 None => (s, None),
55 };
56 if key.is_empty() {
57 continue;
58 }
59 txt.insert_raw(String::from_utf8_lossy(key).to_lowercase(), value);
60 }
61 txt
62 }
63
64 pub fn insert_raw(&mut self, key: String, value: Option<Vec<u8>>) -> bool {
67 if self.contains_key(&key) {
68 return false;
69 }
70 self.entries.push((key, value));
71 true
72 }
73
74 pub fn insert(&mut self, key: &str, value: &str) -> bool {
77 self.insert_raw(key.to_string(), Some(value.as_bytes().to_vec()))
78 }
79
80 pub fn contains_key(&self, key: &str) -> bool {
82 self.entries.iter().any(|(k, _)| key_eq(k, key))
83 }
84
85 pub fn get(&self, key: &str) -> Option<Cow<'_, str>> {
88 self.get_raw(key).map(|v| match v {
89 Some(bytes) => String::from_utf8_lossy(bytes),
90 None => Cow::Borrowed(""),
91 })
92 }
93
94 pub fn get_raw(&self, key: &str) -> Option<Option<&[u8]>> {
96 self.entries
97 .iter()
98 .find(|(k, _)| key_eq(k, key))
99 .map(|(_, v)| v.as_deref())
100 }
101
102 pub fn iter(&self) -> impl Iterator<Item = (&str, Option<&[u8]>)> + '_ {
104 self.entries.iter().map(|(k, v)| (k.as_str(), v.as_deref()))
105 }
106
107 pub fn len(&self) -> usize {
109 self.entries.len()
110 }
111
112 pub fn is_empty(&self) -> bool {
114 self.entries.is_empty()
115 }
116
117 pub fn to_strings(&self) -> Vec<Vec<u8>> {
119 self.entries
120 .iter()
121 .map(|(k, v)| entry_string(k, v.as_deref()))
122 .collect()
123 }
124
125 pub fn wire_size(&self) -> usize {
127 self.entries
128 .iter()
129 .map(|(k, v)| 1 + entry_len(k, v.as_deref()))
130 .sum()
131 }
132}
133
134fn entry_len(key: &str, value: Option<&[u8]>) -> usize {
135 key.len() + value.map_or(0, |v| 1 + v.len())
136}
137
138fn entry_string(key: &str, value: Option<&[u8]>) -> Vec<u8> {
139 let mut s = Vec::with_capacity(entry_len(key, value));
140 s.extend_from_slice(key.as_bytes());
141 if let Some(v) = value {
142 s.push(b'=');
143 s.extend_from_slice(v);
144 }
145 s
146}
147
148#[derive(Clone, Debug, PartialEq, Eq)]
150#[non_exhaustive]
151pub enum TxtError {
152 InvalidKey {
154 service_type: String,
156 key: String,
158 },
159 StringTooLong {
161 service_type: String,
163 key: String,
165 bytes: usize,
167 },
168 NoDeviceId,
170 InvalidDeviceId(String),
172 NoRole,
174 InvalidRole(String),
176 Missing(&'static str),
179 InvalidHomieDomain(String),
181 DuplicateBrokerType(crate::BrokerType),
183}
184
185impl fmt::Display for TxtError {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 match self {
188 Self::InvalidKey { service_type, key } => {
189 write!(f, "{service_type}: invalid TXT key {key:?}")
190 }
191 Self::StringTooLong {
192 service_type,
193 key,
194 bytes,
195 } => write!(
196 f,
197 "{service_type}: TXT {key:?} is {bytes} bytes; one TXT string holds at most {TXT_STRING_MAX}"
198 ),
199 Self::NoDeviceId => f.write_str("identity needs at least one device id"),
200 Self::InvalidDeviceId(id) => {
201 write!(f, "invalid device id {id:?} (empty or contains a comma)")
202 }
203 Self::NoRole => f.write_str("identity needs at least one role"),
204 Self::InvalidRole(role) => write!(f, "invalid role {role:?}"),
205 Self::Missing(field) => write!(f, "identity {field} is required"),
206 Self::InvalidHomieDomain(d) => {
207 write!(f, "invalid Homie domain {d:?} (one topic level)")
208 }
209 Self::DuplicateBrokerType(t) => {
210 write!(f, "more than one broker of type {}", t.as_str())
211 }
212 }
213 }
214}
215
216impl core::error::Error for TxtError {}
217
218#[derive(Clone, Debug, Default, PartialEq, Eq)]
220#[non_exhaustive]
221pub struct TxtReport {
222 pub long_strings: Vec<(String, usize)>,
224 pub total_bytes: usize,
226}
227
228impl TxtReport {
229 pub fn is_large(&self) -> bool {
231 self.total_bytes > TXT_TOTAL_WARN
232 }
233}
234
235pub fn check_txt(service_type: &str, txt: &TxtRecord) -> Result<TxtReport, TxtError> {
240 let mut report = TxtReport::default();
241 for (key, value) in txt.iter() {
242 if key.is_empty() || !key.bytes().all(|b| (0x20..=0x7e).contains(&b) && b != b'=') {
243 return Err(TxtError::InvalidKey {
244 service_type: service_type.to_string(),
245 key: key.to_string(),
246 });
247 }
248 let n = entry_len(key, value);
249 if n > TXT_STRING_MAX {
250 return Err(TxtError::StringTooLong {
251 service_type: service_type.to_string(),
252 key: key.to_string(),
253 bytes: n,
254 });
255 }
256 if n > TXT_STRING_WARN {
257 crate::log::warn!(
258 "txtStringNearLimit",
259 service = service_type,
260 key = key,
261 bytes = n
262 );
263 report.long_strings.push((key.to_string(), n));
264 }
265 }
266 report.total_bytes = txt.wire_size();
267 if report.is_large() {
268 crate::log::warn!(
269 "txtRecordLarge",
270 service = service_type,
271 bytes = report.total_bytes
272 );
273 }
274 Ok(report)
275}
276
277#[derive(Clone, Debug, PartialEq, Eq)]
279#[non_exhaustive]
280pub struct EbusTxt {
281 pub txtvers: String,
283 pub ebus_version: String,
285 pub roles: Vec<String>,
287 pub device_ids: Vec<String>,
289 pub auth_methods: Vec<String>,
291 pub txt: TxtRecord,
293}
294
295impl EbusTxt {
296 pub fn missing(&self) -> Vec<&'static str> {
298 EBUS_REQUIRED_KEYS
299 .into_iter()
300 .filter(|k| self.txt.get(k).is_none_or(|v| v.is_empty()))
301 .collect()
302 }
303
304 pub fn is_broker_host(&self) -> bool {
306 self.roles.iter().any(|r| r == crate::ROLE_BROKER_HOST)
307 }
308
309 pub fn get(&self, key: &str) -> Option<Cow<'_, str>> {
311 self.txt.get(key)
312 }
313}
314
315pub(crate) fn split_list(value: &str) -> Vec<String> {
318 value
319 .split(',')
320 .map(str::trim)
321 .filter(|s| !s.is_empty())
322 .map(String::from)
323 .collect()
324}
325
326pub fn parse_ebus_txt(txt: &TxtRecord) -> EbusTxt {
329 let text = |k: &str| txt.get(k).map(Cow::into_owned).unwrap_or_default();
330 let list = |k: &str| txt.get(k).map(|v| split_list(&v)).unwrap_or_default();
331 EbusTxt {
332 txtvers: text("txtvers"),
333 ebus_version: text("ebus_version"),
334 roles: list("roles"),
335 device_ids: list("device_id"),
336 auth_methods: list("auth_methods"),
337 txt: txt.clone(),
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use alloc::vec;
345
346 #[test]
347 fn decoding() {
348 let txt = TxtRecord::from_strings([
349 &b"Roles=device"[..],
350 b"flag",
351 b"=nokey",
352 b"",
353 b"empty=",
354 b"ROLES=second",
355 b"bin=\xff",
356 b"eq=a=b",
357 ]);
358 let keys: Vec<&str> = txt.iter().map(|(k, _)| k).collect();
359 assert_eq!(keys, ["roles", "flag", "empty", "bin", "eq"]);
360 assert_eq!(txt.get("roles").as_deref(), Some("device"));
361 assert_eq!(txt.get("FLAG").as_deref(), Some(""));
362 assert_eq!(txt.get_raw("flag"), Some(None));
363 assert_eq!(txt.get_raw("empty"), Some(Some(&b""[..])));
364 assert_eq!(txt.get("bin").as_deref(), Some("\u{fffd}"));
365 assert_eq!(txt.get("eq").as_deref(), Some("a=b"));
366 assert_eq!(txt.get("absent"), None);
367 assert_eq!(
368 txt.to_strings(),
369 vec![
370 b"roles=device".to_vec(),
371 b"flag".to_vec(),
372 b"empty=".to_vec(),
373 b"bin=\xff".to_vec(),
374 b"eq=a=b".to_vec()
375 ]
376 );
377 assert_eq!(txt.wire_size(), 13 + 5 + 7 + 6 + 7);
378 }
379
380 #[test]
381 fn insert_keeps_first() {
382 let mut txt = TxtRecord::new();
383 assert!(txt.insert("Key", "1"));
384 assert!(!txt.insert("kEY", "2"));
385 assert_eq!(txt.get("key").as_deref(), Some("1"));
386 assert_eq!(txt.iter().next(), Some(("Key", Some(&b"1"[..]))));
387 }
388
389 #[test]
390 fn checks() {
391 let mut ok = TxtRecord::new();
392 ok.insert("a key", "v");
393 assert_eq!(check_txt("_x._tcp", &ok).map(|r| r.total_bytes), Ok(8));
394 for bad in ["", "a=b", "caf\u{e9}", "tab\t"] {
395 let mut txt = TxtRecord::new();
396 txt.insert_raw(bad.into(), None);
397 assert!(matches!(
398 check_txt("_x._tcp", &txt),
399 Err(TxtError::InvalidKey { .. })
400 ));
401 }
402 let mut long = TxtRecord::new();
403 long.insert("k", &"v".repeat(253));
404 let r = check_txt("_x._tcp", &long).unwrap();
405 assert_eq!(r.long_strings, [("k".to_string(), 255)]);
406 assert!(!r.is_large());
407 let mut too_long = TxtRecord::new();
408 too_long.insert("k", &"v".repeat(254));
409 assert!(matches!(
410 check_txt("_x._tcp", &too_long),
411 Err(TxtError::StringTooLong { bytes: 256, .. })
412 ));
413 let mut large = TxtRecord::new();
414 for i in 0..7 {
415 large.insert(&alloc::format!("k{i}"), &"v".repeat(190));
416 }
417 let r = check_txt("_x._tcp", &large).unwrap();
418 assert!(r.is_large());
419 assert!(r.long_strings.is_empty());
420 }
421
422 #[test]
423 fn ebus_txt() {
424 let txt = TxtRecord::from_strings([
425 "txtvers=1",
426 "roles= device , ,broker-host",
427 "device_id=a,b",
428 "ebus_version=",
429 ]);
430 let e = parse_ebus_txt(&txt);
431 assert_eq!(e.roles, ["device", "broker-host"]);
432 assert_eq!(e.device_ids, ["a", "b"]);
433 assert!(e.auth_methods.is_empty());
434 assert!(e.is_broker_host());
435 assert_eq!(e.missing(), ["ebus_version"]);
436 }
437}