use crate::KeyRing;
pub mod iter;
pub mod scan;
pub use scan::ScanIter;
use std::fs::{self, File};
use std::io::{Read, Seek};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use crate::error::ReadError;
use crate::record::Record;
use crate::storage::format::{
deserialize_record, read_frame_header, SegmentHeader, FRAME_HEADER_SIZE, MIN_RECORD_SIZE,
SEGMENT_HEADER_SIZE,
};
use crate::storage::index::SparseIndex;
use iter::RecordIter;
#[cfg(feature = "compression")]
fn decompress_frame(compressed: &[u8]) -> Result<Vec<u8>, String> {
zstd::decode_all(compressed).map_err(|e| format!("zstd decode: {}", e))
}
#[cfg(not(feature = "compression"))]
fn decompress_frame(_compressed: &[u8]) -> Result<Vec<u8>, String> {
Err("compression feature not enabled".into())
}
#[cfg(feature = "encryption")]
fn decrypt_frame_data(keys: &KeyRing, encrypted: &[u8]) -> Result<Vec<u8>, String> {
let nonce_size = crate::storage::format::ENCRYPTION_NONCE_SIZE;
if encrypted.len() < nonce_size {
return Err("too short".into());
}
use aes_gcm::aead::{Aead, KeyInit};
use aes_gcm::{Aes256Gcm, Key, Nonce};
let nonce = Nonce::from_slice(&encrypted[..nonce_size]);
let ct = &encrypted[nonce_size..];
for (_id, k) in &keys.decrypt_keys {
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&***k));
if let Ok(plain) = cipher.decrypt(nonce, ct) {
return Ok(plain);
}
}
Err("decryption failed".into())
}
#[cfg(not(feature = "encryption"))]
fn decrypt_frame_data(_keys: &KeyRing, _encrypted: &[u8]) -> Result<Vec<u8>, String> {
Err("encryption feature not enabled".into())
}
pub(crate) fn decode_frame_payload(
payload: &[u8],
compressed: bool,
encrypted: bool,
key_ring: Option<&KeyRing>,
) -> Result<Vec<u8>, String> {
let plain = if encrypted {
let kr = key_ring.ok_or_else(|| "encrypted frame but no key provided".to_string())?;
decrypt_frame_data(kr, payload)?
} else {
payload.to_vec()
};
if compressed {
decompress_frame(&plain)
} else {
Ok(plain)
}
}
#[derive(Clone)]
pub(crate) struct ManifestEntry {
segment_id: u32,
path: PathBuf,
base_sequence: u64,
flags: u8,
}
impl ManifestEntry {
pub(crate) fn base_sequence(&self) -> u64 {
self.base_sequence
}
}
pub(crate) struct SegmentManifest {
data_dir: PathBuf,
entries: Vec<ManifestEntry>,
dir_mtime: Option<SystemTime>,
}
impl SegmentManifest {
pub fn new(data_dir: PathBuf) -> Self {
Self {
data_dir,
entries: Vec::new(),
dir_mtime: None,
}
}
fn refresh_if_needed(&mut self) -> Result<bool, ReadError> {
let mtime = fs::metadata(&self.data_dir).and_then(|m| m.modified()).ok();
if mtime == self.dir_mtime && !self.entries.is_empty() {
return Ok(false);
}
let mut entries = Vec::new();
let dir = fs::read_dir(&self.data_dir)
.map_err(|e| ReadError::Io(format!("read_dir {:?}: {}", &self.data_dir, e)))?;
for entry in dir {
let entry = entry.map_err(|e| ReadError::Io(format!("entry: {}", e)))?;
let path = entry.path();
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name.starts_with("segment-") && name.ends_with(".log") {
if let Ok(id) = name[8..name.len() - 4].parse::<u32>() {
if let Some((base_sequence, flags)) = read_header_for_manifest(&path) {
entries.push(ManifestEntry {
segment_id: id,
path,
base_sequence,
flags,
});
}
}
}
}
}
entries.sort_by_key(|e| e.segment_id);
self.entries = entries;
self.dir_mtime = mtime;
Ok(true)
}
fn find_in_cache(&self, seq: u64) -> Option<ManifestEntry> {
let idx = self.entries.partition_point(|e| e.base_sequence <= seq);
if idx == 0 {
None
} else {
Some(self.entries[idx - 1].clone())
}
}
pub(crate) fn force_refresh(&mut self) -> Result<(), ReadError> {
self.dir_mtime = None;
self.refresh_if_needed()?;
Ok(())
}
pub(crate) fn find(&mut self, seq: u64) -> Result<Option<ManifestEntry>, ReadError> {
let refreshed = self.refresh_if_needed()?;
let entry = self.find_in_cache(seq);
if !refreshed {
if let Some(e) = &entry {
if !e.path.exists() {
self.force_refresh()?;
return Ok(self.find_in_cache(seq));
}
}
}
Ok(entry)
}
pub(crate) fn segments_from(&mut self, seq: u64) -> Result<Vec<ManifestEntry>, ReadError> {
self.refresh_if_needed()?;
let start = self
.entries
.partition_point(|e| e.base_sequence <= seq)
.saturating_sub(1);
Ok(self.entries[start..].to_vec())
}
}
fn read_header_for_manifest(path: &Path) -> Option<(u64, u8)> {
let mut file = File::open(path).ok()?;
let mut buf = [0u8; SEGMENT_HEADER_SIZE];
file.read_exact(&mut buf).ok()?;
let header = SegmentHeader::deserialize(&buf).ok()?;
Some((header.base_sequence, header.flags))
}
pub(crate) fn iter_for_segment(
entry: &ManifestEntry,
from_id: u64,
to_id: u64,
key: Option<Arc<KeyRing>>,
) -> Result<iter::RecordIter, ReadError> {
let path = entry.path.clone();
let is_compressed = entry.flags & crate::storage::format::FLAG_COMPRESSED_ZSTD != 0;
let is_encrypted = entry.flags & crate::storage::format::FLAG_ENCRYPTED_AES256GCM != 0;
let file_size = fs::metadata(&path)
.map_err(|e| ReadError::Io(format!("metadata: {}", e)))?
.len();
let start_offset = if is_compressed || is_encrypted {
SEGMENT_HEADER_SIZE as u64
} else {
let idx_path = SparseIndex::index_path(&path);
if idx_path.exists() {
match SparseIndex::load(&idx_path) {
Ok(idx) => match idx.find_anchor(from_id) {
Some((e, _)) => e.file_offset,
None => SEGMENT_HEADER_SIZE as u64,
},
Err(_) => SEGMENT_HEADER_SIZE as u64,
}
} else {
SEGMENT_HEADER_SIZE as u64
}
};
iter::RecordIter::new(
path,
start_offset,
file_size,
from_id,
to_id,
is_compressed,
is_encrypted,
key,
)
}
pub struct Reader {
manifest: Arc<Mutex<SegmentManifest>>,
encryption_keys: Option<Arc<KeyRing>>,
}
impl Reader {
pub(crate) fn new(
manifest: Arc<Mutex<SegmentManifest>>,
encryption_keys: Option<Arc<KeyRing>>,
) -> Self {
Self {
manifest,
encryption_keys,
}
}
pub fn read(&self, record_id: u64) -> Result<Option<Record>, ReadError> {
let entry = match self.manifest.lock().unwrap().find(record_id)? {
Some(e) => e,
None => return Ok(None),
};
let path = entry.path;
let is_compressed = entry.flags & crate::storage::format::FLAG_COMPRESSED_ZSTD != 0;
let is_encrypted = entry.flags & crate::storage::format::FLAG_ENCRYPTED_AES256GCM != 0;
let file_size = fs::metadata(&path)
.map_err(|e| ReadError::Io(format!("metadata: {}", e)))?
.len();
let start_offset = if is_compressed || is_encrypted {
SEGMENT_HEADER_SIZE as u64
} else {
let idx_path = SparseIndex::index_path(&path);
if idx_path.exists() {
match SparseIndex::load(&idx_path) {
Ok(idx) => match idx.find_anchor(record_id) {
Some((e, _)) => e.file_offset,
None => SEGMENT_HEADER_SIZE as u64,
},
Err(_) => SEGMENT_HEADER_SIZE as u64,
}
} else {
SEGMENT_HEADER_SIZE as u64
}
};
let mut file = File::open(&path).map_err(|e| ReadError::Io(format!("open: {}", e)))?;
self.read_from_open_file(
&mut file,
file_size,
is_compressed,
is_encrypted,
start_offset,
record_id,
)
}
fn read_from_open_file(
&self,
file: &mut File,
file_size: u64,
is_compressed: bool,
is_encrypted: bool,
start_offset: u64,
record_id: u64,
) -> Result<Option<Record>, ReadError> {
let mut offset = start_offset;
if is_compressed || is_encrypted {
let key_ring = self.encryption_keys.as_deref();
while offset < file_size {
if offset + FRAME_HEADER_SIZE as u64 > file_size {
break;
}
let mut fh_buf = [0u8; FRAME_HEADER_SIZE];
file.seek(std::io::SeekFrom::Start(offset))
.map_err(|e| ReadError::Io(format!("seek frame: {}", e)))?;
file.read_exact(&mut fh_buf)
.map_err(|e| ReadError::Io(format!("read frame hdr: {}", e)))?;
let (cl, dl) = read_frame_header(&fh_buf);
let cl = cl as usize;
let dl = dl as usize;
if cl == 0 || dl == 0 || offset + FRAME_HEADER_SIZE as u64 + cl as u64 > file_size {
break;
}
let mut cdata = vec![0u8; cl];
file.read_exact(&mut cdata)
.map_err(|e| ReadError::Io(format!("read frame data: {}", e)))?;
let decoded = match decode_frame_payload(&cdata, is_compressed, is_encrypted, key_ring) {
Ok(d) => d,
Err(_) => break,
};
let mut doff = 0usize;
while doff + MIN_RECORD_SIZE <= dl && doff <= decoded.len() {
let total = u32::from_le_bytes([
decoded[doff],
decoded[doff + 1],
decoded[doff + 2],
decoded[doff + 3],
]) as usize;
if total < MIN_RECORD_SIZE || doff + total > dl || doff + total > decoded.len()
{
break;
}
match deserialize_record(&decoded[doff..doff + total]) {
Ok((record, _)) => {
if record.id.sequence == record_id {
return Ok(Some(record));
}
if record.id.sequence > record_id {
break;
}
doff += total;
}
Err(_) => {
doff += total;
}
}
}
offset += FRAME_HEADER_SIZE as u64 + cl as u64;
}
} else {
while offset < file_size {
if offset + 4 > file_size {
break;
}
let mut len_buf = [0u8; 4];
file.seek(std::io::SeekFrom::Start(offset))
.map_err(|e| ReadError::Io(format!("seek: {}", e)))?;
file.read_exact(&mut len_buf)
.map_err(|e| ReadError::Io(format!("read len: {}", e)))?;
let total = u32::from_le_bytes(len_buf) as usize;
if total < MIN_RECORD_SIZE {
offset += 1;
continue;
}
if offset + total as u64 > file_size {
break;
}
let mut record_buf = vec![0u8; total];
file.seek(std::io::SeekFrom::Start(offset))
.map_err(|e| ReadError::Io(format!("seek: {}", e)))?;
file.read_exact(&mut record_buf)
.map_err(|e| ReadError::Io(format!("read record: {}", e)))?;
match deserialize_record(&record_buf) {
Ok((record, _)) => {
if record.id.sequence == record_id {
return Ok(Some(record));
}
if record.id.sequence > record_id {
break;
}
offset += total as u64;
}
Err(_) => {
offset += total as u64;
}
}
}
}
Ok(None)
}
pub fn read_batch(&self, ids: &[u64]) -> Result<Vec<Option<Record>>, ReadError> {
let mut results: Vec<Option<Record>> = vec![None; ids.len()];
if ids.is_empty() {
return Ok(results);
}
let mut resolved: Vec<(usize, ManifestEntry)> = Vec::with_capacity(ids.len());
{
let mut m = self.manifest.lock().unwrap();
for (i, &id) in ids.iter().enumerate() {
if let Some(entry) = m.find(id)? {
resolved.push((i, entry));
}
}
}
resolved.sort_by_key(|(_, e)| e.path.clone());
let mut start = 0;
while start < resolved.len() {
let path = resolved[start].1.path.clone();
let mut end = start + 1;
while end < resolved.len() && resolved[end].1.path == path {
end += 1;
}
let entry = &resolved[start].1;
let is_compressed = entry.flags & crate::storage::format::FLAG_COMPRESSED_ZSTD != 0;
let is_encrypted = entry.flags & crate::storage::format::FLAG_ENCRYPTED_AES256GCM != 0;
let file_size = match fs::metadata(&path) {
Ok(m) => m.len(),
Err(_) => {
start = end;
continue;
}
};
let mut file = match File::open(&path) {
Ok(f) => f,
Err(_) => {
start = end;
continue;
}
};
let index = if is_compressed || is_encrypted {
None
} else {
let idx_path = SparseIndex::index_path(&path);
if idx_path.exists() {
SparseIndex::load(&idx_path).ok()
} else {
None
}
};
let mut group: Vec<(usize, u64)> = (start..end)
.map(|i| (resolved[i].0, ids[resolved[i].0]))
.collect();
group.sort_by_key(|&(_, id)| id);
if !is_compressed && !is_encrypted {
let start_offset = group
.first()
.and_then(|&(_, id)| {
index.as_ref().and_then(|idx| idx.find_anchor(id))
})
.map(|(e, _)| e.file_offset)
.unwrap_or(SEGMENT_HEADER_SIZE as u64);
for (slot, record) in
Self::read_many_raw(&mut file, file_size, start_offset, &group)?
{
results[slot] = Some(record);
}
} else {
for (slot, id) in group {
results[slot] = self.read_from_open_file(
&mut file,
file_size,
is_compressed,
is_encrypted,
SEGMENT_HEADER_SIZE as u64,
id,
)?;
}
}
start = end;
}
Ok(results)
}
fn read_many_raw(
file: &mut File,
file_size: u64,
start_offset: u64,
targets: &[(usize, u64)],
) -> Result<Vec<(usize, Record)>, ReadError> {
let mut out = Vec::with_capacity(targets.len());
if targets.is_empty() {
return Ok(out);
}
let mut t = 0usize;
let mut offset = start_offset;
while offset < file_size && t < targets.len() {
if offset + 4 > file_size {
break;
}
let mut len_buf = [0u8; 4];
file.seek(std::io::SeekFrom::Start(offset))
.map_err(|e| ReadError::Io(format!("seek: {}", e)))?;
if file.read_exact(&mut len_buf).is_err() {
break;
}
let total = u32::from_le_bytes(len_buf) as usize;
if total < MIN_RECORD_SIZE {
offset += 1; continue;
}
if offset + total as u64 > file_size {
break;
}
let mut record_buf = vec![0u8; total];
file.seek(std::io::SeekFrom::Start(offset))
.map_err(|e| ReadError::Io(format!("seek rec: {}", e)))?;
file.read_exact(&mut record_buf)
.map_err(|e| ReadError::Io(format!("read rec: {}", e)))?;
offset += total as u64;
let Ok((record, _)) = deserialize_record(&record_buf) else {
continue;
};
let id = record.id.sequence;
while t < targets.len() && targets[t].1 < id {
t += 1;
}
if t < targets.len() && targets[t].1 == id {
out.push((targets[t].0, record));
t += 1;
}
}
Ok(out)
}
pub fn scan(&self, from_id: u64, to_id: u64) -> Result<RecordIter, ReadError> {
let entry = match self.manifest.lock().unwrap().find(from_id)? {
Some(e) => e,
None => return Err(ReadError::NotFound(from_id)),
};
iter_for_segment(&entry, from_id, to_id, self.encryption_keys.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{QueueFullPolicy, RetentionPolicy};
use crate::ring::Ring;
#[test]
fn read_record_by_id() {
let dir = tempfile::tempdir().unwrap();
let ring = Ring::new(64, false, 0);
let mut mgr = crate::storage::SegmentManager::create(
dir.path().to_path_buf(),
10 * 1024 * 1024,
false,
false,
None,
[0u8; 32],
RetentionPolicy::KeepAll,
0,
)
.unwrap();
for i in 0..5 {
let seq = ring.claim(QueueFullPolicy::Block).unwrap();
let content = format!("record-{}", i);
unsafe {
ring.slot(seq)
.producer_write(seq, i * 100, content.as_bytes());
}
ring.slot(seq).publish(seq);
}
mgr.append_batch(&ring, 0, 4).unwrap();
mgr.fdatasync().unwrap();
drop(mgr);
let reader = Reader::new(
Arc::new(Mutex::new(SegmentManifest::new(dir.path().to_path_buf()))),
None,
);
let record = reader.read(3).unwrap().unwrap();
assert_eq!(record.id.sequence, 3);
assert_eq!(record.content, b"record-3");
}
#[test]
fn read_nonexistent_record() {
let dir = tempfile::tempdir().unwrap();
let ring = Ring::new(64, false, 0);
let mut mgr = crate::storage::SegmentManager::create(
dir.path().to_path_buf(),
10 * 1024 * 1024,
false,
false,
None,
[0u8; 32],
RetentionPolicy::KeepAll,
0,
)
.unwrap();
let seq = ring.claim(QueueFullPolicy::Block).unwrap();
unsafe {
ring.slot(seq).producer_write(seq, 0, b"only-one");
}
ring.slot(seq).publish(seq);
mgr.append_batch(&ring, 0, 0).unwrap();
mgr.fdatasync().unwrap();
drop(mgr);
let reader = Reader::new(
Arc::new(Mutex::new(SegmentManifest::new(dir.path().to_path_buf()))),
None,
);
assert!(reader.read(999).unwrap().is_none());
}
#[test]
fn find_rescans_when_cached_segment_deleted() {
let dir = tempfile::tempdir().unwrap();
let ring = Ring::new(64, false, 0);
let mut mgr = crate::storage::SegmentManager::create(
dir.path().to_path_buf(),
10 * 1024 * 1024,
false,
false,
None,
[0u8; 32],
RetentionPolicy::KeepAll,
0,
)
.unwrap();
let seq = ring.claim(QueueFullPolicy::Block).unwrap();
unsafe {
ring.slot(seq).producer_write(seq, 0, b"rec-0");
}
ring.slot(seq).publish(seq);
mgr.append_batch(&ring, 0, 0).unwrap();
mgr.fdatasync().unwrap();
drop(mgr);
let mut manifest = SegmentManifest::new(dir.path().to_path_buf());
let entry = manifest.find(0).unwrap().expect("segment should be cached");
assert!(entry.path.exists(), "segment file exists before deletion");
std::fs::remove_file(&entry.path).unwrap();
manifest.dir_mtime =
std::fs::metadata(dir.path()).and_then(|m| m.modified()).ok();
let stale = manifest.find(0).unwrap();
assert!(
stale.is_none(),
"find() must not return a stale entry for a deleted segment"
);
}
}
#[cfg(all(test, feature = "encryption"))]
mod keyring_tests {
use super::decode_frame_payload;
use crate::KeyRing;
fn encrypted_frame(key: [u8; 32], plaintext: &[u8]) -> Vec<u8> {
use aes_gcm::aead::{Aead, KeyInit};
use aes_gcm::{Aes256Gcm, Key, Nonce};
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key));
let mut nonce = [0u8; 12];
getrandom::getrandom(&mut nonce).unwrap();
let ct = cipher
.encrypt(Nonce::from_slice(&nonce), plaintext)
.unwrap();
let mut out = Vec::with_capacity(12 + ct.len());
out.extend_from_slice(&nonce);
out.extend_from_slice(&ct);
out
}
#[test]
fn decrypt_tries_keys_until_one_authenticates() {
let key_a = [0xAAu8; 32];
let key_b = [0xBBu8; 32];
let ring = KeyRing::new(key_a, vec![key_b]);
let plaintext = b"hello-rotation-payload";
let frame = encrypted_frame(key_b, plaintext);
let decoded = decode_frame_payload(&frame, false, true, Some(ring.as_ref())).unwrap();
assert_eq!(decoded, plaintext);
}
#[test]
fn decrypt_fails_when_key_retired() {
let ring = KeyRing::single([0xAAu8; 32]); let frame = encrypted_frame([0xBBu8; 32], b"retired"); let err = decode_frame_payload(&frame, false, true, Some(ring.as_ref())).unwrap_err();
assert!(err.contains("decryption failed"), "got: {err}");
}
}