1use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10
11use super::error::{Error, Mutation};
12
13const TOMBSTONE_MAGIC: &[u8; 4] = b"PLTD";
14const TOMBSTONE_WIRE_VERSION: u8 = 1;
15const HEADER_LEN: usize = 4 + 1 + 8 + 4 + 4;
16const TIMESTAMP_OFFSET: usize = 5;
17const ACTOR_LEN_OFFSET: usize = 13;
18const METADATA_COUNT_OFFSET: usize = 17;
19const METADATA_ENTRY_HEADER_LEN: usize = 4 + 8;
20
21#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
27pub struct Tombstone {
28 pub actor: Vec<u8>,
30 pub timestamp_millis: u64,
32 pub causal_metadata: BTreeMap<String, Vec<u8>>,
34}
35
36impl Tombstone {
37 pub fn new(actor: impl Into<Vec<u8>>, timestamp_millis: u64) -> Self {
39 Self {
40 actor: actor.into(),
41 timestamp_millis,
42 causal_metadata: BTreeMap::new(),
43 }
44 }
45
46 pub fn with_causal_metadata(
48 mut self,
49 key: impl Into<String>,
50 value: impl Into<Vec<u8>>,
51 ) -> Self {
52 self.insert_causal_metadata(key, value);
53 self
54 }
55
56 pub fn insert_causal_metadata(
58 &mut self,
59 key: impl Into<String>,
60 value: impl Into<Vec<u8>>,
61 ) -> &mut Self {
62 self.causal_metadata.insert(key.into(), value.into());
63 self
64 }
65
66 pub fn causal_metadata(&self, key: &str) -> Option<&[u8]> {
68 self.causal_metadata.get(key).map(Vec::as_slice)
69 }
70
71 pub fn to_bytes(&self) -> Result<Vec<u8>, Error> {
73 let actor_len = checked_u32_len(self.actor.len(), "actor")?;
74 let metadata_count = checked_u32_len(self.causal_metadata.len(), "causal metadata count")?;
75
76 let mut out = Vec::with_capacity(
77 HEADER_LEN
78 .saturating_add(self.actor.len())
79 .saturating_add(metadata_encoded_len(&self.causal_metadata)),
80 );
81 out.extend_from_slice(TOMBSTONE_MAGIC);
82 out.push(TOMBSTONE_WIRE_VERSION);
83 out.extend_from_slice(&self.timestamp_millis.to_be_bytes());
84 out.extend_from_slice(&actor_len.to_be_bytes());
85 out.extend_from_slice(&metadata_count.to_be_bytes());
86 out.extend_from_slice(&self.actor);
87
88 for (key, value) in &self.causal_metadata {
89 let key_bytes = key.as_bytes();
90 let key_len = checked_u32_len(key_bytes.len(), "causal metadata key")?;
91 let value_len = checked_u64_len(value.len(), "causal metadata value")?;
92 out.extend_from_slice(&key_len.to_be_bytes());
93 out.extend_from_slice(&value_len.to_be_bytes());
94 out.extend_from_slice(key_bytes);
95 out.extend_from_slice(value);
96 }
97
98 Ok(out)
99 }
100
101 pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
103 decode_tombstone(bytes)
104 }
105
106 pub fn from_stored_bytes(bytes: &[u8]) -> Result<Option<Self>, Error> {
111 if is_tombstone_value(bytes) {
112 Self::from_bytes(bytes).map(Some)
113 } else {
114 Ok(None)
115 }
116 }
117
118 pub fn is_encoded(bytes: &[u8]) -> bool {
120 is_tombstone_value(bytes)
121 }
122
123 pub fn to_upsert_mutation(&self, key: impl Into<Vec<u8>>) -> Result<Mutation, Error> {
125 tombstone_upsert(key, self)
126 }
127}
128
129pub fn is_tombstone_value(bytes: &[u8]) -> bool {
131 bytes.starts_with(TOMBSTONE_MAGIC)
132}
133
134pub fn tombstone_upsert(key: impl Into<Vec<u8>>, tombstone: &Tombstone) -> Result<Mutation, Error> {
136 Ok(Mutation::Upsert {
137 key: key.into(),
138 val: tombstone.to_bytes()?,
139 })
140}
141
142pub fn tombstone_compaction(
148 key: impl Into<Vec<u8>>,
149 stored_value: &[u8],
150) -> Result<Option<Mutation>, Error> {
151 match Tombstone::from_stored_bytes(stored_value)? {
152 Some(_) => Ok(Some(Mutation::Delete { key: key.into() })),
153 None => Ok(None),
154 }
155}
156
157fn decode_tombstone(bytes: &[u8]) -> Result<Tombstone, Error> {
158 if bytes.len() < HEADER_LEN {
159 return Err(invalid_tombstone("tombstone envelope is too short"));
160 }
161 if !bytes.starts_with(TOMBSTONE_MAGIC) {
162 return Err(invalid_tombstone("tombstone missing PLTD magic"));
163 }
164
165 let wire_version = bytes[4];
166 if wire_version != TOMBSTONE_WIRE_VERSION {
167 return Err(invalid_tombstone(format!(
168 "unsupported tombstone wire version {wire_version}"
169 )));
170 }
171
172 let timestamp_millis = read_u64(bytes, TIMESTAMP_OFFSET)?;
173 let actor_len = read_u32(bytes, ACTOR_LEN_OFFSET)? as usize;
174 let metadata_count = read_u32(bytes, METADATA_COUNT_OFFSET)? as usize;
175 let actor_end = checked_add(HEADER_LEN, actor_len, "actor")?;
176 if actor_end > bytes.len() {
177 return Err(invalid_tombstone("actor length exceeds envelope length"));
178 }
179
180 let actor = bytes[HEADER_LEN..actor_end].to_vec();
181 let mut offset = actor_end;
182 let mut causal_metadata = BTreeMap::new();
183
184 for _ in 0..metadata_count {
185 let entry_header_end = checked_add(offset, METADATA_ENTRY_HEADER_LEN, "metadata header")?;
186 if entry_header_end > bytes.len() {
187 return Err(invalid_tombstone("metadata entry header is truncated"));
188 }
189
190 let key_len = read_u32(bytes, offset)? as usize;
191 let value_len = usize::try_from(read_u64(bytes, offset + 4)?)
192 .map_err(|_| invalid_tombstone("metadata value length does not fit in usize"))?;
193 offset = entry_header_end;
194
195 let key_end = checked_add(offset, key_len, "metadata key")?;
196 let value_end = checked_add(key_end, value_len, "metadata value")?;
197 if value_end > bytes.len() {
198 return Err(invalid_tombstone(
199 "metadata key/value length exceeds envelope length",
200 ));
201 }
202
203 let key = decode_utf8(&bytes[offset..key_end], "metadata key")?;
204 let value = bytes[key_end..value_end].to_vec();
205 if causal_metadata.insert(key, value).is_some() {
206 return Err(invalid_tombstone("duplicate metadata key"));
207 }
208 offset = value_end;
209 }
210
211 if offset != bytes.len() {
212 return Err(invalid_tombstone(format!(
213 "tombstone has {} trailing bytes",
214 bytes.len() - offset
215 )));
216 }
217
218 Ok(Tombstone {
219 actor,
220 timestamp_millis,
221 causal_metadata,
222 })
223}
224
225fn metadata_encoded_len(metadata: &BTreeMap<String, Vec<u8>>) -> usize {
226 metadata.iter().fold(0usize, |len, (key, value)| {
227 len.saturating_add(METADATA_ENTRY_HEADER_LEN)
228 .saturating_add(key.len())
229 .saturating_add(value.len())
230 })
231}
232
233fn checked_u32_len(len: usize, field: &str) -> Result<u32, Error> {
234 u32::try_from(len).map_err(|_| invalid_tombstone(format!("{field} is too large")))
235}
236
237fn checked_u64_len(len: usize, field: &str) -> Result<u64, Error> {
238 u64::try_from(len).map_err(|_| invalid_tombstone(format!("{field} is too large")))
239}
240
241fn checked_add(base: usize, len: usize, field: &str) -> Result<usize, Error> {
242 base.checked_add(len)
243 .ok_or_else(|| invalid_tombstone(format!("{field} length overflows usize")))
244}
245
246fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, Error> {
247 let value = bytes
248 .get(offset..offset + 4)
249 .ok_or_else(|| invalid_tombstone("tombstone header is truncated"))?;
250 Ok(u32::from_be_bytes(
251 value.try_into().expect("fixed slice length"),
252 ))
253}
254
255fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, Error> {
256 let value = bytes
257 .get(offset..offset + 8)
258 .ok_or_else(|| invalid_tombstone("tombstone header is truncated"))?;
259 Ok(u64::from_be_bytes(
260 value.try_into().expect("fixed slice length"),
261 ))
262}
263
264fn decode_utf8(bytes: &[u8], field: &str) -> Result<String, Error> {
265 std::str::from_utf8(bytes)
266 .map(str::to_owned)
267 .map_err(|err| invalid_tombstone(format!("{field} is not valid UTF-8: {err}")))
268}
269
270fn invalid_tombstone(message: impl Into<String>) -> Error {
271 Error::Deserialize(format!("invalid tombstone: {}", message.into()))
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn tombstone_round_trips_and_encodes_metadata_deterministically() {
280 let left = Tombstone::new(b"agent-a".to_vec(), 1_700_000)
281 .with_causal_metadata("right-root", b"r1".to_vec())
282 .with_causal_metadata("left-root", b"l1".to_vec());
283 let right = Tombstone::new(b"agent-a".to_vec(), 1_700_000)
284 .with_causal_metadata("left-root", b"l1".to_vec())
285 .with_causal_metadata("right-root", b"r1".to_vec());
286
287 assert_eq!(left.to_bytes().unwrap(), right.to_bytes().unwrap());
288
289 let decoded = Tombstone::from_bytes(&left.to_bytes().unwrap()).unwrap();
290 assert_eq!(decoded, left);
291 assert_eq!(decoded.causal_metadata("left-root"), Some(b"l1".as_slice()));
292 }
293
294 #[test]
295 fn stored_bytes_distinguish_non_tombstones_from_invalid_tombstones() {
296 assert_eq!(Tombstone::from_stored_bytes(b"value").unwrap(), None);
297 assert!(matches!(
298 Tombstone::from_stored_bytes(b"PLTD"),
299 Err(Error::Deserialize(_))
300 ));
301 }
302
303 #[test]
304 fn compaction_returns_delete_only_for_tombstones() {
305 let tombstone = Tombstone::new(b"actor".to_vec(), 42);
306 let mutation = tombstone_compaction(b"k".to_vec(), &tombstone.to_bytes().unwrap())
307 .unwrap()
308 .unwrap();
309 assert_eq!(mutation, Mutation::Delete { key: b"k".to_vec() });
310
311 assert_eq!(tombstone_compaction(b"k".to_vec(), b"value").unwrap(), None);
312 }
313}