Skip to main content

umadb_core/
header_node.rs

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