mdns_proto/
txt.rs

1use core::fmt::{self, Write};
2
3use either::Either;
4
5use super::error::{ProtoError, not_enough_read_data};
6
7/// ```text
8/// 3.3.14. TXT RDATA format
9///
10///     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
11///     /                   TXT-DATA                    /
12///     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
13///
14/// where:
15///
16/// TXT-DATA        One or more <character-string>s.
17///
18/// TXT RRs are used to hold descriptive text.  The semantics of the text
19/// depends on the domain where it is found.
20/// ```
21#[derive(Debug, Default, Clone, Copy)]
22pub struct Txt<'container, 'innards> {
23  repr: Repr<'container, 'innards>,
24}
25
26impl<'container, 'innards> From<&'container [&'innards str]> for Txt<'container, 'innards> {
27  fn from(strings: &'container [&'innards str]) -> Self {
28    Self::from_strings(strings)
29  }
30}
31
32#[derive(Debug, Clone, Copy)]
33enum Repr<'container, 'innards> {
34  BytesStrings {
35    /// The original buffer, in totality, that this TXT was parsed from.
36    original: &'innards [u8],
37
38    /// The starting position of this TXT in the original buffer.
39    start: usize,
40
41    /// The ending position of this TXT in the original buffer.
42    end: usize,
43  },
44  Strings(&'container [&'innards str]),
45}
46
47impl Default for Repr<'_, '_> {
48  fn default() -> Self {
49    Self::Strings(&[])
50  }
51}
52
53impl<'container, 'innards> Txt<'container, 'innards> {
54  /// Creates a new Txt record from a slice of string references
55  #[inline]
56  pub const fn from_strings(strings: &'container [&'innards str]) -> Self {
57    Self {
58      repr: Repr::Strings(strings),
59    }
60  }
61
62  /// Creates a new Txt record from a slice
63  #[inline]
64  pub const fn from_bytes(src: &'innards [u8]) -> Self {
65    Self::from_bytes_in(src, 0, src.len())
66  }
67
68  /// Creates a new Txt record from a byte slice with start and end positions
69  #[inline]
70  pub(super) const fn from_bytes_in(original: &'innards [u8], start: usize, end: usize) -> Self {
71    Self {
72      repr: Repr::BytesStrings {
73        original,
74        start,
75        end,
76      },
77    }
78  }
79
80  /// Returns an iterator over the strings in this TXT record
81  #[inline]
82  pub const fn strings(&self) -> Strings<'container, 'innards> {
83    let repr = match &self.repr {
84      Repr::BytesStrings {
85        original,
86        start,
87        end,
88      } => StringsRepr::Bytes {
89        original,
90        position: *start,
91        end: *end,
92      },
93      Repr::Strings(strings) => StringsRepr::Strings {
94        strings,
95        position: 0,
96      },
97    };
98
99    Strings { repr }
100  }
101
102  /// Returns the internal representation of this TXT record
103  #[inline]
104  pub fn repr(&self) -> Either<&'container [&'innards str], &'innards [u8]> {
105    match &self.repr {
106      Repr::BytesStrings {
107        original,
108        start,
109        end,
110      } => Either::Right(&original[*start..*end]),
111      Repr::Strings(strings) => Either::Left(strings),
112    }
113  }
114}
115
116/// A TXT string segment that refers to either raw bytes or a pre-parsed string
117#[derive(Clone, Copy, Debug)]
118pub struct Str<'a> {
119  repr: StrRepr<'a>,
120}
121
122impl fmt::Display for Str<'_> {
123  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124    match &self.repr {
125      StrRepr::Bytes {
126        original,
127        start,
128        length,
129      } => {
130        let bytes = &original[*start..*start + *length];
131        // Properly handle special characters
132        for &byte in bytes {
133          match byte {
134            b'"' | b'\\' => {
135              f.write_str("\\")?;
136              f.write_char(byte as char)?;
137            }
138            b if (b' '..=b'~').contains(&b) => {
139              f.write_char(b as char)?;
140            }
141            b => {
142              f.write_str(
143                simdutf8::basic::from_utf8(escape_bytes(b).as_slice())
144                  .expect("escape bytes must be valid utf8"),
145              )?;
146            }
147          }
148        }
149        Ok(())
150      }
151      StrRepr::String(s) => write!(f, "{}", s),
152    }
153  }
154}
155
156/// Internal representation of a TXT string segment
157#[derive(Clone, Copy, Debug)]
158enum StrRepr<'a> {
159  /// The segment is represented by its range of bytes in the buffer
160  Bytes {
161    /// The original buffer this segment was parsed from
162    original: &'a [u8],
163    /// Starting position (after the length byte)
164    start: usize,
165    /// Length of the string
166    length: usize,
167  },
168  /// The segment is a pre-parsed string
169  String(&'a str),
170}
171
172impl<'a> Str<'a> {
173  /// Create a new segment from a buffer with specific range
174  fn from_bytes(original: &'a [u8], start: usize, length: usize) -> Self {
175    Self {
176      repr: StrRepr::Bytes {
177        original,
178        start,
179        length,
180      },
181    }
182  }
183
184  /// Get the raw bytes of this segment
185  pub fn as_bytes(&self) -> &'a [u8] {
186    match self.repr {
187      StrRepr::Bytes {
188        original,
189        start,
190        length,
191      } => &original[start..start + length],
192      StrRepr::String(s) => s.as_bytes(),
193    }
194  }
195
196  /// Create a new segment from a pre-parsed string
197  #[inline]
198  pub const fn new(s: &'a str) -> Self {
199    Self {
200      repr: StrRepr::String(s),
201    }
202  }
203}
204
205/// Iterator over strings in a TXT record
206enum StringsRepr<'container, 'innards> {
207  Bytes {
208    original: &'innards [u8],
209    position: usize,
210    end: usize,
211  },
212  Strings {
213    strings: &'container [&'innards str],
214    position: usize,
215  },
216}
217
218/// Iterator over strings in a TXT record
219pub struct Strings<'container, 'innards> {
220  repr: StringsRepr<'container, 'innards>,
221}
222
223impl<'innards> Iterator for Strings<'_, 'innards> {
224  type Item = Result<Str<'innards>, ProtoError>;
225
226  fn next(&mut self) -> Option<Self::Item> {
227    match &mut self.repr {
228      StringsRepr::Bytes {
229        original,
230        position,
231        end,
232      } => {
233        if *position >= *end {
234          return None;
235        }
236
237        let result = decode_txt_segment(original, *position, *end);
238        match result {
239          Ok((segment, new_position)) => {
240            *position = new_position;
241            Some(Ok(segment))
242          }
243          Err(e) => {
244            // Advance to end on error to stop iteration
245            *position = *end;
246            Some(Err(e))
247          }
248        }
249      }
250      StringsRepr::Strings { strings, position } => {
251        if *position >= strings.len() {
252          return None;
253        }
254
255        let string = strings[*position];
256        *position += 1;
257        Some(Ok(Str::new(string)))
258      }
259    }
260  }
261}
262
263/// Decodes a single TXT segment from a byte slice without UTF-8 validation
264fn decode_txt_segment(
265  msg: &[u8],
266  mut offset: usize,
267  end: usize,
268) -> Result<(Str<'_>, usize), ProtoError> {
269  if offset + 1 > msg.len() || offset >= end {
270    return Err(not_enough_read_data(1, 0));
271  }
272
273  let length = msg[offset] as usize;
274  offset += 1;
275  let content_start = offset;
276  let content_end = content_start + length;
277
278  if content_end > msg.len() {
279    return Err(not_enough_read_data(length, content_end - msg.len()));
280  }
281
282  if content_end > end {
283    return Err(not_enough_read_data(length, content_end - end));
284  }
285
286  let mut consumed = 0;
287  for (i, &b) in msg[offset..offset + length].iter().enumerate() {
288    match () {
289      () if (b == b'"' || b == b'\\') || !(b' '..=b'~').contains(&b) => {
290        consumed = i + 1;
291      }
292      _ => {}
293    }
294  }
295
296  if consumed == 0 {
297    // no escaping needed
298    return simdutf8::compat::from_utf8(&msg[offset..offset + length])
299      .map(|s| (Str::new(s), offset + length))
300      .map_err(Into::into);
301  }
302
303  let segment = Str::from_bytes(msg, content_start, length);
304  Ok((segment, content_end))
305}
306
307// /// Decode a TXT record from a byte slice, returning the record and the new offset
308// fn decode_txt<'a>(msg: &[u8], offset: usize) -> Result<(Txt<'a, '_>, usize), ProtoError> {
309//   if offset >= msg.len() {
310//     return Err(not_enough_read_data(1, 0));
311//   }
312
313//   // Find the end by parsing through all the strings
314//   let mut position = offset;
315//   while position < msg.len() {
316//     if position + 1 > msg.len() {
317//       break;
318//     }
319
320//     let length = msg[position] as usize;
321//     let next_position = position + 1 + length;
322
323//     if next_position > msg.len() {
324//       break;
325//     }
326
327//     position = next_position;
328//   }
329
330//   let txt = Txt::from_bytes_in(msg, offset, position);
331//   Ok((txt, position))
332// }
333
334// Escape byte without allocation using a fixed buffer
335#[inline]
336const fn escape_bytes(b: u8) -> [u8; 4] {
337  let mut buf = [0; 4];
338  buf[0] = b'\\';
339  buf[1] = b'0' + (b / 100);
340  buf[2] = b'0' + ((b / 10) % 10);
341  buf[3] = b'0' + (b % 10);
342  buf
343}