Skip to main content

idb/innodb/
page.rs

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