Skip to main content

idb/innodb/
page.rs

1//! InnoDB page header and trailer parsing.
2//!
3//! Every InnoDB page begins with a 38-byte FIL header ([`FilHeader`]) containing
4//! the checksum, page number, prev/next pointers, LSN, page type, flush LSN, and
5//! space ID. The last 8 bytes form the FIL trailer ([`FilTrailer`]) with the
6//! old-style checksum and low 32 bits of the LSN.
7//!
8//! Page 0 of every tablespace also contains the FSP header ([`FspHeader`]) at
9//! byte offset 38, which stores the space ID, tablespace size, and feature flags
10//! (page size, compression, encryption).
11
12use byteorder::{BigEndian, ByteOrder};
13use serde::Serialize;
14
15use crate::innodb::constants::*;
16use crate::innodb::page_types::PageType;
17
18/// Parsed FIL header (38 bytes, present at the start of every InnoDB page).
19#[derive(Debug, Clone, Serialize)]
20pub struct FilHeader {
21    /// Checksum (or space id in older formats). Bytes 0-3.
22    pub checksum: u32,
23    /// Page number within the tablespace. Bytes 4-7.
24    pub page_number: u32,
25    /// Previous page in the doubly-linked list. Bytes 8-11.
26    /// FIL_NULL (0xFFFFFFFF) if not used.
27    pub prev_page: u32,
28    /// Next page in the doubly-linked list. Bytes 12-15.
29    /// FIL_NULL (0xFFFFFFFF) if not used.
30    pub next_page: u32,
31    /// LSN of newest modification to this page. Bytes 16-23.
32    pub lsn: u64,
33    /// Page type. Bytes 24-25.
34    pub page_type: PageType,
35    /// Flush LSN (only meaningful for page 0 of system tablespace). Bytes 26-33.
36    pub flush_lsn: u64,
37    /// Space ID this page belongs to. Bytes 34-37.
38    pub space_id: u32,
39}
40
41impl FilHeader {
42    /// Parse a FIL header from a byte slice.
43    ///
44    /// The slice must be at least SIZE_FIL_HEAD (38) bytes.
45    pub fn parse(data: &[u8]) -> Option<Self> {
46        if data.len() < SIZE_FIL_HEAD {
47            return None;
48        }
49
50        Some(FilHeader {
51            checksum: BigEndian::read_u32(&data[FIL_PAGE_SPACE_OR_CHKSUM..]),
52            page_number: BigEndian::read_u32(&data[FIL_PAGE_OFFSET..]),
53            prev_page: BigEndian::read_u32(&data[FIL_PAGE_PREV..]),
54            next_page: BigEndian::read_u32(&data[FIL_PAGE_NEXT..]),
55            lsn: BigEndian::read_u64(&data[FIL_PAGE_LSN..]),
56            page_type: PageType::from_u16(BigEndian::read_u16(&data[FIL_PAGE_TYPE..])),
57            flush_lsn: BigEndian::read_u64(&data[FIL_PAGE_FILE_FLUSH_LSN..]),
58            space_id: BigEndian::read_u32(&data[FIL_PAGE_SPACE_ID..]),
59        })
60    }
61
62    /// Returns true if prev_page is FIL_NULL (not used).
63    pub fn has_prev(&self) -> bool {
64        self.prev_page != FIL_NULL && self.prev_page != 0
65    }
66
67    /// Returns true if next_page is FIL_NULL (not used).
68    pub fn has_next(&self) -> bool {
69        self.next_page != FIL_NULL && self.next_page != 0
70    }
71}
72
73/// Parsed FIL trailer (8 bytes, present at the end of every InnoDB page).
74#[derive(Debug, Clone, Serialize)]
75pub struct FilTrailer {
76    /// Old-style checksum (or low 32 bits of LSN, depending on version). Bytes 0-3 of trailer.
77    pub checksum: u32,
78    /// Low 32 bits of the LSN. Bytes 4-7 of trailer.
79    pub lsn_low32: u32,
80}
81
82impl FilTrailer {
83    /// Parse a FIL trailer from a byte slice.
84    ///
85    /// The slice should be the last 8 bytes of the page, or at least 8 bytes
86    /// starting from the trailer position.
87    pub fn parse(data: &[u8]) -> Option<Self> {
88        if data.len() < SIZE_FIL_TRAILER {
89            return None;
90        }
91
92        Some(FilTrailer {
93            checksum: BigEndian::read_u32(&data[0..]),
94            lsn_low32: BigEndian::read_u32(&data[4..]),
95        })
96    }
97}
98
99/// Parsed FSP header (from page 0 of a tablespace, starts at FIL_PAGE_DATA).
100#[derive(Debug, Clone, Serialize)]
101pub struct FspHeader {
102    /// Space ID.
103    pub space_id: u32,
104    /// Size of the tablespace in pages.
105    pub size: u32,
106    /// Minimum page number not yet initialized.
107    pub free_limit: u32,
108    /// Space flags (contains page size, compression, encryption info).
109    pub flags: u32,
110    /// Number of used pages in the FSP_FREE_FRAG list.
111    pub frag_n_used: u32,
112}
113
114impl FspHeader {
115    /// Parse the FSP header from page 0's data area.
116    ///
117    /// `data` should be the full page buffer. FSP header starts at FIL_PAGE_DATA (byte 38).
118    pub fn parse(page_data: &[u8]) -> Option<Self> {
119        let offset = FIL_PAGE_DATA;
120        if page_data.len() < offset + FSP_HEADER_SIZE {
121            return None;
122        }
123        let data = &page_data[offset..];
124
125        Some(FspHeader {
126            space_id: BigEndian::read_u32(&data[FSP_SPACE_ID..]),
127            size: BigEndian::read_u32(&data[FSP_SIZE..]),
128            free_limit: BigEndian::read_u32(&data[FSP_FREE_LIMIT..]),
129            flags: BigEndian::read_u32(&data[FSP_SPACE_FLAGS..]),
130            frag_n_used: BigEndian::read_u32(&data[FSP_FRAG_N_USED..]),
131        })
132    }
133
134    /// Extract the page size from FSP flags.
135    ///
136    /// Returns the page size in bytes, or None if the flags indicate the default (16K).
137    pub fn page_size_from_flags(&self) -> u32 {
138        let ssize = (self.flags & FSP_FLAGS_MASK_PAGE_SSIZE) >> FSP_FLAGS_POS_PAGE_SSIZE;
139        if ssize == 0 {
140            // Default/uncompressed: 16K
141            SIZE_PAGE_DEFAULT
142        } else {
143            // ssize encodes page size as: 512 << ssize for values 1-7
144            // In practice: ssize=3 => 4K, ssize=4 => 8K, ssize=5 => 16K, etc.
145            // MySQL source: page_size = (512 << ssize) for ssize 1-7
146            // But there's a special case: if ssize >= 1, page_size = 1 << (ssize + 9)
147            // ssize=1 => 1024, ssize=2 => 2048, ssize=3 => 4096, ssize=4 => 8192,
148            // ssize=5 => 16384, ssize=6 => 32768, ssize=7 => 65536
149            1u32 << (ssize + 9)
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    fn make_fil_header_bytes(
159        checksum: u32,
160        page_num: u32,
161        prev: u32,
162        next: u32,
163        lsn: u64,
164        page_type: u16,
165        flush_lsn: u64,
166        space_id: u32,
167    ) -> Vec<u8> {
168        let mut buf = vec![0u8; SIZE_FIL_HEAD];
169        BigEndian::write_u32(&mut buf[FIL_PAGE_SPACE_OR_CHKSUM..], checksum);
170        BigEndian::write_u32(&mut buf[FIL_PAGE_OFFSET..], page_num);
171        BigEndian::write_u32(&mut buf[FIL_PAGE_PREV..], prev);
172        BigEndian::write_u32(&mut buf[FIL_PAGE_NEXT..], next);
173        BigEndian::write_u64(&mut buf[FIL_PAGE_LSN..], lsn);
174        BigEndian::write_u16(&mut buf[FIL_PAGE_TYPE..], page_type);
175        BigEndian::write_u64(&mut buf[FIL_PAGE_FILE_FLUSH_LSN..], flush_lsn);
176        BigEndian::write_u32(&mut buf[FIL_PAGE_SPACE_ID..], space_id);
177        buf
178    }
179
180    #[test]
181    fn test_fil_header_parse() {
182        let data = make_fil_header_bytes(
183            0x12345678, // checksum
184            42,         // page number
185            41,         // prev page
186            43,         // next page
187            1000,       // lsn
188            17855,      // INDEX page type
189            2000,       // flush lsn
190            5,          // space id
191        );
192        let hdr = FilHeader::parse(&data).unwrap();
193        assert_eq!(hdr.checksum, 0x12345678);
194        assert_eq!(hdr.page_number, 42);
195        assert_eq!(hdr.prev_page, 41);
196        assert_eq!(hdr.next_page, 43);
197        assert_eq!(hdr.lsn, 1000);
198        assert_eq!(hdr.page_type, PageType::Index);
199        assert_eq!(hdr.flush_lsn, 2000);
200        assert_eq!(hdr.space_id, 5);
201        assert!(hdr.has_prev());
202        assert!(hdr.has_next());
203    }
204
205    #[test]
206    fn test_fil_header_null_pages() {
207        let data = make_fil_header_bytes(0, 0, FIL_NULL, FIL_NULL, 0, 0, 0, 0);
208        let hdr = FilHeader::parse(&data).unwrap();
209        assert!(!hdr.has_prev());
210        assert!(!hdr.has_next());
211    }
212
213    #[test]
214    fn test_fil_header_too_short() {
215        let data = vec![0u8; 10];
216        assert!(FilHeader::parse(&data).is_none());
217    }
218
219    #[test]
220    fn test_fil_trailer_parse() {
221        let mut data = vec![0u8; 8];
222        BigEndian::write_u32(&mut data[0..], 0xAABBCCDD);
223        BigEndian::write_u32(&mut data[4..], 0x11223344);
224        let trl = FilTrailer::parse(&data).unwrap();
225        assert_eq!(trl.checksum, 0xAABBCCDD);
226        assert_eq!(trl.lsn_low32, 0x11223344);
227    }
228
229    #[test]
230    fn test_fsp_header_page_size() {
231        let fsp = FspHeader {
232            space_id: 0,
233            size: 100,
234            free_limit: 64,
235            flags: 0, // ssize=0 => default 16K
236            frag_n_used: 0,
237        };
238        assert_eq!(fsp.page_size_from_flags(), SIZE_PAGE_DEFAULT);
239
240        // ssize=5 => 16384
241        let fsp_16k = FspHeader {
242            flags: 5 << FSP_FLAGS_POS_PAGE_SSIZE,
243            ..fsp
244        };
245        assert_eq!(fsp_16k.page_size_from_flags(), 16384);
246
247        // ssize=3 => 4096
248        let fsp_4k = FspHeader {
249            flags: 3 << FSP_FLAGS_POS_PAGE_SSIZE,
250            ..fsp
251        };
252        assert_eq!(fsp_4k.page_size_from_flags(), 4096);
253    }
254}