use std::cell::UnsafeCell;
use std::fs::File;
use std::io;
use std::path::Path;
use memmap2::{Mmap, MmapMut};
const ENTRY_SIZE: u64 = 8; const GROW_INCREMENT: u64 = 1_073_741_824; const MAX_FLAT_INDEX_SIZE: u64 = 16 * 1024 * 1024 * 1024;
#[allow(clippy::cast_possible_wrap)]
const COORD_XOR: i32 = 0x5555_5555_u32 as i32;
pub struct NodeIndex {
file: File,
mmap: MmapMut,
file_len: u64,
max_file_size: Option<u64>,
}
impl NodeIndex {
pub fn create(path: &Path) -> io::Result<Self> {
Self::create_internal(path, Some(MAX_FLAT_INDEX_SIZE))
}
pub fn create_unbounded(path: &Path) -> io::Result<Self> {
Self::create_internal(path, None)
}
fn create_internal(path: &Path, max_file_size: Option<u64>) -> io::Result<Self> {
let file = File::options()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)?;
let file_len = GROW_INCREMENT;
file.set_len(file_len)?;
let mmap = unsafe { MmapMut::map_mut(&file)? };
Ok(NodeIndex {
file,
mmap,
file_len,
max_file_size,
})
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
pub fn put(&mut self, node_id: i64, lat_e7: i32, lon_e7: i32) {
if node_id < 0 {
return;
}
let offset = node_id as u64 * ENTRY_SIZE;
let needed = offset + ENTRY_SIZE;
if let Some(max_file_size) = self.max_file_size
&& needed > max_file_size
{
panic!(
"flat node index safety cap exceeded: requested {:.1} GB for node_id={} (cap: {:.1} GB). \
Input is likely unsorted. Use a sorted PBF, run `pbfhogg sort input.pbf -o sorted.pbf`, \
or use --force-sorted only if the PBF is actually sorted.",
needed as f64 / (1024.0 * 1024.0 * 1024.0),
node_id,
max_file_size as f64 / (1024.0 * 1024.0 * 1024.0),
);
}
if needed > self.file_len {
let mut new_len = self.file_len;
while new_len < needed {
new_len += GROW_INCREMENT;
}
self.file
.set_len(new_len)
.expect("failed to grow node index file");
self.mmap =
unsafe { MmapMut::map_mut(&self.file).expect("failed to remap node index") };
self.file_len = new_len;
}
let off = offset as usize;
self.mmap[off..off + 4].copy_from_slice(&(lat_e7 ^ COORD_XOR).to_le_bytes());
self.mmap[off + 4..off + 8].copy_from_slice(&(lon_e7 ^ COORD_XOR).to_le_bytes());
}
#[allow(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::unwrap_used
)]
pub fn get(&self, node_id: i64) -> Option<(i32, i32)> {
get_from_mmap(&self.mmap, self.file_len, node_id)
}
pub fn into_reader(self) -> io::Result<NodeIndexReader> {
let file_len = self.file_len;
let mmap = self.mmap.make_read_only()?;
Ok(NodeIndexReader { mmap, file_len })
}
}
#[allow(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::unwrap_used
)]
fn get_from_mmap(mmap: &[u8], file_len: u64, node_id: i64) -> Option<(i32, i32)> {
if node_id < 0 {
return None;
}
let offset = node_id as u64 * ENTRY_SIZE;
let needed = offset + ENTRY_SIZE;
if needed > file_len {
return None;
}
let off = offset as usize;
let lat_raw = i32::from_le_bytes(mmap[off..off + 4].try_into().unwrap());
let lon_raw = i32::from_le_bytes(mmap[off + 4..off + 8].try_into().unwrap());
if lat_raw == 0 && lon_raw == 0 {
None
} else {
Some((lat_raw ^ COORD_XOR, lon_raw ^ COORD_XOR))
}
}
pub struct NodeIndexReader {
mmap: Mmap,
file_len: u64,
}
impl NodeIndexReader {
pub fn get(&self, node_id: i64) -> Option<(i32, i32)> {
get_from_mmap(&self.mmap, self.file_len, node_id)
}
}
const NODES_PER_CHUNK: usize = 256;
const NODES_PER_GROUP: u64 = 256 * 256; const BITMASK_BYTES: usize = 32;
#[inline]
fn set_bit(mask: &mut [u8; BITMASK_BYTES], pos: u8) {
mask[pos as usize / 8] |= 1 << (pos % 8);
}
#[inline]
fn test_bit(mask: &[u8; BITMASK_BYTES], pos: u8) -> bool {
mask[pos as usize / 8] & (1 << (pos % 8)) != 0
}
#[inline]
fn count_bits_before(mask: &[u8; BITMASK_BYTES], pos: u8) -> usize {
let byte_idx = pos as usize / 8;
let bit_idx = pos % 8;
let full: usize = mask[..byte_idx]
.iter()
.map(|b| b.count_ones() as usize)
.sum();
let partial = (mask[byte_idx] & ((1u8 << bit_idx) - 1)).count_ones() as usize;
full + partial
}
#[inline]
#[allow(clippy::cast_possible_truncation)]
fn count_set_bits(mask: &[u8; BITMASK_BYTES]) -> u16 {
mask.iter().map(|b| u16::from(b.count_ones() as u8)).sum()
}
#[allow(clippy::cast_possible_truncation)]
fn bitpack_values_into(values: &[u32], bit_width: u8, dest: &mut Vec<u8>) {
if bit_width == 0 {
return;
}
let bw = u32::from(bit_width);
let mask = if bw >= 32 { u64::MAX } else { (1u64 << bw) - 1 };
let mut accumulator: u64 = 0;
let mut bits_in_acc: u32 = 0;
for &v in values {
accumulator |= (u64::from(v) & mask) << bits_in_acc;
bits_in_acc += bw;
while bits_in_acc >= 8 {
dest.push(accumulator as u8);
accumulator >>= 8;
bits_in_acc -= 8;
}
}
if bits_in_acc > 0 {
dest.push(accumulator as u8);
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::unwrap_used,
clippy::needless_range_loop
)]
fn bitunpack_values(packed: &[u8], n: usize, bit_width: u8, out: &mut [u32]) {
if bit_width == 0 {
for o in &mut out[..n] {
*o = 0;
}
return;
}
let bw = u32::from(bit_width);
let mask = if bw >= 32 { u64::MAX } else { (1u64 << bw) - 1 };
let mut bit_offset: usize = 0;
for i in 0..n {
let byte_idx = bit_offset / 8;
let bit_idx = bit_offset % 8;
let raw = if byte_idx + 8 <= packed.len() {
u64::from_le_bytes(packed[byte_idx..byte_idx + 8].try_into().unwrap())
} else {
let mut r: u64 = 0;
for b in byte_idx..packed.len() {
r |= u64::from(packed[b]) << ((b - byte_idx) * 8);
}
r
};
out[i] = ((raw >> bit_idx) & mask) as u32;
bit_offset += bw as usize;
}
}
fn required_bits(max_val: u32) -> u8 {
if max_val == 0 {
return 0;
}
#[allow(clippy::cast_possible_truncation)]
{
(32 - max_val.leading_zeros()) as u8
}
}
const CHUNK_HEADER_SIZE: usize = BITMASK_BYTES + 2;
#[inline]
#[allow(clippy::cast_possible_truncation)]
fn encode_flags_and_len(compressed: bool, packed_len: usize) -> u16 {
debug_assert!(
packed_len <= 0x7FFF,
"packed_len {packed_len} exceeds 15-bit limit"
);
let flags = if compressed { 0x8000u16 } else { 0u16 };
flags | packed_len as u16
}
struct Group {
chunk_mask: [u8; BITMASK_BYTES],
data: Box<[u8]>, }
#[hotpath::measure]
#[allow(clippy::cast_possible_truncation)]
fn compress_coords_into(
lats: &[i32],
lons: &[i32],
lat_offsets: &mut Vec<u32>,
lon_offsets: &mut Vec<u32>,
dest: &mut Vec<u8>,
) {
let n = lats.len();
let lat_min = lats.iter().copied().min().unwrap_or(0);
let lon_min = lons.iter().copied().min().unwrap_or(0);
#[allow(clippy::cast_sign_loss)]
let lat_max_offset = lats
.iter()
.map(|&v| (v - lat_min) as u32)
.max()
.unwrap_or(0);
#[allow(clippy::cast_sign_loss)]
let lon_max_offset = lons
.iter()
.map(|&v| (v - lon_min) as u32)
.max()
.unwrap_or(0);
let lat_bits = required_bits(lat_max_offset);
let lon_bits = required_bits(lon_max_offset);
dest.extend_from_slice(&lat_min.to_le_bytes());
dest.extend_from_slice(&lon_min.to_le_bytes());
dest.push(lat_bits);
dest.push(lon_bits);
lat_offsets.clear();
lon_offsets.clear();
#[allow(clippy::cast_sign_loss)]
for i in 0..n {
lat_offsets.push((lats[i] - lat_min) as u32);
lon_offsets.push((lons[i] - lon_min) as u32);
}
bitpack_values_into(lat_offsets, lat_bits, dest);
bitpack_values_into(lon_offsets, lon_bits, dest);
}
#[hotpath::measure]
#[allow(clippy::unwrap_used, clippy::needless_range_loop)]
fn decompress_chunk(
node_mask: &[u8; BITMASK_BYTES],
compressed: bool,
packed: &[u8],
out: &mut [(i32, i32)],
) {
let n = count_set_bits(node_mask) as usize;
if !compressed {
for i in 0..n {
let off = i * 8;
let lat = i32::from_le_bytes(packed[off..off + 4].try_into().unwrap());
let lon = i32::from_le_bytes(packed[off + 4..off + 8].try_into().unwrap());
out[i] = (lat, lon);
}
return;
}
let lat_min = i32::from_le_bytes(packed[0..4].try_into().unwrap());
let lon_min = i32::from_le_bytes(packed[4..8].try_into().unwrap());
let lat_bits = packed[8];
let lon_bits = packed[9];
let lat_packed_bytes = (n * lat_bits as usize).div_ceil(8);
let lat_packed = &packed[10..10 + lat_packed_bytes];
let mut lat_buf = [0u32; NODES_PER_CHUNK];
bitunpack_values(lat_packed, n, lat_bits, &mut lat_buf);
let lon_packed = &packed[10 + lat_packed_bytes..];
let mut lon_buf = [0u32; NODES_PER_CHUNK];
bitunpack_values(lon_packed, n, lon_bits, &mut lon_buf);
for i in 0..n {
#[allow(clippy::cast_possible_wrap)]
{
out[i] = (lat_min + lat_buf[i] as i32, lon_min + lon_buf[i] as i32);
}
}
}
struct ChunkRef<'a> {
node_mask: &'a [u8; BITMASK_BYTES],
compressed: bool,
packed: &'a [u8],
}
#[hotpath::measure]
#[allow(clippy::unwrap_used)]
fn find_chunk_in_blob(data: &[u8], chunk_idx: usize) -> ChunkRef<'_> {
let mut offset = 0;
for _ in 0..chunk_idx {
let fl = u16::from_le_bytes([
data[offset + BITMASK_BYTES],
data[offset + BITMASK_BYTES + 1],
]);
let packed_len = (fl & 0x7FFF) as usize;
offset += CHUNK_HEADER_SIZE + packed_len;
}
let node_mask: &[u8; BITMASK_BYTES] = data[offset..offset + BITMASK_BYTES].try_into().unwrap();
let fl = u16::from_le_bytes([
data[offset + BITMASK_BYTES],
data[offset + BITMASK_BYTES + 1],
]);
let compressed = fl & 0x8000 != 0;
let packed_len = (fl & 0x7FFF) as usize;
let packed_start = offset + CHUNK_HEADER_SIZE;
ChunkRef {
node_mask,
compressed,
packed: &data[packed_start..packed_start + packed_len],
}
}
struct CacheEntry {
group_id: usize,
chunk_idx: usize,
node_mask: [u8; BITMASK_BYTES],
coords: [(i32, i32); NODES_PER_CHUNK],
count: u16, }
impl CacheEntry {
fn new() -> Self {
CacheEntry {
group_id: usize::MAX,
chunk_idx: usize::MAX,
node_mask: [0u8; BITMASK_BYTES],
coords: [(0, 0); NODES_PER_CHUNK],
count: 0,
}
}
}
const CACHE_ENTRIES: usize = 8;
struct DecompressCache {
entries: [CacheEntry; CACHE_ENTRIES],
}
impl DecompressCache {
fn new() -> Self {
DecompressCache {
entries: [
CacheEntry::new(),
CacheEntry::new(),
CacheEntry::new(),
CacheEntry::new(),
CacheEntry::new(),
CacheEntry::new(),
CacheEntry::new(),
CacheEntry::new(),
],
}
}
}
thread_local! {
static DECOMPRESS_CACHE: UnsafeCell<DecompressCache> = UnsafeCell::new(DecompressCache::new());
}
#[inline]
fn get_from_group_cached(
group: &Group,
group_id: usize,
chunk_id: u8,
node_in_chunk: u8,
) -> Option<(i32, i32)> {
if !test_bit(&group.chunk_mask, chunk_id) {
return None;
}
let chunk_idx = count_bits_before(&group.chunk_mask, chunk_id);
DECOMPRESS_CACHE.with(|cell| {
let cache = unsafe { &mut *cell.get() };
for i in 0..CACHE_ENTRIES {
let entry = &cache.entries[i];
if entry.group_id == group_id && entry.chunk_idx == chunk_idx && entry.count != 0 {
if !test_bit(&entry.node_mask, node_in_chunk) {
return None;
}
let node_idx = count_bits_before(&entry.node_mask, node_in_chunk);
let result = entry.coords[node_idx];
if i > 0 {
cache.entries.swap(0, i);
}
return Some(result);
}
}
let chunk = find_chunk_in_blob(&group.data, chunk_idx);
if !test_bit(chunk.node_mask, node_in_chunk) {
return None;
}
let node_idx = count_bits_before(chunk.node_mask, node_in_chunk);
cache.entries.swap(0, CACHE_ENTRIES - 1);
let entry = &mut cache.entries[0];
decompress_chunk(
chunk.node_mask,
chunk.compressed,
chunk.packed,
&mut entry.coords,
);
entry.node_mask = *chunk.node_mask;
entry.group_id = group_id;
entry.chunk_idx = chunk_idx;
entry.count = count_set_bits(chunk.node_mask);
Some(entry.coords[node_idx])
})
}
#[inline]
fn get_from_blob_chunk(data: &[u8], chunk_idx: usize, node_in_chunk: u8) -> Option<(i32, i32)> {
let chunk = find_chunk_in_blob(data, chunk_idx);
if !test_bit(chunk.node_mask, node_in_chunk) {
return None;
}
let node_idx = count_bits_before(chunk.node_mask, node_in_chunk);
let mut coords = [(0i32, 0i32); NODES_PER_CHUNK];
decompress_chunk(chunk.node_mask, chunk.compressed, chunk.packed, &mut coords);
Some(coords[node_idx])
}
#[inline]
fn get_from_group(group: &Group, chunk_id: u8, node_in_chunk: u8) -> Option<(i32, i32)> {
if !test_bit(&group.chunk_mask, chunk_id) {
return None;
}
let chunk_idx = count_bits_before(&group.chunk_mask, chunk_id);
get_from_blob_chunk(&group.data, chunk_idx, node_in_chunk)
}
pub struct SortedNodeStore {
groups: Vec<Option<Box<Group>>>,
current_group_id: u64,
current_chunk_mask: [u8; BITMASK_BYTES],
current_group_data: Vec<u8>, compress_buf: Vec<u8>, scratch_lats: Vec<i32>, scratch_lons: Vec<i32>, scratch_lat_offsets: Vec<u32>, scratch_lon_offsets: Vec<u32>,
current_chunk_id: u8,
current_node_mask: [u8; BITMASK_BYTES],
current_coords: Vec<(i32, i32)>,
last_node_id: i64,
node_count: u64,
}
impl Default for SortedNodeStore {
fn default() -> Self {
Self::new()
}
}
impl SortedNodeStore {
pub fn new() -> Self {
SortedNodeStore {
groups: Vec::new(),
current_group_id: 0,
current_chunk_mask: [0u8; BITMASK_BYTES],
current_group_data: Vec::new(),
compress_buf: Vec::new(),
scratch_lats: Vec::with_capacity(NODES_PER_CHUNK),
scratch_lons: Vec::with_capacity(NODES_PER_CHUNK),
scratch_lat_offsets: Vec::with_capacity(NODES_PER_CHUNK),
scratch_lon_offsets: Vec::with_capacity(NODES_PER_CHUNK),
current_chunk_id: 0,
current_node_mask: [0u8; BITMASK_BYTES],
current_coords: Vec::with_capacity(NODES_PER_CHUNK),
last_node_id: -1,
node_count: 0,
}
}
#[hotpath::measure]
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
pub fn put(&mut self, node_id: i64, lat_e7: i32, lon_e7: i32) {
assert!(
node_id > self.last_node_id,
"SortedNodeStore: node IDs must be strictly increasing, got {node_id} after {}",
self.last_node_id
);
self.last_node_id = node_id;
self.node_count += 1;
let id = node_id as u64;
let group_id = id / NODES_PER_GROUP;
let chunk_id = ((id % NODES_PER_GROUP) / NODES_PER_CHUNK as u64) as u8;
let node_in_chunk = (id % NODES_PER_CHUNK as u64) as u8;
if self.node_count == 1 {
if self.groups.len() <= group_id as usize {
self.groups.resize_with(group_id as usize + 1, || None);
}
self.current_group_id = group_id;
self.current_chunk_id = chunk_id;
} else if group_id != self.current_group_id {
self.flush_chunk();
self.flush_group();
if self.groups.len() <= group_id as usize {
self.groups.resize_with(group_id as usize + 1, || None);
}
self.current_group_id = group_id;
self.current_chunk_mask = [0u8; BITMASK_BYTES];
self.current_chunk_id = chunk_id;
self.current_node_mask = [0u8; BITMASK_BYTES];
self.current_coords.clear();
} else if chunk_id != self.current_chunk_id {
self.flush_chunk();
self.current_chunk_id = chunk_id;
self.current_node_mask = [0u8; BITMASK_BYTES];
self.current_coords.clear();
}
set_bit(&mut self.current_node_mask, node_in_chunk);
self.current_coords.push((lat_e7, lon_e7));
}
#[hotpath::measure]
fn flush_chunk(&mut self) {
if self.current_coords.is_empty() {
return;
}
set_bit(&mut self.current_chunk_mask, self.current_chunk_id);
let n = self.current_coords.len();
let raw_size = n * 8;
self.scratch_lats.clear();
self.scratch_lons.clear();
for &(lat, lon) in &self.current_coords {
self.scratch_lats.push(lat);
self.scratch_lons.push(lon);
}
self.compress_buf.clear();
compress_coords_into(
&self.scratch_lats,
&self.scratch_lons,
&mut self.scratch_lat_offsets,
&mut self.scratch_lon_offsets,
&mut self.compress_buf,
);
let compressed = self.compress_buf.len() < raw_size;
self.current_group_data
.extend_from_slice(&self.current_node_mask);
if compressed {
let fl = encode_flags_and_len(true, self.compress_buf.len());
self.current_group_data.extend_from_slice(&fl.to_le_bytes());
self.current_group_data
.extend_from_slice(&self.compress_buf);
} else {
let fl = encode_flags_and_len(false, raw_size);
self.current_group_data.extend_from_slice(&fl.to_le_bytes());
for &(lat, lon) in &self.current_coords {
self.current_group_data
.extend_from_slice(&lat.to_le_bytes());
self.current_group_data
.extend_from_slice(&lon.to_le_bytes());
}
}
self.current_coords.clear();
}
fn flush_group(&mut self) {
if self.current_group_data.is_empty() {
return;
}
let data = self.current_group_data.clone().into_boxed_slice();
self.current_group_data.clear();
let group = Group {
chunk_mask: self.current_chunk_mask,
data,
};
#[allow(clippy::cast_possible_truncation)]
let gid = self.current_group_id as usize;
self.groups[gid] = Some(Box::new(group));
}
#[allow(clippy::cast_possible_truncation, clippy::unwrap_used)]
pub fn into_reader(mut self) -> SortedNodeStoreReader {
self.flush_chunk();
self.flush_group();
let node_count = self.node_count;
let group_count = self.groups.len();
SortedNodeStoreReader {
groups: self.groups,
node_count,
group_count,
}
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
pub fn get(&self, node_id: i64) -> Option<(i32, i32)> {
if node_id < 0 {
return None;
}
let id = node_id as u64;
let group_id = id / NODES_PER_GROUP;
let chunk_id = ((id % NODES_PER_GROUP) / NODES_PER_CHUNK as u64) as u8;
let node_in_chunk = (id % NODES_PER_CHUNK as u64) as u8;
if self.node_count > 0 && group_id == self.current_group_id {
if chunk_id == self.current_chunk_id {
if !test_bit(&self.current_node_mask, node_in_chunk) {
return None;
}
let idx = count_bits_before(&self.current_node_mask, node_in_chunk);
return Some(self.current_coords[idx]);
}
if test_bit(&self.current_chunk_mask, chunk_id) {
let chunk_idx = count_bits_before(&self.current_chunk_mask, chunk_id);
return get_from_blob_chunk(&self.current_group_data, chunk_idx, node_in_chunk);
}
return None;
}
let group = self.groups.get(group_id as usize)?.as_ref()?;
get_from_group(group, chunk_id, node_in_chunk)
}
pub fn node_count(&self) -> u64 {
self.node_count
}
}
pub struct SortedNodeStoreReader {
groups: Vec<Option<Box<Group>>>,
node_count: u64,
group_count: usize,
}
impl SortedNodeStoreReader {
pub fn node_count(&self) -> u64 {
self.node_count
}
pub fn group_count(&self) -> usize {
self.group_count
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
pub fn get(&self, node_id: i64) -> Option<(i32, i32)> {
if node_id < 0 {
return None;
}
let id = node_id as u64;
let group_id = (id / NODES_PER_GROUP) as usize;
let chunk_id = ((id % NODES_PER_GROUP) / NODES_PER_CHUNK as u64) as u8;
let node_in_chunk = (id % NODES_PER_CHUNK as u64) as u8;
let group = self.groups.get(group_id)?.as_ref()?;
get_from_group_cached(group, group_id, chunk_id, node_in_chunk)
}
}
#[allow(clippy::large_enum_variant)]
pub enum NodeStore {
Flat(NodeIndex),
Sorted(SortedNodeStore),
}
impl NodeStore {
pub fn put(&mut self, node_id: i64, lat_e7: i32, lon_e7: i32) {
match self {
NodeStore::Flat(idx) => idx.put(node_id, lat_e7, lon_e7),
NodeStore::Sorted(store) => store.put(node_id, lat_e7, lon_e7),
}
}
pub fn into_reader(self) -> io::Result<NodeStoreReader> {
match self {
NodeStore::Flat(idx) => Ok(NodeStoreReader::Flat(idx.into_reader()?)),
NodeStore::Sorted(store) => Ok(NodeStoreReader::Sorted(store.into_reader())),
}
}
}
pub enum NodeStoreReader {
Flat(NodeIndexReader),
Sorted(SortedNodeStoreReader),
}
impl NodeStoreReader {
pub fn get(&self, node_id: i64) -> Option<(i32, i32)> {
match self {
NodeStoreReader::Flat(reader) => reader.get(node_id),
NodeStoreReader::Sorted(reader) => reader.get(node_id),
}
}
pub fn sorted_stats(&self) -> Option<(u64, usize)> {
match self {
NodeStoreReader::Flat(_) => None,
NodeStoreReader::Sorted(r) => Some((r.node_count(), r.group_count())),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn create_and_get_empty() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("node_index_test.bin");
let idx = NodeIndex::create(&path).unwrap();
assert_eq!(idx.get(1), None);
}
#[test]
fn put_and_get() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("node_index_test.bin");
let mut idx = NodeIndex::create(&path).unwrap();
idx.put(100, 555_000_000, 133_000_000);
assert_eq!(idx.get(100), Some((555_000_000, 133_000_000)));
}
#[test]
fn put_multiple_get_each() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("node_index_test.bin");
let mut idx = NodeIndex::create(&path).unwrap();
idx.put(10, 100_000, 200_000);
idx.put(20, 300_000, 400_000);
idx.put(30, 500_000, 600_000);
assert_eq!(idx.get(10), Some((100_000, 200_000)));
assert_eq!(idx.get(20), Some((300_000, 400_000)));
assert_eq!(idx.get(30), Some((500_000, 600_000)));
}
#[test]
fn get_nonexistent() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("node_index_test.bin");
let mut idx = NodeIndex::create(&path).unwrap();
idx.put(5, 111, 222);
assert_eq!(idx.get(999), None);
}
#[test]
fn zero_zero_is_valid() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("node_index_test.bin");
let mut idx = NodeIndex::create(&path).unwrap();
idx.put(1, 0, 0);
assert_eq!(idx.get(1), Some((0, 0)));
}
#[test]
fn overwrite_node() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("node_index_test.bin");
let mut idx = NodeIndex::create(&path).unwrap();
idx.put(42, 111_000, 222_000);
idx.put(42, 333_000, 444_000);
assert_eq!(idx.get(42), Some((333_000, 444_000)));
}
#[test]
fn into_reader() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("node_index_test.bin");
let mut idx = NodeIndex::create(&path).unwrap();
idx.put(10, 100_000, 200_000);
idx.put(20, 300_000, 400_000);
let reader = idx.into_reader().unwrap();
assert_eq!(reader.get(10), Some((100_000, 200_000)));
assert_eq!(reader.get(20), Some((300_000, 400_000)));
assert_eq!(reader.get(999), None);
}
#[test]
fn bitmask_set_and_test() {
let mut mask = [0u8; BITMASK_BYTES];
assert!(!test_bit(&mask, 0));
set_bit(&mut mask, 0);
assert!(test_bit(&mask, 0));
assert!(!test_bit(&mask, 1));
set_bit(&mut mask, 255);
assert!(test_bit(&mask, 255));
assert!(!test_bit(&mask, 254));
}
#[test]
fn bitmask_count_before() {
let mut mask = [0u8; BITMASK_BYTES];
set_bit(&mut mask, 3);
set_bit(&mut mask, 7);
set_bit(&mut mask, 10);
assert_eq!(count_bits_before(&mask, 3), 0);
assert_eq!(count_bits_before(&mask, 7), 1);
assert_eq!(count_bits_before(&mask, 10), 2);
assert_eq!(count_bits_before(&mask, 200), 3);
}
#[test]
fn bitmask_count_before_dense() {
let mask = [0xFF_u8; BITMASK_BYTES];
assert_eq!(count_bits_before(&mask, 0), 0);
assert_eq!(count_bits_before(&mask, 128), 128);
assert_eq!(count_bits_before(&mask, 255), 255);
}
#[test]
fn sorted_create_and_get_empty() {
let store = SortedNodeStore::new();
assert_eq!(store.get(1), None);
}
#[test]
fn sorted_put_and_get() {
let mut store = SortedNodeStore::new();
store.put(100, 555_000_000, 133_000_000);
assert_eq!(store.get(100), Some((555_000_000, 133_000_000)));
}
#[test]
fn sorted_put_multiple_get_each() {
let mut store = SortedNodeStore::new();
store.put(10, 100_000, 200_000);
store.put(20, 300_000, 400_000);
store.put(30, 500_000, 600_000);
assert_eq!(store.get(10), Some((100_000, 200_000)));
assert_eq!(store.get(20), Some((300_000, 400_000)));
assert_eq!(store.get(30), Some((500_000, 600_000)));
}
#[test]
fn sorted_get_nonexistent() {
let mut store = SortedNodeStore::new();
store.put(5, 111, 222);
assert_eq!(store.get(999), None);
}
#[test]
fn sorted_zero_zero_is_valid() {
let mut store = SortedNodeStore::new();
store.put(1, 0, 0);
assert_eq!(store.get(1), Some((0, 0)));
}
#[test]
fn sorted_into_reader() {
let mut store = SortedNodeStore::new();
store.put(10, 100_000, 200_000);
store.put(20, 300_000, 400_000);
let reader = store.into_reader();
assert_eq!(reader.get(10), Some((100_000, 200_000)));
assert_eq!(reader.get(20), Some((300_000, 400_000)));
assert_eq!(reader.get(999), None);
}
#[test]
fn sorted_cross_chunk_boundary() {
let mut store = SortedNodeStore::new();
store.put(255, 10, 20);
store.put(256, 30, 40);
assert_eq!(store.get(255), Some((10, 20)));
assert_eq!(store.get(256), Some((30, 40)));
}
#[test]
fn sorted_cross_group_boundary() {
let mut store = SortedNodeStore::new();
store.put(65535, 10, 20);
store.put(65536, 30, 40);
let reader = store.into_reader();
assert_eq!(reader.get(65535), Some((10, 20)));
assert_eq!(reader.get(65536), Some((30, 40)));
}
#[test]
fn sorted_large_gap_in_ids() {
let mut store = SortedNodeStore::new();
store.put(1_000_000, 10, 20);
store.put(10_000_000, 30, 40);
let reader = store.into_reader();
assert_eq!(reader.get(1_000_000), Some((10, 20)));
assert_eq!(reader.get(10_000_000), Some((30, 40)));
assert_eq!(reader.get(5_000_000), None);
}
#[test]
fn sorted_single_node() {
let mut store = SortedNodeStore::new();
store.put(42, 100, 200);
let reader = store.into_reader();
assert_eq!(reader.get(42), Some((100, 200)));
assert_eq!(reader.get(41), None);
assert_eq!(reader.get(43), None);
}
#[test]
fn sorted_dense_chunk() {
let mut store = SortedNodeStore::new();
for i in 0..256i64 {
#[allow(clippy::cast_possible_truncation)]
store.put(i, i as i32 * 100, i as i32 * 200);
}
let reader = store.into_reader();
for i in 0..256i64 {
#[allow(clippy::cast_possible_truncation)]
{
assert_eq!(reader.get(i), Some((i as i32 * 100, i as i32 * 200)));
}
}
}
#[test]
fn sorted_empty_into_reader() {
let store = SortedNodeStore::new();
let reader = store.into_reader();
assert_eq!(reader.get(0), None);
assert_eq!(reader.get(1_000_000), None);
}
#[test]
fn sorted_high_node_id() {
let mut store = SortedNodeStore::new();
store.put(12_000_000_000, 550_000_000, 130_000_000);
let reader = store.into_reader();
assert_eq!(reader.get(12_000_000_000), Some((550_000_000, 130_000_000)));
}
#[test]
#[should_panic(expected = "strictly increasing")]
fn sorted_rejects_non_monotonic() {
let mut store = SortedNodeStore::new();
store.put(10, 100, 200);
store.put(5, 300, 400);
}
#[test]
#[should_panic(expected = "strictly increasing")]
fn sorted_rejects_duplicate_id() {
let mut store = SortedNodeStore::new();
store.put(10, 100, 200);
store.put(10, 300, 400);
}
#[test]
fn node_store_sorted_variant() {
let mut store = NodeStore::Sorted(SortedNodeStore::new());
store.put(100, 555_000_000, 133_000_000);
let reader = store.into_reader().unwrap();
assert_eq!(reader.get(100), Some((555_000_000, 133_000_000)));
assert_eq!(reader.get(999), None);
}
#[test]
fn node_store_flat_variant() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("node_index_test.bin");
let mut store = NodeStore::Flat(NodeIndex::create(&path).unwrap());
store.put(100, 555_000_000, 133_000_000);
let reader = store.into_reader().unwrap();
assert_eq!(reader.get(100), Some((555_000_000, 133_000_000)));
assert_eq!(reader.get(999), None);
}
#[test]
#[should_panic(expected = "flat node index safety cap exceeded")]
fn flat_index_safety_cap_triggers() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("node_index_cap.bin");
let mut idx = NodeIndex::create(&path).unwrap();
#[allow(clippy::cast_possible_wrap)]
let node_id = ((MAX_FLAT_INDEX_SIZE / ENTRY_SIZE) + 1) as i64;
idx.put(node_id, 1, 1);
}
}