use std::path::Path;
use std::sync::{Arc, OnceLock};
use memmap2::Mmap;
use crate::eliasfano::EliasFano;
use crate::error::{Error, Result};
use crate::util::{Advice, advise_mmap, lock_mmap, mmap_file, preload_mmap, unlock_mmap};
const ANCHOR_LEN: usize = 16;
const META_LEN: usize = 24;
const FOOTER_MAGIC: [u8; 8] = *b"erigon\x00\x00";
const FIRST_BYTE_FOOTER: u8 = 0x01;
pub struct BtreeIndex {
ef: Option<EliasFano>,
m: Option<u64>,
mmap: Arc<Mmap>,
node_src: Option<(u64, u64, usize)>,
nodes: OnceLock<Option<Nodes>>,
}
pub struct Nodes {
arena: Vec<u8>,
ends: Vec<u32>,
fixed_len: Option<u32>,
count: usize,
m: u64,
key_count: u64,
}
impl Nodes {
pub fn len(&self) -> usize {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn m(&self) -> u64 {
self.m
}
pub fn heap_bytes(&self) -> usize {
self.arena.len() + self.ends.len() * 4
}
#[inline]
pub fn key(&self, j: usize) -> &[u8] {
match self.fixed_len {
Some(l) => {
let l = l as usize;
&self.arena[j * l..(j + 1) * l]
}
None => {
let start = if j == 0 { 0 } else { self.ends[j - 1] as usize };
&self.arena[start..self.ends[j] as usize]
}
}
}
#[inline]
pub fn narrow(&self, key: &[u8]) -> (u64, u64) {
let mut lo = 0usize;
let mut hi = self.count;
while lo < hi {
let mid = lo + (hi - lo) / 2;
if self.key(mid) <= key {
lo = mid + 1;
} else {
hi = mid;
}
}
if lo == 0 {
return (0, 0); }
let start = (lo as u64 - 1) * self.m;
(start, (start + self.m).min(self.key_count))
}
}
impl BtreeIndex {
pub fn open(path: impl AsRef<Path>) -> Result<BtreeIndex> {
let path = path.as_ref();
let mmap = mmap_file(path)?;
let len = mmap.len();
let mmap = Arc::new(mmap);
if len == 0 {
return Ok(BtreeIndex {
ef: None,
m: None,
mmap,
node_src: None,
nodes: OnceLock::from(None),
});
}
if len >= ANCHOR_LEN && mmap[len - 8..len] == FOOTER_MAGIC {
let anchor = &mmap[len - ANCHOR_LEN..];
let footer_len = u32::from_be_bytes(anchor[0..4].try_into().unwrap()) as usize;
if footer_len < META_LEN || ANCHOR_LEN + footer_len > len {
return Err(Error::format(format!(
"{}: corrupt .bt footer (footer_len={footer_len}, file={len})",
path.display()
)));
}
let footer_start = len - ANCHOR_LEN - footer_len;
let payload = &mmap[footer_start..len - ANCHOR_LEN];
let keys_count = u64::from_be_bytes(payload[0..8].try_into().unwrap());
let m = u64::from_be_bytes(payload[8..16].try_into().unwrap());
let ef_offset = u64::from_be_bytes(payload[16..24].try_into().unwrap()) as usize;
if ef_offset >= footer_start {
return Err(Error::format(format!(
"{}: corrupt .bt footer (ef_offset={ef_offset} >= body={footer_start})",
path.display()
)));
}
let ef = EliasFano::open(Arc::clone(&mmap), ef_offset)?;
if ef.len() != keys_count {
return Err(Error::format(format!(
"{}: .bt EF has {} keys, footer says {keys_count}",
path.display(),
ef.len()
)));
}
return Ok(BtreeIndex {
ef: Some(ef),
m: Some(m),
mmap,
node_src: Some((keys_count, m, ef_offset)),
nodes: OnceLock::new(),
});
}
if mmap[0] == FIRST_BYTE_FOOTER {
return Err(Error::format(format!(
"{}: .bt looks like footer layout but the trailing magic is missing (truncated?)",
path.display()
)));
}
let ef = EliasFano::open(Arc::clone(&mmap), 0)?;
Ok(BtreeIndex {
ef: Some(ef),
m: None,
mmap,
node_src: None,
nodes: OnceLock::from(None),
})
}
pub fn key_count(&self) -> u64 {
self.ef.as_ref().map_or(0, EliasFano::len)
}
pub fn key_offset(&self, i: u64) -> Option<u64> {
let ef = self.ef.as_ref()?;
(i < ef.len()).then(|| ef.get(i))
}
pub fn m(&self) -> Option<u64> {
self.m
}
pub fn nodes(&self) -> Option<&Nodes> {
self.nodes
.get_or_init(|| {
let (key_count, m, ef_offset) = self.node_src?;
parse_nodes(&self.mmap, key_count, m, ef_offset)
})
.as_ref()
}
#[inline]
pub fn narrow(&self, key: &[u8]) -> (u64, u64) {
match self.nodes() {
Some(n) => n.narrow(key),
None => (0, self.key_count()),
}
}
pub fn advise_random(&self) -> std::io::Result<()> {
advise_mmap(&self.mmap, Advice::Random)
}
pub fn mapped_bytes(&self) -> u64 {
self.mmap.len() as u64
}
pub fn preload(&self) -> u64 {
preload_mmap(&self.mmap) as u64
}
pub fn lock(&self) -> std::io::Result<()> {
lock_mmap(&self.mmap)
}
pub fn unlock(&self) -> std::io::Result<()> {
unlock_mmap(&self.mmap)
}
pub fn elias_fano(&self) -> Option<&EliasFano> {
self.ef.as_ref()
}
}
fn parse_nodes(data: &[u8], key_count: u64, m: u64, ef_offset: usize) -> Option<Nodes> {
if key_count == 0 || m == 0 || ef_offset <= 1 || ef_offset > data.len() {
return None;
}
let count = usize::try_from(key_count.div_ceil(m)).ok()?;
let mut arena: Vec<u8> = Vec::new();
let mut ends: Vec<u32> = Vec::with_capacity(count);
let mut fixed_len: Option<u32> = None;
let mut uniform = true;
let mut p = 1usize;
for j in 0..count {
let lb = data.get(p..p + 2)?;
let klen = u16::from_be_bytes(lb.try_into().ok()?) as usize;
p += 2;
if p + klen > ef_offset {
return None;
}
match fixed_len {
None if j == 0 => fixed_len = Some(klen as u32),
Some(l) if l as usize != klen => uniform = false,
_ => {}
}
arena.extend_from_slice(&data[p..p + klen]);
p += klen;
ends.push(u32::try_from(arena.len()).ok()?);
}
if uniform {
ends = Vec::new();
} else {
fixed_len = None;
}
Some(Nodes {
arena,
ends,
fixed_len,
count,
m,
key_count,
})
}