use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
use crate::reader::KvReader;
use crate::salt::Salt;
pub struct KvStack {
readers: Vec<KvReader>,
salt: Option<u32>,
}
impl KvStack {
pub fn open<I, P>(paths: I, salt: Salt) -> Result<KvStack>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
let mut paths: Vec<PathBuf> = paths
.into_iter()
.map(|p| p.as_ref().to_path_buf())
.collect();
if paths.is_empty() {
return Err(Error::format("KvStack::open: no .kv files supplied"));
}
paths.sort_by_key(|p| step_key(p));
let mut readers = Vec::with_capacity(paths.len());
for p in &paths {
readers.push(KvReader::open(p)?);
}
let salt = resolve_and_enable(&mut readers, salt);
Ok(KvStack { readers, salt })
}
pub fn open_dir(dir: impl AsRef<Path>, name_filter: &str, salt: Salt) -> Result<KvStack> {
let dir = dir.as_ref();
let mut kvs: Vec<PathBuf> = std::fs::read_dir(dir)
.map_err(|e| Error::format(format!("read_dir {}: {e}", dir.display())))?
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| {
if p.extension().is_none_or(|x| x != "kv") {
return false;
}
p.file_name()
.map(|s| s.to_string_lossy().contains(name_filter))
.unwrap_or(false)
})
.collect();
if kvs.is_empty() {
return Err(Error::format(format!(
"no .kv files matching {name_filter:?} in {}",
dir.display()
)));
}
kvs.sort_by_key(|p| step_key(p));
KvStack::open(kvs, salt)
}
pub fn salt(&self) -> Option<u32> {
self.salt
}
pub fn bloom_count(&self) -> usize {
self.readers.iter().filter(|r| r.bloom_active()).count()
}
pub fn len(&self) -> usize {
self.readers.len()
}
pub fn is_empty(&self) -> bool {
self.readers.is_empty()
}
pub fn readers(&self) -> &[KvReader] {
&self.readers
}
pub fn files(&self) -> impl Iterator<Item = (&str, u64)> {
self.readers.iter().map(|r| (r.name(), r.key_count()))
}
pub fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
for r in self.readers.iter().rev() {
if let Some(v) = r.get(key)? {
return Ok(Some(v));
}
}
Ok(None)
}
}
fn resolve_and_enable(readers: &mut [KvReader], salt: Salt) -> Option<u32> {
let resolved = match salt {
Salt::None => None,
Salt::Known(s) => Some(s),
Salt::Find(threads) => readers.first().and_then(|r| r.find_salt(threads)),
};
if let Some(s) = resolved {
for r in readers.iter_mut() {
r.enable_bloom(Salt::Known(s));
}
}
resolved
}
fn step_key(p: &Path) -> (u64, u64) {
let name = p
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
for seg in name.split('.') {
if let Some((a, b)) = seg.split_once('-')
&& let (Ok(a), Ok(b)) = (a.parse::<u64>(), b.parse::<u64>())
{
return (a, b);
}
}
(0, 0)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn step_key_parses_range() {
assert_eq!(step_key(Path::new("v1.1-accounts.0-1024.kv")), (0, 1024));
assert_eq!(
step_key(Path::new("v1.1-accounts.1024-2048.kv")),
(1024, 2048)
);
assert_eq!(step_key(Path::new("accounts.kv")), (0, 0));
}
#[test]
fn empty_open_errors() {
let empty: Vec<&Path> = Vec::new();
assert!(KvStack::open(empty, Salt::None).is_err());
}
}