async_zeroconf/txt.rs
1use std::collections::HashMap;
2use std::fmt;
3use std::str::Utf8Error;
4
5use crate::ZeroconfError;
6
7/// Struct containing the entries for TXT records associated with a service
8///
9/// # Examples
10/// ```
11/// # tokio_test::block_on(async {
12/// let mut txt = async_zeroconf::TxtRecord::new();
13/// txt.add("key1".to_string(), "value1".to_string());
14/// txt.add("key2".to_string(), "value2".to_string());
15/// let service_ref = async_zeroconf::Service::new_with_txt("Server", "_http._tcp", 80, txt)
16/// .publish().await?;
17/// # Ok::<(), async_zeroconf::ZeroconfError>(())
18/// # });
19/// ```
20#[derive(Debug, Clone, Eq, PartialEq)]
21pub struct TxtRecord {
22 records: HashMap<String, Vec<u8>>,
23}
24
25impl TxtRecord {
26 /// Create a new TXT record collection
27 pub fn new() -> Self {
28 TxtRecord {
29 records: HashMap::new(),
30 }
31 }
32
33 /// Add an entry from a string
34 pub fn add(&mut self, k: String, v: String) {
35 self.records.insert(k, v.as_bytes().to_vec());
36 }
37
38 /// Add an entry from a slice of u8's
39 pub fn add_vec(&mut self, k: String, v: Vec<u8>) {
40 self.records.insert(k, v);
41 }
42
43 /// Get Iterator
44 ///
45 /// # Examples
46 /// ```
47 /// let mut txt = async_zeroconf::TxtRecord::new();
48 /// txt.add("key".to_string(), "value".to_string());
49 /// // Iterator
50 /// let iter = txt.iter();
51 /// for (k, v) in iter {
52 /// println!("{}, {:?}", k, v);
53 /// }
54 /// ```
55 pub fn iter(&self) -> impl Iterator<Item = (&String, &Vec<u8>)> {
56 self.records.iter()
57 }
58
59 /// Get Iterator including conversion to string. As the conversion to a
60 /// UTF-8 string could fail the value is returned as a `Result`.
61 ///
62 /// # Examples
63 /// ```
64 /// let mut txt = async_zeroconf::TxtRecord::new();
65 /// txt.add("key".to_string(), "value".to_string());
66 /// // String iterator
67 /// let iter = txt.iter_string();
68 /// for (k, v) in iter {
69 /// match v {
70 /// Ok(v) => println!("{}, {}", k, v),
71 /// Err(_) => println!("{} not valid UTF-8", k)
72 /// }
73 /// }
74 /// ```
75 pub fn iter_string(&self) -> impl Iterator<Item = (&String, Result<&str, Utf8Error>)> {
76 self.records
77 .iter()
78 .map(|(k, v)| (k, std::str::from_utf8(v)))
79 }
80
81 /// Get Iterator including conversion to string. If the conversion to UTF-8
82 /// fails, '�' will be returned instead.
83 ///
84 /// # Examples
85 /// ```
86 /// let mut txt = async_zeroconf::TxtRecord::new();
87 /// txt.add("key".to_string(), "value".to_string());
88 /// // String iterator
89 /// let iter = txt.iter_string_lossy();
90 /// for (k, v) in iter {
91 /// println!("{}, {}", k, v);
92 /// }
93 /// ```
94 pub fn iter_string_lossy(&self) -> impl Iterator<Item = (&String, &str)> {
95 self.records
96 .iter()
97 .map(|(k, v)| (k, std::str::from_utf8(v).unwrap_or("�")))
98 }
99
100 /// Validate if this TXT record collection contains all valid values.
101 /// This checks that the key is 9 characters or less, the value is 255
102 /// characters or less and that the key only has printable ASCII characters
103 /// excluding '='.
104 ///
105 /// # Examples
106 /// ```
107 /// let mut valid_txt = async_zeroconf::TxtRecord::new();
108 /// valid_txt.add("key".to_string(), "value".to_string());
109 /// assert!(valid_txt.validate().is_ok());
110 ///
111 /// let mut invalid_txt = async_zeroconf::TxtRecord::new();
112 /// invalid_txt.add("k\0".to_string(), "value".to_string());
113 /// assert!(invalid_txt.validate().is_err());
114 /// ```
115 pub fn validate(&self) -> Result<(), ZeroconfError> {
116 for (k, v) in self.iter() {
117 let all_printable_ascii = k.chars().all(|c| (0x20..=0x7E).contains(&(c as u32)));
118 if k.len() > 9 || v.len() > 255 || k.contains('=') || !all_printable_ascii {
119 return Err(ZeroconfError::InvalidTxtRecord(format!(
120 "{}={}",
121 k,
122 String::from_utf8_lossy(v)
123 )));
124 }
125 }
126 Ok(())
127 }
128
129 /// Empty if no records are associated
130 pub fn is_empty(&self) -> bool {
131 self.records.is_empty()
132 }
133}
134
135impl Default for TxtRecord {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141impl fmt::Display for TxtRecord {
142 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
143 let mut string = "{".to_string();
144 let mut first = true;
145 for (k, v) in self.iter_string_lossy() {
146 if !first {
147 string.push_str(", ");
148 }
149 string.push_str(format!("\"{}\": \"{}\"", k, v).as_str());
150 first = false;
151 }
152 string.push('}');
153 write!(f, "{}", string)
154 }
155}