Skip to main content

simple_dns/dns/
label.rs

1use core::hash::Hasher;
2
3use crate::{
4    dns::MAX_SERVICE_NAME_LENGTH,
5    lib::{
6        fmt::{Debug, Display, Formatter, Result as FmtResult},
7        Cow, Hash, String, ToString, Vec,
8    },
9};
10
11use super::MAX_LABEL_LENGTH;
12
13/// Represents a label in a domain name
14///
15/// A label is a sequence of 1 to 63 octets. On the wire it can hold any octet, including dots
16/// (RFC 1035 section 3.1), so no escaping exists in the binary format.
17///
18/// What counts as a *valid* label depends on the role it plays in a name:
19///
20/// | Method | Role | Rule |
21/// |---|---|---|
22/// | [`Label::is_valid_hostname`] | host name | letters, digits and hyphens |
23/// | [`Label::is_valid_service_label`] | service name | `_` + RFC 6335 service name |
24/// | [`Label::is_valid_instance_label`] | DNS-SD instance or subtype | any content |
25///
26/// [`Label::new`] only checks the length. To build labels with content that is not valid for a
27/// host name (unicode, dots, spaces), use [`Label::new`] or [`Label::new_unchecked`] and
28/// [`Name::new_with_labels`](crate::Name::new_with_labels).
29///
30/// The `Display` implementation uses [`std::string::String::from_utf8_lossy`] to display the
31/// label, and escapes `.` as `\.` and `\` as `\\`, so that labels joined in a [`Name`](crate::Name)
32/// can be told apart from the dots that separate them (RFC 6763 section 4.3).
33#[derive(Eq, Clone)]
34pub struct Label<'a> {
35    pub(crate) data: Cow<'a, [u8]>,
36}
37
38impl<'a> Label<'a> {
39    /// Create a new [`Label`] from text that may contain escape sequences.
40    ///
41    /// The escapes follow the presentation format of RFC 1035 section 5.1 and RFC 4343
42    /// section 2.1:
43    /// - `\DDD`, exactly three decimal digits (at most `255`), is the octet with that value
44    /// - `\X`, where X is not a digit, is the character X (`\.` and `\\` included)
45    ///
46    /// Returns [`SimpleDnsError::InvalidServiceLabel`](crate::SimpleDnsError::InvalidServiceLabel)
47    /// if the label is empty or longer than 63 octets after unescaping, or if an escape is
48    /// malformed (a trailing `\`, or `\` followed by fewer than three digits or a value above 255).
49    ///
50    /// The content is not checked against a naming rule. Use the `is_valid_*` methods for that.
51    ///
52    /// ```
53    /// # use simple_dns::Label;
54    /// let label = Label::new(r"My\.Device".as_bytes()).unwrap();
55    /// assert_eq!(b"My.Device", label.as_ref());
56    ///
57    /// assert!(Label::new(r"bad\1".as_bytes()).is_err());
58    /// ```
59    pub fn new<T: Into<Cow<'a, [u8]>>>(data: T) -> crate::Result<Self> {
60        let data = data.into();
61        let label = if let Some((i, _)) = data.iter().enumerate().find(|(_, c)| **c == b'\\') {
62            unescape_label(data, i)?
63        } else {
64            Self { data }
65        };
66
67        if label.data.is_empty() || label.data.len() > MAX_LABEL_LENGTH {
68            return Err(crate::SimpleDnsError::InvalidServiceLabel);
69        }
70
71        Ok(label)
72    }
73
74    /// Create a new Label from raw bytes, without unescaping and without checking for size
75    /// limits or valid content.
76    /// This function can be used to create labels with unicode characters
77    pub fn new_unchecked<T: Into<Cow<'a, [u8]>>>(data: T) -> Self {
78        Self { data: data.into() }
79    }
80
81    /// Returns the length of the label
82    pub fn len(&self) -> usize {
83        self.data.len()
84    }
85
86    /// Returns true if the label is empty
87    pub fn is_empty(&self) -> bool {
88        self.data.is_empty()
89    }
90
91    /// Transforms the inner data into its owned type
92    pub fn into_owned<'b>(self) -> Label<'b> {
93        Label {
94            data: self.data.into_owned().into(),
95        }
96    }
97
98    /// Returns `true` if the label is a valid host name label (RFC 952, RFC 1123).
99    ///
100    /// It has 1 to 63 octets, made only of ASCII letters, digits and hyphens, and starts and ends
101    /// with a letter or a digit.
102    ///
103    /// ```
104    /// # use simple_dns::Label;
105    /// assert!(Label::new_unchecked(&b"my-host1"[..]).is_valid_hostname());
106    /// assert!(!Label::new_unchecked(&b"_srv"[..]).is_valid_hostname());
107    /// assert!(!Label::new_unchecked(&b"-host"[..]).is_valid_hostname());
108    /// ```
109    pub fn is_valid_hostname(&self) -> bool {
110        if self.data.is_empty() || self.data.len() > MAX_LABEL_LENGTH {
111            return false;
112        }
113
114        if let Some(first) = self.data.first() {
115            if !first.is_ascii_alphanumeric() {
116                return false;
117            }
118        }
119
120        if !self
121            .data
122            .iter()
123            .skip(1)
124            .all(|c| c.is_ascii_alphanumeric() || *c == b'-')
125        {
126            return false;
127        }
128
129        if let Some(last) = self.data.last() {
130            if !last.is_ascii_alphanumeric() {
131                return false;
132            }
133        }
134
135        true
136    }
137
138    /// Returns `true` if the label is a valid DNS-SD service label, such as `_http`
139    /// (RFC 6763 section 7).
140    ///
141    /// It is an underscore followed by a service name as defined in RFC 6335 section 5.1:
142    /// 1 to 15 ASCII letters, digits or hyphens, with at least one letter, that does not begin or
143    /// end with a hyphen and has no adjacent hyphens.
144    ///
145    /// This only checks the service name. The `_tcp` or `_udp` label that follows it is checked
146    /// by [`Name::is_valid_service_name`](crate::Name::is_valid_service_name).
147    ///
148    /// ```
149    /// # use simple_dns::Label;
150    /// assert!(Label::new_unchecked(&b"_http"[..]).is_valid_service_label());
151    /// assert!(Label::new_unchecked(&b"_9p"[..]).is_valid_service_label());
152    /// assert!(!Label::new_unchecked(&b"http"[..]).is_valid_service_label());
153    /// assert!(!Label::new_unchecked(&b"_123"[..]).is_valid_service_label());
154    /// ```
155    pub fn is_valid_service_label(&self) -> bool {
156        if self.data.is_empty() || self.data.len() > MAX_SERVICE_NAME_LENGTH {
157            return false;
158        }
159
160        let Some((b'_', name)) = self.data.split_first() else {
161            return false;
162        };
163
164        // RFC 6335 section 5.1: letters, digits, hyphens; at least one letter;
165        // no leading, trailing or adjacent hyphens
166        name.iter().all(|c| c.is_ascii_alphanumeric() || *c == b'-')
167            && name.iter().any(|c| c.is_ascii_alphabetic())
168            && !name.starts_with(b"-")
169            && !name.ends_with(b"-")
170            && !name.windows(2).any(|w| w == b"--")
171    }
172
173    /// Returns `true` if the label can be used as a DNS-SD instance name or subtype
174    /// (RFC 6763 sections 4.3 and 7.1).
175    ///
176    /// Any content is accepted, including dots, spaces and UTF-8, as long as the label has 1 to 63
177    /// octets. The limit counts octets, not characters.
178    ///
179    /// ```
180    /// # use simple_dns::Label;
181    /// assert!(Label::new_unchecked(&b"My Printer. Floor 2"[..]).is_valid_instance_label());
182    /// assert!(!Label::new_unchecked(&b""[..]).is_valid_instance_label());
183    /// ```
184    pub fn is_valid_instance_label(&self) -> bool {
185        !(self.data.is_empty() || self.data.len() > MAX_LABEL_LENGTH)
186    }
187}
188
189impl PartialEq for Label<'_> {
190    fn eq(&self, other: &Self) -> bool {
191        self.data.eq_ignore_ascii_case(&other.data)
192    }
193}
194
195impl Hash for Label<'_> {
196    fn hash<H: Hasher>(&self, state: &mut H) {
197        state.write_usize(self.data.len());
198        for &b in self.data.iter() {
199            state.write_u8(b.to_ascii_lowercase());
200        }
201    }
202}
203
204impl PartialOrd for Label<'_> {
205    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
206        Some(self.cmp(other))
207    }
208}
209
210impl Ord for Label<'_> {
211    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
212        self.data
213            .iter()
214            .map(u8::to_ascii_lowercase)
215            .cmp(other.data.iter().map(u8::to_ascii_lowercase))
216    }
217}
218
219fn unescape_label<'a>(bytes: Cow<'a, [u8]>, mut position: usize) -> crate::Result<Label<'a>> {
220    let mut out = Vec::with_capacity(bytes.len());
221    out.extend_from_slice(&bytes[..position]);
222
223    while position < bytes.len() {
224        match bytes[position] {
225            // Labels ending with \ are invalid
226            b'\\' if position + 1 == bytes.len() => {
227                return Err(crate::SimpleDnsError::InvalidServiceLabel);
228            }
229
230            b'\\' => {
231                position += 1;
232                if bytes[position].is_ascii_digit() {
233                    // digit caracters must be a sequence of 3 \DDD
234
235                    // not enough bytes, or next two bytes are not digits
236                    if (bytes.len() - position) < 3
237                        || !bytes[position + 1].is_ascii_digit()
238                        || !bytes[position + 2].is_ascii_digit()
239                    {
240                        return Err(crate::SimpleDnsError::InvalidServiceLabel);
241                    }
242
243                    let mut digit = (bytes[position] - b'0') as u16 * 100;
244                    digit += (bytes[position + 1] - b'0') as u16 * 10;
245                    digit += (bytes[position + 2] - b'0') as u16;
246
247                    if digit > 255 {
248                        return Err(crate::SimpleDnsError::InvalidServiceLabel);
249                    }
250
251                    out.push(digit as u8);
252                    position += 3;
253                } else {
254                    out.push(bytes[position]);
255                    position += 1;
256                }
257            }
258            c => {
259                out.push(c);
260                position += 1;
261            }
262        }
263    }
264
265    Ok(Label { data: out.into() })
266}
267
268impl Display for Label<'_> {
269    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
270        use core::fmt::Write;
271
272        let s = String::from_utf8_lossy(&self.data);
273        for c in s.chars() {
274            match c {
275                '.' => {
276                    f.write_char('\\')?;
277                    f.write_char(c)?;
278                }
279                '\\' => {
280                    f.write_str(r"\\")?;
281                }
282                _ => {
283                    f.write_char(c)?;
284                }
285            }
286        }
287
288        Ok(())
289    }
290}
291
292impl Debug for Label<'_> {
293    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
294        f.debug_struct("Label")
295            .field("data", &self.to_string())
296            .finish()
297    }
298}
299
300impl AsRef<[u8]> for Label<'_> {
301    fn as_ref(&self) -> &[u8] {
302        self.data.as_ref()
303    }
304}
305
306/// Key for the name compression map.
307///
308/// Unlike [`Label`], which compares ASCII case-insensitively, this compares the raw bytes, so
309/// names that differ only in case are never compressed into one and keep their original casing.
310#[derive(Clone, Copy)]
311pub(crate) struct ExactLabels<'a, 'b>(&'b [Label<'a>]);
312
313impl<'a, 'b> ExactLabels<'a, 'b> {
314    pub(crate) fn new(labels: &'b [Label<'a>]) -> Self {
315        Self(labels)
316    }
317}
318
319impl PartialEq for ExactLabels<'_, '_> {
320    fn eq(&self, other: &Self) -> bool {
321        self.cmp(other).is_eq()
322    }
323}
324
325impl Eq for ExactLabels<'_, '_> {}
326
327impl PartialOrd for ExactLabels<'_, '_> {
328    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
329        Some(self.cmp(other))
330    }
331}
332
333impl Ord for ExactLabels<'_, '_> {
334    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
335        self.0
336            .iter()
337            .map(|l| &l.data[..])
338            .cmp(other.0.iter().map(|l| &l.data[..]))
339    }
340}
341
342/// An iterator over the labels in a domain name
343pub(crate) struct LabelsIter<'a> {
344    bytes: &'a [u8],
345    current: usize,
346}
347
348impl<'a> LabelsIter<'a> {
349    pub fn new(bytes: &'a [u8]) -> Self {
350        Self { bytes, current: 0 }
351    }
352}
353
354impl<'a> Iterator for LabelsIter<'a> {
355    type Item = Cow<'a, [u8]>;
356
357    fn next(&mut self) -> Option<Self::Item> {
358        let mut i = self.current;
359        // let mut has_escape = false;
360
361        // Find the boundaries of the label
362        while i < self.bytes.len() {
363            match self.bytes[i] {
364                b'\\' if i + 1 == self.bytes.len() => {
365                    // This is an invalid label, ending with \
366                    break;
367                }
368                b'\\' => {
369                    //this label is escaped
370                    // has_escape = true;
371                    i += 2;
372                }
373                b'.' => {
374                    let current = crate::lib::mem::replace(&mut self.current, i + 1);
375                    if i - current == 0 {
376                        // This is an empty label (or a sequence of .), just skip it
377                        // has_escape = false;
378                        i = self.current;
379                        continue;
380                    }
381
382                    return Some(self.bytes[current..i].into());
383                }
384                _ => {
385                    // all other characters, just advance
386                    i += 1;
387                }
388            }
389        }
390
391        if self.current < self.bytes.len() {
392            let current = crate::lib::mem::replace(&mut self.current, self.bytes.len());
393            // Some(unescape_label(&self.bytes[current..], has_escape))
394            Some(self.bytes[current..].into())
395        } else {
396            None
397        }
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404
405    #[test]
406    fn display_invalid_label() {
407        let input = b"invalid\xF0\x90\x80label";
408        let label = Label::new_unchecked(input);
409
410        assert_eq!(label.to_string(), "invalid�label");
411    }
412
413    #[test]
414    fn labels_are_escaped() {
415        let l = Label::new(b"escaped.label").expect("failed to create label");
416        assert_eq!(r"escaped\.label", l.to_string());
417    }
418
419    fn label(s: &str) -> Label<'_> {
420        Label::new_unchecked(s.as_bytes())
421    }
422
423    #[test]
424    fn hostname_label_valid() {
425        // RFC 952 / RFC 1123: letters, digits and hyphens, starting and ending with alphanumeric
426        assert!(label("example").is_valid_hostname());
427        assert!(label("a").is_valid_hostname());
428        assert!(label("EXAMPLE").is_valid_hostname());
429        assert!(label("a-b").is_valid_hostname());
430        assert!(label("a--b").is_valid_hostname());
431        assert!(label("1abc").is_valid_hostname());
432        assert!(label("abc1").is_valid_hostname());
433        assert!(label("123").is_valid_hostname());
434        assert!(label(&"a".repeat(63)).is_valid_hostname());
435    }
436
437    #[test]
438    fn hostname_label_invalid() {
439        assert!(!label("").is_valid_hostname());
440        assert!(!label(&"a".repeat(64)).is_valid_hostname());
441        assert!(!label("-a").is_valid_hostname());
442        assert!(!label("a-").is_valid_hostname());
443        assert!(!label("-").is_valid_hostname());
444        assert!(!label("_a").is_valid_hostname());
445        assert!(!label("a_b").is_valid_hostname());
446        assert!(!label("a.b").is_valid_hostname());
447        assert!(!label("a b").is_valid_hostname());
448        assert!(!label(r"a\b").is_valid_hostname());
449        assert!(!label("caf\u{e9}").is_valid_hostname());
450        assert!(!label("\u{1F600}").is_valid_hostname());
451    }
452
453    #[test]
454    fn service_label_valid() {
455        // RFC 6763 section 7 / RFC 6335 section 5.1: underscore + 1 to 15 chars
456        assert!(label("_http").is_valid_service_label());
457        assert!(label("_a").is_valid_service_label());
458        assert!(label("_HTTP").is_valid_service_label());
459        assert!(label("_ipp-tls").is_valid_service_label());
460        assert!(label("_a-b-c").is_valid_service_label());
461        // digits are allowed as long as there is at least one letter
462        assert!(label("_9p").is_valid_service_label());
463        assert!(label("_1a").is_valid_service_label());
464        assert!(label("_a1").is_valid_service_label());
465        // exactly 15 characters after the underscore
466        assert!(label("_abcdefghijklmno").is_valid_service_label());
467    }
468
469    #[test]
470    fn service_label_invalid() {
471        assert!(!label("").is_valid_service_label());
472        assert!(!label("_").is_valid_service_label());
473        // missing mandatory underscore
474        assert!(!label("http").is_valid_service_label());
475        // 16 characters after the underscore
476        assert!(!label("_abcdefghijklmnop").is_valid_service_label());
477        // must contain at least one letter
478        assert!(!label("_123").is_valid_service_label());
479        assert!(!label("_1-2").is_valid_service_label());
480        // must not begin or end with a hyphen
481        assert!(!label("_-a").is_valid_service_label());
482        assert!(!label("_a-").is_valid_service_label());
483        // hyphens must not be adjacent
484        assert!(!label("_a--b").is_valid_service_label());
485        // only US-ASCII letters, digits and hyphens
486        assert!(!label("_a_b").is_valid_service_label());
487        assert!(!label("_a.b").is_valid_service_label());
488        assert!(!label("_a b").is_valid_service_label());
489        assert!(!label("_caf\u{e9}").is_valid_service_label());
490        // the underscore is only allowed in the first position
491        assert!(!label("__http").is_valid_service_label());
492    }
493
494    #[test]
495    fn instance_label_valid() {
496        // RFC 6763 section 4.3: any characters, including dots, spaces and UTF-8
497        assert!(label("My Printer").is_valid_instance_label());
498        assert!(label("a").is_valid_instance_label());
499        assert!(label("Stuart's Test Server").is_valid_instance_label());
500        assert!(label("My Printer. Floor 2").is_valid_instance_label());
501        assert!(label(r"back\slash").is_valid_instance_label());
502        assert!(label("-leading and trailing-").is_valid_instance_label());
503        assert!(label("_underscore").is_valid_instance_label());
504        assert!(label("caf\u{e9}").is_valid_instance_label());
505        assert!(label("\u{1F5A8}").is_valid_instance_label());
506        assert!(label(&"a".repeat(63)).is_valid_instance_label());
507    }
508
509    #[test]
510    fn instance_label_invalid() {
511        assert!(!label("").is_valid_instance_label());
512        assert!(!label(&"a".repeat(64)).is_valid_instance_label());
513    }
514
515    #[test]
516    fn instance_label_limit_counts_utf8_octets() {
517        // 32 x 2-byte characters = 64 octets
518        assert!(!label(&"\u{e9}".repeat(32)).is_valid_instance_label());
519        // 31 x 2-byte characters + 1 ASCII = 63 octets
520        assert!(label(&("\u{e9}".repeat(31) + "a")).is_valid_instance_label());
521    }
522}