use std::collections::HashMap;
use std::hash::{BuildHasherDefault, Hasher};
#[derive(Default)]
pub(crate) struct IdHasher(u64);
#[inline]
fn mix(x: u64) -> u64 {
let mut z = x.wrapping_mul(0x9E37_79B9_7F4A_7C15);
z ^= z >> 32;
z = z.wrapping_mul(0xD6E8_FEB8_6659_FD93);
z ^ (z >> 32)
}
impl Hasher for IdHasher {
#[inline]
fn write(&mut self, bytes: &[u8]) {
for &b in bytes {
self.0 = mix(self.0 ^ b as u64);
}
}
#[inline]
fn write_u64(&mut self, i: u64) {
self.0 = mix(i);
}
#[inline]
fn finish(&self) -> u64 {
self.0
}
}
pub(crate) type IdBuildHasher = BuildHasherDefault<IdHasher>;
use std::path::Path;
use crate::io;
use crate::{AddError, ConstructError, SearchError, TurboQuantIndex};
#[cfg(test)]
thread_local! {
static TABLE_PROBES: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
#[inline(always)]
fn record_table_probe() {
#[cfg(test)]
{
let _ = TABLE_PROBES.try_with(|p| p.set(p.get() + 1));
}
}
fn table_contains(sorted: &[u64], id: u64) -> bool {
sorted
.binary_search_by(|probe| {
record_table_probe();
probe.cmp(&id)
})
.is_ok()
}
#[derive(Debug, Clone, PartialEq)]
pub struct IdSearchResults {
pub scores: Vec<f32>,
pub ids: Vec<u64>,
pub nq: usize,
pub k: usize,
}
impl IdSearchResults {
pub fn scores_for_query(&self, qi: usize) -> &[f32] {
&self.scores[qi * self.k..(qi + 1) * self.k]
}
pub fn ids_for_query(&self, qi: usize) -> &[u64] {
&self.ids[qi * self.k..(qi + 1) * self.k]
}
}
#[derive(Debug)]
pub struct IdMapIndex {
inner: TurboQuantIndex,
slot_to_id: Vec<u64>,
id_to_slot: std::sync::OnceLock<HashMap<u64, usize, IdBuildHasher>>,
sorted_ids: std::sync::Mutex<Vec<u64>>,
deferred_added: std::sync::Mutex<std::collections::HashSet<u64, IdBuildHasher>>,
}
impl IdMapIndex {
pub fn new(dim: usize, bit_width: usize) -> Result<Self, ConstructError> {
Ok(Self {
inner: TurboQuantIndex::new(dim, bit_width)?,
slot_to_id: Vec::new(),
id_to_slot: std::sync::OnceLock::from(HashMap::default()),
sorted_ids: std::sync::Mutex::new(Vec::new()),
deferred_added: std::sync::Mutex::new(Default::default()),
})
}
pub fn new_lazy(bit_width: usize) -> Result<Self, ConstructError> {
Ok(Self {
inner: TurboQuantIndex::new_lazy(bit_width)?,
slot_to_id: Vec::new(),
id_to_slot: std::sync::OnceLock::from(HashMap::default()),
sorted_ids: std::sync::Mutex::new(Vec::new()),
deferred_added: std::sync::Mutex::new(Default::default()),
})
}
fn ids(&self) -> &HashMap<u64, usize, IdBuildHasher> {
if let Some(m) = self.id_to_slot.get() {
return m;
}
let m = self.id_to_slot.get_or_init(|| {
self.slot_to_id
.iter()
.enumerate()
.map(|(slot, &id)| (id, slot))
.collect()
});
*self.sorted_ids.lock().expect("sorted_ids lock poisoned") = Vec::new();
*self
.deferred_added
.lock()
.expect("deferred_added lock poisoned") = Default::default();
m
}
fn ids_mut(&mut self) -> &mut HashMap<u64, usize, IdBuildHasher> {
self.ids();
self.id_to_slot.get_mut().expect("ids just materialized")
}
pub fn add_with_ids(&mut self, vectors: &[f32], ids: &[u64]) -> Result<(), AddError> {
let dim = self.inner.dim_opt().expect(
"IdMapIndex dim is not set; use add_with_ids_2d(vectors, dim, ids) \
on the first add or construct with IdMapIndex::new(dim, bit_width)",
);
self.add_with_ids_2d(vectors, dim, ids)
}
pub fn add_with_ids_2d(
&mut self,
vectors: &[f32],
dim: usize,
ids: &[u64],
) -> Result<(), AddError> {
if dim == 0 {
return Err(AddError::ZeroDim);
}
if vectors.len() % dim != 0 {
return Err(AddError::VectorBufferNotMultipleOfDim {
vectors_len: vectors.len(),
dim,
});
}
let n = vectors.len() / dim;
if ids.len() != n {
return Err(AddError::IdsCountMismatch {
expected: n,
got: ids.len(),
});
}
let deferred = self.id_to_slot.get().is_none();
let mut seen_this_call: std::collections::HashSet<u64, IdBuildHasher> =
std::collections::HashSet::with_capacity_and_hasher(n, IdBuildHasher::default());
if deferred {
let sorted = self.sorted_ids.get_mut().expect("sorted_ids lock poisoned");
let added = self
.deferred_added
.get_mut()
.expect("deferred_added lock poisoned");
for &id in ids {
if table_contains(sorted, id) || added.contains(&id) {
return Err(AddError::IdAlreadyPresent(id));
}
if !seen_this_call.insert(id) {
return Err(AddError::DuplicateIdInBatch(id));
}
}
} else {
for &id in ids {
if self.ids().contains_key(&id) {
return Err(AddError::IdAlreadyPresent(id));
}
if !seen_this_call.insert(id) {
return Err(AddError::DuplicateIdInBatch(id));
}
}
}
let base_slot = self.inner.len();
self.inner.add_2d(vectors, dim)?;
if deferred {
let added = self
.deferred_added
.get_mut()
.expect("deferred_added lock poisoned");
added.reserve(n);
added.extend(ids.iter().copied());
} else {
self.ids_mut().reserve(n);
for (i, &id) in ids.iter().enumerate() {
self.ids_mut().insert(id, base_slot + i);
}
}
self.slot_to_id.reserve(n);
self.slot_to_id.extend_from_slice(ids);
Ok(())
}
pub fn batch_addable(&self, ids: &[u64]) -> bool {
let mut seen: std::collections::HashSet<u64, IdBuildHasher> =
std::collections::HashSet::with_capacity_and_hasher(ids.len(), IdBuildHasher::default());
if self.id_to_slot.get().is_none() {
let sorted = self.sorted_ids.lock().expect("sorted_ids lock poisoned");
let added = self
.deferred_added
.lock()
.expect("deferred_added lock poisoned");
if self.id_to_slot.get().is_none() {
return ids.iter().all(|&id| {
seen.insert(id) && !table_contains(&sorted, id) && !added.contains(&id)
});
}
drop(sorted);
drop(added);
ids.iter().all(|&id| seen.insert(id) && !self.contains(id))
} else {
ids.iter().all(|&id| seen.insert(id) && !self.contains(id))
}
}
pub fn remove(&mut self, id: u64) -> bool {
let Some(&slot) = self.ids().get(&id) else {
return false;
};
let last = self.slot_to_id.len() - 1;
let moved_from = self.inner.swap_remove(slot);
debug_assert_eq!(moved_from, last);
self.ids_mut().remove(&id);
if slot != last {
let moved_id = self.slot_to_id[last];
self.slot_to_id[slot] = moved_id;
self.ids_mut().insert(moved_id, slot);
}
self.slot_to_id.pop();
true
}
pub fn search(&self, queries: &[f32], k: usize) -> (Vec<f32>, Vec<u64>) {
self.search_with_allowlist(queries, k, None)
.unwrap_or_else(|e| panic!("{e}"))
}
pub fn search_with_allowlist(
&self,
queries: &[f32],
k: usize,
allowlist: Option<&[u64]>,
) -> Result<(Vec<f32>, Vec<u64>), SearchError> {
let res = self.try_search_with_allowlist(queries, k, allowlist)?;
Ok((res.scores, res.ids))
}
pub fn try_search(&self, queries: &[f32], k: usize) -> Result<IdSearchResults, SearchError> {
self.try_search_with_allowlist(queries, k, None)
}
pub fn try_search_with_allowlist(
&self,
queries: &[f32],
k: usize,
allowlist: Option<&[u64]>,
) -> Result<IdSearchResults, SearchError> {
let mask_buf: Option<Vec<bool>> = match allowlist {
Some(ids) => {
if ids.is_empty() {
return Err(SearchError::AllowlistEmpty);
}
let mut mask = vec![false; self.inner.len()];
for &id in ids {
let slot = *self.ids().get(&id).ok_or(SearchError::UnknownId(id))?;
mask[slot] = true;
}
Some(mask)
}
None => None,
};
let res = self
.inner
.try_search_with_mask(queries, k, mask_buf.as_deref())?;
let mut ids = Vec::with_capacity(res.indices.len());
for &slot in &res.indices {
let id = self.slot_to_id[slot as usize];
ids.push(id);
}
Ok(IdSearchResults {
scores: res.scores,
ids,
nq: res.nq,
k: res.k,
})
}
pub fn iter_ids(&self) -> impl ExactSizeIterator<Item = u64> + '_ {
self.slot_to_id.iter().copied()
}
pub fn contains(&self, id: u64) -> bool {
self.ids().contains_key(&id)
}
pub fn len(&self) -> usize {
self.slot_to_id.len()
}
pub fn is_empty(&self) -> bool {
self.slot_to_id.is_empty()
}
#[deprecated(
since = "0.10.0",
note = "returns 0 for a lazy index, which is unsafe to do arithmetic with; use dim_opt()"
)]
pub fn dim(&self) -> usize {
self.inner.dim_opt().unwrap_or(0)
}
pub fn dim_opt(&self) -> Option<usize> {
self.inner.dim_opt()
}
pub fn bit_width(&self) -> usize {
self.inner.bit_width()
}
pub fn prepare(&self) {
self.inner.prepare();
self.ids();
}
pub fn calibrate_2d(
&mut self,
sample: &[f32],
dim: usize,
) -> Result<(), crate::CalibrateError> {
self.inner.calibrate_2d(sample, dim)
}
pub fn calibrate(&mut self, sample: &[f32]) -> Result<(), crate::CalibrateError> {
self.inner.calibrate(sample)
}
pub fn calibration_state(&self) -> crate::CalibrationState {
self.inner.calibration_state()
}
pub fn packed_ready(&self) -> bool {
self.inner.packed_ready()
}
pub fn slots_ready(&self) -> bool {
self.id_to_slot.get().is_some()
}
pub fn write(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
self.write_with_durability(path, io::Durability::Durable)
}
pub fn write_with_durability(
&self,
path: impl AsRef<Path>,
durability: io::Durability,
) -> std::io::Result<()> {
self.inner
.with_sync_source(1, Some(&self.slot_to_id), |src| {
crate::io_v7::write_snapshot(path.as_ref(), src, durability)
})?
}
pub fn load(path: impl AsRef<Path>) -> std::io::Result<Self> {
if crate::io_v7::is_v7(path.as_ref()) {
return Self::load_v7(path.as_ref());
}
Err(io::legacy_format_error(path.as_ref()))
}
pub fn sync(&mut self, path: impl AsRef<Path>) -> std::io::Result<()> {
self.inner.sync_v7_impl(path.as_ref(), 1, Some(&self.slot_to_id))
}
fn load_v7(path: &Path) -> std::io::Result<Self> {
let l = crate::io_v7::load(path, 0, 1)?;
let bind = (l.cursor.nonce != crate::io_v7::UNCLAIMED_NONCE).then_some(path);
Self::from_v7_load(l, bind)
}
pub(crate) fn from_index_and_ids(
inner: TurboQuantIndex,
slot_to_id: Vec<u64>,
) -> std::io::Result<Self> {
if slot_to_id.len() != inner.len() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("{} ids for {} rows", slot_to_id.len(), inner.len()),
));
}
let mut sorted = slot_to_id.clone();
sorted.sort_unstable();
if sorted.windows(2).any(|w| w[0] == w[1]) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"duplicate ids",
));
}
Ok(Self {
inner,
slot_to_id,
id_to_slot: std::sync::OnceLock::new(),
sorted_ids: std::sync::Mutex::new(sorted),
deferred_added: std::sync::Mutex::new(Default::default()),
})
}
fn from_v7_load(mut l: crate::io_v7::V7Load, path: Option<&Path>) -> std::io::Result<Self> {
let slot_to_id = std::mem::take(&mut l.ids);
let inner = TurboQuantIndex::from_v7(l, path)?;
let mut sorted = slot_to_id.clone();
sorted.sort_unstable();
if sorted.windows(2).any(|w| w[0] == w[1]) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"duplicate ids in v7 file",
));
}
Ok(Self {
inner,
slot_to_id,
id_to_slot: std::sync::OnceLock::new(),
sorted_ids: std::sync::Mutex::new(sorted),
deferred_added: std::sync::Mutex::new(Default::default()),
})
}
pub fn write_to_writer<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
self.inner
.with_sync_source(1, Some(&self.slot_to_id), |src| {
crate::io_v7::stream_image(w, src)
})?
}
pub fn to_bytes(&self) -> Vec<u8> {
self.inner
.v7_image(1, Some(&self.slot_to_id))
.expect("with_sync_source handles the lazy sentinel, so this cannot fail")
}
pub fn load_from_reader<R: std::io::Read>(r: &mut R) -> std::io::Result<Self> {
let mut raw = Vec::new();
std::io::Read::read_to_end(r, &mut raw)?;
Self::from_v7_load(crate::io_v7::load_image(raw, 0, 1, "the byte image")?, None)
}
pub fn from_bytes(bytes: &[u8]) -> std::io::Result<Self> {
Self::load_from_reader(&mut &bytes[..])
}
}
#[cfg(test)]
mod hasher_distribution {
use super::{IdBuildHasher, IdHasher};
use std::hash::{BuildHasher, Hasher};
fn hash(id: u64) -> u64 {
let mut h: IdHasher = IdBuildHasher::default().build_hasher();
h.write_u64(id);
h.finish()
}
fn max_bucket_load(ids: impl Iterator<Item = u64>, bits: u32) -> usize {
let mut buckets = vec![0usize; 1 << bits];
for id in ids {
buckets[(hash(id) as usize) & ((1 << bits) - 1)] += 1;
}
buckets.into_iter().max().unwrap_or(0)
}
#[test]
fn composite_ids_spread_across_buckets_at_every_table_size() {
for shift in [8u32, 16, 24, 32, 40, 48] {
let n = 8192usize;
for bits in [4u32, 8, 10, 13] {
let ideal = n as f64 / f64::from(1u32 << bits);
let limit = (ideal * 8.0).ceil() as usize + 8;
let load = max_bucket_load((0..n as u64).map(|i| i << shift), bits);
assert!(
load <= limit,
"ids of the form i << {shift} cluster: {load} of {n} share one \
bucket of {} (limit {limit}) — the hash's low bits, which are \
the bucket index, carry no entropy",
1u32 << bits,
);
}
}
}
#[test]
fn every_bucket_of_a_small_table_is_reachable_from_composite_ids() {
for shift in [32u32, 48] {
let mut seen = vec![false; 256];
for i in 0..4096u64 {
seen[(hash(i << shift) as usize) & 255] = true;
}
let reached = seen.iter().filter(|s| **s).count();
assert_eq!(
reached, 256,
"only {reached}/256 buckets reachable from `i << {shift}` ids",
);
}
}
#[test]
fn sequential_ids_stay_well_distributed() {
for bits in [4u32, 8, 10, 13] {
let n = 8192usize;
let ideal = n as f64 / f64::from(1u32 << bits);
let limit = (ideal * 8.0).ceil() as usize + 8;
let load = max_bucket_load(0..n as u64, bits);
assert!(load <= limit, "sequential ids cluster: {load} in one bucket");
}
}
const K: u64 = 0x9E37_79B9_7F4A_7C15;
const K_INV: u64 = 0xF1DE_83E1_9937_733D;
#[test]
fn mix_is_injective() {
assert_eq!(K.wrapping_mul(K_INV), 1, "K_INV is not K's inverse");
let crafted = (1..2_000u64).flat_map(|i| {
let high = i.wrapping_mul(0x9E37_79B9) | 1;
let low = i.wrapping_mul(0x0123_4567_89AB_CDEF) & 0xFFFF_FFFF;
let z1 = (high << 32) | low;
let z2 = (high << 32) | (low | high);
[z1.wrapping_mul(K_INV), z2.wrapping_mul(K_INV)]
});
let ids: Vec<u64> = (0..100_000u64)
.chain((0..20_000u64).map(|i| (i << 48) | (7 * i)))
.chain((0..20_000u64).map(|i| i << 32))
.chain(crafted)
.collect();
let distinct_in: std::collections::HashSet<u64> = ids.iter().copied().collect();
let distinct_out: std::collections::HashSet<u64> =
ids.iter().map(|&id| hash(id)).collect();
assert_eq!(
distinct_out.len(),
distinct_in.len(),
"mix collapsed {} distinct ids onto {} hashes — the finalizer \
is destroying entropy, so distinct ids share a hash outright \
rather than merely a bucket",
distinct_in.len(),
distinct_out.len(),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn loaded_index() -> IdMapIndex {
let dim = 64usize;
let mut src = IdMapIndex::new(dim, 4).unwrap();
let vectors: Vec<f32> = (0..100 * dim).map(|i| (i % 97) as f32 / 97.0).collect();
let ids: Vec<u64> = (0..100u64).map(|i| 1000 + i * 7).collect();
src.add_with_ids(&vectors, &ids).unwrap();
let mut bytes = Vec::new();
src.write_to_writer(&mut bytes).unwrap();
IdMapIndex::from_bytes(&bytes).unwrap()
}
fn sorted_len(ix: &IdMapIndex) -> usize {
ix.sorted_ids.lock().expect("sorted_ids lock").len()
}
#[test]
fn batch_addable_rejects_duplicates_and_ids_already_present() {
let dim = 64usize;
let mut ix = IdMapIndex::new(dim, 4).unwrap();
let vectors: Vec<f32> = (0..3 * dim).map(|i| (i % 41) as f32 / 41.0).collect();
ix.add_with_ids(&vectors, &[10, 20, 30]).unwrap();
assert!(
ix.batch_addable(&[1, 2, 3]),
"fresh ids, no repeats — the batch is addable"
);
assert!(ix.batch_addable(&[]), "an empty batch is vacuously addable");
assert!(
!ix.batch_addable(&[1, 2, 1]),
"a duplicate *within* the batch must be rejected, even though \
neither copy is in the index"
);
assert!(
!ix.batch_addable(&[1, 20, 3]),
"an id already in the index must be rejected, even with no \
duplicate in the batch"
);
let one: Vec<f32> = (0..3 * dim).map(|i| (i % 17) as f32 / 17.0).collect();
assert!(ix.add_with_ids(&one, &[1, 2, 3]).is_ok());
assert!(
!ix.batch_addable(&[1, 2, 3]),
"the same ids are no longer addable once they are in"
);
}
#[test]
fn batch_addable_stays_deferred_after_a_bytes_load() {
let dim = 64usize;
let mut src = IdMapIndex::new(dim, 4).unwrap();
let vectors: Vec<f32> = (0..3 * dim).map(|i| (i % 41) as f32 / 41.0).collect();
src.add_with_ids(&vectors, &[10, 20, 30]).unwrap();
let ix = IdMapIndex::from_bytes(&src.to_bytes()).unwrap();
assert!(ix.id_to_slot.get().is_none(), "precondition: map unset");
assert!(ix.batch_addable(&[1, 2, 3]));
assert!(!ix.batch_addable(&[1, 20, 3]), "load-time id detected");
assert!(!ix.batch_addable(&[1, 2, 1]), "in-batch duplicate detected");
assert!(
ix.id_to_slot.get().is_none(),
"batch_addable built the id map — the deferred window is lost"
);
let mut ix = ix;
let one: Vec<f32> = (0..dim).map(|i| (i % 17) as f32 / 17.0).collect();
ix.add_with_ids(&one, &[77]).unwrap();
assert!(!ix.batch_addable(&[77]), "deferred-added id detected");
assert!(ix.id_to_slot.get().is_none(), "still no map build");
}
#[test]
fn sorted_ids_freed_when_map_materializes() {
let dim = 64usize;
let mut ix = loaded_index();
assert_eq!(sorted_len(&ix), 100, "load should keep the sorted table");
let more: Vec<f32> = (0..dim).map(|i| (i % 31) as f32 / 31.0).collect();
ix.add_with_ids(&more, &[7]).unwrap();
assert!(ix.contains(1000), "sanity: id present");
assert_eq!(
sorted_len(&ix),
0,
"materializing the map must release the sorted table"
);
assert!(
ix.deferred_added.lock().expect("lock").is_empty(),
"materializing the map must release the deferred-add set"
);
assert!(ix.contains(7), "sanity: deferred-window id present");
}
#[test]
fn deferred_adds_reject_duplicates_without_touching_the_loaded_table() {
let dim = 64usize;
let mut ix = loaded_index();
let more: Vec<f32> = (0..10 * dim).map(|i| (i % 31) as f32 / 31.0).collect();
let new_ids: Vec<u64> = vec![1, 1003, 1500, 999_999, 2, 1004, 1600, 3, 4, 5];
ix.add_with_ids(&more, &new_ids).unwrap();
assert_eq!(
sorted_len(&ix),
100,
"the load-time table must not be rewritten by an add"
);
{
let s = ix.sorted_ids.lock().expect("lock");
assert!(s.windows(2).all(|w| w[0] <= w[1]), "table left unsorted");
}
for dup in [1000u64, 1500] {
let err = ix.add_with_ids(&more[..dim], &[dup]).unwrap_err();
assert!(matches!(err, AddError::IdAlreadyPresent(d) if d == dup));
}
assert!(
ix.id_to_slot.get().is_none(),
"adds must not force the map build"
);
let mut covered: Vec<u64> = ix.sorted_ids.lock().expect("lock").clone();
covered.extend(ix.deferred_added.lock().expect("lock").iter().copied());
covered.sort_unstable();
let mut live = ix.slot_to_id.clone();
live.sort_unstable();
assert_eq!(covered, live);
}
#[test]
fn deferred_adds_below_the_table_do_not_scale_with_n() {
const DIM: usize = 8;
const ADDS: usize = 100;
const BASE: u64 = 10_000_000;
fn add_below_table(n: usize) -> (Vec<u64>, Vec<u64>, usize, bool, u64) {
let mut src = IdMapIndex::new(DIM, 4).unwrap();
let vectors: Vec<f32> = (0..n * DIM).map(|i| (i % 251) as f32 / 251.0).collect();
let ids: Vec<u64> = (0..n as u64).map(|i| BASE + i).collect();
src.add_with_ids(&vectors, &ids).unwrap();
let mut ix = IdMapIndex::from_bytes(&src.to_bytes()).unwrap();
let before = ix.sorted_ids.lock().expect("lock").clone();
let row = vec![0.25f32; DIM];
TABLE_PROBES.with(|p| p.set(0));
for i in 0..ADDS as u64 {
ix.add_with_ids(&row, &[BASE - 1 - i]).unwrap();
}
let probes = TABLE_PROBES.with(|p| p.get());
let after = ix.sorted_ids.lock().expect("lock").clone();
let deferred = ix.deferred_added.lock().expect("lock").len();
(before, after, deferred, ix.id_to_slot.get().is_none(), probes)
}
for n in [2_000usize, 20_000] {
let (before, after, deferred, still_deferred, probes) = add_below_table(n);
assert!(still_deferred, "adds must stay deferred (n={n})");
let max_probes = ADDS as u64 * (usize::BITS - n.leading_zeros() + 2) as u64;
assert!(
probes >= ADDS as u64 && probes <= max_probes,
"presence check made {probes} comparisons for {ADDS} adds against a \
{n}-id table; a binary search makes at most {max_probes} — a linear \
scan here is O(n) per add even with the table left untouched"
);
assert_eq!(
before.len(),
n,
"sanity: the load-time table holds every loaded id (n={n})"
);
assert_eq!(
after, before,
"the load-time sorted table was rewritten by a below-the-table add \
(n={n}): it went from {} to {} entries — the O(n) per-add merge is back",
before.len(),
after.len(),
);
assert_eq!(
deferred, ADDS,
"the deferred set must grow by exactly the rows added (n={n})"
);
}
}
}
#[cfg(test)]
mod v7_matrix_id_tests {
use super::*;
use std::path::PathBuf;
const DIM: usize = 64;
fn rows(n: usize, seed: u64) -> Vec<f32> {
let mut v = vec![0.0f32; n * DIM];
let mut s = seed | 1;
for x in v.iter_mut() {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
*x = ((s >> 40) as f32 / (1u64 << 23) as f32) - 0.5;
}
for row in v.chunks_mut(DIM) {
let norm: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
for x in row.iter_mut() {
*x /= norm;
}
}
v
}
fn temp(name: &str) -> PathBuf {
let mut p = std::env::temp_dir();
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
p.push(format!("turbovec-v7idmatrix-{nonce}-{name}"));
std::fs::create_dir(&p).unwrap();
p.push("index.tvim");
p
}
#[test]
fn every_field_tamper_loads_politely_for_id_maps() {
let path = temp("matrix");
let scratch = path.with_file_name("scratch.tvim");
let mut idx = IdMapIndex::new(DIM, 4).unwrap();
idx.calibrate(&rows(1024, 81)).unwrap();
let ids: Vec<u64> = (0..70u64).map(|i| i * 17 + 3).collect();
idx.add_with_ids(&rows(70, 82), &ids).unwrap();
idx.sync(&path).unwrap();
assert!(idx.remove(3)); idx.add_with_ids(&rows(2, 83), &[9001, 9002]).unwrap();
idx.sync(&path).unwrap();
let base = std::fs::read(&path).unwrap();
let geo = crate::io_v7::Geo {
kind: 1,
dim: DIM,
bit_width: 4,
n_calib: DIM,
};
let try_load = |bytes: &[u8], what: &str| {
std::fs::write(&scratch, bytes).unwrap();
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = IdMapIndex::load(&scratch);
}));
assert!(r.is_ok(), "loader panicked on tamper: {what}");
};
let hostile: [u8; 4] = [0xFF, 0x00, 0x80, 0x01];
let mut targets: Vec<usize> = Vec::new();
let sb_end = geo.hdr_at_for_test(0);
targets.extend(0..sb_end);
for slot in [0usize, 1] {
let at = geo.hdr_at_for_test(slot);
let used = crate::io_v7::hdr_used_for_test(&base, &geo, slot) + 8;
let end = at + geo.hdr_len();
targets.extend(at..(at + used).min(end));
targets.extend(((at + used)..end).step_by(251));
}
let structural_end = geo.unit_at_for_test(0).min(base.len());
let _ = structural_end;
for &at in targets.iter().filter(|&&a| a < base.len()) {
for v in hostile {
if base[at] == v {
continue;
}
let mut bytes = base.clone();
bytes[at] = v;
crate::io_v7::reseal_for_test(&mut bytes, &geo);
try_load(&bytes, &format!("byte {at} <- {v:#04x}"));
}
}
let mut s = 0xDEAD_BEEF_1234_5678u64;
for i in 0..400 {
let mut bytes = base.clone();
for _ in 0..1 + (i % 4) {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
let at = (s as usize) % bytes.len();
bytes[at] = (s >> 32) as u8;
}
crate::io_v7::reseal_for_test(&mut bytes, &geo);
try_load(&bytes, &format!("random tamper {i}"));
}
for cut in [0usize, 4, 11, 19, structural_end / 2, structural_end] {
try_load(&base[..cut.min(base.len())], &format!("truncate {cut}"));
}
}
}
#[cfg(test)]
mod v7_crash_id_tests {
use super::*;
use std::path::PathBuf;
const DIM: usize = 64;
fn rows(n: usize, seed: u64) -> Vec<f32> {
let mut v = vec![0.0f32; n * DIM];
let mut s = seed | 1;
for x in v.iter_mut() {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
*x = ((s >> 40) as f32 / (1u64 << 23) as f32) - 0.5;
}
for row in v.chunks_mut(DIM) {
let norm: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
for x in row.iter_mut() {
*x /= norm;
}
}
v
}
fn temp(name: &str) -> PathBuf {
let mut p = std::env::temp_dir();
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
p.push(format!("turbovec-v7idcrash-{nonce}-{name}"));
std::fs::create_dir(&p).unwrap();
p.push("index.tvim");
p
}
fn apply(file: &mut Vec<u8>, off: u64, bytes: &[u8]) {
let end = off as usize + bytes.len();
if file.len() < end {
file.resize(end, 0);
}
file[off as usize..end].copy_from_slice(bytes);
}
#[test]
fn an_id_mapped_sync_torn_anywhere_restores_ids_exactly() {
let path = temp("idtorn");
let scratch = path.with_file_name("scratch.tvim");
let mut idx = IdMapIndex::new(DIM, 4).unwrap();
idx.calibrate(&rows(1024, 70)).unwrap();
let ids: Vec<u64> = (0..100u64).map(|i| i * 31 + 5).collect();
idx.add_with_ids(&rows(100, 71), &ids).unwrap();
idx.sync(&path).unwrap();
let base = std::fs::read(&path).unwrap();
let state_a = IdMapIndex::load(&path).unwrap().to_bytes();
assert!(idx.remove(5)); assert!(idx.remove(36 * 31 + 5)); idx.add_with_ids(&rows(2, 72), &[9_000_001, 9_000_002]).unwrap();
let plan = idx.inner.plan_next_sync(1, Some(&idx.slot_to_id));
assert_eq!(plan.batches.len(), 1);
let mut done = base.clone();
for b in &plan.batches {
for (off, bytes) in &b.ops {
apply(&mut done, *off, bytes);
}
}
std::fs::write(&scratch, &done).unwrap();
assert_eq!(IdMapIndex::load(&scratch).unwrap().to_bytes(), idx.to_bytes());
for bi in 0..plan.batches.len() {
let ops = &plan.batches[bi].ops;
for (oj, (off, bytes)) in ops.iter().enumerate() {
for cut in [0, bytes.len() / 3, bytes.len() - 1] {
let mut torn = base.clone();
for prev in &plan.batches[..bi] {
for (o, b) in &prev.ops {
apply(&mut torn, *o, b);
}
}
for (o, b) in &ops[..oj] {
apply(&mut torn, *o, b);
}
apply(&mut torn, *off, &bytes[..cut]);
std::fs::write(&scratch, &torn).unwrap();
let got = IdMapIndex::load(&scratch)
.unwrap_or_else(|e| panic!("batch {bi} op {oj} cut {cut}: {e}"))
.to_bytes();
assert_eq!(got, state_a, "batch {bi} op {oj} cut {cut}");
}
}
}
}
}