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