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}