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    ///
46    /// # Examples
47    ///
48    /// ```
49    /// use idb::innodb::page::FilHeader;
50    /// use idb::innodb::page_types::PageType;
51    /// use byteorder::{BigEndian, WriteBytesExt};
52    /// use std::io::{Cursor, Write};
53    ///
54    /// // Build a 38-byte FIL header with known values
55    /// let mut buf = vec![0u8; 38];
56    /// let mut c = Cursor::new(&mut buf);
57    /// c.write_u32::<BigEndian>(0xDEADBEEF).unwrap(); // checksum
58    /// c.write_u32::<BigEndian>(3).unwrap();           // page number
59    /// c.write_u32::<BigEndian>(2).unwrap();           // prev page
60    /// c.write_u32::<BigEndian>(4).unwrap();           // next page
61    /// c.write_u64::<BigEndian>(5000).unwrap();        // LSN
62    /// c.write_u16::<BigEndian>(17855).unwrap();       // page type (INDEX)
63    /// c.write_u64::<BigEndian>(4000).unwrap();        // flush LSN
64    /// c.write_u32::<BigEndian>(1).unwrap();           // space ID
65    ///
66    /// let header = FilHeader::parse(&buf).unwrap();
67    /// assert_eq!(header.checksum, 0xDEADBEEF);
68    /// assert_eq!(header.page_number, 3);
69    /// assert_eq!(header.prev_page, 2);
70    /// assert_eq!(header.next_page, 4);
71    /// assert_eq!(header.lsn, 5000);
72    /// assert_eq!(header.page_type, PageType::Index);
73    /// assert_eq!(header.flush_lsn, 4000);
74    /// assert_eq!(header.space_id, 1);
75    /// ```
76    pub fn parse(data: &[u8]) -> Option<Self> {
77        if data.len() < SIZE_FIL_HEAD {
78            return None;
79        }
80
81        Some(FilHeader {
82            checksum: BigEndian::read_u32(&data[FIL_PAGE_SPACE_OR_CHKSUM..]),
83            page_number: BigEndian::read_u32(&data[FIL_PAGE_OFFSET..]),
84            prev_page: BigEndian::read_u32(&data[FIL_PAGE_PREV..]),
85            next_page: BigEndian::read_u32(&data[FIL_PAGE_NEXT..]),
86            lsn: BigEndian::read_u64(&data[FIL_PAGE_LSN..]),
87            page_type: PageType::from_u16(BigEndian::read_u16(&data[FIL_PAGE_TYPE..])),
88            flush_lsn: BigEndian::read_u64(&data[FIL_PAGE_FILE_FLUSH_LSN..]),
89            space_id: BigEndian::read_u32(&data[FIL_PAGE_SPACE_ID..]),
90        })
91    }
92
93    /// Returns true if prev_page is FIL_NULL (not used).
94    pub fn has_prev(&self) -> bool {
95        self.prev_page != FIL_NULL && self.prev_page != 0
96    }
97
98    /// Returns true if next_page is FIL_NULL (not used).
99    pub fn has_next(&self) -> bool {
100        self.next_page != FIL_NULL && self.next_page != 0
101    }
102}
103
104/// Parsed FIL trailer (8 bytes, present at the end of every InnoDB page).
105#[derive(Debug, Clone, Serialize)]
106pub struct FilTrailer {
107    /// Old-style checksum (or low 32 bits of LSN, depending on version). Bytes 0-3 of trailer.
108    pub checksum: u32,
109    /// Low 32 bits of the LSN. Bytes 4-7 of trailer.
110    pub lsn_low32: u32,
111}
112
113impl FilTrailer {
114    /// Parse a FIL trailer from a byte slice.
115    ///
116    /// The slice should be the last 8 bytes of the page, or at least 8 bytes
117    /// starting from the trailer position.
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use idb::innodb::page::FilTrailer;
123    /// use byteorder::{BigEndian, WriteBytesExt};
124    /// use std::io::Cursor;
125    ///
126    /// // Build an 8-byte FIL trailer
127    /// let mut buf = vec![0u8; 8];
128    /// let mut c = Cursor::new(&mut buf);
129    /// c.write_u32::<BigEndian>(0xAABBCCDD).unwrap(); // old-style checksum
130    /// c.write_u32::<BigEndian>(0x11223344).unwrap(); // LSN low 32 bits
131    ///
132    /// let trailer = FilTrailer::parse(&buf).unwrap();
133    /// assert_eq!(trailer.checksum, 0xAABBCCDD);
134    /// assert_eq!(trailer.lsn_low32, 0x11223344);
135    /// ```
136    pub fn parse(data: &[u8]) -> Option<Self> {
137        if data.len() < SIZE_FIL_TRAILER {
138            return None;
139        }
140
141        Some(FilTrailer {
142            checksum: BigEndian::read_u32(&data[0..]),
143            lsn_low32: BigEndian::read_u32(&data[4..]),
144        })
145    }
146}
147
148/// Parsed FSP header (from page 0 of a tablespace, starts at FIL_PAGE_DATA).
149#[derive(Debug, Clone, Serialize)]
150pub struct FspHeader {
151    /// Space ID.
152    pub space_id: u32,
153    /// Size of the tablespace in pages.
154    pub size: u32,
155    /// Minimum page number not yet initialized.
156    pub free_limit: u32,
157    /// Space flags (contains page size, compression, encryption info).
158    pub flags: u32,
159    /// Number of used pages in the FSP_FREE_FRAG list.
160    pub frag_n_used: u32,
161}
162
163impl FspHeader {
164    /// Parse the FSP header from page 0's data area.
165    ///
166    /// `data` should be the full page buffer. FSP header starts at FIL_PAGE_DATA (byte 38).
167    ///
168    /// # Examples
169    ///
170    /// ```
171    /// use idb::innodb::page::FspHeader;
172    /// use byteorder::{BigEndian, ByteOrder};
173    ///
174    /// // Build a buffer large enough for the FIL header (38 bytes) + FSP header (112 bytes)
175    /// let mut buf = vec![0u8; 150];
176    /// let fsp_offset = 38; // FSP header starts at FIL_PAGE_DATA
177    ///
178    /// // Write FSP header fields at their offsets within the FSP region
179    /// BigEndian::write_u32(&mut buf[fsp_offset..], 42);       // space_id (offset 0)
180    /// BigEndian::write_u32(&mut buf[fsp_offset + 8..], 1000); // size in pages (offset 8)
181    /// BigEndian::write_u32(&mut buf[fsp_offset + 12..], 64);  // free_limit (offset 12)
182    /// BigEndian::write_u32(&mut buf[fsp_offset + 16..], 0);   // flags (offset 16)
183    /// BigEndian::write_u32(&mut buf[fsp_offset + 20..], 10);  // frag_n_used (offset 20)
184    ///
185    /// let fsp = FspHeader::parse(&buf).unwrap();
186    /// assert_eq!(fsp.space_id, 42);
187    /// assert_eq!(fsp.size, 1000);
188    /// assert_eq!(fsp.free_limit, 64);
189    /// assert_eq!(fsp.flags, 0);
190    /// assert_eq!(fsp.frag_n_used, 10);
191    /// ```
192    pub fn parse(page_data: &[u8]) -> Option<Self> {
193        let offset = FIL_PAGE_DATA;
194        if page_data.len() < offset + FSP_HEADER_SIZE {
195            return None;
196        }
197        let data = &page_data[offset..];
198
199        Some(FspHeader {
200            space_id: BigEndian::read_u32(&data[FSP_SPACE_ID..]),
201            size: BigEndian::read_u32(&data[FSP_SIZE..]),
202            free_limit: BigEndian::read_u32(&data[FSP_FREE_LIMIT..]),
203            flags: BigEndian::read_u32(&data[FSP_SPACE_FLAGS..]),
204            frag_n_used: BigEndian::read_u32(&data[FSP_FRAG_N_USED..]),
205        })
206    }
207
208    /// Extract the page size from FSP flags.
209    ///
210    /// Returns the page size in bytes. For MariaDB full_crc32 tablespaces,
211    /// the page size is in bits 0-3 instead of bits 6-9.
212    pub fn page_size_from_flags(&self) -> u32 {
213        use crate::innodb::vendor::detect_vendor_from_flags;
214
215        let vendor_info = detect_vendor_from_flags(self.flags);
216        self.page_size_from_flags_with_vendor(&vendor_info)
217    }
218
219    /// Extract the page size from FSP flags with explicit vendor info.
220    pub fn page_size_from_flags_with_vendor(
221        &self,
222        vendor_info: &crate::innodb::vendor::VendorInfo,
223    ) -> u32 {
224        let ssize = if vendor_info.is_full_crc32() {
225            // MariaDB full_crc32: page size in bits 0-3
226            self.flags & MARIADB_FSP_FLAGS_FCRC32_PAGE_SSIZE_MASK
227        } else {
228            // MySQL / MariaDB original: page size in bits 6-9
229            (self.flags & FSP_FLAGS_MASK_PAGE_SSIZE) >> FSP_FLAGS_POS_PAGE_SSIZE
230        };
231
232        if ssize == 0 {
233            // Default/uncompressed: 16K
234            SIZE_PAGE_DEFAULT
235        } else {
236            // ssize encodes page size as: 1 << (ssize + 9)
237            // ssize=3 => 4K, ssize=4 => 8K, ssize=5 => 16K, ssize=6 => 32K, ssize=7 => 64K
238            1u32 << (ssize + 9)
239        }
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    fn make_fil_header_bytes(
248        checksum: u32,
249        page_num: u32,
250        prev: u32,
251        next: u32,
252        lsn: u64,
253        page_type: u16,
254        flush_lsn: u64,
255        space_id: u32,
256    ) -> Vec<u8> {
257        let mut buf = vec![0u8; SIZE_FIL_HEAD];
258        BigEndian::write_u32(&mut buf[FIL_PAGE_SPACE_OR_CHKSUM..], checksum);
259        BigEndian::write_u32(&mut buf[FIL_PAGE_OFFSET..], page_num);
260        BigEndian::write_u32(&mut buf[FIL_PAGE_PREV..], prev);
261        BigEndian::write_u32(&mut buf[FIL_PAGE_NEXT..], next);
262        BigEndian::write_u64(&mut buf[FIL_PAGE_LSN..], lsn);
263        BigEndian::write_u16(&mut buf[FIL_PAGE_TYPE..], page_type);
264        BigEndian::write_u64(&mut buf[FIL_PAGE_FILE_FLUSH_LSN..], flush_lsn);
265        BigEndian::write_u32(&mut buf[FIL_PAGE_SPACE_ID..], space_id);
266        buf
267    }
268
269    #[test]
270    fn test_fil_header_parse() {
271        let data = make_fil_header_bytes(
272            0x12345678, // checksum
273            42,         // page number
274            41,         // prev page
275            43,         // next page
276            1000,       // lsn
277            17855,      // INDEX page type
278            2000,       // flush lsn
279            5,          // space id
280        );
281        let hdr = FilHeader::parse(&data).unwrap();
282        assert_eq!(hdr.checksum, 0x12345678);
283        assert_eq!(hdr.page_number, 42);
284        assert_eq!(hdr.prev_page, 41);
285        assert_eq!(hdr.next_page, 43);
286        assert_eq!(hdr.lsn, 1000);
287        assert_eq!(hdr.page_type, PageType::Index);
288        assert_eq!(hdr.flush_lsn, 2000);
289        assert_eq!(hdr.space_id, 5);
290        assert!(hdr.has_prev());
291        assert!(hdr.has_next());
292    }
293
294    #[test]
295    fn test_fil_header_null_pages() {
296        let data = make_fil_header_bytes(0, 0, FIL_NULL, FIL_NULL, 0, 0, 0, 0);
297        let hdr = FilHeader::parse(&data).unwrap();
298        assert!(!hdr.has_prev());
299        assert!(!hdr.has_next());
300    }
301
302    #[test]
303    fn test_fil_header_too_short() {
304        let data = vec![0u8; 10];
305        assert!(FilHeader::parse(&data).is_none());
306    }
307
308    #[test]
309    fn test_fil_trailer_parse() {
310        let mut data = vec![0u8; 8];
311        BigEndian::write_u32(&mut data[0..], 0xAABBCCDD);
312        BigEndian::write_u32(&mut data[4..], 0x11223344);
313        let trl = FilTrailer::parse(&data).unwrap();
314        assert_eq!(trl.checksum, 0xAABBCCDD);
315        assert_eq!(trl.lsn_low32, 0x11223344);
316    }
317
318    #[test]
319    fn test_fsp_header_page_size() {
320        let fsp = FspHeader {
321            space_id: 0,
322            size: 100,
323            free_limit: 64,
324            flags: 0, // ssize=0 => default 16K
325            frag_n_used: 0,
326        };
327        assert_eq!(fsp.page_size_from_flags(), SIZE_PAGE_DEFAULT);
328
329        // ssize=5 => 16384
330        let fsp_16k = FspHeader {
331            flags: 5 << FSP_FLAGS_POS_PAGE_SSIZE,
332            ..fsp
333        };
334        assert_eq!(fsp_16k.page_size_from_flags(), 16384);
335
336        // ssize=3 => 4096
337        let fsp_4k = FspHeader {
338            flags: 3 << FSP_FLAGS_POS_PAGE_SSIZE,
339            ..fsp
340        };
341        assert_eq!(fsp_4k.page_size_from_flags(), 4096);
342    }
343
344    #[test]
345    fn test_fsp_header_page_size_mariadb_full_crc32() {
346        use crate::innodb::vendor::{MariaDbFormat, VendorInfo};
347
348        let vendor = VendorInfo::mariadb(MariaDbFormat::FullCrc32);
349
350        // MariaDB full_crc32: ssize in bits 0-3, marker at bit 4
351        // ssize=5 (16K) + full_crc32 marker
352        let fsp = FspHeader {
353            space_id: 0,
354            size: 100,
355            free_limit: 64,
356            flags: 0x10 | 5, // bit 4 marker + ssize=5 in bits 0-3
357            frag_n_used: 0,
358        };
359        assert_eq!(fsp.page_size_from_flags_with_vendor(&vendor), 16384);
360
361        // ssize=3 (4K)
362        let fsp_4k = FspHeader {
363            flags: 0x10 | 3,
364            ..fsp
365        };
366        assert_eq!(fsp_4k.page_size_from_flags_with_vendor(&vendor), 4096);
367
368        // ssize=0 (default 16K)
369        let fsp_default = FspHeader { flags: 0x10, ..fsp };
370        assert_eq!(
371            fsp_default.page_size_from_flags_with_vendor(&vendor),
372            SIZE_PAGE_DEFAULT
373        );
374    }
375}