use std::collections::HashMap;
use std::io;
use std::path::Path;
use roaring::RoaringBitmap;
use xxhash_rust::xxh64::xxh64;
use crate::path::PathIndex;
use crate::path_util::{path_bytes, path_from_bytes};
use crate::posting::roaring_util;
pub(crate) const PATHS_IDX_FILENAME: &str = "paths.idx";
const MAGIC: &[u8; 4] = b"STPI";
pub(crate) const FORMAT_VERSION: u32 = 1;
const HEADER_LEN: usize = 4 + 4 + 8;
pub(crate) const MAX_SIDECAR_SIZE: u64 = 8 * 1024 * 1024 * 1024;
const MAX_PREALLOC_ENTRIES: usize = 1 << 20;
#[derive(Debug)]
pub(crate) enum SidecarError {
Io(io::Error),
TooLarge(u64),
TooShort,
BadMagic,
UnsupportedVersion(u32),
ChecksumMismatch,
Truncated,
Bitmap(String),
}
impl std::fmt::Display for SidecarError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SidecarError::Io(e) => write!(f, "I/O error reading paths.idx: {e}"),
SidecarError::TooLarge(n) => write!(
f,
"paths.idx is {n} bytes, exceeds {MAX_SIDECAR_SIZE}-byte safety cap"
),
SidecarError::TooShort => write!(f, "paths.idx is shorter than its fixed header"),
SidecarError::BadMagic => write!(f, "paths.idx has an invalid magic number"),
SidecarError::UnsupportedVersion(v) => write!(
f,
"paths.idx format version {v} is not supported (expected {FORMAT_VERSION})"
),
SidecarError::ChecksumMismatch => {
write!(f, "paths.idx checksum does not match its contents")
}
SidecarError::Truncated => {
write!(
f,
"paths.idx body ends before all recorded entries were read"
)
}
SidecarError::Bitmap(e) => write!(f, "paths.idx has a corrupt roaring bitmap: {e}"),
}
}
}
pub(crate) fn read_paths_idx(dir: &Path) -> Result<PathIndex, SidecarError> {
let path = dir.join(PATHS_IDX_FILENAME);
let meta = std::fs::metadata(&path).map_err(SidecarError::Io)?;
if meta.len() > MAX_SIDECAR_SIZE {
return Err(SidecarError::TooLarge(meta.len()));
}
let bytes = std::fs::read(&path).map_err(SidecarError::Io)?;
decode(&bytes)
}
fn decode(bytes: &[u8]) -> Result<PathIndex, SidecarError> {
if bytes.len() < HEADER_LEN {
return Err(SidecarError::TooShort);
}
if &bytes[0..4] != MAGIC {
return Err(SidecarError::BadMagic);
}
let version = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
if version != FORMAT_VERSION {
return Err(SidecarError::UnsupportedVersion(version));
}
let checksum = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
let body = &bytes[HEADER_LEN..];
if xxh64(body, 0) != checksum {
return Err(SidecarError::ChecksumMismatch);
}
let mut pos = 0usize;
let path_count = read_u32(body, &mut pos)? as usize;
let mut paths = Vec::with_capacity(path_count.min(MAX_PREALLOC_ENTRIES));
for _ in 0..path_count {
let len = read_u32(body, &mut pos)? as usize;
let slice = read_bytes(body, &mut pos, len)?;
paths.push(path_from_bytes(slice));
}
let extension_to_files = read_bitmap_table(body, &mut pos)?;
let component_to_files = read_bitmap_table(body, &mut pos)?;
if pos != body.len() {
return Err(SidecarError::Truncated);
}
Ok(crate::path::from_sidecar_parts(
paths,
extension_to_files,
component_to_files,
))
}
fn read_u32(body: &[u8], pos: &mut usize) -> Result<u32, SidecarError> {
let bytes = body.get(*pos..*pos + 4).ok_or(SidecarError::Truncated)?;
*pos += 4;
Ok(u32::from_le_bytes(bytes.try_into().unwrap()))
}
fn read_bytes<'a>(body: &'a [u8], pos: &mut usize, len: usize) -> Result<&'a [u8], SidecarError> {
let end = pos.checked_add(len).ok_or(SidecarError::Truncated)?;
let slice = body.get(*pos..end).ok_or(SidecarError::Truncated)?;
*pos = end;
Ok(slice)
}
fn read_bitmap_table(
body: &[u8],
pos: &mut usize,
) -> Result<HashMap<Vec<u8>, RoaringBitmap>, SidecarError> {
let count = read_u32(body, pos)? as usize;
let mut table = HashMap::with_capacity(count.min(MAX_PREALLOC_ENTRIES));
for _ in 0..count {
let key_len = read_u32(body, pos)? as usize;
let key = read_bytes(body, pos, key_len)?.to_vec();
let bm_len = read_u32(body, pos)? as usize;
let bm_bytes = read_bytes(body, pos, bm_len)?;
let bitmap = roaring_util::deserialize(bm_bytes).map_err(SidecarError::Bitmap)?;
table.insert(key, bitmap);
}
Ok(table)
}
fn encode(index: &PathIndex) -> Vec<u8> {
for (i, path) in index.paths.iter().enumerate() {
debug_assert_eq!(
index.file_id(path),
Some(i as u32),
"PathIndex passed to paths_idx::encode must have positional file_ids"
);
}
let mut body = Vec::with_capacity(1024 + index.paths.len() * 24);
body.extend_from_slice(&(index.paths.len() as u32).to_le_bytes());
for path in &index.paths {
let bytes = path_bytes(path);
body.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
body.extend_from_slice(&bytes);
}
write_bitmap_table(&mut body, &index.extension_to_files);
write_bitmap_table(&mut body, &index.component_to_files);
let checksum = xxh64(&body, 0);
let mut out = Vec::with_capacity(4 + 4 + 8 + body.len());
out.extend_from_slice(MAGIC);
out.extend_from_slice(&FORMAT_VERSION.to_le_bytes());
out.extend_from_slice(&checksum.to_le_bytes());
out.extend_from_slice(&body);
out
}
fn write_bitmap_table(body: &mut Vec<u8>, table: &HashMap<Vec<u8>, RoaringBitmap>) {
let mut entries: Vec<(&Vec<u8>, &RoaringBitmap)> = table.iter().collect();
entries.sort_unstable_by(|a, b| a.0.cmp(b.0));
body.extend_from_slice(&(entries.len() as u32).to_le_bytes());
for (key, bitmap) in entries {
body.extend_from_slice(&(key.len() as u32).to_le_bytes());
body.extend_from_slice(key);
let bm_bytes = roaring_util::serialize(bitmap);
body.extend_from_slice(&(bm_bytes.len() as u32).to_le_bytes());
body.extend_from_slice(&bm_bytes);
}
}
pub(crate) fn write_paths_idx(dir: &Path, index: &PathIndex) -> io::Result<()> {
let bytes = encode(index);
let tmp = dir.join(format!("paths-{}.tmp", uuid::Uuid::new_v4()));
let final_path = dir.join(PATHS_IDX_FILENAME);
{
let mut file = std::fs::File::create(&tmp)?;
std::io::Write::write_all(&mut file, &bytes)?;
file.sync_all()?;
}
std::fs::rename(&tmp, &final_path)?;
#[cfg(not(windows))]
std::fs::File::open(dir)?.sync_all()?;
Ok(())
}
#[cfg(test)]
#[path = "paths_idx_tests.rs"]
mod tests;