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)
438            .ok_or_else(|| IdbError::Parse("Failed to parse log file header (block 0)".to_string()))
439    }
440
441    /// Read and parse a checkpoint (slot 0 = block 1, slot 1 = block 3).
442    pub fn read_checkpoint(&mut self, slot: u8) -> Result<LogCheckpoint, IdbError> {
443        let block_no = match slot {
444            0 => 1,
445            1 => 3,
446            _ => {
447                return Err(IdbError::Argument(format!(
448                    "Invalid checkpoint slot {} (must be 0 or 1)",
449                    slot
450                )))
451            }
452        };
453        let block = self.read_block(block_no)?;
454        LogCheckpoint::parse(&block).ok_or_else(|| {
455            IdbError::Parse(format!("Failed to parse checkpoint at block {}", block_no))
456        })
457    }
458
459    /// File size in bytes.
460    pub fn file_size(&self) -> u64 {
461        self.file_size
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468
469    fn make_block() -> Vec<u8> {
470        vec![0u8; LOG_BLOCK_SIZE]
471    }
472
473    #[test]
474    fn test_log_block_header_parse() {
475        let mut block = make_block();
476        // block_no = 42 (no flush bit)
477        BigEndian::write_u32(&mut block[0..], 42);
478        // data_len = 200
479        BigEndian::write_u16(&mut block[4..], 200);
480        // first_rec_group = 50
481        BigEndian::write_u16(&mut block[6..], 50);
482        // checkpoint_no = 7
483        BigEndian::write_u32(&mut block[8..], 7);
484
485        let hdr = LogBlockHeader::parse(&block).unwrap();
486        assert_eq!(hdr.block_no, 42);
487        assert!(!hdr.flush_flag);
488        assert_eq!(hdr.data_len, 200);
489        assert_eq!(hdr.first_rec_group, 50);
490        assert_eq!(hdr.checkpoint_no, 7);
491        assert!(hdr.has_data());
492    }
493
494    #[test]
495    fn test_log_block_flush_bit() {
496        let mut block = make_block();
497        // Set flush bit (bit 31) + block_no = 100
498        BigEndian::write_u32(&mut block[0..], 0x80000064);
499        BigEndian::write_u16(&mut block[4..], 14); // data_len = header only
500
501        let hdr = LogBlockHeader::parse(&block).unwrap();
502        assert!(hdr.flush_flag);
503        assert_eq!(hdr.block_no, 100);
504        assert!(!hdr.has_data());
505    }
506
507    #[test]
508    fn test_log_block_header_empty() {
509        let block = make_block();
510        let hdr = LogBlockHeader::parse(&block).unwrap();
511        assert_eq!(hdr.block_no, 0);
512        assert!(!hdr.flush_flag);
513        assert_eq!(hdr.data_len, 0);
514        assert_eq!(hdr.first_rec_group, 0);
515        assert_eq!(hdr.checkpoint_no, 0);
516        assert!(!hdr.has_data());
517    }
518
519    #[test]
520    fn test_log_block_header_too_small() {
521        let block = vec![0u8; 10]; // less than LOG_BLOCK_HDR_SIZE
522        assert!(LogBlockHeader::parse(&block).is_none());
523    }
524
525    #[test]
526    fn test_log_block_trailer_parse() {
527        let mut block = make_block();
528        BigEndian::write_u32(&mut block[LOG_BLOCK_CHECKSUM_OFFSET..], 0xCAFEBABE);
529
530        let trailer = LogBlockTrailer::parse(&block).unwrap();
531        assert_eq!(trailer.checksum, 0xCAFEBABE);
532    }
533
534    #[test]
535    fn test_log_file_header_parse() {
536        let mut block = make_block();
537        BigEndian::write_u32(&mut block[LOG_HEADER_GROUP_ID..], 1);
538        BigEndian::write_u64(&mut block[LOG_HEADER_START_LSN..], 0x00000000001A2B3C);
539        BigEndian::write_u32(&mut block[LOG_HEADER_FILE_NO..], 0);
540        // "MySQL 8.0.32" as created_by
541        let creator = b"MySQL 8.0.32";
542        block[LOG_HEADER_CREATED_BY..LOG_HEADER_CREATED_BY + creator.len()]
543            .copy_from_slice(creator);
544
545        let hdr = LogFileHeader::parse(&block).unwrap();
546        assert_eq!(hdr.group_id, 1);
547        assert_eq!(hdr.start_lsn, 0x1A2B3C);
548        assert_eq!(hdr.file_no, 0);
549        assert_eq!(hdr.created_by, "MySQL 8.0.32");
550    }
551
552    #[test]
553    fn test_log_file_header_empty_created_by() {
554        let block = make_block();
555        let hdr = LogFileHeader::parse(&block).unwrap();
556        assert_eq!(hdr.created_by, "");
557    }
558
559    #[test]
560    fn test_log_checkpoint_parse() {
561        let mut block = make_block();
562        BigEndian::write_u64(&mut block[LOG_CHECKPOINT_NO..], 99);
563        BigEndian::write_u64(&mut block[LOG_CHECKPOINT_LSN..], 0x00000000DEADBEEF);
564        BigEndian::write_u32(&mut block[LOG_CHECKPOINT_OFFSET..], 2048);
565        BigEndian::write_u32(&mut block[LOG_CHECKPOINT_BUF_SIZE..], 65536);
566        BigEndian::write_u64(
567            &mut block[LOG_CHECKPOINT_ARCHIVED_LSN..],
568            0x00000000CAFEBABE,
569        );
570
571        let cp = LogCheckpoint::parse(&block).unwrap();
572        assert_eq!(cp.number, 99);
573        assert_eq!(cp.lsn, 0xDEADBEEF);
574        assert_eq!(cp.offset, 2048);
575        assert_eq!(cp.buf_size, 65536);
576        assert_eq!(cp.archived_lsn, 0xCAFEBABE);
577    }
578
579    #[test]
580    fn test_mlog_record_type_from_u8_known() {
581        assert_eq!(MlogRecordType::from_u8(1), MlogRecordType::Mlog1Byte);
582        assert_eq!(MlogRecordType::from_u8(9), MlogRecordType::MlogRecInsert);
583        assert_eq!(
584            MlogRecordType::from_u8(36),
585            MlogRecordType::MlogCompRecInsert
586        );
587        assert_eq!(
588            MlogRecordType::from_u8(53),
589            MlogRecordType::MlogZipPageCompress
590        );
591    }
592
593    #[test]
594    fn test_mlog_record_type_unknown() {
595        assert_eq!(MlogRecordType::from_u8(0), MlogRecordType::Unknown(0));
596        assert_eq!(MlogRecordType::from_u8(255), MlogRecordType::Unknown(255));
597        assert_eq!(MlogRecordType::from_u8(100), MlogRecordType::Unknown(100));
598    }
599
600    #[test]
601    fn test_mlog_record_type_name() {
602        assert_eq!(MlogRecordType::Mlog1Byte.name(), "MLOG_1BYTE");
603        assert_eq!(MlogRecordType::MlogRecInsert.name(), "MLOG_REC_INSERT");
604        assert_eq!(MlogRecordType::Unknown(99).name(), "UNKNOWN");
605    }
606
607    #[test]
608    fn test_mlog_record_type_display() {
609        assert_eq!(format!("{}", MlogRecordType::Mlog1Byte), "MLOG_1BYTE");
610        assert_eq!(format!("{}", MlogRecordType::Unknown(99)), "UNKNOWN(99)");
611    }
612
613    #[test]
614    fn test_log_block_checksum_validation() {
615        let mut block = make_block();
616        // Put some data in the block
617        BigEndian::write_u32(&mut block[0..], 5); // block_no = 5
618        BigEndian::write_u16(&mut block[4..], 100); // data_len
619        BigEndian::write_u16(&mut block[6..], 14); // first_rec_group
620        block[14] = 0xAB; // some log data
621
622        // Calculate and store the correct CRC-32C
623        let crc = crc32c::crc32c(&block[..LOG_BLOCK_CHECKSUM_OFFSET]);
624        BigEndian::write_u32(&mut block[LOG_BLOCK_CHECKSUM_OFFSET..], crc);
625
626        assert!(validate_log_block_checksum(&block));
627    }
628
629    #[test]
630    fn test_log_block_checksum_invalid() {
631        let mut block = make_block();
632        BigEndian::write_u32(&mut block[0..], 5);
633        // Wrong checksum
634        BigEndian::write_u32(&mut block[LOG_BLOCK_CHECKSUM_OFFSET..], 0xDEADDEAD);
635
636        assert!(!validate_log_block_checksum(&block));
637    }
638}