Skip to main content

idb/innodb/
log.rs

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