nodedb_lite/engine/document/history/
value.rs1use crate::error::LiteError;
12
13const HEADER_LEN: usize = 17;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[repr(u8)]
19pub enum VersionTag {
20 Live = 0x00,
22 Tombstone = 0xFF,
24 GdprErased = 0xFE,
26}
27
28impl VersionTag {
29 pub fn from_u8(v: u8) -> Option<Self> {
33 match v {
34 0x00 => Some(Self::Live),
35 0xFF => Some(Self::Tombstone),
36 0xFE => Some(Self::GdprErased),
37 _ => None,
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct DecodedVersion {
45 pub tag: VersionTag,
47 pub valid_from_ms: i64,
49 pub valid_until_ms: i64,
51 pub body: Vec<u8>,
53}
54
55impl DecodedVersion {
56 pub fn is_live(&self) -> bool {
58 self.tag == VersionTag::Live
59 }
60}
61
62pub fn encode_value(
67 tag: VersionTag,
68 valid_from_ms: i64,
69 valid_until_ms: i64,
70 body: &[u8],
71) -> Vec<u8> {
72 let mut buf = Vec::with_capacity(HEADER_LEN + body.len());
73 buf.push(tag as u8);
74 buf.extend_from_slice(&valid_from_ms.to_le_bytes());
75 buf.extend_from_slice(&valid_until_ms.to_le_bytes());
76 buf.extend_from_slice(body);
77 buf
78}
79
80pub fn decode_value(bytes: &[u8]) -> Result<DecodedVersion, LiteError> {
85 if bytes.len() < HEADER_LEN {
86 return Err(LiteError::Serialization {
87 detail: format!(
88 "versioned document value too short: {} bytes (need at least {HEADER_LEN})",
89 bytes.len()
90 ),
91 });
92 }
93 let tag_byte = bytes[0];
94 let tag = VersionTag::from_u8(tag_byte).ok_or_else(|| LiteError::Serialization {
95 detail: format!("unknown version tag byte: 0x{tag_byte:02X}"),
96 })?;
97 let valid_from_ms = i64::from_le_bytes(
98 bytes[1..9]
99 .try_into()
100 .expect("length checked above — 8 bytes"),
101 );
102 let valid_until_ms = i64::from_le_bytes(
103 bytes[9..17]
104 .try_into()
105 .expect("length checked above — 8 bytes"),
106 );
107 Ok(DecodedVersion {
108 tag,
109 valid_from_ms,
110 valid_until_ms,
111 body: bytes[HEADER_LEN..].to_vec(),
112 })
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn encode_decode_live_roundtrip() {
121 let body = b"msgpack_body_here";
122 let encoded = encode_value(VersionTag::Live, 1_000, 2_000, body);
123 let decoded = decode_value(&encoded).unwrap();
124 assert_eq!(decoded.tag, VersionTag::Live);
125 assert_eq!(decoded.valid_from_ms, 1_000);
126 assert_eq!(decoded.valid_until_ms, 2_000);
127 assert_eq!(decoded.body, body);
128 }
129
130 #[test]
131 fn encode_decode_tombstone() {
132 let encoded = encode_value(VersionTag::Tombstone, 500, i64::MAX, &[]);
133 let decoded = decode_value(&encoded).unwrap();
134 assert_eq!(decoded.tag, VersionTag::Tombstone);
135 assert!(decoded.body.is_empty());
136 }
137
138 #[test]
139 fn decode_too_short_returns_error() {
140 assert!(decode_value(&[0x00; 16]).is_err());
141 }
142
143 #[test]
144 fn decode_unknown_tag_returns_error() {
145 let mut buf = vec![0u8; 17];
146 buf[0] = 0xAB; assert!(decode_value(&buf).is_err());
148 }
149
150 #[test]
151 fn is_live_false_for_tombstone() {
152 let encoded = encode_value(VersionTag::Tombstone, 0, 0, &[]);
153 let decoded = decode_value(&encoded).unwrap();
154 assert!(!decoded.is_live());
155 }
156}