use crate::error::{PersistError, Result};
pub const MAX_CLASS_TAG_LEN: usize = u8::MAX as usize;
const HEADER_FIXED_LEN: usize = 3;
pub fn encode(
class_version: u16,
class_tag: &str,
payload: &[u8],
) -> Result<Vec<u8>> {
let tag_bytes = class_tag.as_bytes();
if tag_bytes.is_empty() {
return Err(PersistError::SerializationError(
"entity class tag must not be empty".to_string(),
));
}
if tag_bytes.len() > MAX_CLASS_TAG_LEN {
return Err(PersistError::SerializationError(format!(
"entity class tag too long: {} bytes (max {})",
tag_bytes.len(),
MAX_CLASS_TAG_LEN,
)));
}
let mut out =
Vec::with_capacity(HEADER_FIXED_LEN + tag_bytes.len() + payload.len());
out.extend_from_slice(&class_version.to_be_bytes());
out.push(tag_bytes.len() as u8);
out.extend_from_slice(tag_bytes);
out.extend_from_slice(payload);
Ok(out)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodedRecord<'a> {
pub class_version: u16,
pub class_tag: &'a str,
pub payload: &'a [u8],
}
pub fn decode(bytes: &[u8]) -> Result<DecodedRecord<'_>> {
if bytes.len() < HEADER_FIXED_LEN {
return Err(PersistError::SerializationError(format!(
"record too short for entity envelope: {} bytes (need >= {})",
bytes.len(),
HEADER_FIXED_LEN,
)));
}
let class_version = u16::from_be_bytes([bytes[0], bytes[1]]);
let tag_len = bytes[2] as usize;
let tag_start = HEADER_FIXED_LEN;
let tag_end = tag_start + tag_len;
if bytes.len() < tag_end {
return Err(PersistError::SerializationError(format!(
"record too short for tag of length {}: {} bytes",
tag_len,
bytes.len(),
)));
}
let class_tag =
std::str::from_utf8(&bytes[tag_start..tag_end]).map_err(|e| {
PersistError::SerializationError(format!(
"entity class tag is not valid UTF-8: {}",
e
))
})?;
let payload = &bytes[tag_end..];
Ok(DecodedRecord { class_version, class_tag, payload })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_simple() {
let bytes = encode(0, "User", b"hello").unwrap();
let dec = decode(&bytes).unwrap();
assert_eq!(dec.class_version, 0);
assert_eq!(dec.class_tag, "User");
assert_eq!(dec.payload, b"hello");
}
#[test]
fn round_trip_high_version() {
let bytes = encode(0xABCD, "com.example.Foo", b"").unwrap();
let dec = decode(&bytes).unwrap();
assert_eq!(dec.class_version, 0xABCD);
assert_eq!(dec.class_tag, "com.example.Foo");
assert_eq!(dec.payload, b"");
}
#[test]
fn round_trip_empty_payload() {
let bytes = encode(7, "X", b"").unwrap();
let dec = decode(&bytes).unwrap();
assert_eq!(dec.class_version, 7);
assert_eq!(dec.class_tag, "X");
assert!(dec.payload.is_empty());
}
#[test]
fn empty_tag_rejected() {
let r = encode(0, "", b"x");
assert!(r.is_err());
}
#[test]
fn oversized_tag_rejected() {
let big = "a".repeat(MAX_CLASS_TAG_LEN + 1);
let r = encode(0, &big, b"x");
assert!(r.is_err());
}
#[test]
fn decode_truncated_header() {
assert!(decode(&[]).is_err());
assert!(decode(&[0]).is_err());
assert!(decode(&[0, 0]).is_err());
}
#[test]
fn decode_truncated_tag() {
let mut buf = vec![0u8, 0, 10];
buf.extend_from_slice(b"AB");
assert!(decode(&buf).is_err());
}
#[test]
fn decode_invalid_utf8_tag() {
let buf = vec![0u8, 0, 2, 0xFF, 0xFE, 0x00];
assert!(decode(&buf).is_err());
}
#[test]
fn header_fixed_layout() {
let bytes = encode(0x0102, "X", b"yz").unwrap();
assert_eq!(bytes, vec![0x01, 0x02, 0x01, b'X', b'y', b'z']);
}
#[test]
fn payload_bytes_preserved_with_arbitrary_bytes() {
let payload = (0u8..=255).collect::<Vec<u8>>();
let bytes = encode(42, "Bin", &payload).unwrap();
let dec = decode(&bytes).unwrap();
assert_eq!(dec.payload, payload.as_slice());
}
#[test]
fn unicode_tag_round_trip() {
let bytes = encode(0, "naïve.Class", b"x").unwrap();
let dec = decode(&bytes).unwrap();
assert_eq!(dec.class_tag, "naïve.Class");
}
}