Skip to main content

idb/innodb/
log.rs

1//! InnoDB redo log file parsing.
2//!
3//! Reads InnoDB redo log files (`ib_logfile0`/`ib_logfile1` for MySQL < 8.0.30,
4//! or `#ib_redo*` files for 8.0.30+). The file layout consists of a 2048-byte
5//! header (4 blocks of 512 bytes each) containing the log file header and two
6//! checkpoint records, followed by 512-byte data blocks.
7//!
8//! Use [`LogFile::open`] to read and parse the header and checkpoint records,
9//! then [`LogFile::read_block`] to read individual data blocks. Each block's
10//! [`LogBlockHeader`] provides the block number, data length, checkpoint number,
11//! and CRC-32C checksum validation status.
12
13use byteorder::{BigEndian, ByteOrder};
14use serde::Serialize;
15use std::fs::File;
16use std::io::{Read, Seek, SeekFrom};
17
18use crate::IdbError;
19
20/// Size of a redo log block in bytes (from MySQL `log0log.h`).
21pub const LOG_BLOCK_SIZE: usize = 512;
22/// Size of the log block header in bytes.
23pub const LOG_BLOCK_HDR_SIZE: usize = 14;
24/// Size of the log block trailer in bytes.
25pub const LOG_BLOCK_TRL_SIZE: usize = 4;
26/// Bitmask for the flush flag in the block number field (bit 31).
27pub const LOG_BLOCK_FLUSH_BIT_MASK: u32 = 0x80000000;
28/// Byte offset of the CRC-32C checksum within a block (bytes 508-511).
29pub const LOG_BLOCK_CHECKSUM_OFFSET: usize = 508;
30/// Number of reserved header/checkpoint blocks at the start of the file.
31pub const LOG_FILE_HDR_BLOCKS: u64 = 4;
32
33/// Offset of the group ID within the log file header (block 0).
34pub const LOG_HEADER_GROUP_ID: usize = 0;
35/// Offset of the start LSN within the log file header.
36pub const LOG_HEADER_START_LSN: usize = 4;
37/// Offset of the file number within the log file header.
38pub const LOG_HEADER_FILE_NO: usize = 12;
39/// Offset of the creator string within the log file header.
40pub const LOG_HEADER_CREATED_BY: usize = 16;
41/// Maximum length of the creator string.
42pub const LOG_HEADER_CREATED_BY_LEN: usize = 32;
43
44/// Offset of the checkpoint number within a checkpoint block.
45pub const LOG_CHECKPOINT_NO: usize = 0;
46/// Offset of the checkpoint LSN within a checkpoint block.
47pub const LOG_CHECKPOINT_LSN: usize = 8;
48/// Offset of the checkpoint byte-offset within a checkpoint block.
49pub const LOG_CHECKPOINT_OFFSET: usize = 16;
50/// Offset of the log buffer size within a checkpoint block.
51pub const LOG_CHECKPOINT_BUF_SIZE: usize = 20;
52/// Offset of the archived LSN within a checkpoint block.
53pub const LOG_CHECKPOINT_ARCHIVED_LSN: usize = 24;
54
55/// Log file header (block 0 of the redo log file).
56#[derive(Debug, Clone, Serialize)]
57pub struct LogFileHeader {
58    /// Log group ID.
59    pub group_id: u32,
60    /// Start LSN of this log file.
61    pub start_lsn: u64,
62    /// File number within the log group.
63    pub file_no: u32,
64    /// MySQL version string that created this log file (e.g. "MySQL 8.0.32").
65    pub created_by: String,
66}
67
68impl LogFileHeader {
69    /// Parse a log file header from the first 512-byte block.
70    pub fn parse(block: &[u8]) -> Option<Self> {
71        if block.len() < LOG_BLOCK_SIZE {
72            return None;
73        }
74
75        let group_id = BigEndian::read_u32(&block[LOG_HEADER_GROUP_ID..]);
76        let start_lsn = BigEndian::read_u64(&block[LOG_HEADER_START_LSN..]);
77        let file_no = BigEndian::read_u32(&block[LOG_HEADER_FILE_NO..]);
78
79        let created_bytes =
80            &block[LOG_HEADER_CREATED_BY..LOG_HEADER_CREATED_BY + LOG_HEADER_CREATED_BY_LEN];
81        let created_by = created_bytes
82            .iter()
83            .take_while(|&&b| b != 0)
84            .map(|&b| b as char)
85            .collect::<String>();
86
87        Some(LogFileHeader {
88            group_id,
89            start_lsn,
90            file_no,
91            created_by,
92        })
93    }
94}
95
96/// Checkpoint record (blocks 1 and 3 of the redo log file).
97#[derive(Debug, Clone, Serialize)]
98pub struct LogCheckpoint {
99    /// Checkpoint sequence number.
100    pub number: u64,
101    /// LSN at the time of this checkpoint.
102    pub lsn: u64,
103    /// Byte offset of the checkpoint within the log file.
104    pub offset: u32,
105    /// Log buffer size at checkpoint time.
106    pub buf_size: u32,
107    /// LSN up to which log has been archived.
108    pub archived_lsn: u64,
109}
110
111impl LogCheckpoint {
112    /// Parse a checkpoint from a 512-byte block.
113    pub fn parse(block: &[u8]) -> Option<Self> {
114        if block.len() < LOG_BLOCK_SIZE {
115            return None;
116        }
117
118        let number = BigEndian::read_u64(&block[LOG_CHECKPOINT_NO..]);
119        let lsn = BigEndian::read_u64(&block[LOG_CHECKPOINT_LSN..]);
120        let offset = BigEndian::read_u32(&block[LOG_CHECKPOINT_OFFSET..]);
121        let buf_size = BigEndian::read_u32(&block[LOG_CHECKPOINT_BUF_SIZE..]);
122        let archived_lsn = BigEndian::read_u64(&block[LOG_CHECKPOINT_ARCHIVED_LSN..]);
123
124        Some(LogCheckpoint {
125            number,
126            lsn,
127            offset,
128            buf_size,
129            archived_lsn,
130        })
131    }
132}
133
134/// Log block header (first 14 bytes of each 512-byte block).
135#[derive(Debug, Clone, Serialize)]
136pub struct LogBlockHeader {
137    /// Block number (with flush bit masked out).
138    pub block_no: u32,
139    /// Whether this block was the first in a flush batch (bit 31).
140    pub flush_flag: bool,
141    /// Number of bytes of log data in this block (including header).
142    pub data_len: u16,
143    /// Offset of the first log record group starting in this block.
144    pub first_rec_group: u16,
145    /// Checkpoint number when this block was written.
146    pub checkpoint_no: u32,
147}
148
149impl LogBlockHeader {
150    /// Parse a log block header from a 512-byte block.
151    pub fn parse(block: &[u8]) -> Option<Self> {
152        if block.len() < LOG_BLOCK_HDR_SIZE {
153            return None;
154        }
155
156        let raw_block_no = BigEndian::read_u32(&block[0..]);
157        let flush_flag = (raw_block_no & LOG_BLOCK_FLUSH_BIT_MASK) != 0;
158        let block_no = raw_block_no & !LOG_BLOCK_FLUSH_BIT_MASK;
159
160        let data_len = BigEndian::read_u16(&block[4..]);
161        let first_rec_group = BigEndian::read_u16(&block[6..]);
162        let checkpoint_no = BigEndian::read_u32(&block[8..]);
163
164        Some(LogBlockHeader {
165            block_no,
166            flush_flag,
167            data_len,
168            first_rec_group,
169            checkpoint_no,
170        })
171    }
172
173    /// Returns true if this block contains log data (data_len > header size).
174    pub fn has_data(&self) -> bool {
175        self.data_len as usize > LOG_BLOCK_HDR_SIZE
176    }
177}
178
179/// Log block trailer (last 4 bytes of each 512-byte block).
180#[derive(Debug, Clone, Serialize)]
181pub struct LogBlockTrailer {
182    /// CRC-32C checksum of the block (bytes 0..508).
183    pub checksum: u32,
184}
185
186impl LogBlockTrailer {
187    /// Parse a log block trailer from a 512-byte block.
188    pub fn parse(block: &[u8]) -> Option<Self> {
189        if block.len() < LOG_BLOCK_SIZE {
190            return None;
191        }
192
193        let checksum = BigEndian::read_u32(&block[LOG_BLOCK_CHECKSUM_OFFSET..]);
194
195        Some(LogBlockTrailer { checksum })
196    }
197}
198
199/// Validate a log block's CRC-32C checksum.
200///
201/// The checksum covers bytes 0..508 of the block (everything except the checksum field itself).
202pub fn validate_log_block_checksum(block: &[u8]) -> bool {
203    if block.len() < LOG_BLOCK_SIZE {
204        return false;
205    }
206    let stored = BigEndian::read_u32(&block[LOG_BLOCK_CHECKSUM_OFFSET..]);
207    let calculated = crc32c::crc32c(&block[..LOG_BLOCK_CHECKSUM_OFFSET]);
208    stored == calculated
209}
210
211/// MLOG record types (from MySQL mtr0types.h).
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
213pub enum MlogRecordType {
214    Mlog1Byte,
215    Mlog2Bytes,
216    Mlog4Bytes,
217    Mlog8Bytes,
218    MlogRecInsert,
219    MlogRecClustDeleteMark,
220    MlogRecSecDeleteMark,
221    MlogRecUpdate,
222    MlogRecDelete,
223    MlogListEndDelete,
224    MlogListStartDelete,
225    MlogListEndCopyCreated,
226    MlogPageReorganize,
227    MlogPageCreate,
228    MlogUndoInsert,
229    MlogUndoEraseEnd,
230    MlogUndoInit,
231    MlogUndoHdrReuse,
232    MlogRecMinMark,
233    MlogIbufBitmapInit,
234    MlogInitFilePage,
235    MlogWriteString,
236    MlogMultiRecEnd,
237    MlogDummyRecord,
238    MlogFileDelete,
239    MlogCompPageCreate,
240    MlogCompRecInsert,
241    MlogCompRecClustDeleteMark,
242    MlogCompRecSecDeleteMark,
243    MlogCompRecUpdate,
244    MlogCompRecDelete,
245    MlogCompListEndDelete,
246    MlogCompListStartDelete,
247    MlogCompListEndCopyCreated,
248    MlogCompPageReorganize,
249    MlogFileRename,
250    MlogPageCreateRTree,
251    MlogCompPageCreateRTree,
252    MlogTableDynamicMeta,
253    MlogPageCreateSdi,
254    MlogCompPageCreateSdi,
255    MlogFileOpen,
256    MlogFileCreate,
257    MlogZipPageCompress,
258    Unknown(u8),
259}
260
261impl MlogRecordType {
262    /// Convert a u8 type code to MlogRecordType.
263    pub fn from_u8(val: u8) -> Self {
264        match val {
265            1 => MlogRecordType::Mlog1Byte,
266            2 => MlogRecordType::Mlog2Bytes,
267            4 => MlogRecordType::Mlog4Bytes,
268            8 => MlogRecordType::Mlog8Bytes,
269            9 => MlogRecordType::MlogRecInsert,
270            10 => MlogRecordType::MlogRecClustDeleteMark,
271            11 => MlogRecordType::MlogRecSecDeleteMark,
272            13 => MlogRecordType::MlogRecUpdate,
273            14 => MlogRecordType::MlogRecDelete,
274            15 => MlogRecordType::MlogListEndDelete,
275            16 => MlogRecordType::MlogListStartDelete,
276            17 => MlogRecordType::MlogListEndCopyCreated,
277            18 => MlogRecordType::MlogPageReorganize,
278            19 => MlogRecordType::MlogPageCreate,
279            20 => MlogRecordType::MlogUndoInsert,
280            21 => MlogRecordType::MlogUndoEraseEnd,
281            22 => MlogRecordType::MlogUndoInit,
282            24 => MlogRecordType::MlogUndoHdrReuse,
283            28 => MlogRecordType::MlogRecMinMark,
284            29 => MlogRecordType::MlogIbufBitmapInit,
285            30 => MlogRecordType::MlogInitFilePage,
286            31 => MlogRecordType::MlogWriteString,
287            32 => MlogRecordType::MlogMultiRecEnd,
288            33 => MlogRecordType::MlogDummyRecord,
289            34 => MlogRecordType::MlogFileDelete,
290            35 => MlogRecordType::MlogCompPageCreate,
291            36 => MlogRecordType::MlogCompRecInsert,
292            37 => MlogRecordType::MlogCompRecClustDeleteMark,
293            38 => MlogRecordType::MlogCompRecSecDeleteMark,
294            39 => MlogRecordType::MlogCompRecUpdate,
295            40 => MlogRecordType::MlogCompRecDelete,
296            41 => MlogRecordType::MlogCompListEndDelete,
297            42 => MlogRecordType::MlogCompListStartDelete,
298            43 => MlogRecordType::MlogCompListEndCopyCreated,
299            44 => MlogRecordType::MlogCompPageReorganize,
300            45 => MlogRecordType::MlogFileRename,
301            46 => MlogRecordType::MlogPageCreateRTree,
302            47 => MlogRecordType::MlogCompPageCreateRTree,
303            48 => MlogRecordType::MlogTableDynamicMeta,
304            49 => MlogRecordType::MlogPageCreateSdi,
305            50 => MlogRecordType::MlogCompPageCreateSdi,
306            51 => MlogRecordType::MlogFileOpen,
307            52 => MlogRecordType::MlogFileCreate,
308            53 => MlogRecordType::MlogZipPageCompress,
309            v => MlogRecordType::Unknown(v),
310        }
311    }
312
313    /// Display name for this record type.
314    pub fn name(&self) -> &str {
315        match self {
316            MlogRecordType::Mlog1Byte => "MLOG_1BYTE",
317            MlogRecordType::Mlog2Bytes => "MLOG_2BYTES",
318            MlogRecordType::Mlog4Bytes => "MLOG_4BYTES",
319            MlogRecordType::Mlog8Bytes => "MLOG_8BYTES",
320            MlogRecordType::MlogRecInsert => "MLOG_REC_INSERT",
321            MlogRecordType::MlogRecClustDeleteMark => "MLOG_REC_CLUST_DELETE_MARK",
322            MlogRecordType::MlogRecSecDeleteMark => "MLOG_REC_SEC_DELETE_MARK",
323            MlogRecordType::MlogRecUpdate => "MLOG_REC_UPDATE_IN_PLACE",
324            MlogRecordType::MlogRecDelete => "MLOG_REC_DELETE",
325            MlogRecordType::MlogListEndDelete => "MLOG_LIST_END_DELETE",
326            MlogRecordType::MlogListStartDelete => "MLOG_LIST_START_DELETE",
327            MlogRecordType::MlogListEndCopyCreated => "MLOG_LIST_END_COPY_CREATED",
328            MlogRecordType::MlogPageReorganize => "MLOG_PAGE_REORGANIZE",
329            MlogRecordType::MlogPageCreate => "MLOG_PAGE_CREATE",
330            MlogRecordType::MlogUndoInsert => "MLOG_UNDO_INSERT",
331            MlogRecordType::MlogUndoEraseEnd => "MLOG_UNDO_ERASE_END",
332            MlogRecordType::MlogUndoInit => "MLOG_UNDO_INIT",
333            MlogRecordType::MlogUndoHdrReuse => "MLOG_UNDO_HDR_REUSE",
334            MlogRecordType::MlogRecMinMark => "MLOG_REC_MIN_MARK",
335            MlogRecordType::MlogIbufBitmapInit => "MLOG_IBUF_BITMAP_INIT",
336            MlogRecordType::MlogInitFilePage => "MLOG_INIT_FILE_PAGE",
337            MlogRecordType::MlogWriteString => "MLOG_WRITE_STRING",
338            MlogRecordType::MlogMultiRecEnd => "MLOG_MULTI_REC_END",
339            MlogRecordType::MlogDummyRecord => "MLOG_DUMMY_RECORD",
340            MlogRecordType::MlogFileDelete => "MLOG_FILE_DELETE",
341            MlogRecordType::MlogCompPageCreate => "MLOG_COMP_PAGE_CREATE",
342            MlogRecordType::MlogCompRecInsert => "MLOG_COMP_REC_INSERT",
343            MlogRecordType::MlogCompRecClustDeleteMark => "MLOG_COMP_REC_CLUST_DELETE_MARK",
344            MlogRecordType::MlogCompRecSecDeleteMark => "MLOG_COMP_REC_SEC_DELETE_MARK",
345            MlogRecordType::MlogCompRecUpdate => "MLOG_COMP_REC_UPDATE_IN_PLACE",
346            MlogRecordType::MlogCompRecDelete => "MLOG_COMP_REC_DELETE",
347            MlogRecordType::MlogCompListEndDelete => "MLOG_COMP_LIST_END_DELETE",
348            MlogRecordType::MlogCompListStartDelete => "MLOG_COMP_LIST_START_DELETE",
349            MlogRecordType::MlogCompListEndCopyCreated => "MLOG_COMP_LIST_END_COPY_CREATED",
350            MlogRecordType::MlogCompPageReorganize => "MLOG_COMP_PAGE_REORGANIZE",
351            MlogRecordType::MlogFileRename => "MLOG_FILE_RENAME",
352            MlogRecordType::MlogPageCreateRTree => "MLOG_PAGE_CREATE_RTREE",
353            MlogRecordType::MlogCompPageCreateRTree => "MLOG_COMP_PAGE_CREATE_RTREE",
354            MlogRecordType::MlogTableDynamicMeta => "MLOG_TABLE_DYNAMIC_META",
355            MlogRecordType::MlogPageCreateSdi => "MLOG_PAGE_CREATE_SDI",
356            MlogRecordType::MlogCompPageCreateSdi => "MLOG_COMP_PAGE_CREATE_SDI",
357            MlogRecordType::MlogFileOpen => "MLOG_FILE_OPEN",
358            MlogRecordType::MlogFileCreate => "MLOG_FILE_CREATE",
359            MlogRecordType::MlogZipPageCompress => "MLOG_ZIP_PAGE_COMPRESS",
360            MlogRecordType::Unknown(_) => "UNKNOWN",
361        }
362    }
363}
364
365impl std::fmt::Display for MlogRecordType {
366    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367        match self {
368            MlogRecordType::Unknown(v) => write!(f, "UNKNOWN({})", v),
369            _ => write!(f, "{}", self.name()),
370        }
371    }
372}
373
374/// Redo log file reader.
375pub struct LogFile {
376    file: File,
377    file_size: u64,
378}
379
380impl LogFile {
381    /// Open a redo log file.
382    pub fn open(path: &str) -> Result<Self, IdbError> {
383        let file =
384            File::open(path).map_err(|e| IdbError::Io(format!("Cannot open {}: {}", path, e)))?;
385        let file_size = file
386            .metadata()
387            .map_err(|e| IdbError::Io(format!("Cannot stat {}: {}", path, e)))?
388            .len();
389
390        if file_size < (LOG_FILE_HDR_BLOCKS as usize * LOG_BLOCK_SIZE) as u64 {
391            return Err(IdbError::Parse(format!(
392                "File {} is too small for a redo log ({} bytes, minimum {})",
393                path,
394                file_size,
395                LOG_FILE_HDR_BLOCKS as usize * LOG_BLOCK_SIZE
396            )));
397        }
398
399        Ok(LogFile { file, file_size })
400    }
401
402    /// Total number of 512-byte blocks in the file.
403    pub fn block_count(&self) -> u64 {
404        self.file_size / LOG_BLOCK_SIZE as u64
405    }
406
407    /// Number of data blocks (excluding the 4 header/checkpoint blocks).
408    pub fn data_block_count(&self) -> u64 {
409        self.block_count().saturating_sub(LOG_FILE_HDR_BLOCKS)
410    }
411
412    /// Read a single 512-byte block by block number.
413    pub fn read_block(&mut self, block_no: u64) -> Result<Vec<u8>, IdbError> {
414        let offset = block_no * LOG_BLOCK_SIZE as u64;
415        if offset + LOG_BLOCK_SIZE as u64 > self.file_size {
416            return Err(IdbError::Io(format!(
417                "Block {} is beyond end of file (offset {}, file size {})",
418                block_no, offset, self.file_size
419            )));
420        }
421
422        self.file
423            .seek(SeekFrom::Start(offset))
424            .map_err(|e| IdbError::Io(format!("Seek error: {}", e)))?;
425
426        let mut buf = vec![0u8; LOG_BLOCK_SIZE];
427        self.file
428            .read_exact(&mut buf)
429            .map_err(|e| IdbError::Io(format!("Read error at block {}: {}", block_no, e)))?;
430
431        Ok(buf)
432    }
433
434    /// Read and parse the log file header (block 0).
435    pub fn read_header(&mut self) -> Result<LogFileHeader, IdbError> {
436        let block = self.read_block(0)?;
437        LogFileHeader::parse(&block).ok_or_else(|| {
438            IdbError::Parse("Failed to parse log file header (block 0)".to_string())
439        })
440    }
441
442    /// Read and parse a checkpoint (slot 0 = block 1, slot 1 = block 3).
443    pub fn read_checkpoint(&mut self, slot: u8) -> Result<LogCheckpoint, IdbError> {
444        let block_no = match slot {
445            0 => 1,
446            1 => 3,
447            _ => {
448                return Err(IdbError::Argument(format!(
449                    "Invalid checkpoint slot {} (must be 0 or 1)",
450                    slot
451                )))
452            }
453        };
454        let block = self.read_block(block_no)?;
455        LogCheckpoint::parse(&block).ok_or_else(|| {
456            IdbError::Parse(format!("Failed to parse checkpoint at block {}", block_no))
457        })
458    }
459
460    /// File size in bytes.
461    pub fn file_size(&self) -> u64 {
462        self.file_size
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469
470    fn make_block() -> Vec<u8> {
471        vec![0u8; LOG_BLOCK_SIZE]
472    }
473
474    #[test]
475    fn test_log_block_header_parse() {
476        let mut block = make_block();
477        // block_no = 42 (no flush bit)
478        BigEndian::write_u32(&mut block[0..], 42);
479        // data_len = 200
480        BigEndian::write_u16(&mut block[4..], 200);
481        // first_rec_group = 50
482        BigEndian::write_u16(&mut block[6..], 50);
483        // checkpoint_no = 7
484        BigEndian::write_u32(&mut block[8..], 7);
485
486        let hdr = LogBlockHeader::parse(&block).unwrap();
487        assert_eq!(hdr.block_no, 42);
488        assert!(!hdr.flush_flag);
489        assert_eq!(hdr.data_len, 200);
490        assert_eq!(hdr.first_rec_group, 50);
491        assert_eq!(hdr.checkpoint_no, 7);
492        assert!(hdr.has_data());
493    }
494
495    #[test]
496    fn test_log_block_flush_bit() {
497        let mut block = make_block();
498        // Set flush bit (bit 31) + block_no = 100
499        BigEndian::write_u32(&mut block[0..], 0x80000064);
500        BigEndian::write_u16(&mut block[4..], 14); // data_len = header only
501
502        let hdr = LogBlockHeader::parse(&block).unwrap();
503        assert!(hdr.flush_flag);
504        assert_eq!(hdr.block_no, 100);
505        assert!(!hdr.has_data());
506    }
507
508    #[test]
509    fn test_log_block_header_empty() {
510        let block = make_block();
511        let hdr = LogBlockHeader::parse(&block).unwrap();
512        assert_eq!(hdr.block_no, 0);
513        assert!(!hdr.flush_flag);
514        assert_eq!(hdr.data_len, 0);
515        assert_eq!(hdr.first_rec_group, 0);
516        assert_eq!(hdr.checkpoint_no, 0);
517        assert!(!hdr.has_data());
518    }
519
520    #[test]
521    fn test_log_block_header_too_small() {
522        let block = vec![0u8; 10]; // less than LOG_BLOCK_HDR_SIZE
523        assert!(LogBlockHeader::parse(&block).is_none());
524    }
525
526    #[test]
527    fn test_log_block_trailer_parse() {
528        let mut block = make_block();
529        BigEndian::write_u32(&mut block[LOG_BLOCK_CHECKSUM_OFFSET..], 0xCAFEBABE);
530
531        let trailer = LogBlockTrailer::parse(&block).unwrap();
532        assert_eq!(trailer.checksum, 0xCAFEBABE);
533    }
534
535    #[test]
536    fn test_log_file_header_parse() {
537        let mut block = make_block();
538        BigEndian::write_u32(&mut block[LOG_HEADER_GROUP_ID..], 1);
539        BigEndian::write_u64(&mut block[LOG_HEADER_START_LSN..], 0x00000000001A2B3C);
540        BigEndian::write_u32(&mut block[LOG_HEADER_FILE_NO..], 0);
541        // "MySQL 8.0.32" as created_by
542        let creator = b"MySQL 8.0.32";
543        block[LOG_HEADER_CREATED_BY..LOG_HEADER_CREATED_BY + creator.len()]
544            .copy_from_slice(creator);
545
546        let hdr = LogFileHeader::parse(&block).unwrap();
547        assert_eq!(hdr.group_id, 1);
548        assert_eq!(hdr.start_lsn, 0x1A2B3C);
549        assert_eq!(hdr.file_no, 0);
550        assert_eq!(hdr.created_by, "MySQL 8.0.32");
551    }
552
553    #[test]
554    fn test_log_file_header_empty_created_by() {
555        let block = make_block();
556        let hdr = LogFileHeader::parse(&block).unwrap();
557        assert_eq!(hdr.created_by, "");
558    }
559
560    #[test]
561    fn test_log_checkpoint_parse() {
562        let mut block = make_block();
563        BigEndian::write_u64(&mut block[LOG_CHECKPOINT_NO..], 99);
564        BigEndian::write_u64(&mut block[LOG_CHECKPOINT_LSN..], 0x00000000DEADBEEF);
565        BigEndian::write_u32(&mut block[LOG_CHECKPOINT_OFFSET..], 2048);
566        BigEndian::write_u32(&mut block[LOG_CHECKPOINT_BUF_SIZE..], 65536);
567        BigEndian::write_u64(&mut block[LOG_CHECKPOINT_ARCHIVED_LSN..], 0x00000000CAFEBABE);
568
569        let cp = LogCheckpoint::parse(&block).unwrap();
570        assert_eq!(cp.number, 99);
571        assert_eq!(cp.lsn, 0xDEADBEEF);
572        assert_eq!(cp.offset, 2048);
573        assert_eq!(cp.buf_size, 65536);
574        assert_eq!(cp.archived_lsn, 0xCAFEBABE);
575    }
576
577    #[test]
578    fn test_mlog_record_type_from_u8_known() {
579        assert_eq!(MlogRecordType::from_u8(1), MlogRecordType::Mlog1Byte);
580        assert_eq!(MlogRecordType::from_u8(9), MlogRecordType::MlogRecInsert);
581        assert_eq!(
582            MlogRecordType::from_u8(36),
583            MlogRecordType::MlogCompRecInsert
584        );
585        assert_eq!(
586            MlogRecordType::from_u8(53),
587            MlogRecordType::MlogZipPageCompress
588        );
589    }
590
591    #[test]
592    fn test_mlog_record_type_unknown() {
593        assert_eq!(MlogRecordType::from_u8(0), MlogRecordType::Unknown(0));
594        assert_eq!(MlogRecordType::from_u8(255), MlogRecordType::Unknown(255));
595        assert_eq!(MlogRecordType::from_u8(100), MlogRecordType::Unknown(100));
596    }
597
598    #[test]
599    fn test_mlog_record_type_name() {
600        assert_eq!(MlogRecordType::Mlog1Byte.name(), "MLOG_1BYTE");
601        assert_eq!(MlogRecordType::MlogRecInsert.name(), "MLOG_REC_INSERT");
602        assert_eq!(MlogRecordType::Unknown(99).name(), "UNKNOWN");
603    }
604
605    #[test]
606    fn test_mlog_record_type_display() {
607        assert_eq!(format!("{}", MlogRecordType::Mlog1Byte), "MLOG_1BYTE");
608        assert_eq!(format!("{}", MlogRecordType::Unknown(99)), "UNKNOWN(99)");
609    }
610
611    #[test]
612    fn test_log_block_checksum_validation() {
613        let mut block = make_block();
614        // Put some data in the block
615        BigEndian::write_u32(&mut block[0..], 5); // block_no = 5
616        BigEndian::write_u16(&mut block[4..], 100); // data_len
617        BigEndian::write_u16(&mut block[6..], 14); // first_rec_group
618        block[14] = 0xAB; // some log data
619
620        // Calculate and store the correct CRC-32C
621        let crc = crc32c::crc32c(&block[..LOG_BLOCK_CHECKSUM_OFFSET]);
622        BigEndian::write_u32(&mut block[LOG_BLOCK_CHECKSUM_OFFSET..], crc);
623
624        assert!(validate_log_block_checksum(&block));
625    }
626
627    #[test]
628    fn test_log_block_checksum_invalid() {
629        let mut block = make_block();
630        BigEndian::write_u32(&mut block[0..], 5);
631        // Wrong checksum
632        BigEndian::write_u32(&mut block[LOG_BLOCK_CHECKSUM_OFFSET..], 0xDEADDEAD);
633
634        assert!(!validate_log_block_checksum(&block));
635    }
636}