Squashed 'vendor/ruvector/' content from commit b64c2172

git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
commit d803bfe2b1
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
[package]
name = "rvf-wire"
version = "0.1.0"
edition = "2021"
description = "RuVector Format wire format reader/writer -- zero-copy segment serialization"
license = "MIT OR Apache-2.0"
repository = "https://github.com/ruvnet/ruvector"
homepage = "https://github.com/ruvnet/ruvector"
readme = "README.md"
categories = ["encoding", "data-structures"]
keywords = ["vector", "database", "binary-format", "wire-protocol", "rvf"]
rust-version = "1.87"
[features]
default = ["std"]
std = ["rvf-types/std"]
[dependencies]
rvf-types = { version = "0.2.0", path = "../rvf-types" }
xxhash-rust = { version = "0.8", features = ["xxh3"] }
crc32c = "0.6"
[dev-dependencies]
tempfile = "3"

View File

@@ -0,0 +1,30 @@
# rvf-wire
Zero-copy wire format reader and writer for RuVector Format (RVF) segments.
## Overview
`rvf-wire` handles serialization and deserialization of RVF binary segments:
- **Writer** -- append segments with automatic CRC32c and XXH3 checksums
- **Reader** -- stream-parse segments with validation and integrity checks
- **Zero-copy** -- borrows directly from memory-mapped buffers where possible
## Usage
```toml
[dependencies]
rvf-wire = "0.1"
```
```rust
use rvf_wire::{SegmentWriter, SegmentReader};
```
## Features
- `std` (default) -- enable `std` I/O support
## License
MIT OR Apache-2.0

View File

@@ -0,0 +1,122 @@
//! Delta encoding with restart points for sorted integer sequences.
//!
//! Sorted ID sequences are delta-encoded: each value (except the first) is
//! stored as the difference from the previous value. Every `restart_interval`
//! entries, the value is stored absolute (not delta) to allow random access
//! into the middle of a sequence.
use crate::varint::{decode_varint, encode_varint, MAX_VARINT_LEN};
/// Encode a sorted slice of `u64` IDs using delta-varint encoding with restart
/// points. Appends encoded bytes to `buf`.
///
/// Every `restart_interval` entries (counting from 0), the full absolute value
/// is stored. All other entries are stored as the delta from the previous value.
///
/// # Panics
///
/// Panics if `restart_interval` is 0.
pub fn encode_delta(sorted_ids: &[u64], restart_interval: u32, buf: &mut Vec<u8>) {
assert!(restart_interval > 0, "restart_interval must be > 0");
let mut tmp = [0u8; MAX_VARINT_LEN];
let mut prev = 0u64;
for (i, &id) in sorted_ids.iter().enumerate() {
let value = if (i as u32).is_multiple_of(restart_interval) {
prev = id;
id
} else {
let delta = id - prev;
prev = id;
delta
};
let n = encode_varint(value, &mut tmp);
buf.extend_from_slice(&tmp[..n]);
}
}
/// Decode `count` delta-varint encoded IDs from `buf`.
///
/// Every `restart_interval` entries the stored value is absolute; all others
/// are deltas from the previous decoded value.
///
/// # Panics
///
/// Panics if `restart_interval` is 0 or the buffer contains insufficient data.
pub fn decode_delta(buf: &[u8], count: usize, restart_interval: u32) -> Vec<u64> {
assert!(restart_interval > 0, "restart_interval must be > 0");
let mut result = Vec::with_capacity(count);
let mut offset = 0;
let mut prev = 0u64;
for i in 0..count {
let (val, consumed) =
decode_varint(&buf[offset..]).expect("delta decode: unexpected end of data");
offset += consumed;
if (i as u32).is_multiple_of(restart_interval) {
prev = val;
} else {
prev += val;
}
result.push(prev);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_simple() {
let ids = vec![100, 105, 108, 120, 200];
let mut buf = Vec::new();
encode_delta(&ids, 128, &mut buf);
let decoded = decode_delta(&buf, ids.len(), 128);
assert_eq!(decoded, ids);
}
#[test]
fn round_trip_with_restart_points() {
let ids: Vec<u64> = (0..20).map(|i| i * 10 + 100).collect();
let mut buf = Vec::new();
encode_delta(&ids, 4, &mut buf);
let decoded = decode_delta(&buf, ids.len(), 4);
assert_eq!(decoded, ids);
}
#[test]
fn single_element() {
let ids = vec![42u64];
let mut buf = Vec::new();
encode_delta(&ids, 1, &mut buf);
let decoded = decode_delta(&buf, 1, 1);
assert_eq!(decoded, ids);
}
#[test]
fn empty_sequence() {
let ids: Vec<u64> = vec![];
let mut buf = Vec::new();
encode_delta(&ids, 8, &mut buf);
let decoded = decode_delta(&buf, 0, 8);
assert_eq!(decoded, ids);
}
#[test]
fn restart_at_every_entry() {
// When restart_interval=1, every value is absolute
let ids = vec![1000, 2000, 3000, 4000];
let mut buf = Vec::new();
encode_delta(&ids, 1, &mut buf);
let decoded = decode_delta(&buf, ids.len(), 1);
assert_eq!(decoded, ids);
}
#[test]
fn large_values() {
let ids = vec![u64::MAX - 100, u64::MAX - 50, u64::MAX - 10, u64::MAX];
let mut buf = Vec::new();
encode_delta(&ids, 128, &mut buf);
let decoded = decode_delta(&buf, ids.len(), 128);
assert_eq!(decoded, ids);
}
}

View File

@@ -0,0 +1,135 @@
//! Hash computation and verification for RVF segments.
//!
//! The segment header stores a 128-bit content hash. The algorithm is
//! identified by the `checksum_algo` field: 0=deprecated CRC32C (now
//! upgraded to XXH3-128), 1=XXH3-128, 2=SHAKE-256 (first 128 bits).
use rvf_types::SegmentHeader;
/// Compute the XXH3-128 hash of `data`, returning a 16-byte array.
pub fn compute_xxh3_128(data: &[u8]) -> [u8; 16] {
let h = xxhash_rust::xxh3::xxh3_128(data);
h.to_le_bytes()
}
/// Compute the CRC32C checksum of `data`.
pub fn compute_crc32c(data: &[u8]) -> u32 {
crc32c::crc32c(data)
}
/// Compute a 16-byte content hash field value using CRC32C.
///
/// The 4-byte CRC is stored in the first 4 bytes (little-endian), with the
/// remaining 12 bytes set to zero.
pub fn compute_crc32c_hash(data: &[u8]) -> [u8; 16] {
let crc = compute_crc32c(data);
let mut out = [0u8; 16];
out[..4].copy_from_slice(&crc.to_le_bytes());
out
}
/// Compute the content hash for a payload using the algorithm specified
/// by `algo` (the `checksum_algo` field from the segment header).
///
/// - 0 = DEPRECATED CRC32C -- now upgraded to XXH3-128 for all operations.
/// CRC32C produced only 4 bytes of entropy zero-padded to 16, making
/// collision attacks trivial (~2^16 expected operations). All algorithms
/// now use the full 128-bit XXH3 hash.
/// - 1 = XXH3-128 (16 bytes)
/// - Other values fall back to XXH3-128.
pub fn compute_content_hash(_algo: u8, data: &[u8]) -> [u8; 16] {
// All algorithms now use XXH3-128 for full 128-bit collision resistance.
// algo=0 (CRC32C) is deprecated: its 32-bit output zero-padded to 128 bits
// provided only ~32 bits of security, making collisions trivially findable.
compute_xxh3_128(data)
}
/// Verify the content hash stored in a segment header against the actual
/// payload bytes.
///
/// Returns `true` if the computed hash matches `header.content_hash`.
pub fn verify_content_hash(header: &SegmentHeader, payload: &[u8]) -> bool {
let expected = compute_content_hash(header.checksum_algo, payload);
expected == header.content_hash
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn xxh3_128_deterministic() {
let data = b"hello world";
let h1 = compute_xxh3_128(data);
let h2 = compute_xxh3_128(data);
assert_eq!(h1, h2);
assert_ne!(h1, [0u8; 16]);
}
#[test]
fn crc32c_deterministic() {
let data = b"hello world";
let c1 = compute_crc32c(data);
let c2 = compute_crc32c(data);
assert_eq!(c1, c2);
assert_ne!(c1, 0);
}
#[test]
fn crc32c_hash_is_zero_padded() {
let data = b"test payload";
let h = compute_crc32c_hash(data);
let crc = compute_crc32c(data);
assert_eq!(&h[..4], &crc.to_le_bytes());
assert_eq!(&h[4..], &[0u8; 12]);
}
#[test]
fn verify_content_hash_xxh3() {
let payload = b"some vector data";
let hash = compute_xxh3_128(payload);
let header = SegmentHeader {
magic: rvf_types::SEGMENT_MAGIC,
version: 1,
seg_type: 0x01,
flags: 0,
segment_id: 1,
payload_length: payload.len() as u64,
timestamp_ns: 0,
checksum_algo: 1, // XXH3-128
compression: 0,
reserved_0: 0,
reserved_1: 0,
content_hash: hash,
uncompressed_len: 0,
alignment_pad: 0,
};
assert!(verify_content_hash(&header, payload));
assert!(!verify_content_hash(&header, b"wrong data"));
}
#[test]
fn verify_content_hash_algo_zero_uses_xxh3() {
// algo=0 (formerly CRC32C) is now upgraded to XXH3-128, so the
// content hash must be computed via XXH3-128 even when algo=0.
let payload = b"crc payload";
let hash = compute_xxh3_128(payload);
let header = SegmentHeader {
magic: rvf_types::SEGMENT_MAGIC,
version: 1,
seg_type: 0x01,
flags: 0,
segment_id: 2,
payload_length: payload.len() as u64,
timestamp_ns: 0,
checksum_algo: 0, // deprecated CRC32C, now upgraded to XXH3-128
compression: 0,
reserved_0: 0,
reserved_1: 0,
content_hash: hash,
uncompressed_len: 0,
alignment_pad: 0,
};
assert!(verify_content_hash(&header, payload));
}
}

View File

@@ -0,0 +1,219 @@
//! HOT_SEG codec.
//!
//! The hot segment stores the most-accessed vectors in an interleaved
//! (row-major) layout with their neighbor lists co-located for cache
//! locality. Each entry is 64-byte aligned.
/// Hot segment header, stored at the start of the HOT_SEG payload.
#[derive(Clone, Debug, PartialEq)]
pub struct HotHeader {
pub vector_count: u32,
pub dim: u16,
pub dtype: u8,
pub neighbor_m: u16,
}
/// A single interleaved hot entry.
#[derive(Clone, Debug, PartialEq)]
pub struct HotEntry {
pub vector_id: u64,
pub vector_data: Vec<u8>,
pub neighbor_ids: Vec<u64>,
}
/// Size of an element in bytes for a given dtype.
fn dtype_element_size(dtype: u8) -> usize {
match dtype {
0x00 => 4, // f32
0x01 => 2, // f16
0x02 => 2, // bf16
0x03 => 1, // i8
0x04 => 1, // u8
_ => 1,
}
}
const ALIGN: usize = 64;
fn align_up(n: usize) -> usize {
(n + ALIGN - 1) & !(ALIGN - 1)
}
/// Write the HOT_SEG payload from a header and a list of hot entries.
///
/// The header is padded to 64 bytes. Each entry is individually 64-byte
/// aligned.
pub fn write_hot_seg(header: &HotHeader, entries: &[HotEntry]) -> Vec<u8> {
let mut buf = Vec::new();
// Hot header: vector_count(4) + dim(2) + dtype(1) + neighbor_M(2) = 9 bytes
buf.extend_from_slice(&header.vector_count.to_le_bytes());
buf.extend_from_slice(&header.dim.to_le_bytes());
buf.push(header.dtype);
buf.extend_from_slice(&header.neighbor_m.to_le_bytes());
// Pad header to 64 bytes
buf.resize(align_up(buf.len()), 0);
// Write each entry, 64-byte aligned
for entry in entries {
let entry_start = buf.len();
// vector_id: u64
buf.extend_from_slice(&entry.vector_id.to_le_bytes());
// vector data: dtype * dim bytes
buf.extend_from_slice(&entry.vector_data);
// neighbor_count: u16
let neighbor_count = entry.neighbor_ids.len() as u16;
buf.extend_from_slice(&neighbor_count.to_le_bytes());
// neighbor_ids: u64 * count
for &nid in &entry.neighbor_ids {
buf.extend_from_slice(&nid.to_le_bytes());
}
// Pad this entry to 64-byte alignment
let entry_raw_size = buf.len() - entry_start;
let entry_padded = align_up(entry_raw_size);
buf.resize(entry_start + entry_padded, 0);
}
buf
}
/// Read the HOT_SEG header from the start of the payload.
///
/// Returns the header and the byte offset after the (64-byte aligned) header.
pub fn read_hot_header(data: &[u8]) -> Result<(HotHeader, usize), &'static str> {
if data.len() < 9 {
return Err("hot header truncated");
}
let vector_count = u32::from_le_bytes(data[0..4].try_into().unwrap());
let dim = u16::from_le_bytes([data[4], data[5]]);
let dtype = data[6];
let neighbor_m = u16::from_le_bytes([data[7], data[8]]);
let consumed = align_up(9);
Ok((
HotHeader {
vector_count,
dim,
dtype,
neighbor_m,
},
consumed,
))
}
/// Read all hot entries from the payload (after the header).
///
/// `data` should start at the first entry (after the aligned header).
pub fn read_hot_entries(data: &[u8], header: &HotHeader) -> Result<Vec<HotEntry>, &'static str> {
let elem_size = dtype_element_size(header.dtype);
let vector_byte_len = header.dim as usize * elem_size;
let mut entries = Vec::with_capacity(header.vector_count as usize);
let mut pos = 0;
for _ in 0..header.vector_count {
let entry_start = pos;
// Need at least: 8 (id) + vector_byte_len + 2 (neighbor_count)
let min_size = 8 + vector_byte_len + 2;
if data.len() < pos + min_size {
return Err("hot entry truncated");
}
let vector_id = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
pos += 8;
let vector_data = data[pos..pos + vector_byte_len].to_vec();
pos += vector_byte_len;
let neighbor_count = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2;
if data.len() < pos + neighbor_count * 8 {
return Err("neighbor IDs truncated");
}
let mut neighbor_ids = Vec::with_capacity(neighbor_count);
for _ in 0..neighbor_count {
neighbor_ids.push(u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap()));
pos += 8;
}
entries.push(HotEntry {
vector_id,
vector_data,
neighbor_ids,
});
// Advance to next 64-byte boundary
let entry_raw_size = pos - entry_start;
pos = entry_start + align_up(entry_raw_size);
}
Ok(entries)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_entries(dim: u16, dtype: u8, count: u32, neighbor_count: usize) -> Vec<HotEntry> {
let elem_size = dtype_element_size(dtype);
let vector_byte_len = dim as usize * elem_size;
(0..count)
.map(|i| HotEntry {
vector_id: (i as u64) * 100 + 1,
vector_data: vec![(i % 256) as u8; vector_byte_len],
neighbor_ids: (0..neighbor_count as u64).map(|n| n + 1000).collect(),
})
.collect()
}
#[test]
fn round_trip_hot_seg() {
let dim = 4u16;
let dtype = 0u8; // f32
let entries = make_test_entries(dim, dtype, 3, 2);
let header = HotHeader {
vector_count: 3,
dim,
dtype,
neighbor_m: 16,
};
let payload = write_hot_seg(&header, &entries);
let (decoded_header, header_end) = read_hot_header(&payload).unwrap();
assert_eq!(decoded_header, header);
let decoded_entries = read_hot_entries(&payload[header_end..], &decoded_header).unwrap();
assert_eq!(decoded_entries.len(), 3);
assert_eq!(decoded_entries[0].vector_id, 1);
assert_eq!(decoded_entries[1].vector_id, 101);
assert_eq!(decoded_entries[2].vector_id, 201);
for (orig, dec) in entries.iter().zip(decoded_entries.iter()) {
assert_eq!(orig.vector_data, dec.vector_data);
assert_eq!(orig.neighbor_ids, dec.neighbor_ids);
}
}
#[test]
fn empty_hot_seg() {
let header = HotHeader {
vector_count: 0,
dim: 128,
dtype: 1,
neighbor_m: 16,
};
let payload = write_hot_seg(&header, &[]);
let (decoded_header, header_end) = read_hot_header(&payload).unwrap();
assert_eq!(decoded_header.vector_count, 0);
let entries = read_hot_entries(&payload[header_end..], &decoded_header).unwrap();
assert!(entries.is_empty());
}
#[test]
fn alignment_respected() {
let dim = 384u16;
let dtype = 1u8; // f16
let entries = make_test_entries(dim, dtype, 2, 16);
let header = HotHeader {
vector_count: 2,
dim,
dtype,
neighbor_m: 16,
};
let payload = write_hot_seg(&header, &entries);
// Total payload should be aligned
assert_eq!(payload.len() % 64, 0);
}
}

View File

@@ -0,0 +1,282 @@
//! INDEX_SEG codec.
//!
//! Reads and writes HNSW index segments: the index header, restart point
//! index, and adjacency data with varint delta encoding.
use crate::varint::{decode_varint, encode_varint, MAX_VARINT_LEN};
const ALIGN: usize = 64;
fn align_up(n: usize) -> usize {
(n + ALIGN - 1) & !(ALIGN - 1)
}
/// Index header at the start of an INDEX_SEG payload.
#[derive(Clone, Debug, PartialEq)]
pub struct IndexHeader {
/// Index type: 0=HNSW, 1=IVF, 2=flat.
pub index_type: u8,
/// HNSW layer level (A=0, B=1, C=2).
pub layer_level: u8,
/// Maximum neighbors per layer.
pub m: u16,
/// HNSW ef_construction parameter.
pub ef_construction: u32,
/// Number of nodes in this index segment.
pub node_count: u64,
}
/// INDEX_SEG header size before padding: 1+1+2+4+8 = 16 bytes.
const INDEX_HEADER_RAW_SIZE: usize = 16;
/// Restart point index.
#[derive(Clone, Debug, PartialEq)]
pub struct RestartPointIndex {
pub restart_interval: u32,
pub offsets: Vec<u32>,
}
/// A single node's adjacency information.
#[derive(Clone, Debug, PartialEq)]
pub struct NodeAdjacency {
/// Neighbor lists, one per layer. Each layer is a sorted list of node IDs.
pub layers: Vec<Vec<u64>>,
}
/// Write an INDEX_SEG payload from the header, restart index, and adjacency data.
pub fn write_index_seg(
header: &IndexHeader,
restart: &RestartPointIndex,
adjacency: &[NodeAdjacency],
) -> Vec<u8> {
let mut buf = Vec::new();
// Index header
buf.push(header.index_type);
buf.push(header.layer_level);
buf.extend_from_slice(&header.m.to_le_bytes());
buf.extend_from_slice(&header.ef_construction.to_le_bytes());
buf.extend_from_slice(&header.node_count.to_le_bytes());
buf.resize(align_up(buf.len()), 0);
// Restart point index
let restart_count = restart.offsets.len() as u32;
buf.extend_from_slice(&restart.restart_interval.to_le_bytes());
buf.extend_from_slice(&restart_count.to_le_bytes());
for &offset in &restart.offsets {
buf.extend_from_slice(&offset.to_le_bytes());
}
buf.resize(align_up(buf.len()), 0);
// Adjacency data
let mut tmp = [0u8; MAX_VARINT_LEN];
for node in adjacency {
let n = encode_varint(node.layers.len() as u64, &mut tmp);
buf.extend_from_slice(&tmp[..n]);
for layer in &node.layers {
let n = encode_varint(layer.len() as u64, &mut tmp);
buf.extend_from_slice(&tmp[..n]);
// Delta-encode neighbor IDs within each layer
let mut prev = 0u64;
for &nid in layer {
let delta = nid - prev;
let n = encode_varint(delta, &mut tmp);
buf.extend_from_slice(&tmp[..n]);
prev = nid;
}
}
}
// Pad to 64-byte boundary
buf.resize(align_up(buf.len()), 0);
buf
}
/// Read the INDEX_SEG header from the start of the payload.
///
/// Returns the header and the byte offset after the 64-byte aligned header.
pub fn read_index_header(data: &[u8]) -> Result<(IndexHeader, usize), &'static str> {
if data.len() < INDEX_HEADER_RAW_SIZE {
return Err("index header truncated");
}
let header = IndexHeader {
index_type: data[0],
layer_level: data[1],
m: u16::from_le_bytes([data[2], data[3]]),
ef_construction: u32::from_le_bytes(data[4..8].try_into().unwrap()),
node_count: u64::from_le_bytes(data[8..16].try_into().unwrap()),
};
Ok((header, align_up(INDEX_HEADER_RAW_SIZE)))
}
/// Read the restart point index from the payload at the given offset.
///
/// Returns the restart index and the byte offset after it (64-byte aligned).
pub fn read_restart_index(data: &[u8]) -> Result<(RestartPointIndex, usize), &'static str> {
if data.len() < 8 {
return Err("restart index truncated");
}
let restart_interval = u32::from_le_bytes(data[0..4].try_into().unwrap());
let restart_count = u32::from_le_bytes(data[4..8].try_into().unwrap()) as usize;
let offsets_end = 8 + restart_count * 4;
if data.len() < offsets_end {
return Err("restart offsets truncated");
}
let mut offsets = Vec::with_capacity(restart_count);
for i in 0..restart_count {
let base = 8 + i * 4;
offsets.push(u32::from_le_bytes(data[base..base + 4].try_into().unwrap()));
}
let consumed = align_up(offsets_end);
Ok((
RestartPointIndex {
restart_interval,
offsets,
},
consumed,
))
}
/// Read adjacency data for `node_count` nodes from the payload.
///
/// Each node stores: layer_count(varint), then for each layer:
/// neighbor_count(varint) followed by delta-encoded neighbor IDs (varints).
pub fn read_adjacency(data: &[u8], node_count: u64) -> Result<Vec<NodeAdjacency>, &'static str> {
let mut nodes = Vec::with_capacity(node_count as usize);
let mut pos = 0;
for _ in 0..node_count {
let (layer_count, consumed) =
decode_varint(&data[pos..]).map_err(|_| "adjacency layer_count decode failed")?;
pos += consumed;
let mut layers = Vec::with_capacity(layer_count as usize);
for _ in 0..layer_count {
let (neighbor_count, consumed) = decode_varint(&data[pos..])
.map_err(|_| "adjacency neighbor_count decode failed")?;
pos += consumed;
let mut neighbors = Vec::with_capacity(neighbor_count as usize);
let mut prev = 0u64;
for _ in 0..neighbor_count {
let (delta, consumed) =
decode_varint(&data[pos..]).map_err(|_| "adjacency delta decode failed")?;
pos += consumed;
prev += delta;
neighbors.push(prev);
}
layers.push(neighbors);
}
nodes.push(NodeAdjacency { layers });
}
Ok(nodes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn index_header_round_trip() {
let header = IndexHeader {
index_type: 0,
layer_level: 1,
m: 16,
ef_construction: 200,
node_count: 10000,
};
let restart = RestartPointIndex {
restart_interval: 64,
offsets: vec![0, 1024, 2048],
};
let adjacency = vec![]; // empty for header test
let buf = write_index_seg(&header, &restart, &adjacency);
let (decoded_header, header_end) = read_index_header(&buf).unwrap();
assert_eq!(decoded_header, header);
let (decoded_restart, _restart_end) = read_restart_index(&buf[header_end..]).unwrap();
assert_eq!(decoded_restart, restart);
}
#[test]
fn adjacency_round_trip() {
let header = IndexHeader {
index_type: 0,
layer_level: 0,
m: 8,
ef_construction: 100,
node_count: 3,
};
let restart = RestartPointIndex {
restart_interval: 64,
offsets: vec![0],
};
let adjacency = vec![
NodeAdjacency {
layers: vec![vec![10, 20, 30]],
},
NodeAdjacency {
layers: vec![vec![5, 15, 25, 35]],
},
NodeAdjacency {
layers: vec![vec![100, 200], vec![50]],
},
];
let buf = write_index_seg(&header, &restart, &adjacency);
let (_, header_end) = read_index_header(&buf).unwrap();
let (_, restart_end) = read_restart_index(&buf[header_end..]).unwrap();
let adj_data = &buf[header_end + restart_end..];
let decoded = read_adjacency(adj_data, 3).unwrap();
assert_eq!(decoded.len(), 3);
assert_eq!(decoded[0].layers[0], vec![10, 20, 30]);
assert_eq!(decoded[1].layers[0], vec![5, 15, 25, 35]);
assert_eq!(decoded[2].layers[0], vec![100, 200]);
assert_eq!(decoded[2].layers[1], vec![50]);
}
#[test]
fn empty_index() {
let header = IndexHeader {
index_type: 2,
layer_level: 0,
m: 0,
ef_construction: 0,
node_count: 0,
};
let restart = RestartPointIndex {
restart_interval: 1,
offsets: vec![],
};
let buf = write_index_seg(&header, &restart, &[]);
let (decoded_header, header_end) = read_index_header(&buf).unwrap();
assert_eq!(decoded_header.node_count, 0);
let (decoded_restart, restart_end) = read_restart_index(&buf[header_end..]).unwrap();
assert!(decoded_restart.offsets.is_empty());
let adj_data = &buf[header_end + restart_end..];
let decoded_adj = read_adjacency(adj_data, 0).unwrap();
assert!(decoded_adj.is_empty());
}
#[test]
fn alignment() {
let header = IndexHeader {
index_type: 0,
layer_level: 0,
m: 16,
ef_construction: 200,
node_count: 1,
};
let restart = RestartPointIndex {
restart_interval: 64,
offsets: vec![0],
};
let adjacency = vec![NodeAdjacency {
layers: vec![vec![1, 2, 3, 4, 5]],
}];
let buf = write_index_seg(&header, &restart, &adjacency);
assert_eq!(buf.len() % 64, 0);
}
}

View File

@@ -0,0 +1,20 @@
//! RVF wire format reader/writer.
//!
//! This crate implements the binary encoding and decoding for the RuVector
//! Format (RVF): segment headers, varint encoding, delta coding, hash
//! computation, tail scanning, and per-segment-type codecs.
pub mod delta;
pub mod hash;
pub mod hot_seg_codec;
pub mod index_seg_codec;
pub mod manifest_codec;
pub mod reader;
pub mod tail_scan;
pub mod varint;
pub mod vec_seg_codec;
pub mod writer;
pub use reader::{read_segment, read_segment_header, validate_segment};
pub use tail_scan::find_latest_manifest;
pub use writer::{calculate_padded_size, write_segment};

View File

@@ -0,0 +1,294 @@
//! Level 0 root manifest codec.
//!
//! The root manifest is always exactly 4096 bytes, found at the tail of the
//! file (or at the tail of a MANIFEST_SEG payload). It contains hotset
//! pointers for instant boot and a CRC32C checksum at the last 4 bytes.
use crate::hash::compute_crc32c;
use rvf_types::{ErrorCode, RvfError, ROOT_MANIFEST_MAGIC, ROOT_MANIFEST_SIZE};
/// Parsed Level 0 root manifest.
#[derive(Clone, Debug)]
pub struct Level0Root {
pub magic: u32,
pub version: u16,
pub flags: u16,
pub l1_manifest_offset: u64,
pub l1_manifest_length: u64,
pub total_vector_count: u64,
pub dimension: u16,
pub base_dtype: u8,
pub profile_id: u8,
pub epoch: u32,
pub created_ns: u64,
pub modified_ns: u64,
// Hotset pointers
pub entrypoint_seg_offset: u64,
pub entrypoint_block_offset: u32,
pub entrypoint_count: u32,
pub toplayer_seg_offset: u64,
pub toplayer_block_offset: u32,
pub toplayer_node_count: u32,
pub centroid_seg_offset: u64,
pub centroid_block_offset: u32,
pub centroid_count: u32,
pub quantdict_seg_offset: u64,
pub quantdict_block_offset: u32,
pub quantdict_size: u32,
pub hot_cache_seg_offset: u64,
pub hot_cache_block_offset: u32,
pub hot_cache_vector_count: u32,
pub prefetch_map_offset: u64,
pub prefetch_map_entries: u32,
// Checksum
pub root_checksum: u32,
}
impl Default for Level0Root {
fn default() -> Self {
Self {
magic: ROOT_MANIFEST_MAGIC,
version: 1,
flags: 0,
l1_manifest_offset: 0,
l1_manifest_length: 0,
total_vector_count: 0,
dimension: 0,
base_dtype: 0,
profile_id: 0,
epoch: 0,
created_ns: 0,
modified_ns: 0,
entrypoint_seg_offset: 0,
entrypoint_block_offset: 0,
entrypoint_count: 0,
toplayer_seg_offset: 0,
toplayer_block_offset: 0,
toplayer_node_count: 0,
centroid_seg_offset: 0,
centroid_block_offset: 0,
centroid_count: 0,
quantdict_seg_offset: 0,
quantdict_block_offset: 0,
quantdict_size: 0,
hot_cache_seg_offset: 0,
hot_cache_block_offset: 0,
hot_cache_vector_count: 0,
prefetch_map_offset: 0,
prefetch_map_entries: 0,
root_checksum: 0,
}
}
}
fn read_u16_le(data: &[u8], offset: usize) -> u16 {
u16::from_le_bytes([data[offset], data[offset + 1]])
}
fn read_u32_le(data: &[u8], offset: usize) -> u32 {
u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap())
}
fn read_u64_le(data: &[u8], offset: usize) -> u64 {
u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap())
}
fn write_u16_le(buf: &mut [u8], offset: usize, val: u16) {
buf[offset..offset + 2].copy_from_slice(&val.to_le_bytes());
}
fn write_u32_le(buf: &mut [u8], offset: usize, val: u32) {
buf[offset..offset + 4].copy_from_slice(&val.to_le_bytes());
}
fn write_u64_le(buf: &mut [u8], offset: usize, val: u64) {
buf[offset..offset + 8].copy_from_slice(&val.to_le_bytes());
}
/// Read and parse a Level 0 root manifest from a 4096-byte slice.
///
/// Validates the magic (`RVM0`) and CRC32C checksum.
///
/// # Errors
///
/// - `InvalidManifest` if the magic is wrong or the checksum doesn't match.
/// - `TruncatedSegment` if `data` is shorter than 4096 bytes.
pub fn read_root_manifest(data: &[u8]) -> Result<Level0Root, RvfError> {
if data.len() < ROOT_MANIFEST_SIZE {
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
}
let magic = read_u32_le(data, 0x000);
if magic != ROOT_MANIFEST_MAGIC {
return Err(RvfError::Code(ErrorCode::InvalidManifest));
}
// Verify CRC32C: checksum covers bytes 0x000..0xFFC
let stored_checksum = read_u32_le(data, 0xFFC);
let computed_checksum = compute_crc32c(&data[..0xFFC]);
if stored_checksum != computed_checksum {
return Err(RvfError::Code(ErrorCode::InvalidChecksum));
}
Ok(Level0Root {
magic,
version: read_u16_le(data, 0x004),
flags: read_u16_le(data, 0x006),
l1_manifest_offset: read_u64_le(data, 0x008),
l1_manifest_length: read_u64_le(data, 0x010),
total_vector_count: read_u64_le(data, 0x018),
dimension: read_u16_le(data, 0x020),
base_dtype: data[0x022],
profile_id: data[0x023],
epoch: read_u32_le(data, 0x024),
created_ns: read_u64_le(data, 0x028),
modified_ns: read_u64_le(data, 0x030),
// Hotset pointers
entrypoint_seg_offset: read_u64_le(data, 0x038),
entrypoint_block_offset: read_u32_le(data, 0x040),
entrypoint_count: read_u32_le(data, 0x044),
toplayer_seg_offset: read_u64_le(data, 0x048),
toplayer_block_offset: read_u32_le(data, 0x050),
toplayer_node_count: read_u32_le(data, 0x054),
centroid_seg_offset: read_u64_le(data, 0x058),
centroid_block_offset: read_u32_le(data, 0x060),
centroid_count: read_u32_le(data, 0x064),
quantdict_seg_offset: read_u64_le(data, 0x068),
quantdict_block_offset: read_u32_le(data, 0x070),
quantdict_size: read_u32_le(data, 0x074),
hot_cache_seg_offset: read_u64_le(data, 0x078),
hot_cache_block_offset: read_u32_le(data, 0x080),
hot_cache_vector_count: read_u32_le(data, 0x084),
prefetch_map_offset: read_u64_le(data, 0x088),
prefetch_map_entries: read_u32_le(data, 0x090),
root_checksum: stored_checksum,
})
}
/// Serialize a Level 0 root manifest into a 4096-byte array.
///
/// Computes and stores the CRC32C checksum at offset 0xFFC.
pub fn write_root_manifest(root: &Level0Root) -> [u8; ROOT_MANIFEST_SIZE] {
let mut buf = [0u8; ROOT_MANIFEST_SIZE];
write_u32_le(&mut buf, 0x000, root.magic);
write_u16_le(&mut buf, 0x004, root.version);
write_u16_le(&mut buf, 0x006, root.flags);
write_u64_le(&mut buf, 0x008, root.l1_manifest_offset);
write_u64_le(&mut buf, 0x010, root.l1_manifest_length);
write_u64_le(&mut buf, 0x018, root.total_vector_count);
write_u16_le(&mut buf, 0x020, root.dimension);
buf[0x022] = root.base_dtype;
buf[0x023] = root.profile_id;
write_u32_le(&mut buf, 0x024, root.epoch);
write_u64_le(&mut buf, 0x028, root.created_ns);
write_u64_le(&mut buf, 0x030, root.modified_ns);
// Hotset pointers
write_u64_le(&mut buf, 0x038, root.entrypoint_seg_offset);
write_u32_le(&mut buf, 0x040, root.entrypoint_block_offset);
write_u32_le(&mut buf, 0x044, root.entrypoint_count);
write_u64_le(&mut buf, 0x048, root.toplayer_seg_offset);
write_u32_le(&mut buf, 0x050, root.toplayer_block_offset);
write_u32_le(&mut buf, 0x054, root.toplayer_node_count);
write_u64_le(&mut buf, 0x058, root.centroid_seg_offset);
write_u32_le(&mut buf, 0x060, root.centroid_block_offset);
write_u32_le(&mut buf, 0x064, root.centroid_count);
write_u64_le(&mut buf, 0x068, root.quantdict_seg_offset);
write_u32_le(&mut buf, 0x070, root.quantdict_block_offset);
write_u32_le(&mut buf, 0x074, root.quantdict_size);
write_u64_le(&mut buf, 0x078, root.hot_cache_seg_offset);
write_u32_le(&mut buf, 0x080, root.hot_cache_block_offset);
write_u32_le(&mut buf, 0x084, root.hot_cache_vector_count);
write_u64_le(&mut buf, 0x088, root.prefetch_map_offset);
write_u32_le(&mut buf, 0x090, root.prefetch_map_entries);
// Compute and write CRC32C
let checksum = compute_crc32c(&buf[..0xFFC]);
write_u32_le(&mut buf, 0xFFC, checksum);
buf
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_default() {
let root = Level0Root::default();
let buf = write_root_manifest(&root);
assert_eq!(buf.len(), ROOT_MANIFEST_SIZE);
let decoded = read_root_manifest(&buf).unwrap();
assert_eq!(decoded.magic, ROOT_MANIFEST_MAGIC);
assert_eq!(decoded.version, 1);
assert_eq!(decoded.total_vector_count, 0);
}
#[test]
fn round_trip_with_values() {
let root = Level0Root {
magic: ROOT_MANIFEST_MAGIC,
version: 1,
flags: 0,
l1_manifest_offset: 4096,
l1_manifest_length: 2048,
total_vector_count: 1_000_000,
dimension: 384,
base_dtype: 1, // f16
profile_id: 2, // text
epoch: 42,
created_ns: 1700000000000000000,
modified_ns: 1700000001000000000,
entrypoint_seg_offset: 8192,
entrypoint_block_offset: 64,
entrypoint_count: 10,
toplayer_seg_offset: 16384,
toplayer_block_offset: 128,
toplayer_node_count: 100,
centroid_seg_offset: 32768,
centroid_block_offset: 0,
centroid_count: 256,
quantdict_seg_offset: 65536,
quantdict_block_offset: 0,
quantdict_size: 4096,
hot_cache_seg_offset: 131072,
hot_cache_block_offset: 0,
hot_cache_vector_count: 1000,
prefetch_map_offset: 262144,
prefetch_map_entries: 50,
root_checksum: 0, // will be computed
};
let buf = write_root_manifest(&root);
let decoded = read_root_manifest(&buf).unwrap();
assert_eq!(decoded.total_vector_count, 1_000_000);
assert_eq!(decoded.dimension, 384);
assert_eq!(decoded.base_dtype, 1);
assert_eq!(decoded.epoch, 42);
assert_eq!(decoded.entrypoint_count, 10);
assert_eq!(decoded.hot_cache_vector_count, 1000);
}
#[test]
fn invalid_magic_rejected() {
let mut buf = [0u8; ROOT_MANIFEST_SIZE];
buf[0..4].copy_from_slice(&0xDEADBEEFu32.to_le_bytes());
let result = read_root_manifest(&buf);
assert!(result.is_err());
}
#[test]
fn corrupted_checksum_rejected() {
let root = Level0Root::default();
let mut buf = write_root_manifest(&root);
// Corrupt one byte in the data area
buf[0x020] ^= 0xFF;
let result = read_root_manifest(&buf);
assert!(result.is_err());
}
#[test]
fn truncated_data_rejected() {
let result = read_root_manifest(&[0u8; 100]);
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,174 @@
//! Segment header reader and validator.
//!
//! Reads the fixed 64-byte segment header from a byte slice, validates
//! magic and version fields, and optionally verifies the content hash.
use crate::hash::verify_content_hash;
use rvf_types::{
ErrorCode, RvfError, SegmentHeader, SEGMENT_HEADER_SIZE, SEGMENT_MAGIC, SEGMENT_VERSION,
};
/// Read and parse a segment header from the first 64 bytes of `data`.
///
/// Validates the magic number and format version. Does not verify the
/// content hash (use `validate_segment` for that).
///
/// # Errors
///
/// - `InvalidMagic` if the magic number does not match `RVFS`.
/// - `InvalidVersion` if the version is not supported.
/// - `TruncatedSegment` if `data` is shorter than 64 bytes.
pub fn read_segment_header(data: &[u8]) -> Result<SegmentHeader, RvfError> {
if data.len() < SEGMENT_HEADER_SIZE {
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
}
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
if magic != SEGMENT_MAGIC {
return Err(RvfError::BadMagic {
expected: SEGMENT_MAGIC,
got: magic,
});
}
let version = data[4];
if version != SEGMENT_VERSION {
return Err(RvfError::Code(ErrorCode::InvalidVersion));
}
let seg_type = data[5];
let flags = u16::from_le_bytes([data[6], data[7]]);
let segment_id = u64::from_le_bytes(data[0x08..0x10].try_into().unwrap());
let payload_length = u64::from_le_bytes(data[0x10..0x18].try_into().unwrap());
let timestamp_ns = u64::from_le_bytes(data[0x18..0x20].try_into().unwrap());
let checksum_algo = data[0x20];
let compression = data[0x21];
let reserved_0 = u16::from_le_bytes([data[0x22], data[0x23]]);
let reserved_1 = u32::from_le_bytes(data[0x24..0x28].try_into().unwrap());
let mut content_hash = [0u8; 16];
content_hash.copy_from_slice(&data[0x28..0x38]);
let uncompressed_len = u32::from_le_bytes(data[0x38..0x3C].try_into().unwrap());
let alignment_pad = u32::from_le_bytes(data[0x3C..0x40].try_into().unwrap());
Ok(SegmentHeader {
magic,
version,
seg_type,
flags,
segment_id,
payload_length,
timestamp_ns,
checksum_algo,
compression,
reserved_0,
reserved_1,
content_hash,
uncompressed_len,
alignment_pad,
})
}
/// Validate the content hash of a segment.
///
/// Computes the hash of `payload` using the algorithm specified in
/// `header.checksum_algo` and compares it against `header.content_hash`.
///
/// The SEALED flag (0x0008) is intentionally NOT treated as a bypass for hash
/// verification. A segment being sealed only means it was verified during
/// compaction, but an attacker could set the flag on a corrupted segment to
/// skip validation. Always verify the content hash regardless of flags.
///
/// # Errors
///
/// - `InvalidChecksum` if the computed hash does not match.
pub fn validate_segment(header: &SegmentHeader, payload: &[u8]) -> Result<(), RvfError> {
// Always verify the content hash. The SEALED flag is not a reason to skip
// verification -- an attacker could set the flag on a tampered segment to
// bypass integrity checks.
if !verify_content_hash(header, payload) {
return Err(RvfError::Code(ErrorCode::InvalidChecksum));
}
Ok(())
}
/// Read a complete segment: header + payload slice.
///
/// Returns the parsed header and a sub-slice of `data` containing the
/// payload bytes. The payload slice starts at offset 64 and extends for
/// `header.payload_length` bytes.
///
/// # Errors
///
/// - Any error from `read_segment_header`.
/// - `TruncatedSegment` if `data` does not contain enough bytes for the
/// declared payload length.
pub fn read_segment(data: &[u8]) -> Result<(SegmentHeader, &[u8]), RvfError> {
let header = read_segment_header(data)?;
let payload_end = SEGMENT_HEADER_SIZE + header.payload_length as usize;
if data.len() < payload_end {
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
}
let payload = &data[SEGMENT_HEADER_SIZE..payload_end];
Ok((header, payload))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::writer::write_segment;
use rvf_types::{SegmentFlags, SegmentType};
#[test]
fn read_write_round_trip() {
let payload = b"hello vector world";
let flags = SegmentFlags::empty();
let seg = write_segment(SegmentType::Vec as u8, payload, flags, 42);
let (header, decoded_payload) = read_segment(&seg).unwrap();
assert_eq!(header.magic, SEGMENT_MAGIC);
assert_eq!(header.version, SEGMENT_VERSION);
assert_eq!(header.seg_type, SegmentType::Vec as u8);
assert_eq!(header.segment_id, 42);
assert_eq!(decoded_payload, payload);
}
#[test]
fn validate_segment_succeeds() {
let payload = b"data for validation";
let flags = SegmentFlags::empty();
let seg = write_segment(SegmentType::Index as u8, payload, flags, 1);
let (header, decoded_payload) = read_segment(&seg).unwrap();
assert!(validate_segment(&header, decoded_payload).is_ok());
}
#[test]
fn validate_segment_detects_corruption() {
let payload = b"original data";
let flags = SegmentFlags::empty();
let seg = write_segment(SegmentType::Vec as u8, payload, flags, 1);
let (header, _) = read_segment(&seg).unwrap();
assert!(validate_segment(&header, b"corrupted data").is_err());
}
#[test]
fn truncated_header_returns_error() {
let result = read_segment_header(&[0u8; 32]);
assert!(result.is_err());
}
#[test]
fn wrong_magic_returns_error() {
let mut data = [0u8; 64];
data[0..4].copy_from_slice(&0xDEADBEEFu32.to_le_bytes());
let result = read_segment_header(&data);
assert!(result.is_err());
}
#[test]
fn wrong_version_returns_error() {
let mut data = [0u8; 64];
data[0..4].copy_from_slice(&SEGMENT_MAGIC.to_le_bytes());
data[4] = 99; // bad version
let result = read_segment_header(&data);
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,172 @@
//! Tail-scan algorithm for finding the latest manifest segment.
//!
//! An RVF file is discovered from its tail: the Level 0 root manifest is
//! always the last 4096 bytes. If that's invalid, we scan backward at
//! 64-byte boundaries looking for a MANIFEST_SEG header.
use crate::reader::read_segment_header;
use rvf_types::{
ErrorCode, RvfError, SegmentHeader, SegmentType, ROOT_MANIFEST_MAGIC, ROOT_MANIFEST_SIZE,
SEGMENT_ALIGNMENT, SEGMENT_HEADER_SIZE, SEGMENT_MAGIC, SEGMENT_VERSION,
};
/// Find the latest manifest segment in `data` by scanning from the tail.
///
/// **Fast path**: check the last 4096 bytes for the root manifest magic
/// (`RVM0`). If valid, scan backward from that point for the enclosing
/// MANIFEST_SEG header.
///
/// **Slow path**: scan backward from the end of `data` at 64-byte aligned
/// boundaries, looking for a segment header with magic `RVFS` and type
/// `MANIFEST_SEG` (0x05).
///
/// Returns `(byte_offset, SegmentHeader)` of the manifest segment.
///
/// # Errors
///
/// - `ManifestNotFound` if no valid MANIFEST_SEG is found in the entire file.
/// - `TruncatedSegment` if the file is too short to contain any segment.
pub fn find_latest_manifest(data: &[u8]) -> Result<(usize, SegmentHeader), RvfError> {
if data.len() < SEGMENT_HEADER_SIZE {
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
}
// Fast path: check last 4096 bytes for RVM0 magic
if data.len() >= ROOT_MANIFEST_SIZE {
let root_start = data.len() - ROOT_MANIFEST_SIZE;
let root_slice = &data[root_start..];
if root_slice.len() >= 4 {
let root_magic =
u32::from_le_bytes([root_slice[0], root_slice[1], root_slice[2], root_slice[3]]);
if root_magic == ROOT_MANIFEST_MAGIC {
// Scan backward from root_start for the enclosing MANIFEST_SEG header
let scan_limit = root_start.saturating_sub(64 * 1024);
let mut scan_pos = root_start & !(SEGMENT_ALIGNMENT - 1);
loop {
if scan_pos + SEGMENT_HEADER_SIZE <= data.len() {
if let Ok(header) = read_segment_header(&data[scan_pos..]) {
if header.seg_type == SegmentType::Manifest as u8 {
let seg_end =
scan_pos + SEGMENT_HEADER_SIZE + header.payload_length as usize;
if seg_end >= root_start + ROOT_MANIFEST_SIZE
|| seg_end >= data.len()
{
return Ok((scan_pos, header));
}
}
}
}
if scan_pos <= scan_limit || scan_pos == 0 {
break;
}
scan_pos = scan_pos.saturating_sub(SEGMENT_ALIGNMENT);
}
}
}
}
// Slow path: scan backward using the first magic byte ('R' = 0x52) as
// an anchor. On aligned boundaries within the data, we search for the
// magic byte first (memchr-style) to skip large runs of non-magic data,
// then verify the full 4-byte magic + version + type.
let magic_le = SEGMENT_MAGIC.to_le_bytes();
let magic_first = magic_le[0]; // 'R' = 0x52
let last_aligned = (data.len().saturating_sub(SEGMENT_ALIGNMENT)) & !(SEGMENT_ALIGNMENT - 1);
let mut scan_pos = last_aligned;
loop {
if scan_pos + SEGMENT_HEADER_SIZE <= data.len() {
// Quick rejection: check first byte of magic before doing full comparison.
if data[scan_pos] == magic_first {
let magic = u32::from_le_bytes([
data[scan_pos],
data[scan_pos + 1],
data[scan_pos + 2],
data[scan_pos + 3],
]);
if magic == SEGMENT_MAGIC
&& data[scan_pos + 4] == SEGMENT_VERSION
&& data[scan_pos + 5] == SegmentType::Manifest as u8
{
if let Ok(header) = read_segment_header(&data[scan_pos..]) {
return Ok((scan_pos, header));
}
}
}
}
if scan_pos == 0 {
break;
}
scan_pos -= SEGMENT_ALIGNMENT;
}
Err(RvfError::Code(ErrorCode::ManifestNotFound))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::writer::write_segment;
use rvf_types::SegmentFlags;
fn make_manifest_segment(segment_id: u64, payload: &[u8]) -> Vec<u8> {
write_segment(
SegmentType::Manifest as u8,
payload,
SegmentFlags::empty(),
segment_id,
)
}
#[test]
fn find_single_manifest() {
let vec_seg = write_segment(
SegmentType::Vec as u8,
&[1u8; 100],
SegmentFlags::empty(),
0,
);
let manifest_payload = vec![0u8; 64];
let manifest_seg = make_manifest_segment(1, &manifest_payload);
let mut file = vec_seg.clone();
let manifest_offset = file.len();
file.extend_from_slice(&manifest_seg);
let (offset, header) = find_latest_manifest(&file).unwrap();
assert_eq!(offset, manifest_offset);
assert_eq!(header.seg_type, SegmentType::Manifest as u8);
assert_eq!(header.segment_id, 1);
}
#[test]
fn find_latest_of_multiple_manifests() {
let vec_seg = write_segment(SegmentType::Vec as u8, &[0u8; 32], SegmentFlags::empty(), 0);
let m1 = make_manifest_segment(1, &[0u8; 32]);
let m2 = make_manifest_segment(2, &[0u8; 32]);
let mut file = vec_seg;
file.extend_from_slice(&m1);
let m2_offset = file.len();
file.extend_from_slice(&m2);
let (offset, header) = find_latest_manifest(&file).unwrap();
assert_eq!(offset, m2_offset);
assert_eq!(header.segment_id, 2);
}
#[test]
fn no_manifest_returns_error() {
let vec_seg = write_segment(
SegmentType::Vec as u8,
&[0u8; 100],
SegmentFlags::empty(),
0,
);
let result = find_latest_manifest(&vec_seg);
assert!(result.is_err());
}
#[test]
fn too_short_returns_error() {
let result = find_latest_manifest(&[0u8; 10]);
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,150 @@
//! LEB128 unsigned varint encoding and decoding.
//!
//! Values up to `u64::MAX` are encoded in 1-10 bytes. Each byte uses the
//! high bit as a continuation flag: `1` means more bytes follow, `0` means
//! this is the last byte. The remaining 7 bits contribute to the value
//! in little-endian order.
use rvf_types::{ErrorCode, RvfError};
/// Maximum number of bytes a u64 varint can occupy.
pub const MAX_VARINT_LEN: usize = 10;
/// Encode a `u64` value as a LEB128 varint into `buf`.
///
/// Returns the number of bytes written. The caller must ensure `buf` is at
/// least `MAX_VARINT_LEN` bytes long.
///
/// # Panics
///
/// Panics if `buf` is shorter than the number of bytes required.
pub fn encode_varint(mut value: u64, buf: &mut [u8]) -> usize {
let mut i = 0;
loop {
let byte = (value & 0x7F) as u8;
value >>= 7;
if value == 0 {
buf[i] = byte;
return i + 1;
}
buf[i] = byte | 0x80;
i += 1;
}
}
/// Decode a LEB128 varint from `buf`.
///
/// Returns `(value, bytes_consumed)` on success.
///
/// Uses branchless fast paths for the common 1-byte and 2-byte cases,
/// falling back to a loop for longer encodings.
///
/// # Errors
///
/// Returns `RvfError` if the buffer is too short or the varint exceeds 10
/// bytes (which would overflow a u64).
pub fn decode_varint(buf: &[u8]) -> Result<(u64, usize), RvfError> {
if buf.is_empty() {
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
}
// Fast path: 1-byte varint (values 0-127, most common case).
let b0 = buf[0];
if b0 & 0x80 == 0 {
return Ok((b0 as u64, 1));
}
// Fast path: 2-byte varint (values 128-16383).
if buf.len() < 2 {
return Err(RvfError::Code(ErrorCode::TruncatedSegment));
}
let b1 = buf[1];
if b1 & 0x80 == 0 {
let value = ((b0 & 0x7F) as u64) | ((b1 as u64) << 7);
return Ok((value, 2));
}
// Slow path: 3+ byte varint.
let mut value = ((b0 & 0x7F) as u64) | (((b1 & 0x7F) as u64) << 7);
let mut shift: u32 = 14;
let limit = buf.len().min(MAX_VARINT_LEN);
for (i, &byte) in buf.iter().enumerate().take(limit).skip(2) {
value |= ((byte & 0x7F) as u64) << shift;
if byte & 0x80 == 0 {
return Ok((value, i + 1));
}
shift += 7;
}
Err(RvfError::Code(ErrorCode::TruncatedSegment))
}
/// Returns the number of bytes required to encode `value` as a varint.
pub fn varint_size(mut value: u64) -> usize {
let mut size = 1;
while value >= 0x80 {
value >>= 7;
size += 1;
}
size
}
#[cfg(test)]
mod tests {
use super::*;
fn round_trip(value: u64) {
let mut buf = [0u8; MAX_VARINT_LEN];
let written = encode_varint(value, &mut buf);
let (decoded, consumed) = decode_varint(&buf[..written]).unwrap();
assert_eq!(decoded, value);
assert_eq!(consumed, written);
}
#[test]
fn single_byte_values() {
round_trip(0);
round_trip(1);
round_trip(127);
}
#[test]
fn two_byte_values() {
round_trip(128);
round_trip(255);
round_trip(16383);
}
#[test]
fn multi_byte_values() {
round_trip(16384);
round_trip(2_097_151);
round_trip(u32::MAX as u64);
}
#[test]
fn max_u64() {
round_trip(u64::MAX);
}
#[test]
fn encode_size_matches() {
for &val in &[0u64, 1, 127, 128, 16383, 16384, u32::MAX as u64, u64::MAX] {
let mut buf = [0u8; MAX_VARINT_LEN];
let written = encode_varint(val, &mut buf);
assert_eq!(varint_size(val), written);
}
}
#[test]
fn decode_truncated_returns_error() {
// A single byte with continuation bit set, but no following byte
let result = decode_varint(&[0x80]);
assert!(result.is_err());
}
#[test]
fn decode_empty_returns_error() {
let result = decode_varint(&[]);
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,274 @@
//! VEC_SEG block codec.
//!
//! Parses and writes the block directory, columnar vector data, ID map with
//! delta-varint encoding, and per-block CRC32C.
use crate::delta::{decode_delta, encode_delta};
use crate::hash::compute_crc32c;
/// A single entry in the block directory.
#[derive(Clone, Debug, PartialEq)]
pub struct BlockDirEntry {
pub block_offset: u32,
pub vector_count: u32,
pub dim: u16,
pub dtype: u8,
pub tier: u8,
}
/// Size of the block directory header (block_count: u32).
const DIR_HEADER_SIZE: usize = 4;
/// Size of each directory entry: offset(4) + count(4) + dim(2) + dtype(1) + tier(1) = 12.
const DIR_ENTRY_SIZE: usize = 12;
/// Parsed VEC_SEG block directory.
#[derive(Clone, Debug)]
pub struct BlockDirectory {
pub entries: Vec<BlockDirEntry>,
}
/// A decoded VEC_SEG block.
#[derive(Clone, Debug)]
pub struct VecBlock {
/// Columnar vector data (all dims for dim 0, then dim 1, etc).
pub vector_data: Vec<u8>,
/// Decoded vector IDs.
pub ids: Vec<u64>,
/// Dimensions.
pub dim: u16,
/// Data type.
pub dtype: u8,
/// Temperature tier.
pub tier: u8,
}
/// Parse the block directory from the start of a VEC_SEG payload.
///
/// Returns the directory and the number of bytes consumed.
pub fn read_block_directory(data: &[u8]) -> Result<(BlockDirectory, usize), &'static str> {
if data.len() < DIR_HEADER_SIZE {
return Err("block directory truncated");
}
let block_count = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
let dir_size = DIR_HEADER_SIZE + block_count * DIR_ENTRY_SIZE;
if data.len() < dir_size {
return Err("block directory entries truncated");
}
let mut entries = Vec::with_capacity(block_count);
for i in 0..block_count {
let base = DIR_HEADER_SIZE + i * DIR_ENTRY_SIZE;
entries.push(BlockDirEntry {
block_offset: u32::from_le_bytes(data[base..base + 4].try_into().unwrap()),
vector_count: u32::from_le_bytes(data[base + 4..base + 8].try_into().unwrap()),
dim: u16::from_le_bytes([data[base + 8], data[base + 9]]),
dtype: data[base + 10],
tier: data[base + 11],
});
}
// Align consumed size to 64 bytes
let consumed = (dir_size + 63) & !63;
Ok((BlockDirectory { entries }, consumed))
}
/// Write a block directory to a Vec<u8>. Pads to 64-byte alignment.
pub fn write_block_directory(entries: &[BlockDirEntry]) -> Vec<u8> {
let dir_size = DIR_HEADER_SIZE + entries.len() * DIR_ENTRY_SIZE;
let padded = (dir_size + 63) & !63;
let mut buf = vec![0u8; padded];
buf[0..4].copy_from_slice(&(entries.len() as u32).to_le_bytes());
for (i, e) in entries.iter().enumerate() {
let base = DIR_HEADER_SIZE + i * DIR_ENTRY_SIZE;
buf[base..base + 4].copy_from_slice(&e.block_offset.to_le_bytes());
buf[base + 4..base + 8].copy_from_slice(&e.vector_count.to_le_bytes());
buf[base + 8..base + 10].copy_from_slice(&e.dim.to_le_bytes());
buf[base + 10] = e.dtype;
buf[base + 11] = e.tier;
}
buf
}
/// Size of an element in bytes for a given dtype.
fn dtype_element_size(dtype: u8) -> usize {
match dtype {
0x00 => 4, // f32
0x01 => 2, // f16
0x02 => 2, // bf16
0x03 => 1, // i8
0x04 => 1, // u8
_ => 1, // fallback
}
}
/// Default restart interval for delta-encoded ID maps.
const DEFAULT_RESTART_INTERVAL: u32 = 128;
/// Write a single VEC_SEG block (columnar vectors + ID map + CRC32C).
/// Returns the serialized block bytes, padded to 64-byte alignment.
pub fn write_vec_block(block: &VecBlock) -> Vec<u8> {
let mut payload = Vec::new();
// Columnar vector data
payload.extend_from_slice(&block.vector_data);
// ID map header
let encoding: u8 = 1; // delta-varint
let restart_interval: u16 = DEFAULT_RESTART_INTERVAL as u16;
let id_count: u32 = block.ids.len() as u32;
payload.push(encoding);
payload.extend_from_slice(&restart_interval.to_le_bytes());
payload.extend_from_slice(&id_count.to_le_bytes());
// For delta encoding, we store restart offsets (simplified: omit for now)
// and then the encoded IDs.
let _restart_count = if block.ids.is_empty() {
0u32
} else {
((block.ids.len() as u32 - 1) / DEFAULT_RESTART_INTERVAL) + 1
};
let mut id_buf = Vec::new();
encode_delta(&block.ids, DEFAULT_RESTART_INTERVAL, &mut id_buf);
payload.extend_from_slice(&id_buf);
// Block CRC32C
let crc = compute_crc32c(&payload);
payload.extend_from_slice(&crc.to_le_bytes());
// Pad to 64-byte alignment
let padded_len = (payload.len() + 63) & !63;
payload.resize(padded_len, 0);
payload
}
/// Read a single VEC_SEG block at the given offset in the payload.
///
/// `entry` provides the block metadata from the directory.
/// `payload` is the full VEC_SEG payload.
pub fn read_vec_block(payload: &[u8], entry: &BlockDirEntry) -> Result<VecBlock, &'static str> {
let offset = entry.block_offset as usize;
if offset >= payload.len() {
return Err("block offset beyond payload");
}
let block_data = &payload[offset..];
let elem_size = dtype_element_size(entry.dtype);
let vector_data_len = entry.vector_count as usize * entry.dim as usize * elem_size;
if block_data.len() < vector_data_len + 1 + 2 + 4 {
return Err("block data truncated");
}
let vector_data = block_data[..vector_data_len].to_vec();
let mut pos = vector_data_len;
let encoding = block_data[pos];
pos += 1;
let restart_interval = u16::from_le_bytes([block_data[pos], block_data[pos + 1]]);
pos += 2;
let id_count = u32::from_le_bytes(block_data[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4;
let ids = if encoding == 1 {
// Delta-varint encoded
decode_delta(&block_data[pos..], id_count, restart_interval as u32)
} else {
// Raw u64 IDs
let mut ids = Vec::with_capacity(id_count);
for i in 0..id_count {
let id_offset = pos + i * 8;
if id_offset + 8 > block_data.len() {
return Err("raw ID data truncated");
}
ids.push(u64::from_le_bytes(
block_data[id_offset..id_offset + 8].try_into().unwrap(),
));
}
ids
};
Ok(VecBlock {
vector_data,
ids,
dim: entry.dim,
dtype: entry.dtype,
tier: entry.tier,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn block_directory_round_trip() {
let entries = vec![
BlockDirEntry {
block_offset: 64,
vector_count: 100,
dim: 128,
dtype: 0,
tier: 0,
},
BlockDirEntry {
block_offset: 51264,
vector_count: 200,
dim: 128,
dtype: 1,
tier: 1,
},
];
let buf = write_block_directory(&entries);
assert_eq!(buf.len() % 64, 0);
let (dir, _consumed) = read_block_directory(&buf).unwrap();
assert_eq!(dir.entries.len(), 2);
assert_eq!(dir.entries[0].block_offset, 64);
assert_eq!(dir.entries[0].vector_count, 100);
assert_eq!(dir.entries[0].dim, 128);
assert_eq!(dir.entries[1].dtype, 1);
assert_eq!(dir.entries[1].tier, 1);
}
#[test]
fn vec_block_write_read_round_trip() {
// Create a block with 4 vectors of dimension 3 (f32)
let dim = 3u16;
let count = 4u32;
// Columnar: all dim_0, then dim_1, then dim_2
let mut vector_data = Vec::new();
for d in 0..dim {
for v in 0..count {
let val = (d as f32) * 10.0 + (v as f32);
vector_data.extend_from_slice(&val.to_le_bytes());
}
}
let ids = vec![10, 20, 30, 40];
let block = VecBlock {
vector_data: vector_data.clone(),
ids: ids.clone(),
dim,
dtype: 0, // f32
tier: 0,
};
let block_bytes = write_vec_block(&block);
assert_eq!(block_bytes.len() % 64, 0);
// Build a payload with a directory entry pointing to offset 0
let entry = BlockDirEntry {
block_offset: 0,
vector_count: count,
dim,
dtype: 0,
tier: 0,
};
let decoded = read_vec_block(&block_bytes, &entry).unwrap();
assert_eq!(decoded.vector_data, vector_data);
assert_eq!(decoded.ids, ids);
}
#[test]
fn empty_directory() {
let buf = write_block_directory(&[]);
let (dir, _) = read_block_directory(&buf).unwrap();
assert!(dir.entries.is_empty());
}
}

View File

@@ -0,0 +1,166 @@
//! Segment writer: serializes a segment header + payload into a byte buffer.
//!
//! The writer computes the content hash (XXH3-128 by default), sets the
//! timestamp, and pads the output to a 64-byte boundary.
use crate::hash::compute_content_hash;
use rvf_types::{
SegmentFlags, SegmentHeader, SEGMENT_ALIGNMENT, SEGMENT_HEADER_SIZE, SEGMENT_MAGIC,
SEGMENT_VERSION,
};
/// Default checksum algorithm: XXH3-128.
const DEFAULT_CHECKSUM_ALGO: u8 = 1;
/// Calculate the total padded size of a segment (header + payload + padding).
///
/// The result is always a multiple of `SEGMENT_ALIGNMENT` (64 bytes).
pub fn calculate_padded_size(header_size: usize, payload_size: usize) -> usize {
let raw = header_size + payload_size;
(raw + SEGMENT_ALIGNMENT - 1) & !(SEGMENT_ALIGNMENT - 1)
}
/// Serialize a complete segment: 64-byte header + payload + zero-padding to
/// the next 64-byte boundary.
///
/// The content hash is computed over the raw payload using XXH3-128 (algo=1).
/// The timestamp is set to 0 (callers should overwrite if needed).
pub fn write_segment(
seg_type: u8,
payload: &[u8],
flags: SegmentFlags,
segment_id: u64,
) -> Vec<u8> {
write_segment_with_algo(seg_type, payload, flags, segment_id, DEFAULT_CHECKSUM_ALGO)
}
/// Like `write_segment`, but allows specifying the checksum algorithm.
pub fn write_segment_with_algo(
seg_type: u8,
payload: &[u8],
flags: SegmentFlags,
segment_id: u64,
checksum_algo: u8,
) -> Vec<u8> {
let content_hash = compute_content_hash(checksum_algo, payload);
let total_size = calculate_padded_size(SEGMENT_HEADER_SIZE, payload.len());
let padding = total_size - SEGMENT_HEADER_SIZE - payload.len();
let header = SegmentHeader {
magic: SEGMENT_MAGIC,
version: SEGMENT_VERSION,
seg_type,
flags: flags.bits(),
segment_id,
payload_length: payload.len() as u64,
timestamp_ns: 0,
checksum_algo,
compression: 0,
reserved_0: 0,
reserved_1: 0,
content_hash,
uncompressed_len: 0,
alignment_pad: padding as u32,
};
let mut buf = Vec::with_capacity(total_size);
// Serialize header fields in little-endian order
buf.extend_from_slice(&header.magic.to_le_bytes());
buf.push(header.version);
buf.push(header.seg_type);
buf.extend_from_slice(&header.flags.to_le_bytes());
buf.extend_from_slice(&header.segment_id.to_le_bytes());
buf.extend_from_slice(&header.payload_length.to_le_bytes());
buf.extend_from_slice(&header.timestamp_ns.to_le_bytes());
buf.push(header.checksum_algo);
buf.push(header.compression);
buf.extend_from_slice(&header.reserved_0.to_le_bytes());
buf.extend_from_slice(&header.reserved_1.to_le_bytes());
buf.extend_from_slice(&header.content_hash);
buf.extend_from_slice(&header.uncompressed_len.to_le_bytes());
buf.extend_from_slice(&header.alignment_pad.to_le_bytes());
debug_assert_eq!(buf.len(), SEGMENT_HEADER_SIZE);
// Payload
buf.extend_from_slice(payload);
// Zero-padding to 64-byte alignment
buf.resize(total_size, 0);
buf
}
#[cfg(test)]
mod tests {
use super::*;
use rvf_types::SegmentType;
#[test]
fn output_is_64_byte_aligned() {
for payload_size in [0, 1, 10, 63, 64, 65, 127, 128, 1000] {
let payload = vec![0xABu8; payload_size];
let seg = write_segment(SegmentType::Vec as u8, &payload, SegmentFlags::empty(), 0);
assert_eq!(
seg.len() % SEGMENT_ALIGNMENT,
0,
"not aligned for payload_size={payload_size}"
);
}
}
#[test]
fn header_magic_and_version() {
let seg = write_segment(SegmentType::Vec as u8, b"test", SegmentFlags::empty(), 1);
let magic = u32::from_le_bytes([seg[0], seg[1], seg[2], seg[3]]);
assert_eq!(magic, SEGMENT_MAGIC);
assert_eq!(seg[4], SEGMENT_VERSION);
}
#[test]
fn segment_id_is_stored() {
let seg = write_segment(
SegmentType::Index as u8,
b"idx",
SegmentFlags::empty(),
12345,
);
let id = u64::from_le_bytes(seg[0x08..0x10].try_into().unwrap());
assert_eq!(id, 12345);
}
#[test]
fn flags_are_stored() {
let flags = SegmentFlags::empty()
.with(SegmentFlags::COMPRESSED)
.with(SegmentFlags::SEALED);
let seg = write_segment(SegmentType::Vec as u8, b"data", flags, 0);
let stored_flags = u16::from_le_bytes([seg[6], seg[7]]);
assert!(stored_flags & SegmentFlags::COMPRESSED != 0);
assert!(stored_flags & SegmentFlags::SEALED != 0);
}
#[test]
fn payload_length_matches() {
let payload = b"hello world!";
let seg = write_segment(SegmentType::Vec as u8, payload, SegmentFlags::empty(), 0);
let len = u64::from_le_bytes(seg[0x10..0x18].try_into().unwrap());
assert_eq!(len, payload.len() as u64);
}
#[test]
fn calculate_padded_size_examples() {
assert_eq!(calculate_padded_size(64, 0), 64);
assert_eq!(calculate_padded_size(64, 1), 128);
assert_eq!(calculate_padded_size(64, 64), 128);
assert_eq!(calculate_padded_size(64, 65), 192);
}
#[test]
fn empty_payload() {
let seg = write_segment(SegmentType::Meta as u8, &[], SegmentFlags::empty(), 0);
assert_eq!(seg.len(), 64);
let len = u64::from_le_bytes(seg[0x10..0x18].try_into().unwrap());
assert_eq!(len, 0);
}
}