domain/new/base/name/
compressor.rs

1//! Name compression.
2
3use crate::new::base::name::{Label, LabelIter};
4
5use super::{Name, RevName};
6
7/// A domain name compressor.
8///
9/// This struct provides name compression functionality when building DNS
10/// messages.  It compares domain names to those already in the message, and
11/// if a shared suffix is found, the newly-inserted name will point at the
12/// existing instance of the suffix.
13///
14/// This struct stores the positions of domain names already present in the
15/// DNS message, as it is otherwise impossible to differentiate domain names
16/// from other bytes.  Only recently-inserted domain names are stored, and
17/// only from the first 16KiB of the message (as compressed names cannot point
18/// any further).  This is good enough for building small and large messages.
19#[repr(align(64))] // align to a typical cache line
20pub struct NameCompressor {
21    /// The last use position of every entry.
22    ///
23    /// Every time an entry is used (directly or indirectly), its last use
24    /// position is updated so that more stale entries are evicted before it.
25    ///
26    /// The last use position is calculated somewhat approximately; it would
27    /// most appropriately be '(number of children, position of inserted
28    /// name)', but it is approximated as 'position of inserted name + offset
29    /// into the name if it were uncompressed'.  In either formula, the entry
30    /// with the minimum value would be evicted first.
31    ///
32    /// Both formulae guarantee that entries will be evicted before any of
33    /// their dependencies.  The former formula requires at least 19 bits of
34    /// storage, while the latter requires less than 15 bits.  The latter can
35    /// prioritize a deeply-nested name suffix over a slightly more recently
36    /// used name that is less nested, but this should be quite rare.
37    ///
38    /// Valid values:
39    /// - Initialized entries: `[1, 16383+253]`.
40    /// - Uninitialized entries: 0.
41    last_use: [u16; 32],
42
43    /// The position of each entry.
44    ///
45    /// This is a byte offset from the message contents (zero represents the
46    /// first byte after the 12-byte message header).
47    ///
48    /// Valid values:
49    /// - Initialized entries: `[0, 16383]`.
50    /// - Uninitialized entries: 0.
51    pos: [u16; 32],
52
53    /// The length of the relative domain name in each entry.
54    ///
55    /// This is the length of the domain name each entry represents, up to
56    /// (but excluding) the root label or compression pointer.  It is used to
57    /// quickly find the end of the domain name for matching suffixes.
58    ///
59    /// Valid values:
60    /// - Initialized entries: `[2, 253]`.
61    /// - Uninitialized entries: 0.
62    len: [u8; 32],
63
64    /// The parent of this entry, if any.
65    ///
66    /// If this entry represents a compressed domain name, this value stores
67    /// the index of that entry.
68    ///
69    /// Valid values:
70    /// - Initialized entries with parents: `[32, 63]`.
71    /// - Initialized entries without parents: 64.
72    /// - Uninitialized entries: 0.
73    parent: [u8; 32],
74
75    /// A 16-bit hash of the entry's last label.
76    ///
77    /// An existing entry will be used for compressing a new domain name when
78    /// the last label in both of them is identical.  This field stores a hash
79    /// over the last label in every entry, to speed up lookups.
80    ///
81    /// Valid values:
82    /// - Initialized entries: `[0, 65535]`.
83    /// - Uninitialized entries: 0.
84    hash: [u16; 32],
85}
86
87impl NameCompressor {
88    /// Construct an empty [`NameCompressor`].
89    pub const fn new() -> Self {
90        Self {
91            last_use: [0u16; 32],
92            pos: [0u16; 32],
93            len: [0u8; 32],
94            parent: [0u8; 32],
95            hash: [0u16; 32],
96        }
97    }
98
99    /// Compress a [`RevName`].
100    ///
101    /// This is a low-level function; use [`RevName::build_in_message()`] to
102    /// write a [`RevName`] into a DNS message.
103    ///
104    /// Given the contents of the DNS message, determine how to compress the
105    /// given domain name.  If a suitable compression for the name could be
106    /// found, this function returns the length of the uncompressed suffix as
107    /// well as the address of the compressed prefix.
108    ///
109    /// The contents slice should begin immediately after the 12-byte message
110    /// header.  It must end at the position the name will be inserted.  It is
111    /// assumed that the domain names inserted in these contents still exist
112    /// from previous calls to [`compress_name()`] and related methods.  If
113    /// this is not true, panics or silently invalid results may occur.
114    ///
115    /// The compressor's state will be updated to assume the provided name was
116    /// inserted into the message.
117    pub fn compress_revname<'n>(
118        &mut self,
119        contents: &[u8],
120        name: &'n RevName,
121    ) -> Option<(&'n [u8], u16)> {
122        // Treat the name as a byte sequence without the root label.
123        let mut name = &name.as_bytes()[1..];
124
125        if name.is_empty() {
126            // Root names are never compressed.
127            return None;
128        }
129
130        let mut parent = 64u8;
131        let mut parent_offset = None;
132
133        // Repeatedly look up entries that could be used for compression.
134        while !name.is_empty() {
135            match self.lookup_entry_for_revname(contents, name, parent) {
136                Some(entry) => {
137                    let tmp;
138                    (parent, name, tmp) = entry;
139                    parent_offset = Some(tmp);
140
141                    // This entry was successfully used for compression.
142                    // Record its use at this (approximate) position.
143                    let use_pos = contents.len() + name.len();
144                    let use_pos = use_pos.max(1);
145                    if use_pos < 16383 + 253 {
146                        self.last_use[parent as usize] = use_pos as u16;
147                    }
148                }
149                None => break,
150            }
151        }
152
153        // If there is a non-empty uncompressed prefix, register it as a new
154        // entry here.
155        if !name.is_empty() && contents.len() < 16384 {
156            // SAFETY: 'name' is a non-empty sequence of labels.
157            let first = unsafe {
158                LabelIter::new_unchecked(name).next().unwrap_unchecked()
159            };
160
161            // Pick the entry that was least recently used (or uninitialized).
162            //
163            // By the invariants of 'last_use', it is guaranteed that this
164            // entry is not the parent of any others.
165            let index = (0usize..32)
166                .min_by_key(|&i| self.last_use[i])
167                .expect("the iterator has 32 elements");
168
169            self.last_use[index] = contents.len() as u16;
170            self.pos[index] = contents.len() as u16;
171            self.len[index] = name.len() as u8;
172            self.parent[index] = parent;
173            self.hash[index] = Self::hash_label(first);
174        }
175
176        // If 'parent_offset' is 'Some', then at least one entry was found,
177        // and so the name was compressed.
178        parent_offset.map(|offset| (name, offset))
179    }
180
181    /// Look up entries which share a suffix with the given reversed name.
182    ///
183    /// At most one entry ends with a complete label matching the given name.
184    /// We will match suffixes using a linear-time algorithm.
185    ///
186    /// On success, the entry's index, the remainder of the name, and the
187    /// offset of the referenced domain name are returned.
188    fn lookup_entry_for_revname<'n>(
189        &self,
190        contents: &[u8],
191        name: &'n [u8],
192        parent: u8,
193    ) -> Option<(u8, &'n [u8], u16)> {
194        // SAFETY: 'name' is a sequence of labels.
195        let mut name_labels = unsafe { LabelIter::new_unchecked(name) };
196        // SAFETY: 'name' is non-empty.
197        let first = unsafe { name_labels.next().unwrap_unchecked() };
198        let hash = Self::hash_label(first);
199
200        // Search for an entry with a matching hash and parent.
201        for i in 0..32 {
202            // Check the hash first, as it's less likely to match.  It's also
203            // okay if both checks are performed unconditionally.
204            if self.hash[i] != hash || self.parent[i] != parent {
205                continue;
206            };
207
208            // Look up the entry in the message contents.
209            let (pos, len) = (self.pos[i] as usize, self.len[i] as usize);
210            debug_assert_ne!(len, 0);
211            let mut entry = contents.get(pos..pos + len)
212                .unwrap_or_else(|| panic!("'contents' did not correspond to the name compressor state"));
213
214            // Find a shared suffix between the entry and the name.
215            //
216            // Comparing a 'Name' to a 'RevName' properly is difficult.  We're
217            // just going for the lazy and not-pedantically-correct version,
218            // where we blindly match 'RevName' labels against the end of the
219            // 'Name'.  The bytes are definitely correct, but there's a small
220            // chance that we aren't consistent with label boundaries.
221
222            // TODO(1.80): Use 'slice::split_at_checked()'.
223            if entry.len() < first.as_bytes().len()
224                || !entry[entry.len() - first.as_bytes().len()..]
225                    .eq_ignore_ascii_case(first.as_bytes())
226            {
227                continue;
228            }
229            entry = &entry[..entry.len() - first.as_bytes().len()];
230
231            for label in name_labels.clone() {
232                if entry.len() < label.as_bytes().len()
233                    || !entry[entry.len() - label.as_bytes().len()..]
234                        .eq_ignore_ascii_case(label.as_bytes())
235                {
236                    break;
237                }
238                entry = &entry[..entry.len() - label.as_bytes().len()];
239            }
240
241            // Suffixes from 'entry' that were also in 'name' have been
242            // removed.  The remainder of 'entry' does not match with 'name'.
243            // 'name' can be compressed using this entry.
244            let rest = name_labels.remaining();
245            let pos = pos + entry.len();
246            return Some((i as u8, rest, pos as u16));
247        }
248
249        None
250    }
251
252    /// Compress a [`Name`].
253    ///
254    /// This is a low-level function; use [`Name::build_in_message()`] to
255    /// write a [`Name`] into a DNS message.
256    ///
257    /// Given the contents of the DNS message, determine how to compress the
258    /// given domain name.  If a suitable compression for the name could be
259    /// found, this function returns the length of the uncompressed prefix as
260    /// well as the address of the suffix.
261    ///
262    /// The contents slice should begin immediately after the 12-byte message
263    /// header.  It must end at the position the name will be inserted.  It is
264    /// assumed that the domain names inserted in these contents still exist
265    /// from previous calls to [`compress_name()`] and related methods.  If
266    /// this is not true, panics or silently invalid results may occur.
267    ///
268    /// The compressor's state will be updated to assume the provided name was
269    /// inserted into the message.
270    pub fn compress_name<'n>(
271        &mut self,
272        contents: &[u8],
273        name: &'n Name,
274    ) -> Option<(&'n [u8], u16)> {
275        // Treat the name as a byte sequence without the root label.
276        let mut name = &name.as_bytes()[..name.len() - 1];
277
278        if name.is_empty() {
279            // Root names are never compressed.
280            return None;
281        }
282
283        let mut hash = Self::hash_label(Self::last_label(name));
284        let mut parent = 64u8;
285        let mut parent_offset = None;
286
287        // Repeatedly look up entries that could be used for compression.
288        while !name.is_empty() {
289            match self.lookup_entry_for_name(contents, name, parent, hash) {
290                Some(entry) => {
291                    let tmp;
292                    (parent, name, hash, tmp) = entry;
293                    parent_offset = Some(tmp);
294
295                    // This entry was successfully used for compression.
296                    // Record its use at this (approximate) position.
297                    let use_pos = contents.len() + name.len();
298                    let use_pos = use_pos.max(1);
299                    if use_pos < 16383 + 253 {
300                        self.last_use[parent as usize] = use_pos as u16;
301                    }
302                }
303                None => break,
304            }
305        }
306
307        // If there is a non-empty uncompressed prefix, register it as a new
308        // entry here.  We already know what the hash of its last label is.
309        if !name.is_empty() && contents.len() < 16384 {
310            // Pick the entry that was least recently used (or uninitialized).
311            //
312            // By the invariants of 'last_use', it is guaranteed that this
313            // entry is not the parent of any others.
314            let index = (0usize..32)
315                .min_by_key(|&i| self.last_use[i])
316                .expect("the iterator has 32 elements");
317
318            self.last_use[index] = contents.len() as u16;
319            self.pos[index] = contents.len() as u16;
320            self.len[index] = name.len() as u8;
321            self.parent[index] = parent;
322            self.hash[index] = hash;
323        }
324
325        // If 'parent_offset' is 'Some', then at least one entry was found,
326        // and so the name was compressed.
327        parent_offset.map(|offset| (name, offset))
328    }
329
330    /// Look up entries which share a suffix with the given name.
331    ///
332    /// At most one entry ends with a complete label matching the given name.
333    /// We will carefully match suffixes using a linear-time algorithm.
334    ///
335    /// On success, the entry's index, the remainder of the name, the hash of
336    /// the last label in the remainder of the name (if any), and the offset
337    /// of the referenced domain name are returned.
338    fn lookup_entry_for_name<'n>(
339        &self,
340        contents: &[u8],
341        name: &'n [u8],
342        parent: u8,
343        hash: u16,
344    ) -> Option<(u8, &'n [u8], u16, u16)> {
345        // SAFETY: 'name' is a non-empty sequence of labels.
346        let name_labels = unsafe { LabelIter::new_unchecked(name) };
347
348        // Search for an entry with a matching hash and parent.
349        for i in 0..32 {
350            // Check the hash first, as it's less likely to match.  It's also
351            // okay if both checks are performed unconditionally.
352            if self.hash[i] != hash || self.parent[i] != parent {
353                continue;
354            };
355
356            // Look up the entry in the message contents.
357            let (pos, len) = (self.pos[i] as usize, self.len[i] as usize);
358            debug_assert_ne!(len, 0);
359            let entry = contents.get(pos..pos + len)
360                .unwrap_or_else(|| panic!("'contents' did not correspond to the name compressor state"));
361
362            // Find a shared suffix between the entry and the name.
363            //
364            // We're going to use a not-pendantically-correct implementation
365            // where we blindly match the ends of the names.  The bytes are
366            // definitely correct, but there's a small chance we aren't
367            // consistent with label boundaries.
368
369            let suffix_len = core::iter::zip(
370                name.iter().rev().map(u8::to_ascii_lowercase),
371                entry.iter().rev().map(u8::to_ascii_lowercase),
372            )
373            .position(|(a, b)| a != b);
374
375            let Some(suffix_len) = suffix_len else {
376                // 'iter::zip()' simply ignores unequal iterators, stopping
377                // when either iterator finishes.  Even though the two names
378                // had no mismatching bytes, one could be longer than the
379                // other.
380                if name.len() > entry.len() {
381                    // 'entry' is a proper suffix of 'name'.  'name' can be
382                    // compressed using 'entry', and will have at least one
383                    // more label before it.  This label needs to be found and
384                    // hashed.
385
386                    let rest = &name[..name.len() - entry.len()];
387                    let hash = Self::hash_label(Self::last_label(rest));
388                    return Some((i as u8, rest, hash, pos as u16));
389                } else {
390                    // 'name' is a suffix of 'entry'.  'name' can be
391                    // compressed using 'entry', and no labels will be left.
392                    let rest = &name[..0];
393                    let hash = 0u16;
394                    let pos = pos + len - name.len();
395                    return Some((i as u8, rest, hash, pos as u16));
396                }
397            };
398
399            // Walk 'name' until we reach the shared suffix region.
400
401            // NOTE:
402            // - 'suffix_len < min(name.len(), entry.len())'.
403            // - 'name_labels.remaining.len() == name.len()'.
404            // - Thus 'suffix_len < name_labels.remaining.len()'.
405            // - Thus we can move the first statement of the loop here.
406            // SAFETY:
407            // - 'name' and 'entry' have a corresponding but unequal byte.
408            // - Thus 'name' has at least one byte.
409            // - Thus 'name' has at least one label.
410            let mut name_labels = name_labels.clone();
411            let mut prev_in_name =
412                unsafe { name_labels.next().unwrap_unchecked() };
413            while name_labels.remaining().len() > suffix_len {
414                // SAFETY:
415                // - 'LabelIter' is only empty once 'remaining' is empty.
416                // - 'remaining > suffix_len >= 0'.
417                prev_in_name =
418                    unsafe { name_labels.next().unwrap_unchecked() };
419            }
420
421            // 'entry' and 'name' share zero or more labels, and this shared
422            // suffix is equal to 'name_labels'.  The 'name_label' bytes might
423            // not lie on the correct label boundaries in 'entry', but this is
424            // not problematic.  If 'name_labels' is non-empty, 'name' can be
425            // compressed using this entry.
426
427            let suffix_len = name_labels.remaining().len();
428            if suffix_len == 0 {
429                continue;
430            }
431
432            let rest = &name[..name.len() - suffix_len];
433            let hash = Self::hash_label(prev_in_name);
434            let pos = pos + len - suffix_len;
435            return Some((i as u8, rest, hash, pos as u16));
436        }
437
438        None
439    }
440
441    /// Find the last label of a domain name.
442    ///
443    /// The name must be a valid non-empty sequence of labels.
444    fn last_label(name: &[u8]) -> &Label {
445        // The last label begins with a length octet and is followed by
446        // the corresponding number of bytes.  While the length octet
447        // could look like a valid ASCII character, it would have to be
448        // 45 (ASCII '-') or above; most labels are not that long.
449        //
450        // We will search backwards for a byte that could be the length
451        // octet of the last label.  It is highly likely that exactly one
452        // match will be found; this is guaranteed to be the right result.
453        // If more than one match is found, we will fall back to searching
454        // from the beginning.
455        //
456        // It is possible (although unlikely) for LLVM to vectorize this
457        // process, since it performs 64 unconditional byte comparisons
458        // over a fixed array.  A manually vectorized implementation would
459        // generate a 64-byte mask for the valid bytes in 'name', load all
460        // 64 bytes blindly, then do a masked comparison against iota.
461
462        name.iter()
463            // Take the last 64 bytes of the name.
464            .rev()
465            .take(64)
466            // Compare those bytes against valid length octets.
467            .enumerate()
468            .filter_map(|(i, &b)| (i == b as usize).then_some(b))
469            // Look for a single valid length octet.
470            .try_fold(None, |acc, len| match acc {
471                None => Ok(Some(len)),
472                Some(_) => Err(()),
473            })
474            // Unwrap the 'Option' since it's guaranteed to be 'Some'.
475            .transpose()
476            .unwrap_or_else(|| {
477                unreachable!("a valid last label could not be found")
478            })
479            // Locate the selected bytes.
480            .map(|len| {
481                let bytes = &name[name.len() - len as usize - 1..];
482
483                // SAFETY: 'name' is a non-empty sequence of labels, and
484                // we have correctly selected the last label within it.
485                unsafe { Label::from_bytes_unchecked(bytes) }
486            })
487            // Otherwise, fall back to a forward traversal.
488            .unwrap_or_else(|()| {
489                // SAFETY: 'name' is a non-empty sequence of labels.
490                unsafe { LabelIter::new_unchecked(name) }
491                    .last()
492                    .expect("'name' is not '.'")
493            })
494    }
495
496    /// Hash a label.
497    fn hash_label(label: &Label) -> u16 {
498        // This code is copied from the 'hash_bytes()' function of
499        // 'rustc-hash' v2.1.1, with helpers.  The codebase is dual-licensed
500        // under Apache-2.0 and MIT, with no explicit copyright statement.
501        //
502        // 'hash_bytes()' is described as "a wyhash-inspired
503        // non-collision-resistant hash for strings/slices designed by Orson
504        // Peters, with a focus on small strings and small codesize."
505        //
506        // While the output of 'hash_bytes()' would pass through an additional
507        // multiplication in 'add_to_hash()', manual testing on some sample
508        // zonefiles showed that the top 16 bits of the 'hash_bytes()' output
509        // was already very uniform.
510        //
511        // Source: <https://github.com/rust-lang/rustc-hash/blob/dc5c33f1283de2da64d8d7a06401d91aded03ad4/src/lib.rs>
512        //
513        // In order to hash case-insensitively, we aggressively transform the
514        // input bytes.  We cause some collisions, but only in characters we
515        // don't expect to see in domain names.  We do this by mapping bytes
516        // from 'XX0X_XXXX' to 'XX1X_XXXX'.  A full list of effects:
517        //
518        // - Control characters (0x00..0x20) become symbols and digits.  We
519        //   weren't expecting any control characters to appear anyway.
520        //
521        // - Uppercase ASCII characters become lowercased.
522        //
523        // - '@[\]^_' become '`{|}~' and DEL.  Underscores can occur, but DEL
524        //   is not expected, so the collision is not problematic.
525        //
526        // - Half of the non-ASCII space gets folded.  Unicode sequences get
527        //   mapped into ASCII using Punycode, so the chance of a non-ASCII
528        //   character here is very low.
529
530        #[cfg(target_pointer_width = "64")]
531        fn multiply_mix(x: u64, y: u64) -> u64 {
532            let prod = (x as u128) * (y as u128);
533            (prod as u64) ^ ((prod >> 64) as u64)
534        }
535
536        #[cfg(target_pointer_width = "32")]
537        fn multiply_mix(x: u64, y: u64) -> u64 {
538            let a = (x & u32::MAX as u64) * (y >> 32);
539            let b = (y & u32::MAX as u64) * (x >> 32);
540            a ^ b.rotate_right(32)
541        }
542
543        const SEED1: u64 = 0x243f6a8885a308d3;
544        const SEED2: u64 = 0x13198a2e03707344;
545        const M: u64 = 0xa4093822299f31d0;
546
547        let bytes = label.as_bytes();
548        let len = bytes.len();
549        let mut s = (SEED1, SEED2);
550
551        if len <= 16 {
552            // XOR the input into s0, s1.
553            if len >= 8 {
554                let i = [&bytes[..8], &bytes[len - 8..]]
555                    .map(|i| u64::from_le_bytes(i.try_into().unwrap()))
556                    .map(|i| i | 0x20202020_20202020);
557
558                s.0 ^= i[0];
559                s.1 ^= i[1];
560            } else if len >= 4 {
561                let i = [&bytes[..4], &bytes[len - 4..]]
562                    .map(|i| u32::from_le_bytes(i.try_into().unwrap()))
563                    .map(|i| i | 0x20202020);
564
565                s.0 ^= i[0] as u64;
566                s.1 ^= i[1] as u64;
567            } else if len > 0 {
568                let lo = bytes[0] as u64 | 0x20;
569                let mid = bytes[len / 2] as u64 | 0x20;
570                let hi = bytes[len - 1] as u64 | 0x20;
571                s.0 ^= lo;
572                s.1 ^= (hi << 8) | mid;
573            }
574        } else {
575            // Handle bulk (can partially overlap with suffix).
576            let mut off = 0;
577            while off < len - 16 {
578                let bytes = &bytes[off..off + 16];
579                let i = [&bytes[..8], &bytes[8..]]
580                    .map(|i| u64::from_le_bytes(i.try_into().unwrap()))
581                    .map(|i| i | 0x20202020_20202020);
582
583                // Replace s1 with a mix of s0, x, and y, and s0 with s1.
584                // This ensures the compiler can unroll this loop into two
585                // independent streams, one operating on s0, the other on s1.
586                //
587                // Since zeroes are a common input we prevent an immediate
588                // trivial collapse of the hash function by XOR'ing a constant
589                // with y.
590                let t = multiply_mix(s.0 ^ i[0], M ^ i[1]);
591                s.0 = s.1;
592                s.1 = t;
593                off += 16;
594            }
595
596            let bytes = &bytes[len - 16..];
597            let i = [&bytes[..8], &bytes[8..]]
598                .map(|i| u64::from_le_bytes(i.try_into().unwrap()))
599                .map(|i| i | 0x20202020_20202020);
600            s.0 ^= i[0];
601            s.1 ^= i[1];
602        }
603
604        (multiply_mix(s.0, s.1) >> 48) as u16
605    }
606}
607
608impl Default for NameCompressor {
609    fn default() -> Self {
610        Self::new()
611    }
612}
613
614//============ Tests =========================================================
615
616#[cfg(test)]
617mod tests {
618    use crate::new::base::{build::BuildInMessage, name::NameBuf};
619
620    use super::NameCompressor;
621
622    #[test]
623    fn no_compression() {
624        let mut buffer = [0u8; 26];
625        let mut compressor = NameCompressor::new();
626
627        // The TLD is different, so they cannot be compressed together.
628        let a: NameBuf = "example.org".parse().unwrap();
629        let b: NameBuf = "example.com".parse().unwrap();
630
631        let mut off = 0;
632        off = a
633            .build_in_message(&mut buffer, off, &mut compressor)
634            .unwrap();
635        off = b
636            .build_in_message(&mut buffer, off, &mut compressor)
637            .unwrap();
638
639        assert_eq!(off, buffer.len());
640        assert_eq!(
641            &buffer,
642            b"\
643            \x07example\x03org\x00\
644            \x07example\x03com\x00"
645        );
646    }
647
648    #[test]
649    fn single_shared_label() {
650        let mut buffer = [0u8; 23];
651        let mut compressor = NameCompressor::new();
652
653        // Only the TLD will be shared.
654        let a: NameBuf = "example.org".parse().unwrap();
655        let b: NameBuf = "unequal.org".parse().unwrap();
656
657        let mut off = 0;
658        off = a
659            .build_in_message(&mut buffer, off, &mut compressor)
660            .unwrap();
661        off = b
662            .build_in_message(&mut buffer, off, &mut compressor)
663            .unwrap();
664
665        assert_eq!(off, buffer.len());
666        assert_eq!(
667            &buffer,
668            b"\
669            \x07example\x03org\x00\
670            \x07unequal\xC0\x14"
671        );
672    }
673
674    #[test]
675    fn case_insensitive() {
676        let mut buffer = [0u8; 23];
677        let mut compressor = NameCompressor::new();
678
679        // The TLD should be shared, even if it differs in case.
680        let a: NameBuf = "example.org".parse().unwrap();
681        let b: NameBuf = "unequal.ORG".parse().unwrap();
682
683        let mut off = 0;
684        off = a
685            .build_in_message(&mut buffer, off, &mut compressor)
686            .unwrap();
687        off = b
688            .build_in_message(&mut buffer, off, &mut compressor)
689            .unwrap();
690
691        assert_eq!(off, buffer.len());
692        assert_eq!(
693            &buffer,
694            b"\
695            \x07example\x03org\x00\
696            \x07unequal\xC0\x14"
697        );
698    }
699}