Skip to main content

sozu_lib/router/
pattern_trie.rs

1use std::{collections::HashMap, fmt::Debug, iter, str};
2
3use regex::bytes::Regex;
4
5pub type Key = Vec<u8>;
6pub type KeyValue<K, V> = (K, V);
7
8#[derive(Debug, PartialEq, Eq)]
9pub enum InsertResult {
10    Ok,
11    Existing,
12    Failed,
13}
14
15#[derive(Debug, PartialEq, Eq)]
16pub enum RemoveResult {
17    Ok,
18    NotFound,
19}
20
21fn find_last_dot(input: &[u8]) -> Option<usize> {
22    //println!("find_last_dot: input = {}", from_utf8(input).unwrap());
23    (0..input.len()).rev().find(|&i| input[i] == b'.')
24}
25
26fn find_last_slash(input: &[u8]) -> Option<usize> {
27    //println!("find_last_dot: input = {}", from_utf8(input).unwrap());
28    (0..input.len()).rev().find(|&i| input[i] == b'/')
29}
30
31/// Implementation of a trie tree structure.
32/// In Sozu this is used to store and lookup domains recursively.
33/// Each node represents a "level domain".
34/// A leaf node (leftmost label) can be a wildcard, a regex pattern or a plain string.
35/// Leaves also store a value associated with the complete domain.
36/// For Sozu it is a list of (PathRule, MethodRule, ClusterId). See the Router strucure.
37#[derive(Debug, Default)]
38pub struct TrieNode<V> {
39    key_value: Option<KeyValue<Key, V>>,
40    wildcard: Option<KeyValue<Key, V>>,
41    children: HashMap<Key, TrieNode<V>>,
42    regexps: Vec<(Regex, TrieNode<V>)>,
43}
44
45/// One step of a trie traversal where a non-literal segment matched.
46///
47/// `Wildcard` carries the actual segment bytes consumed by a `*` wildcard
48/// (so a router that wants to capture them can splice them into a rewrite
49/// template). `Regexp` carries both the matched bytes and the regex itself
50/// so the caller can re-run `Regex::captures` to pull explicit groups.
51#[derive(Debug)]
52pub enum TrieSubMatch<'a, 'b> {
53    Wildcard(&'a [u8]),
54    Regexp(&'a [u8], &'b Regex),
55}
56
57/// Ordered list of non-literal trie segments visited during a successful
58/// `lookup_with_path` traversal. Routers feed the entries into rewrite
59/// templates (`$HOST[n]`) so frontend rewrites can reach into the matched
60/// segments. Empty when only literal segments matched.
61pub type TrieMatches<'a, 'b> = Vec<TrieSubMatch<'a, 'b>>;
62
63impl<V: PartialEq> std::cmp::PartialEq for TrieNode<V> {
64    fn eq(&self, other: &Self) -> bool {
65        self.key_value == other.key_value
66            && self.wildcard == other.wildcard
67            && self.children == other.children
68            && self.regexps.len() == other.regexps.len()
69            && self
70                .regexps
71                .iter()
72                .zip(other.regexps.iter())
73                .fold(true, |b, (left, right)| {
74                    b && left.0.as_str() == right.0.as_str() && left.1 == right.1
75                })
76    }
77}
78
79impl<V: Debug + Clone> TrieNode<V> {
80    pub fn new(key: Key, value: V) -> TrieNode<V> {
81        TrieNode {
82            key_value: Some((key, value)),
83            wildcard: None,
84            children: HashMap::new(),
85            regexps: Vec::new(),
86        }
87    }
88
89    pub fn wildcard(key: Key, value: V) -> TrieNode<V> {
90        TrieNode {
91            key_value: None,
92            wildcard: Some((key, value)),
93            children: HashMap::new(),
94            regexps: Vec::new(),
95        }
96    }
97
98    pub fn root() -> TrieNode<V> {
99        TrieNode {
100            key_value: None,
101            wildcard: None,
102            children: HashMap::new(),
103            regexps: Vec::new(),
104        }
105    }
106
107    pub fn is_empty(&self) -> bool {
108        self.key_value.is_none()
109            && self.wildcard.is_none()
110            && self.regexps.is_empty()
111            && self.children.is_empty()
112    }
113
114    pub fn insert(&mut self, key: Key, value: V) -> InsertResult {
115        //println!("insert: key == {}", std::str::from_utf8(&key).unwrap());
116        if key.is_empty() {
117            return InsertResult::Failed;
118        }
119        if key[..] == b"."[..] {
120            return InsertResult::Failed;
121        }
122
123        #[cfg(debug_assertions)]
124        let before = self.count_values();
125
126        let insert_result = self.insert_recursive(&key, &key, value);
127
128        // Post: the value count grows by exactly one on a fresh insert,
129        // is unchanged when the key already existed, and is ALSO unchanged
130        // on `Failed` -- every mutating step in `insert_recursive`
131        // (`children.insert`, `regexps.push`) is guarded by an `Ok` from
132        // the level below, so a rejected key can never leave a half-built
133        // branch behind.
134        //
135        // `Failed` is NOT an internal-invariant break: the two cheap guards
136        // above only rule out the empty key and a bare `.`, while
137        // `insert_recursive` rejects a much wider class of malformed
138        // domains (a key ending in `/` with no openable regex segment, a
139        // regex segment that is not `.`-anchored, a segment that is not a
140        // valid regex, an empty label from a leading or doubled `.`).
141        // Those keys arrive from the control plane -- an `AddHttpFrontend`
142        // over the command socket, or a `LoadState` replay -- so this must
143        // stay a graceful rejection the caller reports. It used to be an
144        // `assert_ne!`, which turned a malformed hostname into a worker
145        // panic (and, on state replay, a restart loop).
146        #[cfg(debug_assertions)]
147        {
148            let after = self.count_values();
149            match insert_result {
150                InsertResult::Ok => debug_assert_eq!(
151                    after,
152                    before + 1,
153                    "a fresh insert must add exactly one value to the trie",
154                ),
155                InsertResult::Existing => debug_assert_eq!(
156                    after, before,
157                    "an Existing insert must not change the trie value count",
158                ),
159                InsertResult::Failed => debug_assert_eq!(
160                    after, before,
161                    "a failed insert must leave the trie value count untouched",
162                ),
163            }
164            self.check_invariants();
165        }
166
167        insert_result
168    }
169
170    pub fn insert_recursive(&mut self, partial_key: &[u8], key: &Key, value: V) -> InsertResult {
171        //println!("insert_rec: key == {}", std::str::from_utf8(partial_key).unwrap());
172        // An empty `partial_key` means the caller handed us a key with an
173        // empty label -- a leading `.` (`.example.com`) or a doubled one --
174        // which the dot-split recursion below cannot consume any further.
175        // Reject it like any other malformed domain instead of panicking:
176        // this input comes from the control plane, not from Sozu itself.
177        if partial_key.is_empty() {
178            return InsertResult::Failed;
179        }
180        // `partial_key` is always a suffix of the full `key` being
181        // inserted — the recursion only ever shrinks the head, never
182        // rewrites the tail.
183        debug_assert!(
184            partial_key.len() <= key.len(),
185            "insert recursion must consume the key, never grow past it",
186        );
187
188        if partial_key[partial_key.len() - 1] == b'/' {
189            let pos = find_last_slash(&partial_key[..partial_key.len() - 1]);
190
191            if let Some(pos) = pos {
192                if pos > 0 && partial_key[pos - 1] != b'.' {
193                    return InsertResult::Failed;
194                }
195
196                if let Ok(s) = str::from_utf8(&partial_key[pos + 1..partial_key.len() - 1]) {
197                    let anchored_s = format!("\\A{s}\\z");
198                    debug_assert!(
199                        anchored_s.starts_with("\\A") && anchored_s.ends_with("\\z"),
200                        "segment regex must be fully anchored so it matches the whole segment only",
201                    );
202                    for t in self.regexps.iter_mut() {
203                        if t.0.as_str() == anchored_s {
204                            // `pos > 0`: there is a `.`-separated prefix
205                            // before this regex segment; recurse on it
206                            // (dropping the leading `.` via `pos - 1`).
207                            // `pos == 0`: the regex is the leftmost/only
208                            // segment, so its subtree is already a
209                            // value-bearing leaf (the create-path below
210                            // built it via `TrieNode::new`); re-inserting
211                            // the same host is `Existing`. The pre-fix code
212                            // did `partial_key[..pos - 1]` unconditionally,
213                            // underflowing to `usize::MAX` and panicking on
214                            // `pos == 0` (same latent bug as `lookup_mut`).
215                            if pos > 0 {
216                                return t.1.insert_recursive(&partial_key[..pos - 1], key, value);
217                            } else {
218                                return InsertResult::Existing;
219                            }
220                        }
221                    }
222
223                    // Anchor segment regexes so they only match the entire
224                    // segment, not partial overlaps. Without `\A...\z`, a
225                    // pattern like `cdn[0-9]+` would match `cdn123xxx`,
226                    // which silently widens the routing surface.
227                    let anchored = format!("\\A{s}\\z");
228                    if let Ok(r) = Regex::new(&anchored) {
229                        if pos > 0 {
230                            let mut node = TrieNode::root();
231                            let pos = pos - 1;
232
233                            let res = node.insert_recursive(&partial_key[..pos], key, value);
234
235                            if res == InsertResult::Ok {
236                                self.regexps.push((r, node));
237                            }
238
239                            return res;
240                        } else {
241                            let node = TrieNode::new(key.to_vec(), value);
242                            self.regexps.push((r, node));
243                            return InsertResult::Ok;
244                        }
245                    }
246                }
247            }
248
249            return InsertResult::Failed;
250        }
251
252        let pos = find_last_dot(partial_key);
253        match pos {
254            None => {
255                if self.children.contains_key(partial_key) {
256                    InsertResult::Existing
257                } else if partial_key == &b"*"[..] {
258                    if self.wildcard.is_some() {
259                        InsertResult::Existing
260                    } else {
261                        self.wildcard = Some((key.to_vec(), value));
262                        InsertResult::Ok
263                    }
264                } else {
265                    let node = TrieNode::new(key.to_vec(), value);
266                    self.children.insert(partial_key.to_vec(), node);
267                    InsertResult::Ok
268                }
269            }
270            Some(pos) => {
271                // The dot at `pos` is kept on the child key (suffix) and
272                // stripped from the recursive prefix; the two slices
273                // partition `partial_key` exactly.
274                debug_assert_eq!(
275                    partial_key[..pos].len() + partial_key[pos..].len(),
276                    partial_key.len(),
277                    "dot-split must partition partial_key without losing bytes",
278                );
279                debug_assert_eq!(
280                    partial_key[pos], b'.',
281                    "find_last_dot must point at a '.' byte",
282                );
283                if let Some(child) = self.children.get_mut(&partial_key[pos..]) {
284                    return child.insert_recursive(&partial_key[..pos], key, value);
285                }
286
287                let mut node = TrieNode::root();
288                let res = node.insert_recursive(&partial_key[..pos], key, value);
289
290                if res == InsertResult::Ok {
291                    self.children.insert(partial_key[pos..].to_vec(), node);
292                }
293
294                res
295            }
296        }
297    }
298
299    pub fn remove(&mut self, key: &Key) -> RemoveResult {
300        #[cfg(debug_assertions)]
301        let before = self.count_values();
302
303        let remove_result = self.remove_recursive(key);
304
305        // Post: a successful remove drops exactly one value; a NotFound
306        // is a no-op on the value count. The structural invariants then
307        // guarantee no emptied subtree was stranded by the prune.
308        #[cfg(debug_assertions)]
309        {
310            let after = self.count_values();
311            match remove_result {
312                RemoveResult::Ok => debug_assert_eq!(
313                    after + 1,
314                    before,
315                    "a successful remove must drop exactly one value from the trie",
316                ),
317                RemoveResult::NotFound => debug_assert_eq!(
318                    after, before,
319                    "a NotFound remove must not change the trie value count",
320                ),
321            }
322            self.check_invariants();
323        }
324
325        remove_result
326    }
327
328    pub fn remove_recursive(&mut self, partial_key: &[u8]) -> RemoveResult {
329        //println!("remove: key == {}", std::str::from_utf8(partial_key).unwrap());
330
331        if partial_key.is_empty() {
332            if self.key_value.is_some() {
333                self.key_value = None;
334                return RemoveResult::Ok;
335            } else {
336                return RemoveResult::NotFound;
337            }
338        }
339
340        if partial_key == &b"*"[..] {
341            if self.wildcard.is_some() {
342                self.wildcard = None;
343                return RemoveResult::Ok;
344            } else {
345                return RemoveResult::NotFound;
346            }
347        }
348
349        if partial_key[partial_key.len() - 1] == b'/' {
350            let pos = find_last_slash(&partial_key[..partial_key.len() - 1]);
351
352            if let Some(pos) = pos {
353                if pos > 0 && partial_key[pos - 1] != b'.' {
354                    return RemoveResult::NotFound;
355                }
356
357                if let Ok(s) = str::from_utf8(&partial_key[pos + 1..partial_key.len() - 1]) {
358                    let anchored_s = format!("\\A{s}\\z");
359                    if pos > 0 {
360                        let mut remove_result = RemoveResult::NotFound;
361                        for t in self.regexps.iter_mut() {
362                            if t.0.as_str() == anchored_s
363                                && t.1.remove_recursive(&partial_key[..pos - 1]) == RemoveResult::Ok
364                            {
365                                remove_result = RemoveResult::Ok;
366                            }
367                        }
368                        return remove_result;
369                    } else {
370                        let len = self.regexps.len();
371                        self.regexps.retain(|(r, _)| r.as_str() != anchored_s);
372                        if len > self.regexps.len() {
373                            return RemoveResult::Ok;
374                        }
375                    }
376                }
377            }
378
379            return RemoveResult::NotFound;
380        }
381
382        let pos = find_last_dot(partial_key);
383        let (prefix, suffix) = match pos {
384            None => (&b""[..], partial_key),
385            Some(pos) => (&partial_key[..pos], &partial_key[pos..]),
386        };
387        //println!("remove: prefix|suffix: {} | {}", std::str::from_utf8(prefix).unwrap(), std::str::from_utf8(suffix).unwrap());
388        debug_assert_eq!(
389            prefix.len() + suffix.len(),
390            partial_key.len(),
391            "dot-split must partition the key without losing or duplicating bytes",
392        );
393
394        match self.children.get_mut(suffix) {
395            Some(child) => match child.remove_recursive(prefix) {
396                RemoveResult::NotFound => RemoveResult::NotFound,
397                RemoveResult::Ok => {
398                    // An emptied child subtree MUST be pruned here so the
399                    // parent never strands a node with no value. After the
400                    // prune the suffix key is gone from `children`.
401                    if child.is_empty() {
402                        self.children.remove(suffix);
403                        debug_assert!(
404                            !self.children.contains_key(suffix),
405                            "an emptied child subtree must be removed from the parent",
406                        );
407                    } else {
408                        // `count_values` is debug-only; gate the whole
409                        // assert so the call does not have to compile in
410                        // release (HARD RULE 2 — E0425 guard).
411                        #[cfg(debug_assertions)]
412                        debug_assert!(
413                            child.count_values() > 0,
414                            "a retained child subtree must still hold at least one value",
415                        );
416                    }
417                    RemoveResult::Ok
418                }
419            },
420            None => RemoveResult::NotFound,
421        }
422    }
423
424    /// Look up `partial_key` and additionally collect the non-literal segments
425    /// that matched along the way (`TrieMatches`).
426    ///
427    /// Equivalent to `lookup` for callers that don't need the captures, but
428    /// frontends with `$HOST[n]` rewrite templates need the matched segments
429    /// to fill the placeholders. The accumulator is passed in by value so
430    /// callers can pre-size it (`Vec::with_capacity`) and we own the path
431    /// returned alongside the value.
432    pub fn lookup_with_path<'a, 'b>(
433        &'b self,
434        partial_key: &'a [u8],
435        accept_wildcard: bool,
436        mut trace: TrieMatches<'a, 'b>,
437    ) -> Option<(&'b KeyValue<Key, V>, TrieMatches<'a, 'b>)> {
438        if partial_key.is_empty() {
439            return self.key_value.as_ref().map(|kv| (kv, trace));
440        }
441
442        let pos = find_last_dot(partial_key);
443        let (prefix, suffix) = match pos {
444            None => (&b""[..], partial_key),
445            Some(pos) => (&partial_key[..pos], &partial_key[pos..]),
446        };
447        // The dot-split partitions the key exactly: prefix ++ suffix is
448        // the whole input, and a dotted split puts the `.` at the head
449        // of the suffix (this is the byte the wildcard/regex arms strip).
450        debug_assert_eq!(
451            prefix.len() + suffix.len(),
452            partial_key.len(),
453            "dot-split must partition the key without losing or duplicating bytes",
454        );
455        debug_assert!(
456            pos.is_none() || suffix.first() == Some(&b'.'),
457            "a dotted split must place the separator at the head of the suffix",
458        );
459
460        match self.children.get(suffix) {
461            Some(child) => child.lookup_with_path(prefix, accept_wildcard, trace),
462            None => {
463                if prefix.is_empty() && self.wildcard.is_some() && accept_wildcard {
464                    let segment = if !suffix.is_empty() && suffix[0] == b'.' {
465                        &suffix[1..]
466                    } else {
467                        suffix
468                    };
469                    trace.push(TrieSubMatch::Wildcard(segment));
470                    self.wildcard.as_ref().map(|kv| (kv, trace))
471                } else {
472                    for (regexp, child) in self.regexps.iter() {
473                        let segment = if !suffix.is_empty() && suffix[0] == b'.' {
474                            &suffix[1..]
475                        } else {
476                            suffix
477                        };
478                        if regexp.is_match(segment) {
479                            let mut next = trace;
480                            next.push(TrieSubMatch::Regexp(segment, regexp));
481                            return child.lookup_with_path(prefix, accept_wildcard, next);
482                        }
483                    }
484                    None
485                }
486            }
487        }
488    }
489
490    pub fn lookup(&self, partial_key: &[u8], accept_wildcard: bool) -> Option<&KeyValue<Key, V>> {
491        //println!("lookup: key == {}", std::str::from_utf8(partial_key).unwrap());
492
493        if partial_key.is_empty() {
494            return self.key_value.as_ref();
495        }
496
497        let pos = find_last_dot(partial_key);
498        let (prefix, suffix) = match pos {
499            None => (&b""[..], partial_key),
500            Some(pos) => (&partial_key[..pos], &partial_key[pos..]),
501        };
502        //println!("lookup: prefix|suffix: {} | {}", std::str::from_utf8(prefix).unwrap(), std::str::from_utf8(suffix).unwrap());
503        debug_assert_eq!(
504            prefix.len() + suffix.len(),
505            partial_key.len(),
506            "dot-split must partition the key without losing or duplicating bytes",
507        );
508        debug_assert!(
509            !suffix.is_empty(),
510            "the suffix the trie matches children against must be non-empty",
511        );
512
513        match self.children.get(suffix) {
514            Some(child) => child.lookup(prefix, accept_wildcard),
515            None => {
516                //println!("no child found, testing wildcard and regexps");
517
518                if prefix.is_empty() && self.wildcard.is_some() && accept_wildcard {
519                    //println!("no dot, wildcard applies");
520                    self.wildcard.as_ref()
521                } else {
522                    //println!("there's still a subdomain, wildcard does not apply");
523
524                    for (regexp, child) in self.regexps.iter() {
525                        let suffix = if suffix[0] == b'.' {
526                            &suffix[1..]
527                        } else {
528                            suffix
529                        };
530                        //println!("testing regexp: {} on suffix {}", r.as_str(), str::from_utf8(s).unwrap());
531
532                        if regexp.is_match(suffix) {
533                            //println!("matched");
534                            return child.lookup(prefix, accept_wildcard);
535                        }
536                    }
537
538                    None
539                }
540            }
541        }
542    }
543
544    pub fn lookup_mut(
545        &mut self,
546        partial_key: &[u8],
547        accept_wildcard: bool,
548    ) -> Option<&mut KeyValue<Key, V>> {
549        //println!("lookup: key == {}", std::str::from_utf8(partial_key).unwrap());
550
551        if partial_key.is_empty() {
552            return self.key_value.as_mut();
553        }
554
555        if partial_key == &b"*"[..] {
556            return self.wildcard.as_mut();
557        }
558
559        if partial_key[partial_key.len() - 1] == b'/' {
560            let pos = find_last_slash(&partial_key[..partial_key.len() - 1]);
561
562            if let Some(pos) = pos {
563                if pos > 0 && partial_key[pos - 1] != b'.' {
564                    return None;
565                }
566
567                if let Ok(s) = str::from_utf8(&partial_key[pos + 1..partial_key.len() - 1]) {
568                    let anchored_s = format!("\\A{s}\\z");
569                    for t in self.regexps.iter_mut() {
570                        if t.0.as_str() == anchored_s {
571                            // `pos == 0` means the regex is the leftmost
572                            // segment and its subtree is a value-bearing
573                            // leaf, reachable via the empty-prefix recursion
574                            // (`lookup_mut(b"")` returns `key_value`). The
575                            // pre-fix `partial_key[..pos - 1]` underflowed to
576                            // `usize::MAX` and panicked on `pos == 0` — the
577                            // same latent bug as the insert dedup loop. Drop
578                            // the leading `.` only when there is one.
579                            let rest = if pos > 0 {
580                                &partial_key[..pos - 1]
581                            } else {
582                                &partial_key[..0]
583                            };
584                            return t.1.lookup_mut(rest, accept_wildcard);
585                        }
586                    }
587                }
588            }
589
590            return None;
591        }
592
593        let pos = find_last_dot(partial_key);
594        let (prefix, suffix) = match pos {
595            None => (&b""[..], partial_key),
596            Some(pos) => (&partial_key[..pos], &partial_key[pos..]),
597        };
598        //println!("lookup: prefix|suffix: {} | {}", std::str::from_utf8(prefix).unwrap(), std::str::from_utf8(suffix).unwrap());
599        debug_assert_eq!(
600            prefix.len() + suffix.len(),
601            partial_key.len(),
602            "dot-split must partition the key without losing or duplicating bytes",
603        );
604        debug_assert!(
605            !suffix.is_empty(),
606            "the suffix the trie matches children against must be non-empty",
607        );
608
609        match self.children.get_mut(suffix) {
610            Some(child) => child.lookup_mut(prefix, accept_wildcard),
611            None => {
612                //println!("no child found, testing wildcard and regexps");
613
614                if prefix.is_empty() && self.wildcard.is_some() && accept_wildcard {
615                    //println!("no dot, wildcard applies");
616                    self.wildcard.as_mut()
617                } else {
618                    //println!("there's still a subdomain, wildcard does not apply");
619
620                    for &mut (ref regexp, ref mut child) in self.regexps.iter_mut() {
621                        let suffix = if suffix[0] == b'.' {
622                            &suffix[1..]
623                        } else {
624                            suffix
625                        };
626                        //println!("testing regexp: {} on suffix {}", r.as_str(), str::from_utf8(s).unwrap());
627
628                        if regexp.is_match(suffix) {
629                            //println!("matched");
630                            return child.lookup_mut(prefix, accept_wildcard);
631                        }
632                    }
633
634                    None
635                }
636            }
637        }
638    }
639
640    pub fn print(&self) {
641        self.print_recursive(b"", 0)
642    }
643
644    pub fn print_recursive(&self, partial_key: &[u8], indent: u8) {
645        let raw_prefix: Vec<u8> = iter::repeat_n(b' ', 2 * indent as usize).collect();
646        let prefix = str::from_utf8(&raw_prefix).unwrap();
647
648        print!("{}{}: ", prefix, str::from_utf8(partial_key).unwrap());
649        if let Some((ref key, ref value)) = self.key_value {
650            print!("({}, {:?}) | ", str::from_utf8(key).unwrap(), value);
651        } else {
652            print!("None | ");
653        }
654
655        if let Some((key, value)) = &self.wildcard {
656            println!("({}, {:?})", str::from_utf8(key).unwrap(), value);
657        } else {
658            println!("None");
659        }
660
661        for (child_key, child) in self.children.iter() {
662            child.print_recursive(child_key, indent + 1);
663        }
664
665        for (regexp, child) in self.regexps.iter() {
666            //print!("{}{}:", prefix, regexp.as_str());
667            child.print_recursive(regexp.as_str().as_bytes(), indent + 1);
668        }
669    }
670
671    /// Visit every stored value in the trie (the literal `key_value` and
672    /// the leftmost `wildcard` slot of every node, plus all
673    /// regex-subtree leaves) and invoke `f` on each. Used by the router
674    /// to walk all routes for cross-cutting refreshes (e.g. listener-
675    /// default HSTS reflow) without rebuilding the trie.
676    pub fn for_each_value_mut<F: FnMut(&mut V)>(&mut self, f: &mut F) {
677        if let Some((_, ref mut value)) = self.key_value {
678            f(value);
679        }
680        if let Some((_, ref mut value)) = self.wildcard {
681            f(value);
682        }
683        for child in self.children.values_mut() {
684            child.for_each_value_mut(f);
685        }
686        for (_, child) in self.regexps.iter_mut() {
687            child.for_each_value_mut(f);
688        }
689    }
690
691    /// Count every value slot reachable from this node: the literal
692    /// `key_value`, the leftmost `wildcard`, plus all values stored in
693    /// child subtrees and regex subtrees. Used only by the
694    /// `#[cfg(debug_assertions)]` invariant checks as the leaf-count
695    /// accounting (`inserts − removes`); never called in release.
696    #[cfg(debug_assertions)]
697    fn count_values(&self) -> usize {
698        let local = self.key_value.is_some() as usize + self.wildcard.is_some() as usize;
699        let in_children: usize = self.children.values().map(TrieNode::count_values).sum();
700        let in_regexps: usize = self.regexps.iter().map(|(_, c)| c.count_values()).sum();
701        local + in_children + in_regexps
702    }
703
704    /// Full structural invariant sweep for the trie, asserted as a
705    /// run-to-completion postcondition at the end of every mutating
706    /// public operation. Encodes the cross-field invariants that the
707    /// recursive insert/remove logic must preserve:
708    ///
709    /// - **No stranded interior node**: every non-root node reachable
710    ///   through `children` / `regexps` must hold a value somewhere in
711    ///   its subtree (`!is_empty()` and `count_values() > 0`). A node
712    ///   that holds neither a value nor any descendant value is a leak —
713    ///   `remove_recursive` is supposed to prune it via `is_empty()`.
714    /// - **Unique regex segments**: the anchored pattern strings stored
715    ///   in `regexps` are unique within a node (insert dedups by
716    ///   `as_str()` before pushing a new subtree).
717    /// - **Child-key invariant**: no child is keyed by the empty slice.
718    ///
719    /// `debug_assertions`-only; compiled out of release builds.
720    #[cfg(debug_assertions)]
721    fn check_invariants(&self) {
722        // Regex segment patterns are unique per node.
723        for i in 0..self.regexps.len() {
724            for j in (i + 1)..self.regexps.len() {
725                debug_assert_ne!(
726                    self.regexps[i].0.as_str(),
727                    self.regexps[j].0.as_str(),
728                    "trie node must not hold two subtrees for the same regex segment",
729                );
730            }
731        }
732
733        for (child_key, child) in self.children.iter() {
734            debug_assert!(
735                !child_key.is_empty(),
736                "trie child must not be keyed by the empty segment",
737            );
738            // A child subtree that has been fully emptied must have been
739            // pruned by remove_recursive; reaching it here means a
740            // subtree was stranded.
741            debug_assert!(
742                !child.is_empty(),
743                "trie must not strand an empty child subtree (remove must prune)",
744            );
745            debug_assert!(
746                child.count_values() > 0,
747                "trie child subtree must lead to at least one value",
748            );
749            child.check_invariants();
750        }
751
752        for (_, child) in self.regexps.iter() {
753            debug_assert!(
754                !child.is_empty(),
755                "trie must not strand an empty regex subtree (remove must prune)",
756            );
757            debug_assert!(
758                child.count_values() > 0,
759                "trie regex subtree must lead to at least one value",
760            );
761            child.check_invariants();
762        }
763    }
764
765    pub fn domain_insert(&mut self, key: Key, value: V) -> InsertResult {
766        self.insert(key, value)
767    }
768
769    pub fn domain_remove(&mut self, key: &Key) -> RemoveResult {
770        self.remove(key)
771    }
772
773    pub fn domain_lookup(&self, key: &[u8], accept_wildcard: bool) -> Option<&KeyValue<Key, V>> {
774        self.lookup(key, accept_wildcard)
775    }
776
777    pub fn domain_lookup_mut(
778        &mut self,
779        key: &[u8],
780        accept_wildcard: bool,
781    ) -> Option<&mut KeyValue<Key, V>> {
782        self.lookup_mut(key, accept_wildcard)
783    }
784
785    pub fn size(&self) -> usize {
786        ::std::mem::size_of::<TrieNode<V>>()
787            + ::std::mem::size_of::<Option<KeyValue<Key, V>>>() * 2
788            + self
789                .children
790                .iter()
791                .fold(0, |acc, c| acc + c.0.len() + c.1.size())
792    }
793
794    pub fn to_hashmap(&self) -> HashMap<Key, V> {
795        let mut h = HashMap::new();
796
797        self.to_hashmap_recursive(&mut h);
798
799        h
800    }
801
802    pub fn to_hashmap_recursive(&self, h: &mut HashMap<Key, V>) {
803        if let Some((key, value)) = &self.key_value {
804            h.insert(key.clone(), value.clone());
805        }
806
807        if let Some((key, value)) = &self.wildcard {
808            h.insert(key.clone(), value.clone());
809        }
810
811        for child in self.children.values() {
812            child.to_hashmap_recursive(h);
813        }
814    }
815}
816
817#[cfg(test)]
818mod tests {
819    use super::*;
820
821    #[test]
822    fn insert() {
823        let mut root: TrieNode<u8> = TrieNode::root();
824        root.print();
825
826        assert_eq!(
827            root.domain_insert(Vec::from(&b"abcd"[..]), 1),
828            InsertResult::Ok
829        );
830        root.print();
831        assert_eq!(
832            root.domain_insert(Vec::from(&b"abce"[..]), 2),
833            InsertResult::Ok
834        );
835        root.print();
836        assert_eq!(
837            root.domain_insert(Vec::from(&b"abgh"[..]), 3),
838            InsertResult::Ok
839        );
840        root.print();
841
842        assert_eq!(
843            root.domain_lookup(&b"abce"[..], true),
844            Some(&(b"abce"[..].to_vec(), 2))
845        );
846        //assert!(false);
847    }
848
849    #[test]
850    fn remove() {
851        let mut root: TrieNode<u8> = TrieNode::root();
852        println!("creating root:");
853        root.print();
854
855        println!("adding (abcd, 1)");
856        assert_eq!(root.insert(Vec::from(&b"abcd"[..]), 1), InsertResult::Ok);
857        root.print();
858        println!("adding (abce, 2)");
859        assert_eq!(root.insert(Vec::from(&b"abce"[..]), 2), InsertResult::Ok);
860        root.print();
861        println!("adding (abgh, 3)");
862        assert_eq!(root.insert(Vec::from(&b"abgh"[..]), 3), InsertResult::Ok);
863        root.print();
864
865        let mut root2: TrieNode<u8> = TrieNode::root();
866
867        assert_eq!(root2.insert(Vec::from(&b"abcd"[..]), 1), InsertResult::Ok);
868        assert_eq!(root2.insert(Vec::from(&b"abgh"[..]), 3), InsertResult::Ok);
869
870        println!("before remove");
871        root.print();
872        assert_eq!(root.remove(&Vec::from(&b"abce"[..])), RemoveResult::Ok);
873        println!("after remove");
874        root.print();
875
876        println!("expected");
877        root2.print();
878        assert_eq!(root, root2);
879
880        assert_eq!(root.remove(&Vec::from(&b"abgh"[..])), RemoveResult::Ok);
881        println!("after remove");
882        root.print();
883        println!("expected");
884        let mut root3: TrieNode<u8> = TrieNode::root();
885        assert_eq!(root3.insert(Vec::from(&b"abcd"[..]), 1), InsertResult::Ok);
886        root3.print();
887        assert_eq!(root, root3);
888    }
889
890    #[test]
891    fn insert_remove_through_regex() {
892        let mut root: TrieNode<u8> = TrieNode::root();
893        println!("creating root:");
894        root.print();
895
896        println!("adding (www./.*/.com, 1)");
897        assert_eq!(
898            root.insert(Vec::from(&b"www./.*/.com"[..]), 1),
899            InsertResult::Ok
900        );
901        root.print();
902        println!("adding (www.doc./.*/.com, 2)");
903        assert_eq!(
904            root.insert(Vec::from(&b"www.doc./.*/.com"[..]), 2),
905            InsertResult::Ok
906        );
907        root.print();
908        assert_eq!(
909            root.domain_lookup(b"www.sozu.com".as_ref(), false),
910            Some(&(b"www./.*/.com".to_vec(), 1))
911        );
912        assert_eq!(
913            root.domain_lookup(b"www.doc.sozu.com".as_ref(), false),
914            Some(&(b"www.doc./.*/.com".to_vec(), 2))
915        );
916
917        assert_eq!(
918            root.domain_remove(&b"www./.*/.com".to_vec()),
919            RemoveResult::Ok
920        );
921        root.print();
922        assert_eq!(root.domain_lookup(b"www.sozu.com".as_ref(), false), None);
923        assert_eq!(
924            root.domain_lookup(b"www.doc.sozu.com".as_ref(), false),
925            Some(&(b"www.doc./.*/.com".to_vec(), 2))
926        );
927    }
928
929    /// Segment regexes must match the entire segment, not just a prefix.
930    /// Without `\A...\z` anchoring the previous behaviour matched any
931    /// segment whose prefix satisfied the pattern, silently widening the
932    /// routing surface. This regression test exercises the exact-match
933    /// invariant that anchoring guarantees.
934    /// `insert` must REJECT a malformed domain, never panic. These keys
935    /// come from the control plane (`AddHttpFrontend`, or a `LoadState`
936    /// replay), so an `assert_ne!(_, Failed)` here -- which is what this
937    /// used to be -- turned a bad hostname into a worker crash, repeated
938    /// on every restart replay. A rejected key must also leave the trie
939    /// completely untouched.
940    #[test]
941    fn insert_rejects_malformed_keys_without_panicking() {
942        for key in [
943            &b""[..],
944            b".",
945            b"..",
946            b"...",
947            b"/",
948            b"///",
949            b"a/",
950            b".a",
951            b".a.b",
952            b"example.com/",
953            b"www.example.com/",
954            // A regex segment must be `.`-anchored on its left.
955            b"abc/[0-9]+/.example.com",
956            // ... and must actually compile as a regex.
957            b"/[/.example.com",
958            b"a/*/",
959        ] {
960            let mut root: TrieNode<u8> = TrieNode::root();
961            assert_eq!(
962                root.insert(key.to_vec(), 1),
963                InsertResult::Failed,
964                "{:?} must be rejected",
965                String::from_utf8_lossy(key),
966            );
967            assert!(
968                root.is_empty(),
969                "{:?} was rejected but still mutated the trie",
970                String::from_utf8_lossy(key),
971            );
972            assert_eq!(root.domain_lookup(key, false), None);
973            assert_eq!(root.domain_lookup(key, true), None);
974        }
975    }
976
977    /// A rejected key must not disturb the entries already in the trie
978    /// either -- the failed insert has to be a no-op on a populated table.
979    #[test]
980    fn a_rejected_insert_leaves_existing_entries_intact() {
981        let mut root: TrieNode<u8> = TrieNode::root();
982        assert_eq!(
983            root.insert(b"www.example.com".to_vec(), 1),
984            InsertResult::Ok
985        );
986        assert_eq!(root.insert(b"*.wild.com".to_vec(), 2), InsertResult::Ok);
987        assert_eq!(
988            root.insert(b"www.example.com/".to_vec(), 3),
989            InsertResult::Failed
990        );
991        assert_eq!(
992            root.domain_lookup(b"www.example.com", false),
993            Some(&(b"www.example.com".to_vec(), 1))
994        );
995        assert_eq!(
996            root.domain_lookup(b"any.wild.com", true),
997            Some(&(b"*.wild.com".to_vec(), 2))
998        );
999    }
1000
1001    #[test]
1002    fn segment_regex_rejects_partial_matches() {
1003        let mut root: TrieNode<u8> = TrieNode::root();
1004        // The regex segment `cdn[0-9]+` must match `cdn1`, `cdn99`, etc.
1005        // exactly — never `cdn1xxx` or `xxxcdn1` as a prefix/suffix.
1006        assert_eq!(
1007            root.insert(Vec::from(&b"/cdn[0-9]+/.example.com"[..]), 7),
1008            InsertResult::Ok
1009        );
1010
1011        // Exact-match cases still resolve.
1012        assert_eq!(
1013            root.domain_lookup(b"cdn1.example.com".as_ref(), false),
1014            Some(&(b"/cdn[0-9]+/.example.com".to_vec(), 7))
1015        );
1016        assert_eq!(
1017            root.domain_lookup(b"cdn123.example.com".as_ref(), false),
1018            Some(&(b"/cdn[0-9]+/.example.com".to_vec(), 7))
1019        );
1020
1021        // Trailing characters past the digit run must fail. Pre-anchoring
1022        // the trie would have matched `cdn1xxx` because `cdn[0-9]+` ate
1023        // the `cdn1` prefix; with `\A...\z` the segment is rejected.
1024        assert_eq!(
1025            root.domain_lookup(b"cdn1xxx.example.com".as_ref(), false),
1026            None
1027        );
1028        // Leading characters likewise must fail.
1029        assert_eq!(
1030            root.domain_lookup(b"xxxcdn1.example.com".as_ref(), false),
1031            None
1032        );
1033        // Non-digit middle bytes break the digit run and the segment.
1034        assert_eq!(
1035            root.domain_lookup(b"cdnabc.example.com".as_ref(), false),
1036            None
1037        );
1038    }
1039
1040    #[test]
1041    fn add_child_to_leaf() {
1042        let mut root1: TrieNode<u8> = TrieNode::root();
1043
1044        println!("creating root1:");
1045        root1.print();
1046        println!("adding (abcd, 1)");
1047        assert_eq!(root1.insert(Vec::from(&b"abcd"[..]), 1), InsertResult::Ok);
1048        root1.print();
1049        println!("adding (abce, 2)");
1050        assert_eq!(root1.insert(Vec::from(&b"abce"[..]), 2), InsertResult::Ok);
1051        root1.print();
1052        println!("adding (abc, 3)");
1053        assert_eq!(root1.insert(Vec::from(&b"abc"[..]), 3), InsertResult::Ok);
1054
1055        println!("root1:");
1056        root1.print();
1057
1058        let mut root2: TrieNode<u8> = TrieNode::root();
1059
1060        assert_eq!(root2.insert(Vec::from(&b"abc"[..]), 3), InsertResult::Ok);
1061        assert_eq!(root2.insert(Vec::from(&b"abcd"[..]), 1), InsertResult::Ok);
1062        assert_eq!(root2.insert(Vec::from(&b"abce"[..]), 2), InsertResult::Ok);
1063
1064        println!("root2:");
1065        root2.print();
1066        assert_eq!(root2.remove(&Vec::from(&b"abc"[..])), RemoveResult::Ok);
1067
1068        println!("root2 after,remove:");
1069        root2.print();
1070        let mut expected: TrieNode<u8> = TrieNode::root();
1071
1072        assert_eq!(
1073            expected.insert(Vec::from(&b"abcd"[..]), 1),
1074            InsertResult::Ok
1075        );
1076        assert_eq!(
1077            expected.insert(Vec::from(&b"abce"[..]), 2),
1078            InsertResult::Ok
1079        );
1080
1081        println!("root2 after insert");
1082        root2.print();
1083        println!("expected");
1084        expected.print();
1085        assert_eq!(root2, expected);
1086    }
1087
1088    #[test]
1089    fn domains() {
1090        let mut root: TrieNode<u8> = TrieNode::root();
1091        root.print();
1092
1093        assert_eq!(
1094            root.domain_insert(Vec::from(&b"www.example.com"[..]), 1),
1095            InsertResult::Ok
1096        );
1097        root.print();
1098        assert_eq!(
1099            root.domain_insert(Vec::from(&b"test.example.com"[..]), 2),
1100            InsertResult::Ok
1101        );
1102        root.print();
1103        assert_eq!(
1104            root.domain_insert(Vec::from(&b"*.alldomains.org"[..]), 3),
1105            InsertResult::Ok
1106        );
1107        root.print();
1108        assert_eq!(
1109            root.domain_insert(Vec::from(&b"alldomains.org"[..]), 4),
1110            InsertResult::Ok
1111        );
1112        assert_eq!(
1113            root.domain_insert(Vec::from(&b"pouet.alldomains.org"[..]), 5),
1114            InsertResult::Ok
1115        );
1116        root.print();
1117        assert_eq!(
1118            root.domain_insert(Vec::from(&b"hello.com"[..]), 6),
1119            InsertResult::Ok
1120        );
1121        assert_eq!(
1122            root.domain_insert(Vec::from(&b"*.hello.com"[..]), 7),
1123            InsertResult::Ok
1124        );
1125        assert_eq!(
1126            root.domain_insert(Vec::from(&b"images./cdn[0-9]+/.hello.com"[..]), 8),
1127            InsertResult::Ok
1128        );
1129        root.print();
1130        assert_eq!(
1131            root.domain_insert(Vec::from(&b"/test[0-9]+/.www.hello.com"[..]), 9),
1132            InsertResult::Ok
1133        );
1134        root.print();
1135
1136        assert_eq!(root.domain_lookup(&b"example.com"[..], true), None);
1137        assert_eq!(
1138            root.domain_lookup(&b"blah.test.example.com"[..], true),
1139            None
1140        );
1141        assert_eq!(
1142            root.domain_lookup(&b"www.example.com"[..], true),
1143            Some(&(b"www.example.com"[..].to_vec(), 1))
1144        );
1145        assert_eq!(
1146            root.domain_lookup(&b"alldomains.org"[..], true),
1147            Some(&(b"alldomains.org"[..].to_vec(), 4))
1148        );
1149        assert_eq!(
1150            root.domain_lookup(&b"test.hello.com"[..], true),
1151            Some(&(b"*.hello.com"[..].to_vec(), 7))
1152        );
1153        assert_eq!(
1154            root.domain_lookup(&b"images.cdn10.hello.com"[..], true),
1155            Some(&(b"images./cdn[0-9]+/.hello.com"[..].to_vec(), 8))
1156        );
1157        assert_eq!(
1158            root.domain_lookup(&b"test42.www.hello.com"[..], true),
1159            Some(&(b"/test[0-9]+/.www.hello.com"[..].to_vec(), 9))
1160        );
1161        assert_eq!(
1162            root.domain_lookup(&b"test.alldomains.org"[..], true),
1163            Some(&(b"*.alldomains.org"[..].to_vec(), 3))
1164        );
1165        assert_eq!(
1166            root.domain_lookup(&b"hello.alldomains.org"[..], true),
1167            Some(&(b"*.alldomains.org"[..].to_vec(), 3))
1168        );
1169        assert_eq!(
1170            root.domain_lookup(&b"pouet.alldomains.org"[..], true),
1171            Some(&(b"pouet.alldomains.org"[..].to_vec(), 5))
1172        );
1173        assert_eq!(
1174            root.domain_lookup(&b"blah.test.alldomains.org"[..], true),
1175            None
1176        );
1177
1178        assert_eq!(
1179            root.domain_remove(&Vec::from(&b"alldomains.org"[..])),
1180            RemoveResult::Ok
1181        );
1182        println!("after remove");
1183        root.print();
1184        assert_eq!(root.domain_lookup(&b"alldomains.org"[..], true), None);
1185        assert_eq!(
1186            root.domain_lookup(&b"test.alldomains.org"[..], true),
1187            Some(&(b"*.alldomains.org"[..].to_vec(), 3))
1188        );
1189        assert_eq!(
1190            root.domain_lookup(&b"hello.alldomains.org"[..], true),
1191            Some(&(b"*.alldomains.org"[..].to_vec(), 3))
1192        );
1193        assert_eq!(
1194            root.domain_lookup(&b"pouet.alldomains.org"[..], true),
1195            Some(&(b"pouet.alldomains.org"[..].to_vec(), 5))
1196        );
1197        assert_eq!(
1198            root.domain_lookup(&b"test.hello.com"[..], true),
1199            Some(&(b"*.hello.com"[..].to_vec(), 7))
1200        );
1201        assert_eq!(
1202            root.domain_lookup(&b"blah.test.alldomains.org"[..], true),
1203            None
1204        );
1205    }
1206
1207    #[test]
1208    fn wildcard() {
1209        let mut root: TrieNode<u8> = TrieNode::root();
1210        root.print();
1211        root.domain_insert("*.clever-cloud.com".as_bytes().to_vec(), 2u8);
1212        root.domain_insert("services.clever-cloud.com".as_bytes().to_vec(), 0u8);
1213        root.domain_insert("*.services.clever-cloud.com".as_bytes().to_vec(), 1u8);
1214
1215        let res = root.domain_lookup(b"test.services.clever-cloud.com", true);
1216        println!("query result: {res:?}");
1217
1218        assert_eq!(
1219            root.domain_lookup(b"pgstudio.services.clever-cloud.com", true),
1220            Some(&("*.services.clever-cloud.com".as_bytes().to_vec(), 1u8))
1221        );
1222    }
1223
1224    fn hm_insert(h: std::collections::HashMap<String, u32>) -> bool {
1225        let mut root: TrieNode<u32> = TrieNode::root();
1226
1227        for (k, v) in h.iter() {
1228            if k.is_empty() {
1229                continue;
1230            }
1231
1232            if k.as_bytes()[0] == b'.' {
1233                continue;
1234            }
1235
1236            if k.contains('/') {
1237                continue;
1238            }
1239
1240            if k == "*" {
1241                continue;
1242            }
1243
1244            //println!("inserting key: '{}', value: '{}'", k, v);
1245            //assert_eq!(root.domain_insert(Vec::from(k.as_bytes()), *v), InsertResult::Ok);
1246            assert_eq!(
1247                root.insert(Vec::from(k.as_bytes()), *v),
1248                InsertResult::Ok,
1249                "could not insert ({k}, {v})"
1250            );
1251            //root.print();
1252        }
1253
1254        //root.print();
1255        for (k, v) in h.iter() {
1256            if k.is_empty() {
1257                continue;
1258            }
1259
1260            if k.as_bytes()[0] == b'.' {
1261                continue;
1262            }
1263
1264            if k.contains('/') {
1265                continue;
1266            }
1267
1268            if k == "*" {
1269                continue;
1270            }
1271
1272            //match root.domain_lookup(k.as_bytes()) {
1273            match root.lookup(k.as_bytes(), false) {
1274                None => {
1275                    println!("did not find key '{k}'");
1276                    return false;
1277                }
1278                Some(&(ref k1, v1)) => {
1279                    if k.as_bytes() != &k1[..] || *v != v1 {
1280                        println!(
1281                            "request ({}, {}), got ({}, {})",
1282                            k,
1283                            v,
1284                            str::from_utf8(&k1[..]).unwrap(),
1285                            v1
1286                        );
1287                        return false;
1288                    }
1289                }
1290            }
1291        }
1292
1293        true
1294    }
1295
1296    /* FIXME: randomly fails
1297    quickcheck! {
1298      fn qc_insert(h: std::collections::HashMap<String, u32>) -> bool {
1299        hm_insert(h)
1300      }
1301    }
1302    */
1303
1304    #[test]
1305    fn insert_disappearing_tree() {
1306        let h: std::collections::HashMap<String, u32> = [
1307            (String::from("\n\u{3}"), 0),
1308            (String::from("\n\u{0}"), 1),
1309            (String::from("\n"), 2),
1310        ]
1311        .iter()
1312        .cloned()
1313        .collect();
1314        assert!(hm_insert(h));
1315    }
1316
1317    #[test]
1318    fn size() {
1319        assert_size!(TrieNode<u32>, 136);
1320    }
1321
1322    /// Regression: a hostname whose LEFTMOST segment is a regex
1323    /// (`/test[0-9]/.example.com`) used to underflow `pos - 1` (to
1324    /// `usize::MAX`) and panic on the second insert (the dedup loop) and
1325    /// on any `lookup_mut`. Both paths now special-case `pos == 0`
1326    /// (regex is the leftmost/only segment → value-bearing leaf). This
1327    /// asserts the panic is gone and the entry resolves correctly.
1328    #[test]
1329    fn leftmost_regex_segment_reinsert_and_lookup_mut_do_not_panic() {
1330        let mut root: TrieNode<u8> = TrieNode::root();
1331
1332        assert_eq!(
1333            root.insert(Vec::from(&b"/test[0-9]/.example.com"[..]), 7),
1334            InsertResult::Ok
1335        );
1336        // Second insert of the SAME leftmost-regex host: dedup loop with
1337        // pos == 0. Previously panicked; must now report Existing.
1338        assert_eq!(
1339            root.insert(Vec::from(&b"/test[0-9]/.example.com"[..]), 8),
1340            InsertResult::Existing
1341        );
1342
1343        // lookup_mut on the existing leftmost-regex host: previously
1344        // panicked at `partial_key[..pos - 1]`; must now resolve the leaf
1345        // (value unchanged from the first insert — Existing did not
1346        // overwrite).
1347        let resolved = root.domain_lookup_mut(b"test4.example.com", false);
1348        assert_eq!(
1349            resolved.map(|(_, v)| *v),
1350            Some(7),
1351            "leftmost-regex host must resolve via lookup_mut without panicking",
1352        );
1353
1354        // The immutable lookup path (never buggy) agrees.
1355        assert_eq!(
1356            root.domain_lookup(b"test4.example.com", false),
1357            Some(&(b"/test[0-9]/.example.com"[..].to_vec(), 7))
1358        );
1359
1360        // Removing the last rule clears the host.
1361        assert_eq!(
1362            root.domain_remove(&Vec::from(&b"/test[0-9]/.example.com"[..])),
1363            RemoveResult::Ok
1364        );
1365        assert_eq!(root.domain_lookup(b"test4.example.com", false), None);
1366    }
1367}