use std::io;
use std::path::Path;
use roaring::RoaringBitmap;
use xxhash_rust::xxh64::xxh64;
use crate::posting::roaring_util;
const MAGIC: &[u8; 4] = b"STDL";
pub(crate) const FORMAT_VERSION: u32 = 1;
const HEADER_LEN: usize = 4 + 4 + 8;
pub(crate) const MAX_SIDECAR_SIZE: u64 = 1024 * 1024 * 1024;
#[derive(Debug)]
pub(crate) enum SidecarError {
Io(io::Error),
TooLarge(u64),
TooShort,
BadMagic,
UnsupportedVersion(u32),
ChecksumMismatch,
BadFilename,
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 deletes.idx: {e}"),
SidecarError::TooLarge(n) => write!(
f,
"deletes.idx is {n} bytes, exceeds {MAX_SIDECAR_SIZE}-byte safety cap"
),
SidecarError::TooShort => write!(f, "deletes.idx is shorter than its fixed header"),
SidecarError::BadMagic => write!(f, "deletes.idx has an invalid magic number"),
SidecarError::UnsupportedVersion(v) => write!(
f,
"deletes.idx format version {v} is not supported (expected {FORMAT_VERSION})"
),
SidecarError::ChecksumMismatch => {
write!(f, "deletes.idx checksum does not match its contents")
}
SidecarError::BadFilename => {
write!(
f,
"deletes.idx filename in manifest is not a plain filename"
)
}
SidecarError::Bitmap(e) => write!(f, "deletes.idx has a corrupt roaring bitmap: {e}"),
}
}
}
fn is_plain_filename(name: &str) -> bool {
!name.is_empty()
&& !name.contains('/')
&& !name.contains('\\')
&& !name.contains("..")
&& !Path::new(name).is_absolute()
}
pub(crate) fn new_filename() -> String {
format!("deletes-{}.idx", uuid::Uuid::new_v4())
}
pub(crate) fn read_deletes_idx(dir: &Path, name: &str) -> Result<RoaringBitmap, SidecarError> {
if !is_plain_filename(name) {
return Err(SidecarError::BadFilename);
}
let path = dir.join(name);
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<RoaringBitmap, 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);
}
roaring_util::deserialize(body).map_err(SidecarError::Bitmap)
}
fn encode(bitmap: &RoaringBitmap) -> Vec<u8> {
let body = roaring_util::serialize(bitmap);
let checksum = xxh64(&body, 0);
let mut out = Vec::with_capacity(HEADER_LEN + 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
}
pub(crate) fn write_deletes_idx(dir: &Path, name: &str, bitmap: &RoaringBitmap) -> io::Result<()> {
if !is_plain_filename(name) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"deletes.idx filename is not a plain filename",
));
}
let bytes = encode(bitmap);
let tmp = dir.join(format!("deletes-{}.tmp", uuid::Uuid::new_v4()));
let final_path = dir.join(name);
{
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 = "deletes_idx_tests.rs"]
mod tests;