use std::{collections::HashMap, fmt::Debug, iter, str};
use regex::bytes::{Regex, RegexBuilder};
pub type Key = Vec<u8>;
pub type KeyValue<K, V> = (K, V);
#[derive(Debug, PartialEq, Eq)]
pub enum InsertResult {
Ok,
Existing,
Failed,
}
#[derive(Debug, PartialEq, Eq)]
pub enum RemoveResult {
Ok,
NotFound,
}
fn find_last_dot(input: &[u8]) -> Option<usize> {
(0..input.len()).rev().find(|&i| input[i] == b'.')
}
fn find_last_slash(input: &[u8]) -> Option<usize> {
(0..input.len()).rev().find(|&i| input[i] == b'/')
}
fn anchored_segment(segment: &str) -> Option<String> {
Regex::new(segment).ok()?;
let anchored = format!("\\A(?:{segment})\\z");
debug_assert!(
anchored.starts_with("\\A(?:") && anchored.ends_with(")\\z"),
"segment regex must be fully anchored AND grouped so every alternation \
branch matches the whole segment only",
);
Some(anchored)
}
fn compiled_segment(segment: &str) -> Option<Regex> {
let anchored = anchored_segment(segment)?;
let compiled = RegexBuilder::new(&anchored)
.case_insensitive(true)
.build()
.ok()?;
debug_assert_eq!(
compiled.as_str(),
anchored,
"a `regexps` entry is addressed by `as_str()`, so the builder must \
not respell the pattern `anchored_segment` produced",
);
Some(compiled)
}
#[derive(Debug, Default, Clone)]
pub struct TrieNode<V> {
key_value: Option<KeyValue<Key, V>>,
wildcard: Option<KeyValue<Key, V>>,
children: HashMap<Key, TrieNode<V>>,
regexps: Vec<(Regex, TrieNode<V>)>,
}
#[derive(Debug)]
pub enum TrieSubMatch<'a, 'b> {
Wildcard(&'a [u8]),
Regexp(&'a [u8], &'b Regex),
}
pub type TrieMatches<'a, 'b> = Vec<TrieSubMatch<'a, 'b>>;
trait SubMatchSink<'a, 'b> {
fn mark(&self) -> usize;
fn rewind(&mut self, mark: usize);
fn push(&mut self, sub_match: TrieSubMatch<'a, 'b>);
}
struct NoTrace;
impl<'a, 'b> SubMatchSink<'a, 'b> for NoTrace {
fn mark(&self) -> usize {
0
}
fn rewind(&mut self, _mark: usize) {}
fn push(&mut self, _sub_match: TrieSubMatch<'a, 'b>) {}
}
impl<'a, 'b> SubMatchSink<'a, 'b> for TrieMatches<'a, 'b> {
fn mark(&self) -> usize {
self.len()
}
fn rewind(&mut self, mark: usize) {
Vec::truncate(self, mark);
}
fn push(&mut self, sub_match: TrieSubMatch<'a, 'b>) {
Vec::push(self, sub_match);
}
}
const INLINE_TRIE_MATCHES: usize = 16;
pub(crate) struct InlineTrieMatches<'a, 'b> {
inline: [Option<TrieSubMatch<'a, 'b>>; INLINE_TRIE_MATCHES],
spilled: Vec<TrieSubMatch<'a, 'b>>,
len: usize,
}
impl<'a, 'b> InlineTrieMatches<'a, 'b> {
pub(crate) fn new() -> Self {
Self {
inline: [const { None }; INLINE_TRIE_MATCHES],
spilled: Vec::new(),
len: 0,
}
}
pub(crate) fn iter(&self) -> impl DoubleEndedIterator<Item = &TrieSubMatch<'a, 'b>> {
self.inline[..self.len.min(INLINE_TRIE_MATCHES)]
.iter()
.flatten()
.chain(self.spilled.iter())
}
}
impl<'a, 'b> SubMatchSink<'a, 'b> for InlineTrieMatches<'a, 'b> {
fn mark(&self) -> usize {
self.len
}
fn rewind(&mut self, mark: usize) {
debug_assert!(mark <= self.len, "a rewind never moves forward");
self.spilled
.truncate(mark.saturating_sub(INLINE_TRIE_MATCHES));
self.len = mark;
}
fn push(&mut self, sub_match: TrieSubMatch<'a, 'b>) {
match self.inline.get_mut(self.len) {
Some(slot) => *slot = Some(sub_match),
None => self.spilled.push(sub_match),
}
self.len += 1;
}
}
impl<V: PartialEq> std::cmp::PartialEq for TrieNode<V> {
fn eq(&self, other: &Self) -> bool {
self.key_value == other.key_value
&& self.wildcard == other.wildcard
&& self.children == other.children
&& self.regexps.len() == other.regexps.len()
&& self
.regexps
.iter()
.zip(other.regexps.iter())
.fold(true, |b, (left, right)| {
b && left.0.as_str() == right.0.as_str() && left.1 == right.1
})
}
}
impl<V: Debug + Clone> TrieNode<V> {
pub fn new(key: Key, value: V) -> TrieNode<V> {
TrieNode {
key_value: Some((key, value)),
wildcard: None,
children: HashMap::new(),
regexps: Vec::new(),
}
}
pub fn wildcard(key: Key, value: V) -> TrieNode<V> {
TrieNode {
key_value: None,
wildcard: Some((key, value)),
children: HashMap::new(),
regexps: Vec::new(),
}
}
pub fn root() -> TrieNode<V> {
TrieNode {
key_value: None,
wildcard: None,
children: HashMap::new(),
regexps: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
self.key_value.is_none()
&& self.wildcard.is_none()
&& self.regexps.is_empty()
&& self.children.is_empty()
}
fn insert_own_value(&mut self, key: &Key, value: V) -> InsertResult {
if self.key_value.is_none() {
self.key_value = Some((key.to_vec(), value));
InsertResult::Ok
} else {
InsertResult::Existing
}
}
pub fn insert(&mut self, key: Key, value: V) -> InsertResult {
if key.is_empty() {
return InsertResult::Failed;
}
if key[..] == b"."[..] {
return InsertResult::Failed;
}
#[cfg(debug_assertions)]
let before = self.count_values();
let insert_result = self.insert_recursive(&key, &key, value);
#[cfg(debug_assertions)]
{
let after = self.count_values();
match insert_result {
InsertResult::Ok => debug_assert_eq!(
after,
before + 1,
"a fresh insert must add exactly one value to the trie",
),
InsertResult::Existing => debug_assert_eq!(
after, before,
"an Existing insert must not change the trie value count",
),
InsertResult::Failed => debug_assert_eq!(
after, before,
"a failed insert must leave the trie value count untouched",
),
}
self.check_invariants();
}
insert_result
}
pub fn insert_recursive(&mut self, partial_key: &[u8], key: &Key, value: V) -> InsertResult {
if partial_key.is_empty() {
return InsertResult::Failed;
}
debug_assert!(
partial_key.len() <= key.len(),
"insert recursion must consume the key, never grow past it",
);
if partial_key[partial_key.len() - 1] == b'/' {
let pos = find_last_slash(&partial_key[..partial_key.len() - 1]);
if let Some(pos) = pos {
if pos > 0 && partial_key[pos - 1] != b'.' {
return InsertResult::Failed;
}
if let Ok(s) = str::from_utf8(&partial_key[pos + 1..partial_key.len() - 1]) {
let Some(anchored_s) = anchored_segment(s) else {
return InsertResult::Failed;
};
for t in self.regexps.iter_mut() {
if t.0.as_str() == anchored_s {
if pos > 0 {
return t.1.insert_recursive(&partial_key[..pos - 1], key, value);
}
return t.1.insert_own_value(key, value);
}
}
if let Some(r) = compiled_segment(s) {
if pos > 0 {
let mut node = TrieNode::root();
let pos = pos - 1;
let res = node.insert_recursive(&partial_key[..pos], key, value);
if res == InsertResult::Ok {
self.regexps.push((r, node));
}
return res;
} else {
let node = TrieNode::new(key.to_vec(), value);
self.regexps.push((r, node));
return InsertResult::Ok;
}
}
}
}
return InsertResult::Failed;
}
let pos = find_last_dot(partial_key);
match pos {
None => {
if self.children.contains_key(partial_key) {
InsertResult::Existing
} else if partial_key == &b"*"[..] {
if self.wildcard.is_some() {
InsertResult::Existing
} else {
self.wildcard = Some((key.to_vec(), value));
InsertResult::Ok
}
} else {
let node = TrieNode::new(key.to_vec(), value);
self.children.insert(partial_key.to_vec(), node);
InsertResult::Ok
}
}
Some(pos) => {
debug_assert_eq!(
partial_key[..pos].len() + partial_key[pos..].len(),
partial_key.len(),
"dot-split must partition partial_key without losing bytes",
);
debug_assert_eq!(
partial_key[pos], b'.',
"find_last_dot must point at a '.' byte",
);
if let Some(child) = self.children.get_mut(&partial_key[pos..]) {
return child.insert_recursive(&partial_key[..pos], key, value);
}
let mut node = TrieNode::root();
let res = node.insert_recursive(&partial_key[..pos], key, value);
if res == InsertResult::Ok {
self.children.insert(partial_key[pos..].to_vec(), node);
}
res
}
}
}
pub fn remove(&mut self, key: &Key) -> RemoveResult {
#[cfg(debug_assertions)]
let before = self.count_values();
let remove_result = self.remove_recursive(key);
#[cfg(debug_assertions)]
{
let after = self.count_values();
match remove_result {
RemoveResult::Ok => debug_assert_eq!(
after + 1,
before,
"a successful remove must drop exactly one value from the trie",
),
RemoveResult::NotFound => debug_assert_eq!(
after, before,
"a NotFound remove must not change the trie value count",
),
}
self.check_invariants();
}
remove_result
}
pub fn remove_recursive(&mut self, partial_key: &[u8]) -> RemoveResult {
if partial_key.is_empty() {
if self.key_value.is_some() {
self.key_value = None;
return RemoveResult::Ok;
} else {
return RemoveResult::NotFound;
}
}
if partial_key == &b"*"[..] {
if self.wildcard.is_some() {
self.wildcard = None;
return RemoveResult::Ok;
} else {
return RemoveResult::NotFound;
}
}
if partial_key[partial_key.len() - 1] == b'/' {
let pos = find_last_slash(&partial_key[..partial_key.len() - 1]);
if let Some(pos) = pos {
if pos > 0 && partial_key[pos - 1] != b'.' {
return RemoveResult::NotFound;
}
if let Ok(s) = str::from_utf8(&partial_key[pos + 1..partial_key.len() - 1]) {
let Some(anchored_s) = anchored_segment(s) else {
return RemoveResult::NotFound;
};
let rest: &[u8] = if pos > 0 {
&partial_key[..pos - 1]
} else {
&partial_key[..0]
};
if let Some(index) = self
.regexps
.iter()
.position(|(r, _)| r.as_str() == anchored_s)
&& self.regexps[index].1.remove_recursive(rest) == RemoveResult::Ok
{
if self.regexps[index].1.is_empty() {
self.regexps.remove(index);
}
return RemoveResult::Ok;
}
}
}
return RemoveResult::NotFound;
}
let pos = find_last_dot(partial_key);
let (prefix, suffix) = match pos {
None => (&b""[..], partial_key),
Some(pos) => (&partial_key[..pos], &partial_key[pos..]),
};
debug_assert_eq!(
prefix.len() + suffix.len(),
partial_key.len(),
"dot-split must partition the key without losing or duplicating bytes",
);
match self.children.get_mut(suffix) {
Some(child) => match child.remove_recursive(prefix) {
RemoveResult::NotFound => RemoveResult::NotFound,
RemoveResult::Ok => {
if child.is_empty() {
self.children.remove(suffix);
debug_assert!(
!self.children.contains_key(suffix),
"an emptied child subtree must be removed from the parent",
);
} else {
#[cfg(debug_assertions)]
debug_assert!(
child.count_values() > 0,
"a retained child subtree must still hold at least one value",
);
}
RemoveResult::Ok
}
},
None => RemoveResult::NotFound,
}
}
fn lookup_recursive<'a, 'b, S: SubMatchSink<'a, 'b>>(
&'b self,
partial_key: &'a [u8],
accept_wildcard: bool,
trace: &mut S,
accept: &mut dyn FnMut(&KeyValue<Key, V>) -> bool,
) -> Option<&'b KeyValue<Key, V>> {
if partial_key.is_empty() {
return match self.key_value.as_ref() {
Some(key_value) if accept(key_value) => Some(key_value),
_ => None,
};
}
let pos = find_last_dot(partial_key);
let (prefix, suffix) = match pos {
None => (&b""[..], partial_key),
Some(pos) => (&partial_key[..pos], &partial_key[pos..]),
};
debug_assert_eq!(
prefix.len() + suffix.len(),
partial_key.len(),
"dot-split must partition the key without losing or duplicating bytes",
);
debug_assert!(
!suffix.is_empty(),
"the suffix the trie matches children against must be non-empty",
);
debug_assert!(
pos.is_none() || suffix.first() == Some(&b'.'),
"a dotted split must place the separator at the head of the suffix",
);
if let Some(child) = self.children.get(suffix) {
let mark = trace.mark();
if let Some(found) = child.lookup_recursive(prefix, accept_wildcard, trace, accept) {
return Some(found);
}
trace.rewind(mark);
}
let segment = if suffix[0] == b'.' {
&suffix[1..]
} else {
suffix
};
for (regexp, child) in self.regexps.iter() {
if !regexp.is_match(segment) {
continue;
}
let mark = trace.mark();
trace.push(TrieSubMatch::Regexp(segment, regexp));
if let Some(found) = child.lookup_recursive(prefix, accept_wildcard, trace, accept) {
return Some(found);
}
trace.rewind(mark);
}
if prefix.is_empty()
&& accept_wildcard
&& let Some(key_value) = self.wildcard.as_ref()
&& accept(key_value)
{
trace.push(TrieSubMatch::Wildcard(segment));
return Some(key_value);
}
None
}
pub fn lookup_with_path<'a, 'b>(
&'b self,
partial_key: &'a [u8],
accept_wildcard: bool,
mut trace: TrieMatches<'a, 'b>,
accept: &mut dyn FnMut(&KeyValue<Key, V>) -> bool,
) -> Option<(&'b KeyValue<Key, V>, TrieMatches<'a, 'b>)> {
self.lookup_recursive(partial_key, accept_wildcard, &mut trace, accept)
.map(|key_value| (key_value, trace))
}
pub(crate) fn lookup_with_inline_path<'a, 'b>(
&'b self,
partial_key: &'a [u8],
accept_wildcard: bool,
trace: &mut InlineTrieMatches<'a, 'b>,
accept: &mut dyn FnMut(&KeyValue<Key, V>) -> bool,
) -> Option<&'b KeyValue<Key, V>> {
debug_assert_eq!(trace.mark(), 0, "a lookup starts from an empty record");
self.lookup_recursive(partial_key, accept_wildcard, trace, accept)
}
pub fn lookup(&self, partial_key: &[u8], accept_wildcard: bool) -> Option<&KeyValue<Key, V>> {
self.lookup_recursive(
partial_key,
accept_wildcard,
&mut NoTrace,
&mut |_: &KeyValue<Key, V>| true,
)
}
pub fn lookup_mut(
&mut self,
partial_key: &[u8],
accept_wildcard: bool,
) -> Option<&mut KeyValue<Key, V>> {
if partial_key.is_empty() {
return self.key_value.as_mut();
}
if partial_key == &b"*"[..] {
return self.wildcard.as_mut();
}
if partial_key[partial_key.len() - 1] == b'/' {
let pos = find_last_slash(&partial_key[..partial_key.len() - 1]);
if let Some(pos) = pos {
if pos > 0 && partial_key[pos - 1] != b'.' {
return None;
}
if let Ok(s) = str::from_utf8(&partial_key[pos + 1..partial_key.len() - 1]) {
let anchored_s = anchored_segment(s)?;
for t in self.regexps.iter_mut() {
if t.0.as_str() == anchored_s {
let rest = if pos > 0 {
&partial_key[..pos - 1]
} else {
&partial_key[..0]
};
return t.1.lookup_mut(rest, accept_wildcard);
}
}
}
}
return None;
}
let pos = find_last_dot(partial_key);
let (prefix, suffix) = match pos {
None => (&b""[..], partial_key),
Some(pos) => (&partial_key[..pos], &partial_key[pos..]),
};
debug_assert_eq!(
prefix.len() + suffix.len(),
partial_key.len(),
"dot-split must partition the key without losing or duplicating bytes",
);
debug_assert!(
!suffix.is_empty(),
"the suffix the trie matches children against must be non-empty",
);
match self.children.get_mut(suffix) {
Some(child) => child.lookup_mut(prefix, accept_wildcard),
None => {
if prefix.is_empty() && self.wildcard.is_some() && accept_wildcard {
self.wildcard.as_mut()
} else {
if !accept_wildcard {
return None;
}
for &mut (ref regexp, ref mut child) in self.regexps.iter_mut() {
let suffix = if suffix[0] == b'.' {
&suffix[1..]
} else {
suffix
};
if regexp.is_match(suffix) {
return child.lookup_mut(prefix, accept_wildcard);
}
}
None
}
}
}
}
pub fn print(&self) {
self.print_recursive(b"", 0)
}
pub fn print_recursive(&self, partial_key: &[u8], indent: u8) {
let raw_prefix: Vec<u8> = iter::repeat_n(b' ', 2 * indent as usize).collect();
let prefix = str::from_utf8(&raw_prefix).unwrap();
print!("{}{}: ", prefix, str::from_utf8(partial_key).unwrap());
if let Some((ref key, ref value)) = self.key_value {
print!("({}, {:?}) | ", str::from_utf8(key).unwrap(), value);
} else {
print!("None | ");
}
if let Some((key, value)) = &self.wildcard {
println!("({}, {:?})", str::from_utf8(key).unwrap(), value);
} else {
println!("None");
}
for (child_key, child) in self.children.iter() {
child.print_recursive(child_key, indent + 1);
}
for (regexp, child) in self.regexps.iter() {
child.print_recursive(regexp.as_str().as_bytes(), indent + 1);
}
}
pub fn for_each_value_mut<F: FnMut(&mut V)>(&mut self, f: &mut F) {
if let Some((_, ref mut value)) = self.key_value {
f(value);
}
if let Some((_, ref mut value)) = self.wildcard {
f(value);
}
for child in self.children.values_mut() {
child.for_each_value_mut(f);
}
for (_, child) in self.regexps.iter_mut() {
child.for_each_value_mut(f);
}
}
#[cfg(debug_assertions)]
fn count_values(&self) -> usize {
let local = self.key_value.is_some() as usize + self.wildcard.is_some() as usize;
let in_children: usize = self.children.values().map(TrieNode::count_values).sum();
let in_regexps: usize = self.regexps.iter().map(|(_, c)| c.count_values()).sum();
local + in_children + in_regexps
}
#[cfg(debug_assertions)]
fn check_invariants(&self) {
for i in 0..self.regexps.len() {
for j in (i + 1)..self.regexps.len() {
debug_assert_ne!(
self.regexps[i].0.as_str(),
self.regexps[j].0.as_str(),
"trie node must not hold two subtrees for the same regex segment",
);
}
}
for (child_key, child) in self.children.iter() {
debug_assert!(
!child_key.is_empty(),
"trie child must not be keyed by the empty segment",
);
debug_assert!(
!child.is_empty(),
"trie must not strand an empty child subtree (remove must prune)",
);
debug_assert!(
child.count_values() > 0,
"trie child subtree must lead to at least one value",
);
child.check_invariants();
}
for (_, child) in self.regexps.iter() {
debug_assert!(
!child.is_empty(),
"trie must not strand an empty regex subtree (remove must prune)",
);
debug_assert!(
child.count_values() > 0,
"trie regex subtree must lead to at least one value",
);
child.check_invariants();
}
}
pub fn domain_insert(&mut self, key: Key, value: V) -> InsertResult {
self.insert(key, value)
}
pub fn domain_remove(&mut self, key: &Key) -> RemoveResult {
self.remove(key)
}
pub fn domain_lookup(&self, key: &[u8], accept_wildcard: bool) -> Option<&KeyValue<Key, V>> {
self.lookup(key, accept_wildcard)
}
pub fn domain_lookup_mut(
&mut self,
key: &[u8],
accept_wildcard: bool,
) -> Option<&mut KeyValue<Key, V>> {
self.lookup_mut(key, accept_wildcard)
}
pub fn size(&self) -> usize {
::std::mem::size_of::<TrieNode<V>>()
+ ::std::mem::size_of::<Option<KeyValue<Key, V>>>() * 2
+ self
.children
.iter()
.fold(0, |acc, c| acc + c.0.len() + c.1.size())
}
pub fn to_hashmap(&self) -> HashMap<Key, V> {
let mut h = HashMap::new();
self.to_hashmap_recursive(&mut h);
h
}
pub fn to_hashmap_recursive(&self, h: &mut HashMap<Key, V>) {
if let Some((key, value)) = &self.key_value {
h.insert(key.clone(), value.clone());
}
if let Some((key, value)) = &self.wildcard {
h.insert(key.clone(), value.clone());
}
for child in self.children.values() {
child.to_hashmap_recursive(h);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use quickcheck::quickcheck;
#[test]
fn insert() {
let mut root: TrieNode<u8> = TrieNode::root();
root.print();
assert_eq!(
root.domain_insert(Vec::from(&b"abcd"[..]), 1),
InsertResult::Ok
);
root.print();
assert_eq!(
root.domain_insert(Vec::from(&b"abce"[..]), 2),
InsertResult::Ok
);
root.print();
assert_eq!(
root.domain_insert(Vec::from(&b"abgh"[..]), 3),
InsertResult::Ok
);
root.print();
assert_eq!(
root.domain_lookup(&b"abce"[..], true),
Some(&(b"abce"[..].to_vec(), 2))
);
}
#[test]
fn remove() {
let mut root: TrieNode<u8> = TrieNode::root();
println!("creating root:");
root.print();
println!("adding (abcd, 1)");
assert_eq!(root.insert(Vec::from(&b"abcd"[..]), 1), InsertResult::Ok);
root.print();
println!("adding (abce, 2)");
assert_eq!(root.insert(Vec::from(&b"abce"[..]), 2), InsertResult::Ok);
root.print();
println!("adding (abgh, 3)");
assert_eq!(root.insert(Vec::from(&b"abgh"[..]), 3), InsertResult::Ok);
root.print();
let mut root2: TrieNode<u8> = TrieNode::root();
assert_eq!(root2.insert(Vec::from(&b"abcd"[..]), 1), InsertResult::Ok);
assert_eq!(root2.insert(Vec::from(&b"abgh"[..]), 3), InsertResult::Ok);
println!("before remove");
root.print();
assert_eq!(root.remove(&Vec::from(&b"abce"[..])), RemoveResult::Ok);
println!("after remove");
root.print();
println!("expected");
root2.print();
assert_eq!(root, root2);
assert_eq!(root.remove(&Vec::from(&b"abgh"[..])), RemoveResult::Ok);
println!("after remove");
root.print();
println!("expected");
let mut root3: TrieNode<u8> = TrieNode::root();
assert_eq!(root3.insert(Vec::from(&b"abcd"[..]), 1), InsertResult::Ok);
root3.print();
assert_eq!(root, root3);
}
#[test]
fn insert_remove_through_regex() {
let mut root: TrieNode<u8> = TrieNode::root();
println!("creating root:");
root.print();
println!("adding (www./.*/.com, 1)");
assert_eq!(
root.insert(Vec::from(&b"www./.*/.com"[..]), 1),
InsertResult::Ok
);
root.print();
println!("adding (www.doc./.*/.com, 2)");
assert_eq!(
root.insert(Vec::from(&b"www.doc./.*/.com"[..]), 2),
InsertResult::Ok
);
root.print();
assert_eq!(
root.domain_lookup(b"www.sozu.com".as_ref(), false),
Some(&(b"www./.*/.com".to_vec(), 1))
);
assert_eq!(
root.domain_lookup(b"www.doc.sozu.com".as_ref(), false),
Some(&(b"www.doc./.*/.com".to_vec(), 2))
);
assert_eq!(
root.domain_remove(&b"www./.*/.com".to_vec()),
RemoveResult::Ok
);
root.print();
assert_eq!(root.domain_lookup(b"www.sozu.com".as_ref(), false), None);
assert_eq!(
root.domain_lookup(b"www.doc.sozu.com".as_ref(), false),
Some(&(b"www.doc./.*/.com".to_vec(), 2))
);
}
#[test]
fn insert_rejects_malformed_keys_without_panicking() {
for key in [
&b""[..],
b".",
b"..",
b"...",
b"/",
b"///",
b"a/",
b".a",
b".a.b",
b"example.com/",
b"www.example.com/",
b"abc/[0-9]+/.example.com",
b"/[/.example.com",
b"a/*/",
] {
let mut root: TrieNode<u8> = TrieNode::root();
assert_eq!(
root.insert(key.to_vec(), 1),
InsertResult::Failed,
"{:?} must be rejected",
String::from_utf8_lossy(key),
);
assert!(
root.is_empty(),
"{:?} was rejected but still mutated the trie",
String::from_utf8_lossy(key),
);
assert_eq!(root.domain_lookup(key, false), None);
assert_eq!(root.domain_lookup(key, true), None);
}
}
#[test]
fn a_rejected_insert_leaves_existing_entries_intact() {
let mut root: TrieNode<u8> = TrieNode::root();
assert_eq!(
root.insert(b"www.example.com".to_vec(), 1),
InsertResult::Ok
);
assert_eq!(root.insert(b"*.wild.com".to_vec(), 2), InsertResult::Ok);
assert_eq!(
root.insert(b"www.example.com/".to_vec(), 3),
InsertResult::Failed
);
assert_eq!(
root.domain_lookup(b"www.example.com", false),
Some(&(b"www.example.com".to_vec(), 1))
);
assert_eq!(
root.domain_lookup(b"any.wild.com", true),
Some(&(b"*.wild.com".to_vec(), 2))
);
}
#[test]
fn segment_regex_rejects_partial_matches() {
let mut root: TrieNode<u8> = TrieNode::root();
assert_eq!(
root.insert(Vec::from(&b"/cdn[0-9]+/.example.com"[..]), 7),
InsertResult::Ok
);
assert_eq!(
root.domain_lookup(b"cdn1.example.com".as_ref(), false),
Some(&(b"/cdn[0-9]+/.example.com".to_vec(), 7))
);
assert_eq!(
root.domain_lookup(b"cdn123.example.com".as_ref(), false),
Some(&(b"/cdn[0-9]+/.example.com".to_vec(), 7))
);
assert_eq!(
root.domain_lookup(b"cdn1xxx.example.com".as_ref(), false),
None
);
assert_eq!(
root.domain_lookup(b"xxxcdn1.example.com".as_ref(), false),
None
);
assert_eq!(
root.domain_lookup(b"cdnabc.example.com".as_ref(), false),
None
);
}
#[test]
fn anchored_segment_wraps_in_a_non_capturing_group() {
assert_eq!(
anchored_segment("cdn[0-9]+").as_deref(),
Some("\\A(?:cdn[0-9]+)\\z"),
"the wrapper must anchor AND group",
);
assert_eq!(
Regex::new(&anchored_segment("a|b|c").expect("an alternation must wrap"))
.expect("the wrapped alternation must compile")
.captures_len(),
1,
"the group must be non-capturing, so `$HOST[n]` indices are untouched",
);
assert_eq!(
Regex::new(&anchored_segment("cdn([0-9]+)").expect("a group must wrap"))
.expect("the wrapped group must compile")
.captures_len(),
2,
"the operator's own group must stay at index 1",
);
let unbalanced = format!("a{}b", ")(");
assert_eq!(
anchored_segment(&unbalanced),
None,
"{unbalanced} must not be rescued into a valid pattern",
);
let rescued = Regex::new(&format!("\\A(?:a{}b)\\z", ")("))
.expect("the wrapping balances the segment");
assert!(
rescued.is_match(b"ab"),
"{} is what the missing pre-validation would have built",
rescued.as_str(),
);
}
#[test]
fn compiled_segment_folds_case_without_respelling_its_identity() {
let anchored = anchored_segment("cdn[0-9]+").expect("the segment must wrap");
assert_eq!(
compiled_segment("cdn[0-9]+")
.expect("the segment must compile")
.as_str(),
anchored,
"the compiled entry must carry `anchored_segment`'s exact string",
);
assert_eq!(
compiled_segment("a|b|c")
.expect("an alternation must compile")
.captures_len(),
1,
"folding must not add a capture group",
);
assert_eq!(
compiled_segment("cdn([0-9]+)")
.expect("a group must compile")
.captures_len(),
2,
"the operator's own group must stay at index 1",
);
let uppercase = compiled_segment("API[0-9]").expect("the segment must compile");
assert!(
uppercase.is_match(b"api7"),
"an uppercase literal must meet the ASCII-lowercased lookup key",
);
assert!(uppercase.is_match(b"API7"), "and its own spelling too",);
assert!(
!uppercase.is_match(b"apix"),
"folding must not widen the class the operator wrote",
);
let not_a_digit = compiled_segment("\\D+").expect("the segment must compile");
assert!(not_a_digit.is_match(b"abc"), "`\\D+` matches non-digits");
assert!(
!not_a_digit.is_match(b"777"),
"`\\D+` must never match digits — folded to `\\d+` it matched \
exactly and only those",
);
let unbalanced = format!("a{}b", ")(");
assert!(
compiled_segment(&unbalanced).is_none(),
"{unbalanced} must not be rescued into a live entry",
);
}
#[test]
fn every_branch_of_an_alternating_segment_regex_matches_the_whole_label_only() {
let mut root: TrieNode<u8> = TrieNode::root();
assert_eq!(
root.insert(Vec::from(&b"/a|b|c/.example.com"[..]), 9),
InsertResult::Ok
);
for whole in ["a.example.com", "b.example.com", "c.example.com"] {
assert_eq!(
root.domain_lookup(whole.as_bytes(), false),
Some(&(b"/a|b|c/.example.com".to_vec(), 9)),
"{whole} is a whole-label match for one branch and must resolve",
);
}
for leak in [
"axx.example.com",
"xxc.example.com",
"zzbzz.example.com",
"bzz.example.com",
"zzb.example.com",
] {
assert_eq!(
root.domain_lookup(leak.as_bytes(), false),
None,
"{leak} matches no branch WHOLE and must not resolve",
);
}
}
#[test]
fn add_child_to_leaf() {
let mut root1: TrieNode<u8> = TrieNode::root();
println!("creating root1:");
root1.print();
println!("adding (abcd, 1)");
assert_eq!(root1.insert(Vec::from(&b"abcd"[..]), 1), InsertResult::Ok);
root1.print();
println!("adding (abce, 2)");
assert_eq!(root1.insert(Vec::from(&b"abce"[..]), 2), InsertResult::Ok);
root1.print();
println!("adding (abc, 3)");
assert_eq!(root1.insert(Vec::from(&b"abc"[..]), 3), InsertResult::Ok);
println!("root1:");
root1.print();
let mut root2: TrieNode<u8> = TrieNode::root();
assert_eq!(root2.insert(Vec::from(&b"abc"[..]), 3), InsertResult::Ok);
assert_eq!(root2.insert(Vec::from(&b"abcd"[..]), 1), InsertResult::Ok);
assert_eq!(root2.insert(Vec::from(&b"abce"[..]), 2), InsertResult::Ok);
println!("root2:");
root2.print();
assert_eq!(root2.remove(&Vec::from(&b"abc"[..])), RemoveResult::Ok);
println!("root2 after,remove:");
root2.print();
let mut expected: TrieNode<u8> = TrieNode::root();
assert_eq!(
expected.insert(Vec::from(&b"abcd"[..]), 1),
InsertResult::Ok
);
assert_eq!(
expected.insert(Vec::from(&b"abce"[..]), 2),
InsertResult::Ok
);
println!("root2 after insert");
root2.print();
println!("expected");
expected.print();
assert_eq!(root2, expected);
}
#[test]
fn domains() {
let mut root: TrieNode<u8> = TrieNode::root();
root.print();
assert_eq!(
root.domain_insert(Vec::from(&b"www.example.com"[..]), 1),
InsertResult::Ok
);
root.print();
assert_eq!(
root.domain_insert(Vec::from(&b"test.example.com"[..]), 2),
InsertResult::Ok
);
root.print();
assert_eq!(
root.domain_insert(Vec::from(&b"*.alldomains.org"[..]), 3),
InsertResult::Ok
);
root.print();
assert_eq!(
root.domain_insert(Vec::from(&b"alldomains.org"[..]), 4),
InsertResult::Ok
);
assert_eq!(
root.domain_insert(Vec::from(&b"pouet.alldomains.org"[..]), 5),
InsertResult::Ok
);
root.print();
assert_eq!(
root.domain_insert(Vec::from(&b"hello.com"[..]), 6),
InsertResult::Ok
);
assert_eq!(
root.domain_insert(Vec::from(&b"*.hello.com"[..]), 7),
InsertResult::Ok
);
assert_eq!(
root.domain_insert(Vec::from(&b"images./cdn[0-9]+/.hello.com"[..]), 8),
InsertResult::Ok
);
root.print();
assert_eq!(
root.domain_insert(Vec::from(&b"/test[0-9]+/.www.hello.com"[..]), 9),
InsertResult::Ok
);
root.print();
assert_eq!(root.domain_lookup(&b"example.com"[..], true), None);
assert_eq!(
root.domain_lookup(&b"blah.test.example.com"[..], true),
None
);
assert_eq!(
root.domain_lookup(&b"www.example.com"[..], true),
Some(&(b"www.example.com"[..].to_vec(), 1))
);
assert_eq!(
root.domain_lookup(&b"alldomains.org"[..], true),
Some(&(b"alldomains.org"[..].to_vec(), 4))
);
assert_eq!(
root.domain_lookup(&b"test.hello.com"[..], true),
Some(&(b"*.hello.com"[..].to_vec(), 7))
);
assert_eq!(
root.domain_lookup(&b"images.cdn10.hello.com"[..], true),
Some(&(b"images./cdn[0-9]+/.hello.com"[..].to_vec(), 8))
);
assert_eq!(
root.domain_lookup(&b"test42.www.hello.com"[..], true),
Some(&(b"/test[0-9]+/.www.hello.com"[..].to_vec(), 9))
);
assert_eq!(
root.domain_lookup(&b"test.alldomains.org"[..], true),
Some(&(b"*.alldomains.org"[..].to_vec(), 3))
);
assert_eq!(
root.domain_lookup(&b"hello.alldomains.org"[..], true),
Some(&(b"*.alldomains.org"[..].to_vec(), 3))
);
assert_eq!(
root.domain_lookup(&b"pouet.alldomains.org"[..], true),
Some(&(b"pouet.alldomains.org"[..].to_vec(), 5))
);
assert_eq!(
root.domain_lookup(&b"blah.test.alldomains.org"[..], true),
None
);
assert_eq!(
root.domain_remove(&Vec::from(&b"alldomains.org"[..])),
RemoveResult::Ok
);
println!("after remove");
root.print();
assert_eq!(root.domain_lookup(&b"alldomains.org"[..], true), None);
assert_eq!(
root.domain_lookup(&b"test.alldomains.org"[..], true),
Some(&(b"*.alldomains.org"[..].to_vec(), 3))
);
assert_eq!(
root.domain_lookup(&b"hello.alldomains.org"[..], true),
Some(&(b"*.alldomains.org"[..].to_vec(), 3))
);
assert_eq!(
root.domain_lookup(&b"pouet.alldomains.org"[..], true),
Some(&(b"pouet.alldomains.org"[..].to_vec(), 5))
);
assert_eq!(
root.domain_lookup(&b"test.hello.com"[..], true),
Some(&(b"*.hello.com"[..].to_vec(), 7))
);
assert_eq!(
root.domain_lookup(&b"blah.test.alldomains.org"[..], true),
None
);
}
#[test]
fn wildcard() {
let mut root: TrieNode<u8> = TrieNode::root();
root.print();
root.domain_insert("*.clever-cloud.com".as_bytes().to_vec(), 2u8);
root.domain_insert("services.clever-cloud.com".as_bytes().to_vec(), 0u8);
root.domain_insert("*.services.clever-cloud.com".as_bytes().to_vec(), 1u8);
let res = root.domain_lookup(b"test.services.clever-cloud.com", true);
println!("query result: {res:?}");
assert_eq!(
root.domain_lookup(b"pgstudio.services.clever-cloud.com", true),
Some(&("*.services.clever-cloud.com".as_bytes().to_vec(), 1u8))
);
}
fn hm_insert_skips(k: &str) -> bool {
k.is_empty()
|| k.as_bytes()[0] == b'.'
|| k.contains('/')
|| k.split('.').next() == Some("*")
}
fn hm_insert(h: std::collections::HashMap<String, u32>) -> bool {
let mut root: TrieNode<u32> = TrieNode::root();
for (k, v) in h.iter() {
if hm_insert_skips(k) {
continue;
}
assert_eq!(
root.insert(Vec::from(k.as_bytes()), *v),
InsertResult::Ok,
"could not insert ({k}, {v})"
);
}
for (k, v) in h.iter() {
if hm_insert_skips(k) {
continue;
}
match root.lookup(k.as_bytes(), false) {
None => {
println!("did not find key '{k}'");
return false;
}
Some(&(ref k1, v1)) => {
if k.as_bytes() != &k1[..] || *v != v1 {
println!(
"request ({}, {}), got ({}, {})",
k,
v,
str::from_utf8(&k1[..]).unwrap(),
v1
);
return false;
}
}
}
}
true
}
quickcheck! {
fn qc_insert(h: std::collections::HashMap<String, u32>) -> bool {
hm_insert(h)
}
}
#[test]
fn insert_disappearing_tree() {
let h: std::collections::HashMap<String, u32> = [
(String::from("\n\u{3}"), 0),
(String::from("\n\u{0}"), 1),
(String::from("\n"), 2),
]
.iter()
.cloned()
.collect();
assert!(hm_insert(h));
}
#[test]
fn size() {
assert_size!(TrieNode<u32>, 136);
}
#[test]
fn leftmost_regex_segment_reinsert_and_lookup_mut_do_not_panic() {
let mut root: TrieNode<u8> = TrieNode::root();
assert_eq!(
root.insert(Vec::from(&b"/test[0-9]/.example.com"[..]), 7),
InsertResult::Ok
);
assert_eq!(
root.insert(Vec::from(&b"/test[0-9]/.example.com"[..]), 8),
InsertResult::Existing
);
let resolved = root.domain_lookup_mut(b"/test[0-9]/.example.com", false);
assert_eq!(
resolved.map(|(_, v)| *v),
Some(7),
"leftmost-regex host must resolve via lookup_mut without panicking",
);
assert_eq!(
root.domain_lookup(b"test4.example.com", false),
Some(&(b"/test[0-9]/.example.com"[..].to_vec(), 7))
);
assert_eq!(
root.domain_remove(&Vec::from(&b"/test[0-9]/.example.com"[..])),
RemoveResult::Ok
);
assert_eq!(root.domain_lookup(b"test4.example.com", false), None);
}
#[test]
fn inserting_a_leftmost_regex_host_after_a_deeper_sibling_succeeds() {
let mut root: TrieNode<u8> = TrieNode::root();
assert_eq!(
root.domain_insert(Vec::from(&b"foo./test[0-9]/.example.com"[..]), 2),
InsertResult::Ok
);
assert_eq!(
root.domain_insert(Vec::from(&b"/test[0-9]/.example.com"[..]), 1),
InsertResult::Ok,
"a leftmost-regex host must be insertable after a deeper sibling opened its segment",
);
assert_eq!(
root.domain_lookup(b"test4.example.com", false),
Some(&(b"/test[0-9]/.example.com"[..].to_vec(), 1)),
"the leftmost-regex host must resolve to the rule just inserted",
);
assert_eq!(
root.domain_lookup(b"foo.test4.example.com", false),
Some(&(b"foo./test[0-9]/.example.com"[..].to_vec(), 2)),
"the deeper sibling must survive the second insert",
);
assert_eq!(
root.domain_insert(Vec::from(&b"/test[0-9]/.example.com"[..]), 9),
InsertResult::Existing
);
assert_eq!(
root.domain_lookup(b"test4.example.com", false),
Some(&(b"/test[0-9]/.example.com"[..].to_vec(), 1)),
"an Existing insert must not overwrite the stored value",
);
}
#[test]
fn inserting_a_deeper_sibling_after_a_leftmost_regex_host_still_works() {
let mut root: TrieNode<u8> = TrieNode::root();
assert_eq!(
root.domain_insert(Vec::from(&b"/test[0-9]/.example.com"[..]), 1),
InsertResult::Ok
);
assert_eq!(
root.domain_insert(Vec::from(&b"foo./test[0-9]/.example.com"[..]), 2),
InsertResult::Ok
);
assert_eq!(
root.domain_lookup(b"test4.example.com", false),
Some(&(b"/test[0-9]/.example.com"[..].to_vec(), 1))
);
assert_eq!(
root.domain_lookup(b"foo.test4.example.com", false),
Some(&(b"foo./test[0-9]/.example.com"[..].to_vec(), 2))
);
assert_eq!(
root.domain_insert(Vec::from(&b"/test[0-9]/.example.com"[..]), 8),
InsertResult::Existing
);
assert_eq!(
root.domain_insert(Vec::from(&b"foo./test[0-9]/.example.com"[..]), 8),
InsertResult::Existing
);
}
#[test]
fn removing_regex_segment_hosts_behaves_in_either_insertion_order() {
for remove_leftmost_first in [true, false] {
let mut root: TrieNode<u8> = TrieNode::root();
assert_eq!(
root.domain_insert(Vec::from(&b"foo./test[0-9]/.example.com"[..]), 2),
InsertResult::Ok
);
assert_eq!(
root.domain_insert(Vec::from(&b"/test[0-9]/.example.com"[..]), 1),
InsertResult::Ok
);
assert_regex_segment_pair_removes_cleanly(&mut root, remove_leftmost_first);
}
for remove_leftmost_first in [true, false] {
let mut root: TrieNode<u8> = TrieNode::root();
assert_eq!(
root.domain_insert(Vec::from(&b"/test[0-9]/.example.com"[..]), 1),
InsertResult::Ok
);
assert_eq!(
root.domain_insert(Vec::from(&b"foo./test[0-9]/.example.com"[..]), 2),
InsertResult::Ok
);
assert_regex_segment_pair_removes_cleanly(&mut root, remove_leftmost_first);
}
}
fn assert_regex_segment_pair_removes_cleanly(
root: &mut TrieNode<u8>,
remove_leftmost_first: bool,
) {
let leftmost = Vec::from(&b"/test[0-9]/.example.com"[..]);
let deeper = Vec::from(&b"foo./test[0-9]/.example.com"[..]);
let (first, second) = if remove_leftmost_first {
(&leftmost, &deeper)
} else {
(&deeper, &leftmost)
};
let (first_host, second_host): (&[u8], &[u8]) = if remove_leftmost_first {
(b"test4.example.com", b"foo.test4.example.com")
} else {
(b"foo.test4.example.com", b"test4.example.com")
};
assert_eq!(root.domain_remove(first), RemoveResult::Ok);
assert_eq!(
root.domain_lookup(first_host, false),
None,
"the removed host must be gone",
);
assert!(
root.domain_lookup(second_host, false).is_some(),
"the other host under the same regex segment must survive",
);
assert_eq!(root.domain_remove(first), RemoveResult::NotFound);
assert!(root.domain_lookup(second_host, false).is_some());
assert_eq!(root.domain_remove(second), RemoveResult::Ok);
assert_eq!(root.domain_lookup(second_host, false), None);
assert!(
root.is_empty(),
"the emptied regex subtree must be pruned back to an empty root",
);
}
#[test]
fn removing_a_leftmost_regex_host_keeps_its_sibling_domains() {
let mut root: TrieNode<u8> = TrieNode::root();
assert_eq!(
root.domain_insert(Vec::from(&b"/test[0-9]/.example.com"[..]), 1),
InsertResult::Ok
);
assert_eq!(
root.domain_insert(Vec::from(&b"foo./test[0-9]/.example.com"[..]), 2),
InsertResult::Ok
);
assert_eq!(
root.domain_lookup(b"test4.example.com", false),
Some(&(b"/test[0-9]/.example.com"[..].to_vec(), 1))
);
assert_eq!(
root.domain_lookup(b"foo.test4.example.com", false),
Some(&(b"foo./test[0-9]/.example.com"[..].to_vec(), 2))
);
assert_eq!(
root.domain_remove(&Vec::from(&b"/test[0-9]/.example.com"[..])),
RemoveResult::Ok
);
assert_eq!(
root.domain_lookup(b"test4.example.com", false),
None,
"the removed leftmost-regex host must be gone",
);
assert_eq!(
root.domain_lookup(b"foo.test4.example.com", false),
Some(&(b"foo./test[0-9]/.example.com"[..].to_vec(), 2)),
"a sibling domain under the same regex segment must survive",
);
assert_eq!(
root.domain_remove(&Vec::from(&b"/test[0-9]/.example.com"[..])),
RemoveResult::NotFound
);
assert_eq!(
root.domain_lookup(b"foo.test4.example.com", false),
Some(&(b"foo./test[0-9]/.example.com"[..].to_vec(), 2))
);
}
#[test]
fn removing_the_last_host_under_a_regex_segment_prunes_the_subtree() {
let mut root: TrieNode<u8> = TrieNode::root();
assert_eq!(
root.domain_insert(Vec::from(&b"www./.*/.com"[..]), 1),
InsertResult::Ok
);
assert_eq!(
root.domain_lookup(b"www.sozu.com", false),
Some(&(b"www./.*/.com"[..].to_vec(), 1))
);
assert_eq!(
root.domain_remove(&Vec::from(&b"www./.*/.com"[..])),
RemoveResult::Ok
);
assert_eq!(root.domain_lookup(b"www.sozu.com", false), None);
assert!(
root.is_empty(),
"dropping the last host under a regex segment must leave an empty trie",
);
}
#[test]
fn lookup_mut_does_not_resolve_a_literal_host_through_a_regex_segment() {
let mut root: TrieNode<u8> = TrieNode::root();
assert_eq!(
root.domain_insert(Vec::from(&b"/test[0-9]/.example.com"[..]), 7),
InsertResult::Ok
);
assert_eq!(
root.domain_lookup_mut(b"test4.example.com", false)
.map(|(_, v)| *v),
None,
"a literal host is not a key of this trie just because a regex \
segment matches it",
);
assert_eq!(
root.domain_lookup_mut(b"/test[0-9]/.example.com", false)
.map(|(_, v)| *v),
Some(7),
"the regex segment's own key still resolves, by regex identity",
);
assert_eq!(
root.domain_lookup(b"test4.example.com", false),
Some(&(b"/test[0-9]/.example.com"[..].to_vec(), 7)),
"while the request-addressed lookup still matches the family",
);
assert_eq!(
root.domain_insert(Vec::from(&b"test4.example.com"[..]), 4),
InsertResult::Ok
);
assert_eq!(
root.domain_lookup(b"test4.example.com", false),
Some(&(b"test4.example.com"[..].to_vec(), 4)),
"and it outranks the regex segment that also matches it",
);
}
#[test]
fn lookup_prefers_exact_then_regex_then_wildcard() {
let mut root: TrieNode<u8> = TrieNode::root();
assert_eq!(
root.domain_insert(Vec::from(&b"*.example.com"[..]), 1),
InsertResult::Ok
);
assert_eq!(
root.domain_insert(Vec::from(&b"/test[0-9]/.example.com"[..]), 2),
InsertResult::Ok
);
assert_eq!(
root.domain_insert(Vec::from(&b"test4.example.com"[..]), 3),
InsertResult::Ok
);
assert_eq!(
root.domain_lookup(b"test4.example.com", true)
.map(|(_, v)| *v),
Some(3),
"the exact child wins over both",
);
assert_eq!(
root.domain_lookup(b"test7.example.com", true)
.map(|(_, v)| *v),
Some(2),
"a regex segment wins over the wildcard",
);
assert_eq!(
root.domain_lookup(b"other.example.com", true)
.map(|(_, v)| *v),
Some(1),
"the wildcard answers what neither claims",
);
assert_eq!(
root.domain_lookup(b"test7.example.com", false)
.map(|(_, v)| *v),
Some(2),
"and the regex segment still answers when the wildcard is \
refused, which is what makes the order above observable",
);
}
#[test]
fn a_regex_segment_holding_no_value_falls_back_to_the_wildcard() {
let mut root: TrieNode<u8> = TrieNode::root();
assert_eq!(
root.domain_insert(Vec::from(&b"images./cdn[0-9]+/.hello.com"[..]), 8),
InsertResult::Ok
);
assert_eq!(
root.domain_insert(Vec::from(&b"*.hello.com"[..]), 7),
InsertResult::Ok
);
assert_eq!(
root.domain_lookup(b"images.cdn10.hello.com", true)
.map(|(_, v)| *v),
Some(8),
"the regex segment serves the host it was opened for",
);
assert_eq!(
root.domain_lookup(b"cdn10.hello.com", true)
.map(|(_, v)| *v),
Some(7),
"and the wildcard serves the one it was not",
);
}
#[test]
fn inline_trie_matches_rewinds_across_the_spill_like_a_vec() {
let labels: Vec<Vec<u8>> = (0..INLINE_TRIE_MATCHES + 8)
.map(|index| format!("s{index}").into_bytes())
.collect();
let mut inline = InlineTrieMatches::new();
let mut heap: TrieMatches<'_, '_> = Vec::new();
let segments = |sub_matches: Vec<&TrieSubMatch<'_, '_>>| -> Vec<Vec<u8>> {
sub_matches
.into_iter()
.map(|sub_match| match sub_match {
TrieSubMatch::Wildcard(segment) | TrieSubMatch::Regexp(segment, _) => {
segment.to_vec()
}
})
.collect()
};
let steps: [(usize, usize); 3] = [
(INLINE_TRIE_MATCHES + 4, INLINE_TRIE_MATCHES - 3),
(INLINE_TRIE_MATCHES + 8, INLINE_TRIE_MATCHES + 2),
(INLINE_TRIE_MATCHES + 8, 0),
];
for (fill, rewind) in steps {
for label in &labels[inline.mark()..fill] {
SubMatchSink::push(&mut inline, TrieSubMatch::Wildcard(label));
SubMatchSink::push(&mut heap, TrieSubMatch::Wildcard(label));
}
assert_eq!(
segments(inline.iter().collect()),
segments(heap.iter().collect()),
"after filling to {fill}",
);
SubMatchSink::rewind(&mut inline, rewind);
SubMatchSink::rewind(&mut heap, rewind);
assert_eq!(inline.mark(), heap.mark());
assert_eq!(
segments(inline.iter().collect()),
segments(heap.iter().collect()),
"after rewinding to {rewind}",
);
}
}
}