use std::io::{Read, Write};
use crate::error::{LaurusError, Result};
use crate::vector::core::rerank::RerankStorageKind;
pub const RERANK_SIDECAR_MAGIC: [u8; 4] = *b"LRS1";
pub const CURRENT_VERSION: u16 = 1;
pub const FIXED_HEADER_SIZE: usize = 16;
pub const DIM_AND_COUNT_SIZE: usize = 8;
pub const HEADER_SIZE: usize = FIXED_HEADER_SIZE + DIM_AND_COUNT_SIZE;
pub const RERANK_SIDECAR_FOOTER_MAGIC: u32 = 0x4C52_4331;
pub const FOOTER_SIZE: usize = 8;
pub mod storage_kind {
pub const NONE: u16 = 0;
pub const F32: u16 = 1;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RerankSidecarHeader {
pub version: u16,
pub storage_kind: RerankStorageKind,
pub dim: u32,
pub vector_count: u32,
}
impl RerankSidecarHeader {
pub fn new(storage_kind: RerankStorageKind, dim: u32, vector_count: u32) -> Self {
Self {
version: CURRENT_VERSION,
storage_kind,
dim,
vector_count,
}
}
pub fn payload_size(&self) -> Result<usize> {
let bytes = (self.vector_count as u64)
.checked_mul(self.dim as u64)
.and_then(|v| v.checked_mul(self.storage_kind.bytes_per_element() as u64))
.ok_or_else(|| {
LaurusError::index(format!(
"rerank sidecar payload size overflow: vector_count={} * dim={} * \
bytes_per_element={} exceeds u64",
self.vector_count,
self.dim,
self.storage_kind.bytes_per_element()
))
})?;
usize::try_from(bytes).map_err(|_| {
LaurusError::index(format!(
"rerank sidecar payload size {bytes} bytes does not fit in usize on this platform"
))
})
}
pub fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> {
writer.write_all(&RERANK_SIDECAR_MAGIC)?;
writer.write_all(&self.version.to_le_bytes())?;
writer.write_all(&self.storage_kind.tag().to_le_bytes())?;
writer.write_all(&[0u8; 8])?;
writer.write_all(&self.dim.to_le_bytes())?;
writer.write_all(&self.vector_count.to_le_bytes())?;
Ok(())
}
pub fn read_from<R: Read>(reader: &mut R) -> Result<Self> {
let mut magic = [0u8; 4];
reader.read_exact(&mut magic)?;
if magic != RERANK_SIDECAR_MAGIC {
return Err(LaurusError::IncompatibleFormat(format!(
"expected rerank sidecar magic {:?} (\"LRS1\"), found {:?}",
RERANK_SIDECAR_MAGIC, magic
)));
}
let mut version_bytes = [0u8; 2];
reader.read_exact(&mut version_bytes)?;
let version = u16::from_le_bytes(version_bytes);
if version != CURRENT_VERSION {
return Err(LaurusError::IncompatibleFormat(format!(
"unsupported rerank sidecar header version {version} \
(this build supports {CURRENT_VERSION})"
)));
}
let mut kind_bytes = [0u8; 2];
reader.read_exact(&mut kind_bytes)?;
let kind_code = u16::from_le_bytes(kind_bytes);
let storage_kind = match kind_code {
storage_kind::NONE => {
return Err(LaurusError::IncompatibleFormat(
"rerank sidecar storage_kind = 0 is reserved (no storage); \
callers expressing 'no sidecar' must omit the file entirely"
.to_string(),
));
}
storage_kind::F32 => RerankStorageKind::F32,
other => {
return Err(LaurusError::IncompatibleFormat(format!(
"unknown rerank sidecar storage_kind = {other}"
)));
}
};
let mut reserved = [0u8; 8];
reader.read_exact(&mut reserved)?;
let mut dim_bytes = [0u8; 4];
reader.read_exact(&mut dim_bytes)?;
let dim = u32::from_le_bytes(dim_bytes);
let mut count_bytes = [0u8; 4];
reader.read_exact(&mut count_bytes)?;
let vector_count = u32::from_le_bytes(count_bytes);
Ok(Self {
version,
storage_kind,
dim,
vector_count,
})
}
}
fn read_up_to<R: Read>(reader: &mut R, buf: &mut [u8]) -> std::io::Result<usize> {
let mut filled = 0;
while filled < buf.len() {
match reader.read(&mut buf[filled..]) {
Ok(0) => break,
Ok(n) => filled += n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
Ok(filled)
}
pub fn write_sidecar<W: Write>(
writer: &mut W,
storage_kind: RerankStorageKind,
dim: u32,
vectors: &[f32],
) -> Result<()> {
if dim == 0 {
return Err(LaurusError::InvalidOperation(
"rerank sidecar dim must be > 0".to_string(),
));
}
let dim_usize = dim as usize;
if !vectors.len().is_multiple_of(dim_usize) {
return Err(LaurusError::InvalidOperation(format!(
"rerank sidecar payload length {} is not divisible by dim {dim}",
vectors.len()
)));
}
let vector_count = (vectors.len() / dim_usize) as u32;
let header = RerankSidecarHeader::new(storage_kind, dim, vector_count);
let content_crc = {
let mut crc_writer = crate::storage::checksum::CrcWriter::new(&mut *writer);
header.write_to(&mut crc_writer)?;
match storage_kind {
RerankStorageKind::F32 => {
for v in vectors {
crc_writer.write_all(&v.to_le_bytes())?;
}
}
}
crc_writer.checksum()
};
writer.write_all(&RERANK_SIDECAR_FOOTER_MAGIC.to_le_bytes())?;
writer.write_all(&content_crc.to_le_bytes())?;
Ok(())
}
pub fn read_sidecar<R: Read>(
reader: &mut R,
file_size: u64,
) -> Result<(RerankSidecarHeader, Vec<u8>)> {
let mut crc_reader = crate::storage::checksum::CrcReader::new(&mut *reader);
let header = RerankSidecarHeader::read_from(&mut crc_reader)?;
let payload_size = header.payload_size()?;
let min_content = (HEADER_SIZE as u64)
.checked_add(payload_size as u64)
.ok_or_else(|| {
LaurusError::index(
"rerank sidecar declared content length overflows u64: \
.hnsw.f32 file is corrupted",
)
})?;
if min_content > file_size {
return Err(LaurusError::index(format!(
"rerank sidecar header declares a {payload_size}-byte payload but the file is only \
{file_size} bytes: .hnsw.f32 file is corrupted"
)));
}
let mut payload = vec![0u8; payload_size];
crc_reader.read_exact(&mut payload)?;
let computed = crc_reader.checksum();
let inner = crc_reader.get_mut();
let mut footer = [0u8; FOOTER_SIZE];
let trailing = read_up_to(inner, &mut footer)?;
match trailing {
0 => Ok((header, payload)),
FOOTER_SIZE => {
let magic = u32::from_le_bytes([footer[0], footer[1], footer[2], footer[3]]);
if magic != RERANK_SIDECAR_FOOTER_MAGIC {
return Err(LaurusError::index(
"rerank sidecar has unexpected trailing bytes: .hnsw.f32 file is corrupted",
));
}
let stored = u32::from_le_bytes([footer[4], footer[5], footer[6], footer[7]]);
if stored != computed {
return Err(LaurusError::index(
"rerank sidecar checksum mismatch: .hnsw.f32 file is corrupted",
));
}
let mut extra = [0u8; 1];
if read_up_to(inner, &mut extra)? != 0 {
return Err(LaurusError::index(
"rerank sidecar has trailing bytes after the checksum footer: \
.hnsw.f32 file is corrupted",
));
}
Ok((header, payload))
}
_ => Err(LaurusError::index(
"rerank sidecar has a truncated checksum footer: .hnsw.f32 file is corrupted",
)),
}
}
pub(crate) fn load_rerank_sidecar(
storage: &dyn crate::storage::Storage,
file_name: &str,
dimension: usize,
vector_ids: &[(u64, u16)],
field_dict: &[std::sync::Arc<str>],
) -> Result<Option<std::sync::Arc<crate::vector::index::rerank_storage::RerankStoragePool>>> {
if !matches!(storage.loading_mode(), crate::storage::LoadingMode::Eager) {
return Ok(None);
}
let sidecar_name = format!("{file_name}.f32");
if !storage.file_exists(&sidecar_name) {
return Ok(None);
}
let mut sidecar_in = storage.open_input(&sidecar_name)?;
let sidecar_size = sidecar_in.size()?;
let (header, payload) = read_sidecar(&mut sidecar_in, sidecar_size)?;
if header.dim as usize != dimension {
return Err(LaurusError::InvalidOperation(format!(
"rerank sidecar dim mismatch: segment uses {dimension}, sidecar uses {}",
header.dim
)));
}
if header.vector_count as usize != vector_ids.len() {
return Err(LaurusError::InvalidOperation(format!(
"rerank sidecar vector_count mismatch: segment has {} vectors, sidecar has {}",
vector_ids.len(),
header.vector_count
)));
}
let assignment: Vec<(u64, String)> = vector_ids
.iter()
.map(|&(id, fid)| (id, field_dict[fid as usize].to_string()))
.collect();
let pool = crate::vector::index::rerank_storage::RerankStoragePool::from_sidecar_payload(
header.storage_kind,
dimension,
header.vector_count as usize,
payload,
&assignment,
)?;
Ok(Some(std::sync::Arc::new(pool)))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn sample_header() -> RerankSidecarHeader {
RerankSidecarHeader::new(RerankStorageKind::F32, 8, 4)
}
#[test]
fn roundtrip_header() {
let header = sample_header();
let mut buf: Vec<u8> = Vec::new();
header.write_to(&mut buf).unwrap();
assert_eq!(buf.len(), HEADER_SIZE);
let parsed = RerankSidecarHeader::read_from(&mut Cursor::new(&buf)).unwrap();
assert_eq!(parsed, header);
}
#[test]
fn header_layout_starts_with_magic_then_version_then_kind() {
let header = sample_header();
let mut buf: Vec<u8> = Vec::new();
header.write_to(&mut buf).unwrap();
assert_eq!(&buf[0..4], b"LRS1");
assert_eq!(u16::from_le_bytes([buf[4], buf[5]]), CURRENT_VERSION);
assert_eq!(u16::from_le_bytes([buf[6], buf[7]]), storage_kind::F32);
assert_eq!(&buf[8..16], &[0u8; 8]);
assert_eq!(u32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]), 8);
assert_eq!(u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]), 4);
}
#[test]
fn payload_size_is_count_times_dim_times_element_bytes() {
let header = RerankSidecarHeader::new(RerankStorageKind::F32, 16, 100);
assert_eq!(header.payload_size().unwrap(), 100 * 16 * 4);
}
#[test]
fn payload_size_rejects_overflowing_dimensions() {
let header = RerankSidecarHeader::new(RerankStorageKind::F32, u32::MAX, u32::MAX);
let err = header.payload_size().unwrap_err();
assert_index_error(err, "payload size overflow");
}
#[test]
fn missing_magic_returns_incompatible_format() {
let buf = [0u8; HEADER_SIZE];
let err = RerankSidecarHeader::read_from(&mut Cursor::new(&buf[..])).unwrap_err();
match err {
LaurusError::IncompatibleFormat(msg) => {
assert!(msg.contains("LRS1"), "message should mention LRS1");
}
other => panic!("expected IncompatibleFormat, got {other:?}"),
}
}
#[test]
fn unsupported_version_returns_incompatible_format() {
let mut buf = Vec::new();
buf.extend_from_slice(b"LRS1");
buf.extend_from_slice(&99u16.to_le_bytes());
buf.extend_from_slice(&storage_kind::F32.to_le_bytes());
buf.extend_from_slice(&[0u8; 8]);
buf.extend_from_slice(&8u32.to_le_bytes());
buf.extend_from_slice(&4u32.to_le_bytes());
let err = RerankSidecarHeader::read_from(&mut Cursor::new(&buf)).unwrap_err();
match err {
LaurusError::IncompatibleFormat(msg) => {
assert!(msg.contains("99"), "message should mention the version");
}
other => panic!("expected IncompatibleFormat, got {other:?}"),
}
}
#[test]
fn storage_kind_zero_is_rejected() {
let mut buf = Vec::new();
buf.extend_from_slice(b"LRS1");
buf.extend_from_slice(&CURRENT_VERSION.to_le_bytes());
buf.extend_from_slice(&storage_kind::NONE.to_le_bytes());
buf.extend_from_slice(&[0u8; 8]);
buf.extend_from_slice(&8u32.to_le_bytes());
buf.extend_from_slice(&4u32.to_le_bytes());
let err = RerankSidecarHeader::read_from(&mut Cursor::new(&buf)).unwrap_err();
assert!(matches!(err, LaurusError::IncompatibleFormat(_)));
}
#[test]
fn unknown_storage_kind_is_rejected() {
let mut buf = Vec::new();
buf.extend_from_slice(b"LRS1");
buf.extend_from_slice(&CURRENT_VERSION.to_le_bytes());
buf.extend_from_slice(&999u16.to_le_bytes());
buf.extend_from_slice(&[0u8; 8]);
buf.extend_from_slice(&8u32.to_le_bytes());
buf.extend_from_slice(&4u32.to_le_bytes());
let err = RerankSidecarHeader::read_from(&mut Cursor::new(&buf)).unwrap_err();
match err {
LaurusError::IncompatibleFormat(msg) => {
assert!(msg.contains("999"));
}
other => panic!("expected IncompatibleFormat, got {other:?}"),
}
}
#[test]
fn truncated_header_returns_io_error() {
let buf = b"LRS1".to_vec();
let err = RerankSidecarHeader::read_from(&mut Cursor::new(&buf)).unwrap_err();
assert!(matches!(err, LaurusError::Io(_)));
}
#[test]
fn write_then_read_sidecar_roundtrips_f32_payload() {
let dim = 4u32;
let vectors: Vec<f32> = vec![
0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, -1.0, -2.0, -3.0, -4.0,
];
let mut buf: Vec<u8> = Vec::new();
write_sidecar(&mut buf, RerankStorageKind::F32, dim, &vectors).unwrap();
let (header, payload) = read_sidecar(&mut Cursor::new(&buf), buf.len() as u64).unwrap();
assert_eq!(header.dim, dim);
assert_eq!(header.vector_count, 3);
assert_eq!(header.storage_kind, RerankStorageKind::F32);
assert_eq!(payload.len(), header.payload_size().unwrap());
for (i, expected) in vectors.iter().enumerate() {
let lo = i * 4;
let actual = f32::from_le_bytes([
payload[lo],
payload[lo + 1],
payload[lo + 2],
payload[lo + 3],
]);
assert_eq!(actual, *expected, "element {i}");
}
}
#[test]
fn write_sidecar_rejects_zero_dim() {
let mut buf: Vec<u8> = Vec::new();
let err = write_sidecar(&mut buf, RerankStorageKind::F32, 0, &[]).unwrap_err();
assert!(matches!(err, LaurusError::InvalidOperation(_)));
}
#[test]
fn write_sidecar_rejects_misaligned_payload() {
let mut buf: Vec<u8> = Vec::new();
let err = write_sidecar(&mut buf, RerankStorageKind::F32, 4, &[1.0, 2.0, 3.0]).unwrap_err();
assert!(matches!(err, LaurusError::InvalidOperation(_)));
}
#[test]
fn header_constants_are_consistent() {
assert_eq!(FIXED_HEADER_SIZE, 16);
assert_eq!(DIM_AND_COUNT_SIZE, 8);
assert_eq!(HEADER_SIZE, 24);
}
fn sample_sidecar_bytes() -> Vec<u8> {
let vectors: Vec<f32> = vec![0.5, -1.5, 2.5, -3.5, 4.5, 5.5, -6.5, 7.5];
let mut buf: Vec<u8> = Vec::new();
write_sidecar(&mut buf, RerankStorageKind::F32, 4, &vectors).unwrap();
buf
}
fn assert_index_error(err: LaurusError, expected_fragment: &str) {
match err {
LaurusError::Index(msg) => {
assert!(
msg.contains(expected_fragment),
"message {msg:?} should contain {expected_fragment:?}"
);
}
other => panic!("expected Index error, got {other:?}"),
}
}
#[test]
fn write_sidecar_appends_crc_footer() {
let buf = sample_sidecar_bytes();
let payload_len = 8 * 4; assert_eq!(buf.len(), HEADER_SIZE + payload_len + FOOTER_SIZE);
let content_end = buf.len() - FOOTER_SIZE;
let magic = u32::from_le_bytes([
buf[content_end],
buf[content_end + 1],
buf[content_end + 2],
buf[content_end + 3],
]);
assert_eq!(magic, RERANK_SIDECAR_FOOTER_MAGIC);
let stored = u32::from_le_bytes([
buf[content_end + 4],
buf[content_end + 5],
buf[content_end + 6],
buf[content_end + 7],
]);
assert_eq!(stored, crc32fast::hash(&buf[..content_end]));
}
#[test]
fn read_sidecar_accepts_legacy_footerless_payload() {
let vectors: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0];
let header = RerankSidecarHeader::new(RerankStorageKind::F32, 4, 1);
let mut buf: Vec<u8> = Vec::new();
header.write_to(&mut buf).unwrap();
for v in &vectors {
buf.extend_from_slice(&v.to_le_bytes());
}
let (parsed, payload) = read_sidecar(&mut Cursor::new(&buf), buf.len() as u64).unwrap();
assert_eq!(parsed, header);
assert_eq!(payload.len(), parsed.payload_size().unwrap());
}
#[test]
fn corrupted_payload_with_footer_is_rejected() {
let mut buf = sample_sidecar_bytes();
let payload_mid = HEADER_SIZE + (buf.len() - HEADER_SIZE - FOOTER_SIZE) / 2;
buf[payload_mid] ^= 0xff;
let err = read_sidecar(&mut Cursor::new(&buf), buf.len() as u64).unwrap_err();
assert_index_error(err, "checksum mismatch");
}
#[test]
fn corrupted_header_with_footer_is_rejected() {
let mut buf = sample_sidecar_bytes();
buf[10] ^= 0xff;
let err = read_sidecar(&mut Cursor::new(&buf), buf.len() as u64).unwrap_err();
assert_index_error(err, "checksum mismatch");
}
#[test]
fn footer_with_wrong_magic_is_rejected() {
let mut buf = sample_sidecar_bytes();
let magic_pos = buf.len() - FOOTER_SIZE;
buf[magic_pos] ^= 0xff;
let err = read_sidecar(&mut Cursor::new(&buf), buf.len() as u64).unwrap_err();
assert_index_error(err, "unexpected trailing bytes");
}
#[test]
fn truncated_footer_is_rejected() {
let buf = sample_sidecar_bytes();
for keep in 1..FOOTER_SIZE {
let truncated = &buf[..buf.len() - FOOTER_SIZE + keep];
let err =
read_sidecar(&mut Cursor::new(truncated), truncated.len() as u64).unwrap_err();
assert_index_error(err, "truncated checksum footer");
}
}
#[test]
fn trailing_bytes_after_footer_are_rejected() {
let mut buf = sample_sidecar_bytes();
buf.push(0u8);
let err = read_sidecar(&mut Cursor::new(&buf), buf.len() as u64).unwrap_err();
assert_index_error(err, "trailing bytes after the checksum footer");
}
#[test]
fn oversized_header_is_rejected_before_allocating() {
let dim = 16u32;
let huge_count = 1u32 << 28; let header = RerankSidecarHeader::new(RerankStorageKind::F32, dim, huge_count);
let mut buf: Vec<u8> = Vec::new();
header.write_to(&mut buf).unwrap();
assert_eq!(buf.len(), HEADER_SIZE);
assert_eq!(
header.payload_size().unwrap(),
huge_count as usize * dim as usize * 4
);
let err = read_sidecar(&mut Cursor::new(&buf), buf.len() as u64).unwrap_err();
assert_index_error(err, "declares a");
}
#[test]
fn header_declaring_one_byte_too_many_is_rejected() {
let header = RerankSidecarHeader::new(RerankStorageKind::F32, 4, 1);
let mut buf: Vec<u8> = Vec::new();
header.write_to(&mut buf).unwrap();
buf.extend_from_slice(&[0u8; 15]);
let err = read_sidecar(&mut Cursor::new(&buf), buf.len() as u64).unwrap_err();
assert_index_error(err, "declares a");
}
}