nodedb_array/tile/
cell_payload.rs1pub use nodedb_types::OPEN_UPPER;
11use nodedb_types::Surrogate;
12use serde::{Deserialize, Serialize};
13
14use crate::error::{ArrayError, ArrayResult};
15use crate::types::cell_value::value::CellValue;
16
17pub const CELL_TOMBSTONE_SENTINEL: &[u8] = &[0xFF];
19
20pub const CELL_GDPR_ERASURE_SENTINEL: &[u8] = &[0xFE];
22
23#[derive(
27 Debug,
28 Clone,
29 PartialEq,
30 Serialize,
31 Deserialize,
32 zerompk::ToMessagePack,
33 zerompk::FromMessagePack,
34)]
35pub struct CellPayload {
36 pub valid_from_ms: i64,
37 pub valid_until_ms: i64,
38 pub attrs: Vec<CellValue>,
39 pub surrogate: Surrogate,
40}
41
42impl CellPayload {
43 pub fn encode(&self) -> ArrayResult<Vec<u8>> {
46 self.check_bound()?;
47 zerompk::to_msgpack_vec(self).map_err(|e| ArrayError::SegmentCorruption {
48 detail: format!("encode CellPayload: {e}"),
49 })
50 }
51
52 pub fn decode(bytes: &[u8]) -> ArrayResult<Self> {
54 let payload: Self =
55 zerompk::from_msgpack(bytes).map_err(|e| ArrayError::SegmentCorruption {
56 detail: format!("decode CellPayload: {e}"),
57 })?;
58 payload.check_bound()?;
59 Ok(payload)
60 }
61
62 fn check_bound(&self) -> ArrayResult<()> {
63 if self.surrogate == Surrogate::ZERO {
64 return Err(ArrayError::SegmentCorruption {
65 detail: "live cell payload carries Surrogate::ZERO, which names no row".into(),
66 });
67 }
68 Ok(())
69 }
70}
71
72pub fn is_cell_sentinel(bytes: &[u8]) -> bool {
74 is_cell_tombstone(bytes) || is_cell_gdpr_erasure(bytes)
75}
76
77pub fn is_cell_tombstone(bytes: &[u8]) -> bool {
79 bytes == CELL_TOMBSTONE_SENTINEL
80}
81
82pub fn is_cell_gdpr_erasure(bytes: &[u8]) -> bool {
84 bytes == CELL_GDPR_ERASURE_SENTINEL
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use nodedb_types::Surrogate;
91
92 fn sample_payload() -> CellPayload {
93 CellPayload {
94 valid_from_ms: 1_000,
95 valid_until_ms: OPEN_UPPER,
96 attrs: vec![CellValue::Int64(42)],
97 surrogate: Surrogate::new(7),
98 }
99 }
100
101 #[test]
102 fn a_payload_under_zero_is_refused() {
103 let mut p = sample_payload();
104 p.surrogate = Surrogate::ZERO;
105 assert!(p.encode().is_err());
106 let raw = zerompk::to_msgpack_vec(&p).unwrap();
107 assert!(CellPayload::decode(&raw).is_err());
108 }
109
110 #[test]
111 fn payload_msgpack_roundtrip() {
112 let p = sample_payload();
113 let bytes = p.encode().unwrap();
114 let decoded = CellPayload::decode(&bytes).unwrap();
115 assert_eq!(decoded.valid_from_ms, 1_000);
116 assert_eq!(decoded.valid_until_ms, OPEN_UPPER);
117 assert_eq!(decoded.attrs, vec![CellValue::Int64(42)]);
118 assert_eq!(decoded.surrogate, Surrogate::new(7));
119 }
120
121 #[test]
122 fn encoded_payload_first_byte_is_fixarray() {
123 let bytes = sample_payload().encode().unwrap();
124 assert_eq!(bytes[0], 0x94);
126 }
127
128 #[test]
129 fn sentinels_are_disjoint_from_payload() {
130 let bytes = sample_payload().encode().unwrap();
131 assert!(is_cell_sentinel(&[0xFF]));
132 assert!(is_cell_sentinel(&[0xFE]));
133 assert!(is_cell_tombstone(&[0xFF]));
134 assert!(is_cell_gdpr_erasure(&[0xFE]));
135 assert!(!is_cell_sentinel(&bytes[..1]));
136 }
137
138 #[test]
139 fn tombstone_and_erasure_are_distinct() {
140 assert!(!is_cell_tombstone(CELL_GDPR_ERASURE_SENTINEL));
141 assert!(!is_cell_gdpr_erasure(CELL_TOMBSTONE_SENTINEL));
142 }
143}