umadb_core/
header_node.rs

1use crate::common::Position;
2use crate::common::{PageID, Tsn};
3use bitflags::bitflags;
4use byteorder::{ByteOrder, LittleEndian};
5use umadb_dcb::{DCBError, DCBResult};
6
7bitflags! {
8    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
9    pub struct HeaderFlags: u16 {
10        const HAS_TRACKING_ROOT_ID = 0b0000_0001;
11    }
12}
13
14// Node type definitions
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct HeaderNode {
17    pub tsn: Tsn,
18    pub free_lists_tree_root_id: PageID,
19    pub events_tree_root_id: PageID,
20    pub tags_tree_root_id: PageID,
21    pub next_page_id: PageID,
22    pub next_position: Position,
23    /// On-disk schema version for the header node
24    pub schema_version: u32,
25    pub tracking_root_page_id: PageID,
26}
27
28impl Default for HeaderNode {
29    fn default() -> Self {
30        Self {
31            tsn: Tsn(0),
32            free_lists_tree_root_id: PageID(0),
33            events_tree_root_id: PageID(0),
34            tags_tree_root_id: PageID(0),
35            next_page_id: PageID(0),
36            next_position: Position(0),
37            schema_version: crate::db::DB_SCHEMA_VERSION,
38            tracking_root_page_id: PageID(0),
39        }
40    }
41}
42
43impl HeaderNode {
44    /// Writes the serialized HeaderNode into the provided buffer and returns the number of bytes written (52).
45    /// The buffer must be at least 52 bytes long.
46    pub fn calc_serialized_size(&self) -> usize {
47        let mut required_buf = 52;
48        let mut flags = HeaderFlags::empty();
49        if self.tracking_root_page_id != PageID(0) {
50            flags |= HeaderFlags::HAS_TRACKING_ROOT_ID;
51            required_buf += 8;
52        }
53        if !flags.is_empty() {
54            required_buf += 2;
55        }
56        required_buf
57    }
58
59    pub fn serialize_into(&self, buf: &mut [u8]) -> usize {
60        let mut required_buf = 52;
61        assert!(
62            buf.len() >= required_buf,
63            "HeaderNode::serialize_into buf must be at least 52 bytes"
64        );
65        let mut flags = HeaderFlags::empty();
66
67        // Write fields in little-endian order (first 48 bytes are the legacy layout)
68        buf[0..8].copy_from_slice(&self.tsn.0.to_le_bytes());
69        buf[8..16].copy_from_slice(&self.next_page_id.0.to_le_bytes());
70        buf[16..24].copy_from_slice(&self.free_lists_tree_root_id.0.to_le_bytes());
71        buf[24..32].copy_from_slice(&self.events_tree_root_id.0.to_le_bytes());
72        buf[32..40].copy_from_slice(&self.tags_tree_root_id.0.to_le_bytes());
73        buf[40..48].copy_from_slice(&self.next_position.0.to_le_bytes());
74        // Append schema version at the end (new field, keeps first 48 bytes compatible)
75        buf[48..52].copy_from_slice(&self.schema_version.to_le_bytes());
76        if self.tracking_root_page_id != PageID(0) {
77            flags |= HeaderFlags::HAS_TRACKING_ROOT_ID;
78            required_buf += 8;
79        }
80        if flags.is_empty() {
81            return required_buf;
82        }
83        required_buf += 2;
84        assert!(
85            buf.len() >= required_buf,
86            "HeaderNode::serialize_into buf must be at least {required_buf} bytes",
87        );
88        // Set the flags.
89        buf[52..54].copy_from_slice(&flags.bits().to_le_bytes());
90        if flags.contains(HeaderFlags::HAS_TRACKING_ROOT_ID) {
91            buf[54..62].copy_from_slice(&self.tracking_root_page_id.0.to_le_bytes())
92        }
93        required_buf
94    }
95
96    /// Creates a HeaderNode from a byte slice
97    /// Accepts legacy 48-byte layout (no schema, schema_version=0) or the current 52-byte layout (schema as u32 at the end).
98    /// Layout (first 48 bytes):
99    /// - 8 bytes for tsn
100    /// - 8 bytes for next_page_id
101    /// - 8 bytes for free_lists_tree_root_id
102    /// - 8 bytes for events_tree_root_id
103    /// - 8 bytes for tags_tree_root_id
104    /// - 8 bytes for next_position
105    ///
106    /// # Arguments
107    /// * `slice` - The byte slice to deserialize from
108    ///
109    /// # Returns
110    /// * `Result<Self>` - The deserialized HeaderNode or an error
111    pub fn from_slice(slice: &[u8]) -> DCBResult<Self> {
112        if slice.len() < 48 {
113            return Err(DCBError::DeserializationError(format!(
114                "Expected at least 48 bytes, got {}",
115                slice.len()
116            )));
117        }
118
119        let tsn = LittleEndian::read_u64(&slice[0..8]);
120        let next_page_id = LittleEndian::read_u64(&slice[8..16]);
121        let freetree_root_id = LittleEndian::read_u64(&slice[16..24]);
122        let position_root_id = LittleEndian::read_u64(&slice[24..32]);
123        let tags_root_id = LittleEndian::read_u64(&slice[32..40]);
124        let next_position = LittleEndian::read_u64(&slice[40..48]);
125        let schema_version: u32 = if slice.len() >= 52 {
126            LittleEndian::read_u32(&slice[48..52])
127        } else {
128            0u32
129        };
130        let flags = if slice.len() >= 54 {
131            HeaderFlags::from_bits(LittleEndian::read_u16(&slice[52..54])).ok_or(
132                DCBError::DeserializationError("unknown flag bits set".to_string()),
133            )?
134        } else {
135            HeaderFlags::empty()
136        };
137        let mut required_len = 54;
138        let mut tracking_tree_root_id = 0u64;
139
140        if flags.contains(HeaderFlags::HAS_TRACKING_ROOT_ID) {
141            required_len += 8;
142            if slice.len() < required_len {
143                return Err(DCBError::DeserializationError(format!(
144                    "Expected at least {required_len} bytes, got {}",
145                    slice.len()
146                )));
147            }
148            tracking_tree_root_id = LittleEndian::read_u64(&slice[required_len - 8..required_len]);
149        }
150
151        Ok(HeaderNode {
152            tsn: Tsn(tsn),
153            next_page_id: PageID(next_page_id),
154            free_lists_tree_root_id: PageID(freetree_root_id),
155            events_tree_root_id: PageID(position_root_id),
156            tags_tree_root_id: PageID(tags_root_id),
157            next_position: Position(next_position),
158            schema_version,
159            tracking_root_page_id: PageID(tracking_tree_root_id),
160        })
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    #[test]
168    fn test_header_serialize_without_tracking_root_page_id() {
169        // Create a HeaderNode with known values
170        let header_node = HeaderNode {
171            tsn: Tsn(42),
172            next_page_id: PageID(123),
173            free_lists_tree_root_id: PageID(456),
174            events_tree_root_id: PageID(789),
175            tags_tree_root_id: PageID(321),
176            next_position: Position(9876543210),
177            schema_version: crate::db::DB_SCHEMA_VERSION,
178            tracking_root_page_id: PageID(0),
179        };
180
181        assert_eq!(52, header_node.calc_serialized_size());
182
183        // Serialize the HeaderNode
184        let mut serialized = [0u8; 52];
185        let serialized_size = header_node.serialize_into(&mut serialized);
186
187        // Verify the serialized output has the correct length
188        assert_eq!(52, serialized_size);
189
190        // Verify the serialized output has the correct byte values
191        // TSN(42) = 42u64 = [42, 0, 0, 0, 0, 0, 0, 0] in little-endian
192        assert_eq!(&42u64.to_le_bytes(), &serialized[0..8]);
193
194        // PageID(123) as u64
195        assert_eq!(&123u64.to_le_bytes(), &serialized[8..16]);
196
197        // PageID(456) as u64
198        assert_eq!(&456u64.to_le_bytes(), &serialized[16..24]);
199
200        // PageID(789) as u64
201        assert_eq!(&789u64.to_le_bytes(), &serialized[24..32]);
202
203        // root_tags_tree_id PageID(321) as u64
204        assert_eq!(&321u64.to_le_bytes(), &serialized[32..40]);
205
206        // next_position 9876543210u64 => little-endian bytes
207        assert_eq!(&9876543210u64.to_le_bytes(), &serialized[40..48]);
208
209        // schema_version
210        assert_eq!(
211            &crate::db::DB_SCHEMA_VERSION.to_le_bytes(),
212            &serialized[48..52]
213        );
214
215        // Deserialize back to a HeaderNode
216        let deserialized =
217            HeaderNode::from_slice(&serialized).expect("Failed to deserialize HeaderNode");
218
219        // Verify that the deserialized node matches the original
220        assert_eq!(header_node.tsn, deserialized.tsn);
221        assert_eq!(header_node.next_page_id, deserialized.next_page_id);
222        assert_eq!(
223            header_node.free_lists_tree_root_id,
224            deserialized.free_lists_tree_root_id
225        );
226        assert_eq!(
227            header_node.events_tree_root_id,
228            deserialized.events_tree_root_id
229        );
230        assert_eq!(header_node.next_position, deserialized.next_position);
231        assert_eq!(header_node.schema_version, deserialized.schema_version);
232        assert_eq!(
233            header_node.tracking_root_page_id,
234            deserialized.tracking_root_page_id
235        );
236    }
237
238    #[test]
239    fn test_header_serialize_with_tracking_root_page_id() {
240        // Create a HeaderNode with known values
241        let header_node = HeaderNode {
242            tsn: Tsn(42),
243            next_page_id: PageID(123),
244            free_lists_tree_root_id: PageID(456),
245            events_tree_root_id: PageID(789),
246            tags_tree_root_id: PageID(321),
247            next_position: Position(9876543210),
248            schema_version: crate::db::DB_SCHEMA_VERSION,
249            tracking_root_page_id: PageID(953),
250        };
251
252        assert_eq!(62, header_node.calc_serialized_size());
253
254        // Serialize the HeaderNode
255        let mut serialized = [0u8; 62];
256        let serialized_size = header_node.serialize_into(&mut serialized);
257
258        // Verify the serialized output has the correct length
259        assert_eq!(62, serialized_size);
260
261        // Verify the serialized output has the correct byte values
262        // TSN(42) = 42u64 = [42, 0, 0, 0, 0, 0, 0, 0] in little-endian
263        assert_eq!(&42u64.to_le_bytes(), &serialized[0..8]);
264
265        // PageID(123) as u64
266        assert_eq!(&123u64.to_le_bytes(), &serialized[8..16]);
267
268        // PageID(456) as u64
269        assert_eq!(&456u64.to_le_bytes(), &serialized[16..24]);
270
271        // PageID(789) as u64
272        assert_eq!(&789u64.to_le_bytes(), &serialized[24..32]);
273
274        // root_tags_tree_id PageID(321) as u64
275        assert_eq!(&321u64.to_le_bytes(), &serialized[32..40]);
276
277        // next_position 9876543210u64 => little-endian bytes
278        assert_eq!(&9876543210u64.to_le_bytes(), &serialized[40..48]);
279
280        // schema_version
281        assert_eq!(
282            &crate::db::DB_SCHEMA_VERSION.to_le_bytes(),
283            &serialized[48..52]
284        );
285
286        // bit flags
287        assert_eq!(&1u16.to_le_bytes(), &serialized[52..54]);
288
289        // tracking tree root ID
290        assert_eq!(&953u64.to_le_bytes(), &serialized[54..62]);
291
292        // Deserialize back to a HeaderNode
293        let deserialized =
294            HeaderNode::from_slice(&serialized).expect("Failed to deserialize HeaderNode");
295
296        // Verify that the deserialized node matches the original
297        assert_eq!(header_node.tsn, deserialized.tsn);
298        assert_eq!(header_node.next_page_id, deserialized.next_page_id);
299        assert_eq!(
300            header_node.free_lists_tree_root_id,
301            deserialized.free_lists_tree_root_id
302        );
303        assert_eq!(
304            header_node.events_tree_root_id,
305            deserialized.events_tree_root_id
306        );
307        assert_eq!(header_node.next_position, deserialized.next_position);
308        assert_eq!(header_node.schema_version, deserialized.schema_version);
309        assert_eq!(
310            header_node.tracking_root_page_id,
311            deserialized.tracking_root_page_id
312        );
313    }
314}
315
316#[cfg(test)]
317mod header_node_legacy_tests {
318    use super::*;
319
320    #[test]
321    fn test_legacy_48_byte_deserialize_sets_schema_version_0() {
322        // Build a header and serialize to current 52-byte format
323        let header_node = HeaderNode {
324            tsn: Tsn(1),
325            next_page_id: PageID(2),
326            free_lists_tree_root_id: PageID(3),
327            events_tree_root_id: PageID(4),
328            tags_tree_root_id: PageID(5),
329            next_position: Position(6),
330            schema_version: crate::db::DB_SCHEMA_VERSION,
331            tracking_root_page_id: PageID(0),
332        };
333        let mut bytes52 = [0u8; 52];
334        header_node.serialize_into(&mut bytes52);
335
336        // Take only the first 48 bytes to simulate legacy on-disk header
337        let mut bytes48 = [0u8; 48];
338        bytes48.copy_from_slice(&bytes52[..48]);
339
340        // Deserialize and verify schema_version defaults to 0
341        let deserialized =
342            HeaderNode::from_slice(&bytes48).expect("legacy 48-byte header should deserialize");
343        assert_eq!(deserialized.tsn, Tsn(1));
344        assert_eq!(deserialized.next_page_id, PageID(2));
345        assert_eq!(deserialized.free_lists_tree_root_id, PageID(3));
346        assert_eq!(deserialized.events_tree_root_id, PageID(4));
347        assert_eq!(deserialized.tags_tree_root_id, PageID(5));
348        assert_eq!(deserialized.next_position, Position(6));
349        assert_eq!(deserialized.schema_version, 0);
350    }
351}