use crate::kv::{KvEntry, VersionToken};
use crate::snapshot::SnapshotError;
pub(crate) fn encode_value_into(
buf: &mut Vec<u8>,
value: &[u8],
version: &VersionToken,
) -> Result<(), SnapshotError> {
let vb = version.as_bytes();
let ver_len = u8::try_from(vb.len()).map_err(|_| {
SnapshotError::InvalidFormat(format!(
"version too long: {} bytes (max {})",
vb.len(),
u8::MAX
))
})?;
buf.clear();
buf.reserve(1 + vb.len() + value.len());
buf.push(ver_len);
buf.extend_from_slice(vb);
buf.extend_from_slice(value);
Ok(())
}
pub(crate) fn decode_entry(key: &str, raw: &[u8]) -> Result<KvEntry, SnapshotError> {
let ver_len = *raw.first().ok_or_else(|| {
SnapshotError::InvalidFormat("snapshot value record is empty (no version length)".into())
})? as usize;
let value_off = 1 + ver_len;
if raw.len() < value_off {
return Err(SnapshotError::InvalidFormat(format!(
"snapshot value record truncated: need {value_off} bytes for version, have {}",
raw.len()
)));
}
let version = VersionToken::from_raw(&raw[1..value_off]).ok_or_else(|| {
SnapshotError::InvalidFormat(format!(
"version length {ver_len} exceeds version token capacity"
))
})?;
Ok(KvEntry {
key: key.to_string(),
value: raw[value_off..].to_vec(),
version,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_decode_round_trips_fdb_versionstamp() {
let vs = VersionToken::from_fdb_versionstamp(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
let mut enc = Vec::new();
encode_value_into(&mut enc, b"payload", &vs).expect("encode");
let entry = decode_entry("k", &enc).expect("decode");
assert_eq!(entry.version.as_bytes(), &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
assert!(
entry.version.as_u64().is_none(),
"a 10-byte token has no u64 form — it must not be flattened"
);
assert_eq!(entry.value, b"payload");
}
#[test]
fn encode_decode_round_trips_empty_value() {
let mut enc = Vec::new();
encode_value_into(&mut enc, b"", &VersionToken::from_u64(7)).expect("encode");
let entry = decode_entry("k", &enc).expect("decode");
assert!(entry.value.is_empty());
assert_eq!(entry.version.as_u64(), Some(7));
}
#[test]
fn decode_entry_rejects_empty_record() {
let err = decode_entry("k", &[]).unwrap_err();
assert!(
matches!(err, SnapshotError::InvalidFormat(_)),
"empty record must be a format error, got {err:?}"
);
}
#[test]
fn decode_entry_rejects_truncated_version() {
let raw = [5u8, 0xAA, 0xBB];
let err = decode_entry("k", &raw).unwrap_err();
assert!(
matches!(err, SnapshotError::InvalidFormat(_)),
"truncated version must be a format error, got {err:?}"
);
}
#[test]
fn decode_entry_rejects_oversized_version() {
let mut raw = vec![11u8];
raw.extend_from_slice(&[0u8; 11]);
let err = decode_entry("k", &raw).unwrap_err();
assert!(
matches!(err, SnapshotError::InvalidFormat(_)),
"oversized version must be a format error, got {err:?}"
);
}
}