use std::fs;
use std::io::Write;
use std::path::Path;
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::cipher::SegmentCipher;
use crate::error::{Result, SegmentError};
const SEGMENT_PREFIX: &str = "seg_";
const SEGMENT_SUFFIX: &str = ".zst";
const TMP_SUFFIX: &str = ".tmp";
const NONCE_LEN: usize = 12;
const ENVELOPE_MAGIC: [u8; 4] = *b"SBF1";
const ENVELOPE_VERSION: u8 = 1;
const ENVELOPE_LEN: usize = 8;
#[derive(Debug, Clone, Copy)]
pub(crate) struct SegmentRange {
pub(crate) start: u64,
pub(crate) end: u64,
}
impl SegmentRange {
pub(crate) fn new(start: u64, end: u64) -> Self {
debug_assert!(
start <= end,
"SegmentRange invariant violated: start ({start}) > end ({end})"
);
Self { start, end }
}
}
pub(crate) fn filename(start: u64, end: u64) -> String {
format!("{SEGMENT_PREFIX}{start:012}_{end:012}{SEGMENT_SUFFIX}")
}
pub(crate) fn parse_filename(name: &str) -> Option<SegmentRange> {
let core = name
.strip_prefix(SEGMENT_PREFIX)?
.strip_suffix(SEGMENT_SUFFIX)?;
let (start_str, end_str) = core.split_once('_')?;
let start = start_str.parse().ok()?;
let end = end_str.parse().ok()?;
Some(SegmentRange { start, end })
}
pub(crate) fn scan(dir: &Path) -> Result<Vec<SegmentRange>> {
let mut segments = Vec::new();
for entry in fs::read_dir(dir)? {
let entry = entry?;
if let Some(range) = parse_filename(&entry.file_name().to_string_lossy()) {
segments.push(range);
}
}
segments.sort_by_key(|s| s.start);
Ok(segments)
}
pub(crate) fn clean_tmp(dir: &Path) -> Result<usize> {
let mut removed = 0usize;
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path
.file_name()
.is_some_and(|n| n.to_string_lossy().ends_with(TMP_SUFFIX))
&& fs::remove_file(&path).is_ok()
{
removed += 1;
}
}
Ok(removed)
}
const ENVELOPE_RESERVED_LEN: usize = 3;
pub(crate) fn unwrap_envelope(raw: &[u8]) -> (Option<u8>, &[u8]) {
let reserved_range = ENVELOPE_MAGIC.len() + 1..ENVELOPE_LEN;
let reserved_zero = [0u8; ENVELOPE_RESERVED_LEN];
if raw.len() >= ENVELOPE_LEN
&& raw[..ENVELOPE_MAGIC.len()] == ENVELOPE_MAGIC
&& raw[reserved_range] == reserved_zero
{
(Some(raw[ENVELOPE_MAGIC.len()]), &raw[ENVELOPE_LEN..])
} else {
(None, raw)
}
}
pub(crate) fn wrap_envelope(payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(ENVELOPE_LEN + payload.len());
out.extend_from_slice(&ENVELOPE_MAGIC);
out.push(ENVELOPE_VERSION);
out.extend_from_slice(&[0u8; ENVELOPE_LEN - ENVELOPE_MAGIC.len() - 1]);
out.extend_from_slice(payload);
out
}
pub(crate) fn encode_payload<T: Serialize>(
cipher: Option<&dyn SegmentCipher>,
level: i32,
path: &Path,
events: &[T],
) -> Result<Vec<u8>> {
let mut cbor_buf = Vec::new();
ciborium::into_writer(events, &mut cbor_buf).map_err(|e| SegmentError::Cbor {
phase: "serialize",
path: path.to_path_buf(),
message: e.to_string(),
})?;
let compressed = zstd::encode_all(cbor_buf.as_slice(), level)?;
match cipher {
Some(cipher) => cipher
.encrypt(&compressed)
.map_err(|e| SegmentError::Cipher {
path: path.to_path_buf(),
message: e.to_string(),
}),
None => Ok(compressed),
}
}
pub(crate) fn decode_payload<T: DeserializeOwned>(
cipher: Option<&dyn SegmentCipher>,
payload: &[u8],
path: &Path,
) -> Result<Vec<T>> {
use std::borrow::Cow;
let decrypted;
let compressed: Cow<[u8]> = match cipher {
Some(cipher) => {
decrypted = cipher.decrypt(payload).map_err(|e| SegmentError::Cipher {
path: path.to_path_buf(),
message: e.to_string(),
})?;
Cow::Owned(decrypted)
}
None => Cow::Borrowed(payload),
};
let cbor_buf = zstd::decode_all(compressed.as_ref())?;
ciborium::from_reader(cbor_buf.as_slice()).map_err(|e| SegmentError::Cbor {
phase: "deserialize",
path: path.to_path_buf(),
message: e.to_string(),
})
}
pub(crate) fn write<T: Serialize>(
dir: &Path,
cipher: Option<&dyn SegmentCipher>,
level: i32,
range: SegmentRange,
events: &[T],
) -> Result<u64> {
let seg_name = filename(range.start, range.end);
let seg_path = dir.join(&seg_name);
let payload = encode_payload(cipher, level, &seg_path, events)?;
let final_bytes = wrap_envelope(&payload);
let tmp_path = dir.join(format!("{seg_name}{TMP_SUFFIX}"));
{
let mut file = fs::File::create(&tmp_path)?;
file.write_all(&final_bytes)?;
file.sync_all()?;
}
fs::rename(&tmp_path, &seg_path)?;
Ok(final_bytes.len() as u64)
}
pub(crate) fn read<T: DeserializeOwned>(
dir: &Path,
cipher: Option<&dyn SegmentCipher>,
range: SegmentRange,
) -> Result<Vec<T>> {
let path = dir.join(filename(range.start, range.end));
let raw = fs::read(&path)?;
let (_version, payload) = unwrap_envelope(&raw);
if cipher.is_some() && payload.len() < NONCE_LEN {
return Err(SegmentError::Integrity {
path,
reason: "encrypted payload too small for AEAD nonce",
});
}
decode_payload(cipher, payload, &path)
}