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 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, PartialEq, Eq, Hash)]
pub struct SegmentRange {
pub start: u64,
pub 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 fn filename(start: u64, end: u64) -> String {
format!("{SEGMENT_PREFIX}{start:012}_{end:012}{SEGMENT_SUFFIX}")
}
pub 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 })
}
const ENVELOPE_RESERVED_LEN: usize = 3;
pub 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 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 + Send + Sync)>,
compressor: &mut zstd::bulk::Compressor<'static>,
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 = compressor.compress(&cbor_buf)?;
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 + Send + Sync)>,
decompressor: &mut zstd::bulk::Decompressor<'static>,
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 = match zstd::zstd_safe::get_frame_content_size(compressed.as_ref()) {
Ok(Some(size)) => {
let cap = usize::try_from(size).unwrap_or(compressed.len() * 8);
match decompressor.decompress(compressed.as_ref(), cap) {
Ok(buf) => buf,
Err(_) => zstd::decode_all(compressed.as_ref())?,
}
}
_ => 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 encode_segment<T: Serialize>(
cipher: Option<&(dyn SegmentCipher + Send + Sync)>,
compressor: &mut zstd::bulk::Compressor<'static>,
path: &Path,
events: &[T],
) -> Result<Vec<u8>> {
let payload = encode_payload(cipher, compressor, path, events)?;
Ok(wrap_envelope(&payload))
}
pub(crate) fn decode_segment<T: DeserializeOwned>(
cipher: Option<&(dyn SegmentCipher + Send + Sync)>,
decompressor: &mut zstd::bulk::Decompressor<'static>,
raw: &[u8],
path: &Path,
) -> Result<Vec<T>> {
let (_version, payload) = unwrap_envelope(raw);
if cipher.is_some() && payload.len() < NONCE_LEN {
return Err(SegmentError::Integrity {
path: path.to_path_buf(),
reason: "encrypted payload too small for AEAD nonce",
});
}
decode_payload(cipher, decompressor, payload, path)
}