use alloc::vec::Vec;
use crate::InternalValue;
use crate::runtime_config::ChecksumAlgorithm;
pub const FOOTER_TAIL_LEN: usize = 1 + 4;
#[must_use]
pub fn kv_digest(item: &InternalValue, algo: ChecksumAlgorithm) -> Option<u64> {
#[expect(
clippy::cast_possible_truncation,
reason = "user keys are bounded well below u32::MAX"
)]
let user_key_len = item.key.user_key.len() as u32;
let mut head = [0u8; 1 + 8 + 4];
head[0] = u8::from(item.key.value_type);
head[1..9].copy_from_slice(&item.key.seqno.to_le_bytes());
head[9..13].copy_from_slice(&user_key_len.to_le_bytes());
algo.compute_chunks(&[&head, &item.key.user_key, &item.value])
}
pub fn append_footer(payload: &mut Vec<u8>, digests: &[u64], algo: ChecksumAlgorithm) {
let size = algo.digest_size();
assert!(size <= 8, "digest_size must fit in u64 LE bytes");
for &d in digests {
let le = d.to_le_bytes();
#[expect(clippy::indexing_slicing, reason = "size <= 8 enforced above")]
payload.extend_from_slice(&le[..size]);
}
payload.push(algo.wire_tag());
#[expect(
clippy::cast_possible_truncation,
reason = "a data block never holds more than u32::MAX entries"
)]
payload.extend_from_slice(&(digests.len() as u32).to_le_bytes());
}
#[must_use]
pub fn descriptor_byte(algo: Option<ChecksumAlgorithm>) -> u8 {
match algo {
None => 0,
Some(a) => 1 + a.wire_tag(),
}
}
pub fn descriptor_from_byte(byte: u8) -> crate::Result<Option<ChecksumAlgorithm>> {
if byte == 0 {
return Ok(None);
}
ChecksumAlgorithm::from_wire_tag(byte - 1)
.map(Some)
.ok_or(crate::Error::InvalidTrailer)
}
pub fn split_inner(bytes: &[u8]) -> crate::Result<&[u8]> {
let total = bytes.len();
if total < FOOTER_TAIL_LEN {
return Err(crate::Error::InvalidTrailer);
}
let tail_start = total - FOOTER_TAIL_LEN;
let tail = bytes
.get(tail_start..total)
.ok_or(crate::Error::InvalidTrailer)?;
let algo_tag = *tail.first().ok_or(crate::Error::InvalidTrailer)?;
let algo = ChecksumAlgorithm::from_wire_tag(algo_tag).ok_or(crate::Error::InvalidTrailer)?;
let count = u32::from_le_bytes(
tail.get(1..)
.and_then(|s| s.try_into().ok())
.ok_or(crate::Error::InvalidTrailer)?,
) as usize;
let array_len = count
.checked_mul(algo.digest_size())
.ok_or(crate::Error::InvalidTrailer)?;
if array_len > tail_start {
return Err(crate::Error::InvalidTrailer);
}
let array_start = tail_start - array_len;
bytes.get(..array_start).ok_or(crate::Error::InvalidTrailer)
}
#[cfg_attr(
not(feature = "std"),
allow(
dead_code,
reason = "core+alloc per-KV scrub view; the verify/scrub consumer is std-gated, so unused under no_std"
)
)]
pub struct SplitFull<'a> {
pub inner: &'a [u8],
digest_array: &'a [u8],
pub algo: ChecksumAlgorithm,
count: usize,
}
#[cfg_attr(
not(feature = "std"),
allow(
dead_code,
reason = "core+alloc per-KV scrub accessors; the verify/scrub consumer is std-gated, so unused under no_std"
)
)]
impl SplitFull<'_> {
#[must_use]
pub fn count(&self) -> usize {
self.count
}
#[must_use]
pub fn digest(&self, index: usize) -> Option<u64> {
let size = self.algo.digest_size();
let off = index.checked_mul(size)?;
let end = off.checked_add(size)?;
let chunk = self.digest_array.get(off..end)?;
let mut word = [0u8; 8];
word.get_mut(..size)?.copy_from_slice(chunk);
Some(u64::from_le_bytes(word))
}
}
#[cfg_attr(
not(feature = "std"),
allow(
dead_code,
reason = "core+alloc per-KV scrub splitter; the verify/scrub consumer is std-gated, so unused under no_std"
)
)]
pub fn split_full(bytes: &[u8]) -> crate::Result<SplitFull<'_>> {
let total = bytes.len();
if total < FOOTER_TAIL_LEN {
return Err(crate::Error::InvalidTrailer);
}
let tail_start = total - FOOTER_TAIL_LEN;
let tail = bytes
.get(tail_start..total)
.ok_or(crate::Error::InvalidTrailer)?;
let algo_tag = *tail.first().ok_or(crate::Error::InvalidTrailer)?;
let algo = ChecksumAlgorithm::from_wire_tag(algo_tag).ok_or(crate::Error::InvalidTrailer)?;
let count = u32::from_le_bytes(
tail.get(1..)
.and_then(|s| s.try_into().ok())
.ok_or(crate::Error::InvalidTrailer)?,
) as usize;
let size = algo.digest_size();
let array_len = count
.checked_mul(size)
.ok_or(crate::Error::InvalidTrailer)?;
if array_len > tail_start {
return Err(crate::Error::InvalidTrailer);
}
let array_start = tail_start - array_len;
let digest_array = bytes
.get(array_start..tail_start)
.ok_or(crate::Error::InvalidTrailer)?;
let inner = bytes
.get(..array_start)
.ok_or(crate::Error::InvalidTrailer)?;
Ok(SplitFull {
inner,
digest_array,
algo,
count,
})
}
#[cfg(test)]
#[expect(clippy::expect_used, reason = "test code")]
mod tests;