#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use crate::config::LocatorPrecision;
use crate::table::filter::ribbon::burr::{BurrBuilder, BurrParams};
pub const SECTION_VERSION: u8 = 1;
pub const SECTION_HEADER_LEN: usize = 4;
#[derive(Copy, Clone, Debug)]
pub struct LocatorSpec {
pub precision: LocatorPrecision,
pub block_id_bits: Option<u8>,
pub slot_bits: Option<u8>,
}
fn bits_for(max: u64) -> u8 {
#[expect(
clippy::cast_possible_truncation,
reason = "64 - leading_zeros() is in 0..=64, fits u8"
)]
let bits = (64 - max.leading_zeros()) as u8;
bits.max(1)
}
const PRECISION_RESTART: u8 = 0;
const PRECISION_ENTRY: u8 = 1;
const PRECISION_BLOCK: u8 = 2;
fn precision_byte(p: LocatorPrecision) -> u8 {
match p {
LocatorPrecision::Restart => PRECISION_RESTART,
LocatorPrecision::Entry => PRECISION_ENTRY,
LocatorPrecision::Block => PRECISION_BLOCK,
}
}
#[must_use]
pub fn build_locator_section(entries: &[(u64, u64, u64)], spec: LocatorSpec) -> Option<Vec<u8>> {
if entries.is_empty() {
return None;
}
let max_block = entries.iter().map(|e| e.1).max().unwrap_or(0);
let max_slot = entries.iter().map(|e| e.2).max().unwrap_or(0);
let block_only = spec.precision == LocatorPrecision::Block;
let block_id_bits = spec.block_id_bits.unwrap_or_else(|| bits_for(max_block));
let slot_bits = if block_only {
0
} else {
spec.slot_bits.unwrap_or_else(|| bits_for(max_slot))
};
let block_fits = block_id_bits >= bits_for(max_block);
let slot_fits = block_only || slot_bits >= bits_for(max_slot);
let r = u16::from(block_id_bits) + u16::from(slot_bits);
if !block_fits || !slot_fits || r == 0 || r > 64 {
log::debug!(
"locator section skipped: widths block_id_bits={block_id_bits} slot_bits={slot_bits} \
cannot represent max_block={max_block} max_slot={max_slot} (r={r})"
);
return None;
}
let slot_mask: u64 = if slot_bits == 0 {
0
} else if slot_bits >= 64 {
u64::MAX
} else {
(1u64 << slot_bits) - 1
};
#[expect(
clippy::cast_possible_truncation,
reason = "r is validated to 1..=64 above, fits u8"
)]
let r_u8 = r as u8;
let mut hashes = Vec::with_capacity(entries.len());
let mut locators = Vec::with_capacity(entries.len());
for &(hash, block_id, slot) in entries {
hashes.push(hash);
locators.push((block_id << slot_bits) | (slot & slot_mask));
}
let params = match BurrParams::with_bpk(entries.len(), f32::from(r_u8)) {
Ok(p) => p,
Err(e) => {
log::debug!("locator section skipped: BurrParams::with_bpk failed: {e}");
return None;
}
};
let builder = match BurrBuilder::new(params) {
Ok(b) => b,
Err(e) => {
log::debug!("locator section skipped: BurrBuilder::new failed: {e}");
return None;
}
};
let filter = match builder.build_from_hashes_with_values(&hashes, &locators) {
Ok(f) => f,
Err(e) => {
log::debug!("locator section skipped: retrieval ribbon build failed: {e}");
return None;
}
};
let wire = filter.to_wire_bytes();
let mut section = Vec::with_capacity(SECTION_HEADER_LEN + wire.len());
section.push(SECTION_VERSION);
section.push(precision_byte(spec.precision));
section.push(block_id_bits);
section.push(slot_bits);
section.extend_from_slice(&wire);
Some(section)
}
#[expect(
clippy::indexing_slicing,
reason = "header bytes [0..4) are gated by the `section.len() < SECTION_HEADER_LEN` \
check on the line above; the wire slice starts at the validated header end"
)]
pub fn locate(section: &[u8], hash: u64) -> crate::Result<Option<(u64, u64)>> {
use crate::table::filter::ribbon::burr::recover_value_from_bytes;
if section.len() < SECTION_HEADER_LEN {
return Err(crate::Error::InvalidHeader("LocatorSection"));
}
if section[0] != SECTION_VERSION {
return Err(crate::Error::InvalidHeader("LocatorSection version"));
}
let slot_bits = section[3];
let Some(packed) = recover_value_from_bytes(§ion[SECTION_HEADER_LEN..], hash)? else {
return Ok(None);
};
let slot_mask = if slot_bits == 0 {
0
} else if slot_bits >= 64 {
u64::MAX
} else {
(1u64 << slot_bits) - 1
};
let block_id = if slot_bits >= 64 {
0
} else {
packed >> slot_bits
};
Ok(Some((block_id, packed & slot_mask)))
}
#[derive(Debug)]
pub struct LoadedLocator {
section: crate::Slice,
blocks: Vec<crate::table::BlockHandle>,
precision: u8,
}
pub type Located = (crate::table::BlockHandle, Option<(u64, bool)>);
impl LoadedLocator {
#[must_use]
pub fn new(section: crate::Slice, blocks: Vec<crate::table::BlockHandle>) -> Self {
let precision = section.get(1).copied().unwrap_or(PRECISION_BLOCK);
Self {
section,
blocks,
precision,
}
}
pub fn locate_block(&self, key_hash: u64) -> crate::Result<Option<Located>> {
let Some((block_id, slot)) = locate(&self.section, key_hash)? else {
return Ok(None);
};
let Some(handle) = usize::try_from(block_id)
.ok()
.and_then(|i| self.blocks.get(i))
.copied()
else {
return Ok(None);
};
let hint = match self.precision {
PRECISION_RESTART => Some((slot, false)),
PRECISION_ENTRY => Some((slot, true)),
_ => None,
};
Ok(Some((handle, hint)))
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::expect_used,
clippy::unwrap_used,
clippy::indexing_slicing,
reason = "test assertions over known-good fixtures; failure surfaces via panic"
)]
use super::*;
fn key_hash(i: u64) -> u64 {
crate::hash::hash64(&i.to_le_bytes())
}
#[test]
fn build_returns_none_for_empty_input() {
let spec = LocatorSpec {
precision: LocatorPrecision::Restart,
block_id_bits: None,
slot_bits: None,
};
assert!(build_locator_section(&[], spec).is_none());
}
#[test]
fn auto_width_section_round_trips_block_id_and_slot() {
let spec = LocatorSpec {
precision: LocatorPrecision::Restart,
block_id_bits: None,
slot_bits: None,
};
let entries: Vec<(u64, u64, u64)> =
(0..300u64).map(|i| (key_hash(i), i % 12, i % 25)).collect();
let bytes = build_locator_section(&entries, spec).expect("section built");
assert_eq!(bytes[2], 4);
assert_eq!(bytes[3], 5);
for i in 0..300u64 {
assert_eq!(
locate(&bytes, key_hash(i)).unwrap(),
Some((i % 12, i % 25)),
"key {i} locator mismatch",
);
}
}
#[test]
fn explicit_widths_too_small_skips_gracefully() {
let spec = LocatorSpec {
precision: LocatorPrecision::Restart,
block_id_bits: Some(4),
slot_bits: Some(4),
};
let entries: Vec<(u64, u64, u64)> =
(0..200u64).map(|i| (key_hash(i), i % 101, i % 8)).collect();
assert!(build_locator_section(&entries, spec).is_none());
}
#[test]
fn explicit_widths_that_fit_round_trip() {
let spec = LocatorSpec {
precision: LocatorPrecision::Entry,
block_id_bits: Some(10),
slot_bits: Some(12),
};
let entries: Vec<(u64, u64, u64)> = (0..500u64)
.map(|i| (key_hash(i), i % 1000, i % 4000))
.collect();
let bytes = build_locator_section(&entries, spec).expect("section built");
assert_eq!(bytes[2], 10);
assert_eq!(bytes[3], 12);
for i in 0..500u64 {
assert_eq!(
locate(&bytes, key_hash(i)).unwrap(),
Some((i % 1000, i % 4000))
);
}
}
#[test]
fn block_precision_section_round_trips_block_id_only() {
let spec = LocatorSpec {
precision: LocatorPrecision::Block,
block_id_bits: None,
slot_bits: None,
};
let entries: Vec<(u64, u64, u64)> =
(0..300u64).map(|i| (key_hash(i), i % 12, i % 25)).collect();
let bytes = build_locator_section(&entries, spec).expect("section built");
assert_eq!(bytes[3], 0, "block precision must record slot_bits = 0");
for i in 0..300u64 {
assert_eq!(
locate(&bytes, key_hash(i)).unwrap(),
Some((i % 12, 0)),
"key {i} must resolve to its block with slot 0",
);
}
}
#[test]
fn locate_rejects_truncated_section() {
let err = locate(&[0u8; SECTION_HEADER_LEN - 1], 123).unwrap_err();
assert!(matches!(err, crate::Error::InvalidHeader("LocatorSection")));
}
#[test]
fn build_skips_when_ribbon_cannot_satisfy_conflicting_values() {
let spec = LocatorSpec {
precision: LocatorPrecision::Restart,
block_id_bits: None,
slot_bits: None,
};
let mut entries: Vec<(u64, u64, u64)> =
(0..200u64).map(|i| (key_hash(i), i % 8, i % 4)).collect();
entries.push((key_hash(0), 7, 3));
assert!(
build_locator_section(&entries, spec).is_none(),
"a conflicting hash collision must skip the section, not panic or abort",
);
}
#[test]
fn locate_does_not_panic_on_forged_slot_bits_64() {
let spec = LocatorSpec {
precision: LocatorPrecision::Restart,
block_id_bits: None,
slot_bits: None,
};
let entries: Vec<(u64, u64, u64)> =
(0..100u64).map(|i| (key_hash(i), i % 8, i % 4)).collect();
let mut bytes = build_locator_section(&entries, spec).expect("section built");
bytes[3] = 64; for i in 0..100u64 {
let _ = locate(&bytes, key_hash(i)).expect("locate must not error");
}
}
#[test]
fn locate_rejects_unknown_version() {
let mut section = [0u8; SECTION_HEADER_LEN];
section[0] = SECTION_VERSION.wrapping_add(1);
let err = locate(§ion, 123).unwrap_err();
assert!(matches!(
err,
crate::Error::InvalidHeader("LocatorSection version")
));
}
}