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}