use std::collections::HashSet;
use std::ops::Bound;
use std::sync::{Arc, Mutex, MutexGuard};
use fjall::Keyspace;
const SEP: u8 = 0xFF;
const FOLD_EXTRA_AT: usize = 16_384;
const SEEK_AFTER_RUN: usize = 8;
const WALK_ABOVE_PERCENT: usize = 5;
fn key_order(a: &[u8], b: &[u8]) -> std::cmp::Ordering {
a.len().cmp(&b.len()).then_with(|| a.cmp(b))
}
#[derive(Default)]
struct Snapshot {
blob: Vec<u8>,
starts: Vec<u32>,
}
impl Snapshot {
fn from_sorted<'a>(names: impl Iterator<Item = &'a [u8]>) -> Self {
let mut blob = Vec::new();
let mut starts = Vec::new();
for name in names {
starts.push(blob.len() as u32);
blob.extend_from_slice(name);
blob.push(SEP);
}
starts.push(blob.len() as u32);
blob.shrink_to_fit();
starts.shrink_to_fit();
Self { blob, starts }
}
fn len(&self) -> usize {
self.starts.len().saturating_sub(1)
}
fn name(&self, index: usize) -> &[u8] {
&self.blob[self.starts[index] as usize..self.starts[index + 1] as usize - 1]
}
fn contains(&self, name: &[u8]) -> bool {
let (mut lo, mut hi) = (0, self.len());
while lo < hi {
let mid = lo + (hi - lo) / 2;
match key_order(self.name(mid), name) {
std::cmp::Ordering::Less => lo = mid + 1,
std::cmp::Ordering::Greater => hi = mid,
std::cmp::Ordering::Equal => return true,
}
}
false
}
fn matching<'a>(&'a self, finder: &memchr::memmem::Finder<'_>) -> Vec<&'a [u8]> {
if finder.needle().is_empty() {
return (0..self.len()).map(|i| self.name(i)).collect();
}
let mut out = Vec::new();
let mut from = 0usize;
while from < self.blob.len() {
let Some(found) = finder.find(&self.blob[from..]) else {
break;
};
let pos = from + found;
let index = self.starts.partition_point(|&start| (start as usize) <= pos) - 1;
out.push(self.name(index));
from = self.starts[index + 1] as usize;
}
out
}
}
#[derive(Default)]
struct State {
snapshot: Option<Arc<Snapshot>>,
extra: HashSet<String>,
building: bool,
disabled: bool,
}
pub struct NameDict {
state: Mutex<State>,
walk_above_percent: std::sync::atomic::AtomicUsize,
}
impl Default for NameDict {
fn default() -> Self {
Self {
state: Mutex::default(),
walk_above_percent: std::sync::atomic::AtomicUsize::new(WALK_ABOVE_PERCENT),
}
}
}
impl NameDict {
fn lock(&self) -> MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub(crate) fn record<'a>(&self, names: impl Iterator<Item = &'a str>) {
let mut state = self.lock();
for name in names {
if name.len() > usize::from(u16::MAX) {
continue;
}
if state.snapshot.as_ref().is_some_and(|s| s.contains(name.as_bytes())) || state.extra.contains(name) {
continue;
}
state.extra.insert(name.to_owned());
}
}
pub(crate) fn matching_prefixes(self: &Arc<Self>, keyspace: &Keyspace, needle: &str) -> Option<Vec<Vec<u8>>> {
let total;
let (snapshot, extra) = {
let mut state = self.lock();
if state.disabled {
return None;
}
if state.snapshot.is_none() {
if !state.building {
state.building = true;
let (dict, keyspace) = (Arc::clone(self), keyspace.clone());
let spawned = std::thread::Builder::new()
.name("name-dict-build".into())
.spawn(move || dict.build(&keyspace));
if spawned.is_err() {
state.building = false;
state.disabled = true;
}
}
return None;
}
if state.extra.len() > FOLD_EXTRA_AT {
Self::fold(&mut state);
}
let snapshot = Arc::clone(state.snapshot.as_ref()?);
let extra: Vec<String> = state.extra.iter().filter(|n| n.contains(needle)).cloned().collect();
total = snapshot.len() + state.extra.len();
(snapshot, extra)
};
let finder = memchr::memmem::Finder::new(needle.as_bytes());
let mut names: Vec<&[u8]> = snapshot.matching(&finder);
let known = names.len();
names.extend(extra.iter().map(String::as_bytes));
if names.len() > known {
names.sort_unstable_by(|a, b| key_order(a, b));
names.dedup();
}
let percent = self.walk_above_percent.load(std::sync::atomic::Ordering::Relaxed);
if names.len().saturating_mul(100) > total.saturating_mul(percent) {
return None;
}
Some(
names
.into_iter()
.map(|name| {
let mut prefix = Vec::with_capacity(2 + name.len());
prefix.extend_from_slice(&(name.len() as u16).to_be_bytes());
prefix.extend_from_slice(name);
prefix
})
.collect(),
)
}
fn fold(state: &mut State) {
let snapshot = state.snapshot.clone();
let mut merged: Vec<&[u8]> = Vec::new();
if let Some(snapshot) = &snapshot {
merged.extend((0..snapshot.len()).map(|i| snapshot.name(i)));
}
merged.extend(state.extra.iter().map(String::as_bytes));
merged.sort_unstable_by(|a, b| key_order(a, b));
merged.dedup();
let folded = Snapshot::from_sorted(merged.into_iter());
state.snapshot = Some(Arc::new(folded));
state.extra.clear();
}
fn build(&self, keyspace: &Keyspace) {
let result = distinct_names(keyspace);
let mut state = self.lock();
state.building = false;
match result {
Ok(names) => {
state.snapshot = Some(Arc::new(Snapshot::from_sorted(names.iter().map(Vec::as_slice))));
Self::fold(&mut state);
}
Err(error) => {
tracing::warn!(%error, "name dictionary build failed; falling back to full keyspace walks");
state.disabled = true;
}
}
}
#[cfg(test)]
pub(crate) fn build_blocking(&self, keyspace: &Keyspace) {
self.lock().building = true;
self.build(keyspace);
}
#[cfg(test)]
pub(crate) fn always_use_dictionary(&self) {
self.walk_above_percent
.store(usize::MAX / 1_000, std::sync::atomic::Ordering::Relaxed);
}
#[cfg(test)]
pub(crate) fn set_disabled(&self, disabled: bool) {
self.lock().disabled = disabled;
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
let state = self.lock();
state.snapshot.as_ref().map_or(0, |s| s.len()) + state.extra.len()
}
#[cfg(test)]
pub(crate) fn snapshot_bytes(&self) -> usize {
self.lock()
.snapshot
.as_ref()
.map_or(0, |s| s.blob.len() + s.starts.len() * 4)
}
}
fn distinct_names(keyspace: &Keyspace) -> Result<Vec<Vec<u8>>, fjall::Error> {
let mut names: Vec<Vec<u8>> = Vec::new();
let mut lower: Bound<Vec<u8>> = Bound::Unbounded;
'seek: loop {
let mut run = 0usize;
for guard in keyspace.range::<Vec<u8>, _>((lower.clone(), Bound::Unbounded)) {
let (key, _) = guard.into_inner()?;
let Some(prefix_len) = key.get(..2).map(|b| 2 + usize::from(u16::from_be_bytes([b[0], b[1]]))) else {
continue;
};
let Some(prefix) = key.get(..prefix_len) else {
continue;
};
let same = names
.last()
.is_some_and(|last| last.len() + 2 == prefix_len && last[..] == prefix[2..]);
if same {
run += 1;
if run >= SEEK_AFTER_RUN {
match upper_bound(prefix) {
Some(next) => {
lower = Bound::Included(next);
continue 'seek;
}
None => break 'seek,
}
}
} else {
if std::str::from_utf8(&prefix[2..]).is_ok() {
names.push(prefix[2..].to_vec());
}
run = 1;
}
}
break;
}
Ok(names)
}
pub(crate) fn upper_bound(prefix: &[u8]) -> Option<Vec<u8>> {
let mut out = prefix.to_vec();
while let Some(last) = out.last_mut() {
if *last == 0xFF {
out.pop();
continue;
}
*last += 1;
return Some(out);
}
None
}
pub(crate) fn scan_prefixes(
keyspace: &Keyspace,
prefixes: Vec<Vec<u8>>,
cursor_after: Option<Vec<u8>>,
) -> impl Iterator<Item = fjall::Guard> + '_ {
prefixes.into_iter().flat_map(move |prefix| {
let lower = match &cursor_after {
Some(cursor) if cursor.as_slice() > prefix.as_slice() && !cursor.starts_with(&prefix) => None,
Some(cursor) if cursor.starts_with(&prefix) => Some(Bound::Excluded(cursor.clone())),
_ => Some(Bound::Included(prefix.clone())),
};
let upper = match upper_bound(&prefix) {
Some(bound) => Bound::Excluded(bound),
None => Bound::Unbounded,
};
lower
.map(|lower| keyspace.range::<Vec<u8>, _>((lower, upper)))
.into_iter()
.flatten()
})
}
pub(crate) fn name_ordered_keys<'a>(
keyspace: &'a Keyspace,
dict: &Arc<NameDict>,
needle: &str,
cursor_after: Option<&[u8]>,
lower: Bound<Vec<u8>>,
) -> Box<dyn Iterator<Item = fjall::Guard> + 'a> {
match dict.matching_prefixes(keyspace, needle) {
Some(prefixes) => Box::new(scan_prefixes(keyspace, prefixes, cursor_after.map(<[u8]>::to_vec))),
None => Box::new(keyspace.range::<Vec<u8>, _>((lower, Bound::Unbounded))),
}
}