use std::collections::HashMap;
use std::io::Cursor;
use memmap2::Mmap;
use crate::error::{HoronError, HoronResult};
use crate::quant::SemLayout;
use crate::snapshot::NodeEntry;
#[derive(Debug, Clone)]
pub struct EntryLoc {
pub key: String,
pub off: usize,
pub len: usize,
pub hilbert: u128,
}
pub struct SnapView {
mmap: Mmap,
raw_start: usize,
pub entries: Vec<EntryLoc>,
pub by_key: HashMap<String, usize>,
layout: SemLayout,
}
impl SnapView {
pub fn scan(
mmap: Mmap,
raw_start: usize,
raw_len: usize,
node_count: usize,
layout: SemLayout,
hilbert_of: Option<&dyn Fn(&[u8]) -> u128>,
) -> HoronResult<Self> {
let region = mmap
.get(raw_start..raw_start + raw_len)
.ok_or_else(|| HoronError::InvalidFormat(
"snapshot region exceeds file length".to_string(),
))?;
let mut entries = Vec::with_capacity(node_count.min(1_000_000));
let mut by_key = HashMap::with_capacity(node_count.min(1_000_000));
let mut pos = 0usize;
for _ in 0..node_count {
let start = pos;
let key_len = read_u16(region, &mut pos)? as usize;
let key_bytes = read_slice(region, &mut pos, key_len)?;
let key = std::str::from_utf8(key_bytes)
.map_err(|e| HoronError::InvalidFormat(format!("invalid UTF-8 key: {}", e)))?
.to_string();
let data_len = read_u32(region, &mut pos)? as usize;
skip(region, &mut pos, data_len)?;
let meta_count = read_u16(region, &mut pos)? as usize;
for _ in 0..meta_count {
let mk_len = read_u16(region, &mut pos)? as usize;
skip(region, &mut pos, mk_len)?;
let mv_len = read_u16(region, &mut pos)? as usize;
skip(region, &mut pos, mv_len)?;
}
let sem = read_slice(region, &mut pos, layout.disk_bytes())?;
let hilbert = hilbert_of.map(|f| f(sem)).unwrap_or(0);
let idx = entries.len();
entries.push(EntryLoc { key: key.clone(), off: start, len: pos - start, hilbert });
by_key.insert(key, idx);
}
Ok(Self { mmap, raw_start, entries, by_key, layout })
}
pub fn decode(&self, i: usize) -> HoronResult<NodeEntry> {
let loc = self.entries.get(i).ok_or_else(|| {
HoronError::InvalidFormat(format!("entry index {} out of range", i))
})?;
let abs = self.raw_start + loc.off;
let bytes = self.mmap.get(abs..abs + loc.len).ok_or_else(|| {
HoronError::InvalidFormat("entry range exceeds file length".to_string())
})?;
NodeEntry::read_from(&mut Cursor::new(bytes), &self.layout)
}
pub fn semantic_of(&self, i: usize) -> HoronResult<&[u8]> {
let loc = self.entries.get(i).ok_or_else(|| {
HoronError::InvalidFormat(format!("entry index {} out of range", i))
})?;
let abs_end = self.raw_start + loc.off + loc.len;
let abs_start = abs_end.checked_sub(self.layout.disk_bytes()).ok_or_else(|| {
HoronError::InvalidFormat("entry shorter than its semantic tail".to_string())
})?;
self.mmap
.get(abs_start..abs_end)
.ok_or_else(|| HoronError::InvalidFormat(
"semantic range exceeds file length".to_string(),
))
}
pub fn layout(&self) -> SemLayout {
self.layout
}
pub fn hilbert_position(&self, target: u128) -> usize {
self.entries
.partition_point(|e| e.hilbert < target)
}
}
fn read_u16(region: &[u8], pos: &mut usize) -> HoronResult<u16> {
let b = read_slice(region, pos, 2)?;
Ok(u16::from_le_bytes([b[0], b[1]]))
}
fn read_u32(region: &[u8], pos: &mut usize) -> HoronResult<u32> {
let b = read_slice(region, pos, 4)?;
Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
fn read_slice<'a>(region: &'a [u8], pos: &mut usize, len: usize) -> HoronResult<&'a [u8]> {
let end = pos.checked_add(len).ok_or_else(|| {
HoronError::InvalidFormat("entry offset overflow".to_string())
})?;
let s = region.get(*pos..end).ok_or_else(|| {
HoronError::InvalidFormat("truncated snapshot entry".to_string())
})?;
*pos = end;
Ok(s)
}
fn skip(region: &[u8], pos: &mut usize, len: usize) -> HoronResult<()> {
read_slice(region, pos, len).map(|_| ())
}