umadb_core/
page.rs

1use crate::common::PageID;
2use crate::node::Node;
3use std::ops::Range;
4use umadb_dcb::{DCBError, DCBResult};
5
6// Page structure
7#[derive(Debug, Clone)]
8pub struct Page {
9    pub page_id: PageID,
10    pub node: Node,
11}
12
13// Page header format: type(1) + crc(4) + len(4)
14pub const PAGE_HEADER_SIZE: usize = 9;
15const HEADER_LAYOUT_NODE_TYPE_BYTE: usize = 0;
16const HEADER_LAYOUT_CRC_BYTES: Range<usize> = 1..5;
17const HEADER_LAYOUT_BODY_LEN_BYTES: Range<usize> = 5..9;
18
19// Implementation for Page
20impl Page {
21    pub fn new(page_id: PageID, node: Node) -> Self {
22        Self { page_id, node }
23    }
24
25    #[inline]
26    pub fn calc_serialized_size(&self) -> usize {
27        // The total serialized size is the size of the page header plus the size of the serialized node
28        PAGE_HEADER_SIZE + self.node.calc_serialized_size()
29    }
30
31    /// Serialized page (header + body + zero padding) into `buf`.
32    pub fn serialize_into(&self, buf: &mut [u8]) -> DCBResult<()> {
33        serialize_page_into(buf, &self.node)?;
34        Ok(())
35    }
36
37    #[inline]
38    pub fn deserialize(page_id: PageID, page_data: &[u8]) -> DCBResult<Self> {
39        if page_data.len() < PAGE_HEADER_SIZE {
40            return Err(DCBError::DatabaseCorrupted(
41                "Page data too short".to_string(),
42            ));
43        }
44
45        // Extract header information with minimal bounds checks
46        let header = &page_data[..PAGE_HEADER_SIZE];
47        let node_type = header[HEADER_LAYOUT_NODE_TYPE_BYTE];
48        let crc = u32::from_le_bytes(header[HEADER_LAYOUT_CRC_BYTES].try_into().unwrap());
49        let data_len =
50            u32::from_le_bytes(header[HEADER_LAYOUT_BODY_LEN_BYTES].try_into().unwrap()) as usize;
51
52        if PAGE_HEADER_SIZE + data_len > page_data.len() {
53            return Err(DCBError::DatabaseCorrupted(
54                "Page data length mismatch".to_string(),
55            ));
56        }
57
58        // Extract the data
59        let data = &page_data[PAGE_HEADER_SIZE..PAGE_HEADER_SIZE + data_len];
60
61        // Verify CRC
62        let calculated_crc = calc_crc(data);
63
64        if calculated_crc != crc {
65            return Err(DCBError::DatabaseCorrupted(format!(
66                "CRC mismatch (page ID: {page_id:?})"
67            )));
68        }
69
70        // Deserialize the node
71        let node = Node::deserialize(node_type, data)?;
72
73        Ok(Self { page_id, node })
74    }
75}
76
77pub fn serialize_page_into(buf: &mut [u8], node_ref: &Node) -> Result<(), DCBError> {
78    let body_len = serialize_page_node_into(buf, node_ref)?;
79    serialize_page_header_into(buf, body_len, node_ref.get_type_byte());
80    Ok(())
81}
82
83#[inline(always)]
84fn serialize_page_node_into(buf: &mut [u8], node_ref: &Node) -> Result<usize, DCBError> {
85    // Serialize body into the front of the body region using the space after header
86    let body_len = {
87        let body_slice = &mut buf[PAGE_HEADER_SIZE..];
88        node_ref.serialize_into(body_slice)?
89    };
90
91    // Zero-fill the remainder of the page after the serialized body
92    let tail_start = PAGE_HEADER_SIZE + body_len;
93    if tail_start < buf.len() {
94        buf[tail_start..].fill(0);
95    }
96    Ok(body_len)
97}
98
99#[inline(always)]
100fn serialize_page_header_into(buf: &mut [u8], body_len: usize, node_type_byte: u8) {
101    // Compute CRC over the actual body bytes
102    let crc = calc_crc(&buf[PAGE_HEADER_SIZE..PAGE_HEADER_SIZE + body_len]);
103
104    // Fill page header
105    buf[HEADER_LAYOUT_NODE_TYPE_BYTE] = node_type_byte;
106    buf[HEADER_LAYOUT_CRC_BYTES].copy_from_slice(&crc.to_le_bytes());
107    buf[HEADER_LAYOUT_BODY_LEN_BYTES].copy_from_slice(&(body_len as u32).to_le_bytes());
108}
109
110/// Calculate CRC32 checksum for data
111#[inline(always)]
112pub fn calc_crc(data: &[u8]) -> u32 {
113    crc32fast::hash(data)
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::common::Position;
120    use crate::common::{PageID, Tsn};
121    use crate::header_node::HeaderNode;
122
123    #[test]
124    fn test_page_serialization_and_size() {
125        // Create a HeaderNode (simplest node type)
126        let node = Node::Header(HeaderNode {
127            tsn: Tsn(42),
128            next_page_id: PageID(123),
129            free_lists_tree_root_id: PageID(456),
130            events_tree_root_id: PageID(789),
131            tags_tree_root_id: PageID(1011),
132            next_position: Position(1234),
133        });
134
135        // Create a Page with the node
136        let page_id = PageID(1);
137        let page = Page::new(page_id, node);
138
139        // Calculate the serialized size
140        let calculated_size = page.calc_serialized_size();
141
142        // Serialize the page into a fixed-size buffer using serialize_into_vec
143        let mut page_buf = vec![0u8; crate::db::DEFAULT_PAGE_SIZE];
144        page.serialize_into(&mut page_buf)
145            .expect("Failed to serialize page into buffer");
146
147        // Check that the effective serialized data size (header + body_len from header) matches the calculated size
148        let body_len = u32::from_le_bytes(page_buf[5..9].try_into().unwrap()) as usize;
149        let effective_len = PAGE_HEADER_SIZE + body_len;
150        assert_eq!(
151            calculated_size, effective_len,
152            "Calculated size {} should match effective serialized size {}",
153            calculated_size, effective_len
154        );
155
156        // Deserialize the serialized data from the full page buffer
157        let deserialized =
158            Page::deserialize(page_id, &page_buf).expect("Failed to deserialize page");
159
160        // Check that the deserialized page is the same as the original
161        assert_eq!(
162            page.page_id, deserialized.page_id,
163            "Original page_id {:?} should match deserialized page_id {:?}",
164            page.page_id, deserialized.page_id
165        );
166        assert_eq!(
167            page.node, deserialized.node,
168            "Original node {:?} should match deserialized node {:?}",
169            page.node, deserialized.node
170        );
171    }
172}