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}