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//! ## Log format versions
9//!
10//! The redo log format has evolved across MySQL versions:
11//!
12//! | Version | Introduced | Key changes |
13//! |---------|-----------|-------------|
14//! | 1 | MySQL 5.7.9 | Initial versioned format |
15//! | 2 | MySQL 8.0.1 | Removed MLOG_FILE_NAME, added MLOG_FILE_OPEN |
16//! | 3 | MySQL 8.0.3 | checkpoint_lsn can point to any byte |
17//! | 4 | MySQL 8.0.19 | Expanded compressed ulint form |
18//! | 5 | MySQL 8.0.28 | Row versioning header; new MLOG record types (67-76) |
19//! | 6 | MySQL 8.0.30 | innodb_redo_log_capacity; `#ib_redo*` file naming |
20//!
21//! MySQL 9.0 and 9.1 continue to use format version 6.
22//!
23//! Use [`LogFile::open`] to read and parse the header and checkpoint records,
24//! then [`LogFile::read_block`] to read individual data blocks. Each block's
25//! [`LogBlockHeader`] provides the block number, data length, first record
26//! group offset, epoch number, and CRC-32C checksum validation status.
27
28use byteorder::{BigEndian, ByteOrder};
29use serde::Serialize;
30use std::io::{Cursor, Read, Seek, SeekFrom};
31
32use crate::IdbError;
33
34/// Supertrait combining `Read + Seek` for type-erased readers.
35trait ReadSeek: Read + Seek {}
36impl<T: Read + Seek> ReadSeek for T {}
37
38/// Size of a redo log block in bytes (from MySQL `log0constants.h`).
39pub const LOG_BLOCK_SIZE: usize = 512;
40/// Maximum size of the log block header in bytes.
41///
42/// Prior to MySQL 8.0.30, the header was 14 bytes (with a 4-byte checkpoint
43/// number at offset 8). In 8.0.30+ (format version 6), the header is 12
44/// bytes with a 4-byte epoch number at offset 8. We use 14 as the universal
45/// threshold for `has_data()` to correctly handle both old and new formats.
46pub const LOG_BLOCK_HDR_SIZE: usize = 14;
47/// Size of the log block trailer in bytes.
48pub const LOG_BLOCK_TRL_SIZE: usize = 4;
49/// Bitmask for the flush flag in the block number field (bit 31).
50pub const LOG_BLOCK_FLUSH_BIT_MASK: u32 = 0x80000000;
51/// Byte offset of the CRC-32C checksum within a block (bytes 508-511).
52pub const LOG_BLOCK_CHECKSUM_OFFSET: usize = 508;
53/// Number of reserved header/checkpoint blocks at the start of the file.
54pub const LOG_FILE_HDR_BLOCKS: u64 = 4;
55
56/// Offset of the format version within the log file header (block 0).
57///
58/// This field was called "group ID" in MySQL < 8.0.30. In 8.0.30+ (format
59/// version 6), it stores the log format version number (e.g. 6 for MySQL 9.x).
60pub const LOG_HEADER_FORMAT: usize = 0;
61/// Offset of the group ID within the log file header (block 0).
62///
63/// Alias for [`LOG_HEADER_FORMAT`] for backward compatibility. In MySQL < 8.0.30,
64/// this field stored the log group ID. In 8.0.30+ it stores the format version.
65pub const LOG_HEADER_GROUP_ID: usize = 0;
66/// Offset of the log UUID within the log file header (bytes 4-7).
67///
68/// In MySQL 8.0.30+, this is a 4-byte UUID identifying the data directory.
69/// In older formats, this was part of the start LSN field.
70pub const LOG_HEADER_LOG_UUID: usize = 4;
71/// Offset of the start LSN within the log file header (bytes 8-15).
72pub const LOG_HEADER_START_LSN: usize = 8;
73/// Offset of the file number within the log file header (bytes 12-15).
74///
75/// Only meaningful for MySQL < 8.0.30 where the start LSN was at offset 4.
76/// Overlaps with the lower 4 bytes of `LOG_HEADER_START_LSN` in the 8.0.30+ layout.
77#[deprecated(note = "Use LOG_HEADER_START_LSN instead; file_no is not a separate field in 8.0.30+")]
78pub const LOG_HEADER_FILE_NO: usize = 12;
79/// Offset of the creator string within the log file header (bytes 16-47).
80pub const LOG_HEADER_CREATED_BY: usize = 16;
81/// Maximum length of the creator string.
82pub const LOG_HEADER_CREATED_BY_LEN: usize = 32;
83
84/// Offset of the checkpoint number within a checkpoint block.
85///
86/// Only meaningful for MySQL < 8.0.30. In 8.0.30+ the checkpoint block
87/// only contains the checkpoint LSN at offset 8.
88pub const LOG_CHECKPOINT_NO: usize = 0;
89/// Offset of the checkpoint LSN within a checkpoint block.
90pub const LOG_CHECKPOINT_LSN: usize = 8;
91/// Offset of the checkpoint byte-offset within a checkpoint block.
92///
93/// Only meaningful for MySQL < 8.0.30.
94pub const LOG_CHECKPOINT_OFFSET: usize = 16;
95/// Offset of the log buffer size within a checkpoint block.
96///
97/// Only meaningful for MySQL < 8.0.30.
98pub const LOG_CHECKPOINT_BUF_SIZE: usize = 20;
99/// Offset of the archived LSN within a checkpoint block.
100///
101/// Only meaningful for MySQL < 8.0.30.
102pub const LOG_CHECKPOINT_ARCHIVED_LSN: usize = 24;
103
104/// Log file header (block 0 of the redo log file).
105///
106/// The header layout changed in MySQL 8.0.30 (format version 6):
107///
108/// | Offset | Pre-8.0.30 | 8.0.30+ (incl. 9.x) |
109/// |--------|------------|---------------------|
110/// | 0 | group_id (u32) | format_version (u32) |
111/// | 4 | start_lsn (u64) | log_uuid (u32) |
112/// | 8 | (start_lsn cont.) | start_lsn (u64) |
113/// | 12 | file_no (u32) | (start_lsn cont.) |
114/// | 16 | created_by (32 bytes) | created_by (32 bytes) |
115///
116/// The `format_version` and `group_id` fields share offset 0; in practice,
117/// format version values 1-6 are distinguishable from typical group IDs.
118#[derive(Debug, Clone, Serialize)]
119pub struct LogFileHeader {
120    /// Log format version (offset 0).
121    ///
122    /// In MySQL < 8.0.30 this was the log group ID. In 8.0.30+ this is the
123    /// format version number (e.g. 6 for MySQL 8.0.30+, 9.0, 9.1).
124    /// Aliased as `group_id` for backward compatibility.
125    pub format_version: u32,
126    /// Start LSN of this log file.
127    pub start_lsn: u64,
128    /// Log UUID (MySQL 8.0.30+) or 0 for older formats.
129    ///
130    /// A 4-byte identifier for the data directory, stored at offset 4.
131    #[serde(skip_serializing_if = "is_zero_u32")]
132    pub log_uuid: u32,
133    /// MySQL version string that created this log file (e.g. "MySQL 9.0.1").
134    pub created_by: String,
135}
136
137fn is_zero_u32(v: &u32) -> bool {
138    *v == 0
139}
140
141impl LogFileHeader {
142    /// Backward-compatible accessor: returns `format_version` as the group ID.
143    ///
144    /// In MySQL < 8.0.30, offset 0 stored the log group ID. In 8.0.30+
145    /// it stores the format version. This accessor preserves API compatibility.
146    pub fn group_id(&self) -> u32 {
147        self.format_version
148    }
149
150    /// Parse a log file header from the first 512-byte block.
151    ///
152    /// Handles both pre-8.0.30 and 8.0.30+ header layouts. The format is
153    /// auto-detected: format version >= 6 indicates the 8.0.30+ layout
154    /// where offset 4 is the log UUID and offset 8 is the start LSN.
155    /// Versions 1-5 use the pre-8.0.30 layout where offset 4 is the
156    /// start LSN (u64) and offset 12 is the file number.
157    ///
158    /// # Examples
159    ///
160    /// ```
161    /// use idb::innodb::log::{LogFileHeader, LOG_BLOCK_SIZE,
162    ///     LOG_HEADER_FORMAT, LOG_HEADER_START_LSN,
163    ///     LOG_HEADER_LOG_UUID, LOG_HEADER_CREATED_BY};
164    /// use byteorder::{BigEndian, ByteOrder};
165    ///
166    /// // MySQL 8.0.30+ / 9.x format (format_version = 6)
167    /// let mut block = vec![0u8; LOG_BLOCK_SIZE];
168    /// BigEndian::write_u32(&mut block[LOG_HEADER_FORMAT..], 6);
169    /// BigEndian::write_u32(&mut block[LOG_HEADER_LOG_UUID..], 0x12345678);
170    /// BigEndian::write_u64(&mut block[LOG_HEADER_START_LSN..], 0x1000);
171    /// block[LOG_HEADER_CREATED_BY..LOG_HEADER_CREATED_BY + 12]
172    ///     .copy_from_slice(b"MySQL 9.0.1\0");
173    ///
174    /// let hdr = LogFileHeader::parse(&block).unwrap();
175    /// assert_eq!(hdr.format_version, 6);
176    /// assert_eq!(hdr.start_lsn, 0x1000);
177    /// assert_eq!(hdr.log_uuid, 0x12345678);
178    /// assert_eq!(hdr.created_by, "MySQL 9.0.1");
179    /// ```
180    pub fn parse(block: &[u8]) -> Option<Self> {
181        if block.len() < LOG_BLOCK_SIZE {
182            return None;
183        }
184
185        let format_version = BigEndian::read_u32(&block[LOG_HEADER_FORMAT..]);
186
187        let (log_uuid, start_lsn) = if format_version >= 6 {
188            // 8.0.30+ layout: log_uuid at offset 4, start_lsn at offset 8
189            (
190                BigEndian::read_u32(&block[LOG_HEADER_LOG_UUID..]),
191                BigEndian::read_u64(&block[LOG_HEADER_START_LSN..]),
192            )
193        } else {
194            // Pre-8.0.30 layout: start_lsn at offset 4 (u64), file_no at offset 12
195            (0u32, BigEndian::read_u64(&block[4..]))
196        };
197
198        let created_bytes =
199            &block[LOG_HEADER_CREATED_BY..LOG_HEADER_CREATED_BY + LOG_HEADER_CREATED_BY_LEN];
200        let created_by = created_bytes
201            .iter()
202            .take_while(|&&b| b != 0)
203            .map(|&b| b as char)
204            .collect::<String>();
205
206        Some(LogFileHeader {
207            format_version,
208            start_lsn,
209            log_uuid,
210            created_by,
211        })
212    }
213}
214
215/// Checkpoint record (blocks 1 and 3 of the redo log file).
216///
217/// In MySQL 8.0.30+ (format version 6, including MySQL 9.x), the checkpoint
218/// block only contains the checkpoint LSN at offset 8. The `number`, `offset`,
219/// `buf_size`, and `archived_lsn` fields are not written and will be zero.
220#[derive(Debug, Clone, Serialize)]
221pub struct LogCheckpoint {
222    /// Checkpoint sequence number (pre-8.0.30 only; zero in 8.0.30+).
223    #[serde(skip_serializing_if = "is_zero_u64")]
224    pub number: u64,
225    /// LSN at the time of this checkpoint.
226    pub lsn: u64,
227    /// Byte offset of the checkpoint within the log file (pre-8.0.30 only).
228    #[serde(skip_serializing_if = "is_zero_u32")]
229    pub offset: u32,
230    /// Log buffer size at checkpoint time (pre-8.0.30 only).
231    #[serde(skip_serializing_if = "is_zero_u32")]
232    pub buf_size: u32,
233    /// LSN up to which log has been archived (pre-8.0.30 only).
234    #[serde(skip_serializing_if = "is_zero_u64")]
235    pub archived_lsn: u64,
236}
237
238fn is_zero_u64(v: &u64) -> bool {
239    *v == 0
240}
241
242impl LogCheckpoint {
243    /// Parse a checkpoint from a 512-byte block.
244    ///
245    /// Reads all fields for backward compatibility. In MySQL 8.0.30+ (format
246    /// version 6), only the `lsn` field at offset 8 is meaningful; all other
247    /// fields will be zero.
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// use idb::innodb::log::{LogCheckpoint, LOG_BLOCK_SIZE,
253    ///     LOG_CHECKPOINT_NO, LOG_CHECKPOINT_LSN, LOG_CHECKPOINT_OFFSET,
254    ///     LOG_CHECKPOINT_BUF_SIZE, LOG_CHECKPOINT_ARCHIVED_LSN};
255    /// use byteorder::{BigEndian, ByteOrder};
256    ///
257    /// let mut block = vec![0u8; LOG_BLOCK_SIZE];
258    /// BigEndian::write_u64(&mut block[LOG_CHECKPOINT_NO..], 42);
259    /// BigEndian::write_u64(&mut block[LOG_CHECKPOINT_LSN..], 0xDEADBEEF);
260    /// BigEndian::write_u32(&mut block[LOG_CHECKPOINT_OFFSET..], 2048);
261    /// BigEndian::write_u32(&mut block[LOG_CHECKPOINT_BUF_SIZE..], 65536);
262    /// BigEndian::write_u64(&mut block[LOG_CHECKPOINT_ARCHIVED_LSN..], 0xCAFEBABE);
263    ///
264    /// let cp = LogCheckpoint::parse(&block).unwrap();
265    /// assert_eq!(cp.number, 42);
266    /// assert_eq!(cp.lsn, 0xDEADBEEF);
267    /// assert_eq!(cp.offset, 2048);
268    /// assert_eq!(cp.buf_size, 65536);
269    /// assert_eq!(cp.archived_lsn, 0xCAFEBABE);
270    /// ```
271    pub fn parse(block: &[u8]) -> Option<Self> {
272        if block.len() < LOG_BLOCK_SIZE {
273            return None;
274        }
275
276        let number = BigEndian::read_u64(&block[LOG_CHECKPOINT_NO..]);
277        let lsn = BigEndian::read_u64(&block[LOG_CHECKPOINT_LSN..]);
278        let offset = BigEndian::read_u32(&block[LOG_CHECKPOINT_OFFSET..]);
279        let buf_size = BigEndian::read_u32(&block[LOG_CHECKPOINT_BUF_SIZE..]);
280        let archived_lsn = BigEndian::read_u64(&block[LOG_CHECKPOINT_ARCHIVED_LSN..]);
281
282        Some(LogCheckpoint {
283            number,
284            lsn,
285            offset,
286            buf_size,
287            archived_lsn,
288        })
289    }
290}
291
292/// Log block header (first 12 bytes of each 512-byte block).
293///
294/// The block header layout is consistent across MySQL versions:
295///
296/// | Offset | Size | Field |
297/// |--------|------|-------|
298/// | 0 | 4 | Block number (bit 31 = flush flag) |
299/// | 4 | 2 | Data length (including header bytes) |
300/// | 6 | 2 | First record group offset |
301/// | 8 | 4 | Epoch number (called checkpoint_no in older versions) |
302///
303/// In MySQL < 8.0.30, the header was 14 bytes with 2 extra bytes at offset
304/// 12-13 that were part of the checkpoint number. In 8.0.30+ the header is
305/// 12 bytes and offset 8 stores the epoch number.
306#[derive(Debug, Clone, Serialize)]
307pub struct LogBlockHeader {
308    /// Block number (with flush bit masked out).
309    pub block_no: u32,
310    /// Whether this block was the first in a flush batch (bit 31).
311    pub flush_flag: bool,
312    /// Number of bytes of log data in this block (including header).
313    pub data_len: u16,
314    /// Offset of the first log record group starting in this block.
315    pub first_rec_group: u16,
316    /// Epoch number (MySQL 8.0.30+) or checkpoint number (older versions).
317    ///
318    /// In MySQL 8.0.30+ and 9.x, this field stores the epoch number used
319    /// for log block validation. In older versions, it was the checkpoint
320    /// sequence number.
321    pub epoch_no: u32,
322}
323
324impl LogBlockHeader {
325    /// Parse a log block header from a 512-byte block.
326    ///
327    /// # Examples
328    ///
329    /// ```
330    /// use idb::innodb::log::{LogBlockHeader, LOG_BLOCK_HDR_SIZE};
331    /// use byteorder::{BigEndian, ByteOrder};
332    ///
333    /// let mut block = vec![0u8; LOG_BLOCK_HDR_SIZE];
334    /// BigEndian::write_u32(&mut block[0..], 0x80000005); // flush bit + block_no=5
335    /// BigEndian::write_u16(&mut block[4..], 200);        // data_len
336    /// BigEndian::write_u16(&mut block[6..], 14);         // first_rec_group
337    /// BigEndian::write_u32(&mut block[8..], 3);          // epoch_no
338    ///
339    /// let hdr = LogBlockHeader::parse(&block).unwrap();
340    /// assert_eq!(hdr.block_no, 5);
341    /// assert!(hdr.flush_flag);
342    /// assert_eq!(hdr.data_len, 200);
343    /// assert_eq!(hdr.first_rec_group, 14);
344    /// assert_eq!(hdr.epoch_no, 3);
345    /// assert!(hdr.has_data());
346    /// ```
347    pub fn parse(block: &[u8]) -> Option<Self> {
348        if block.len() < LOG_BLOCK_HDR_SIZE {
349            return None;
350        }
351
352        let raw_block_no = BigEndian::read_u32(&block[0..]);
353        let flush_flag = (raw_block_no & LOG_BLOCK_FLUSH_BIT_MASK) != 0;
354        let block_no = raw_block_no & !LOG_BLOCK_FLUSH_BIT_MASK;
355
356        let data_len = BigEndian::read_u16(&block[4..]);
357        let first_rec_group = BigEndian::read_u16(&block[6..]);
358        let epoch_no = BigEndian::read_u32(&block[8..]);
359
360        Some(LogBlockHeader {
361            block_no,
362            flush_flag,
363            data_len,
364            first_rec_group,
365            epoch_no,
366        })
367    }
368
369    /// Backward-compatible accessor: returns `epoch_no` as the checkpoint number.
370    ///
371    /// In MySQL < 8.0.30, offset 8 stored the checkpoint number. In 8.0.30+
372    /// it stores the epoch number. This accessor preserves API compatibility.
373    pub fn checkpoint_no(&self) -> u32 {
374        self.epoch_no
375    }
376
377    /// Returns true if this block contains log data (data_len > header size).
378    pub fn has_data(&self) -> bool {
379        self.data_len as usize > LOG_BLOCK_HDR_SIZE
380    }
381}
382
383/// Log block trailer (last 4 bytes of each 512-byte block).
384#[derive(Debug, Clone, Serialize)]
385pub struct LogBlockTrailer {
386    /// CRC-32C checksum of the block (bytes 0..508).
387    pub checksum: u32,
388}
389
390impl LogBlockTrailer {
391    /// Parse a log block trailer from a 512-byte block.
392    pub fn parse(block: &[u8]) -> Option<Self> {
393        if block.len() < LOG_BLOCK_SIZE {
394            return None;
395        }
396
397        let checksum = BigEndian::read_u32(&block[LOG_BLOCK_CHECKSUM_OFFSET..]);
398
399        Some(LogBlockTrailer { checksum })
400    }
401}
402
403/// Validate a log block's CRC-32C checksum.
404///
405/// The checksum covers bytes 0..508 of the block (everything except the checksum field itself).
406pub fn validate_log_block_checksum(block: &[u8]) -> bool {
407    if block.len() < LOG_BLOCK_SIZE {
408        return false;
409    }
410    let stored = BigEndian::read_u32(&block[LOG_BLOCK_CHECKSUM_OFFSET..]);
411    let calculated = crc32c::crc32c(&block[..LOG_BLOCK_CHECKSUM_OFFSET]);
412    stored == calculated
413}
414
415/// MLOG record types from MySQL `mtr0types.h`.
416///
417/// Type codes are assigned to match the MySQL source exactly. Types suffixed
418/// with `_8027` are the pre-8.0.28 variants; MySQL 8.0.28+ introduced new
419/// type codes (67-76) for records with row versioning support. The old codes
420/// are retained for backward compatibility with older redo logs.
421///
422/// In MySQL 9.x, the `_8027` types are officially marked obsolete but may
423/// still appear in redo logs created by MySQL 8.0.27 and earlier.
424#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
425pub enum MlogRecordType {
426    // ── Basic write types ────────────────────────────────────────────
427    /// Write 1 byte to a page (type 1).
428    Mlog1Byte,
429    /// Write 2 bytes to a page (type 2).
430    Mlog2Bytes,
431    /// Write 4 bytes to a page (type 4).
432    Mlog4Bytes,
433    /// Write 8 bytes to a page (type 8).
434    Mlog8Bytes,
435
436    // ── Pre-8.0.28 record types (9-18) ──────────────────────────────
437    /// Insert record, pre-8.0.28 format (type 9).
438    MlogRecInsert8027,
439    /// Clustered index delete-mark, pre-8.0.28 format (type 10).
440    MlogRecClustDeleteMark8027,
441    /// Secondary index delete-mark (type 11).
442    MlogRecSecDeleteMark,
443    /// Update in place, pre-8.0.28 format (type 13).
444    MlogRecUpdateInPlace8027,
445    /// Delete record, pre-8.0.28 format (type 14).
446    MlogRecDelete8027,
447    /// Delete from end of page list, pre-8.0.28 format (type 15).
448    MlogListEndDelete8027,
449    /// Delete from start of page list, pre-8.0.28 format (type 16).
450    MlogListStartDelete8027,
451    /// End-copy of created page list, pre-8.0.28 format (type 17).
452    MlogListEndCopyCreated8027,
453    /// Page reorganize, pre-8.0.28 format (type 18).
454    MlogPageReorganize8027,
455
456    // ── Page and undo types (19-24) ─────────────────────────────────
457    /// Create a page (type 19).
458    MlogPageCreate,
459    /// Insert undo log record (type 20).
460    MlogUndoInsert,
461    /// Erase undo log page end (type 21).
462    MlogUndoEraseEnd,
463    /// Initialize undo log header (type 22).
464    MlogUndoInit,
465    /// Reuse undo log header (type 24).
466    MlogUndoHdrReuse,
467
468    // ── Types added/renumbered in MySQL 8.0 (25-35) ─────────────────
469    /// Create undo log header (type 25).
470    MlogUndoHdrCreate,
471    /// Set minimum record mark (type 26).
472    MlogRecMinMark,
473    /// Initialize insert buffer bitmap (type 27).
474    MlogIbufBitmapInit,
475    /// LSN marker, debug only (type 28).
476    MlogLsn,
477    /// Initialize file page, deprecated (type 29).
478    MlogInitFilePage,
479    /// Write a string to a page (type 30).
480    MlogWriteString,
481    /// End of multi-record mini-transaction (type 31).
482    MlogMultiRecEnd,
483    /// Dummy record for padding (type 32).
484    MlogDummyRecord,
485    /// Create a tablespace file (type 33).
486    MlogFileCreate,
487    /// Rename a tablespace file (type 34).
488    MlogFileRename,
489    /// Delete a tablespace file (type 35).
490    MlogFileDelete,
491
492    // ── Compact page types (36-46, pre-8.0.28 variants) ─────────────
493    /// Set minimum record mark, compact format (type 36).
494    MlogCompRecMinMark,
495    /// Create compact page (type 37).
496    MlogCompPageCreate,
497    /// Insert record, compact format, pre-8.0.28 (type 38).
498    MlogCompRecInsert8027,
499    /// Clustered delete-mark, compact format, pre-8.0.28 (type 39).
500    MlogCompRecClustDeleteMark8027,
501    /// Secondary delete-mark, compact format (type 40).
502    MlogCompRecSecDeleteMark,
503    /// Update in place, compact format, pre-8.0.28 (type 41).
504    MlogCompRecUpdateInPlace8027,
505    /// Delete record, compact format, pre-8.0.28 (type 42).
506    MlogCompRecDelete8027,
507    /// Delete from end of list, compact format, pre-8.0.28 (type 43).
508    MlogCompListEndDelete8027,
509    /// Delete from start of list, compact format, pre-8.0.28 (type 44).
510    MlogCompListStartDelete8027,
511    /// End-copy created, compact format, pre-8.0.28 (type 45).
512    MlogCompListEndCopyCreated8027,
513    /// Page reorganize, compact format, pre-8.0.28 (type 46).
514    MlogCompPageReorganize8027,
515
516    // ── Compressed page types (48-53) ───────────────────────────────
517    /// Write node pointer in compressed page (type 48).
518    MlogZipWriteNodePtr,
519    /// Write BLOB pointer in compressed page (type 49).
520    MlogZipWriteBlobPtr,
521    /// Write header in compressed page (type 50).
522    MlogZipWriteHeader,
523    /// Compress a page (type 51).
524    MlogZipPageCompress,
525    /// Compress page with no data, pre-8.0.28 (type 52).
526    MlogZipPageCompressNoData8027,
527    /// Reorganize compressed page, pre-8.0.28 (type 53).
528    MlogZipPageReorganize8027,
529
530    // ── Extended types (57-66) ──────────────────────────────────────
531    /// Create R-Tree page (type 57).
532    MlogPageCreateRTree,
533    /// Create compact R-Tree page (type 58).
534    MlogCompPageCreateRTree,
535    /// Initialize file page v2 (type 59).
536    MlogInitFilePage2,
537    /// Index load notification (type 61).
538    MlogIndexLoad,
539    /// Table dynamic metadata (type 62).
540    MlogTableDynamicMeta,
541    /// Create SDI page (type 63).
542    MlogPageCreateSdi,
543    /// Create compact SDI page (type 64).
544    MlogCompPageCreateSdi,
545    /// Extend a tablespace file (type 65).
546    MlogFileExtend,
547    /// Test record, unit tests only (type 66).
548    MlogTest,
549
550    // ── MySQL 8.0.28+ new record types (67-76) ─────────────────────
551    /// Insert record with row versioning (type 67).
552    MlogRecInsert,
553    /// Clustered index delete-mark with row versioning (type 68).
554    MlogRecClustDeleteMark,
555    /// Delete record with row versioning (type 69).
556    MlogRecDelete,
557    /// Update in place with row versioning (type 70).
558    MlogRecUpdateInPlace,
559    /// End-copy of created page list with row versioning (type 71).
560    MlogListEndCopyCreated,
561    /// Page reorganize with row versioning (type 72).
562    MlogPageReorganize,
563    /// Compressed page reorganize with row versioning (type 73).
564    MlogZipPageReorganize,
565    /// Compress page with no data, with row versioning (type 74).
566    MlogZipPageCompressNoData,
567    /// Delete from end of page list with row versioning (type 75).
568    MlogListEndDelete,
569    /// Delete from start of page list with row versioning (type 76).
570    MlogListStartDelete,
571
572    /// Unknown or unrecognized record type.
573    Unknown(u8),
574}
575
576impl MlogRecordType {
577    /// Convert a u8 type code to MlogRecordType.
578    ///
579    /// Maps all known type codes from MySQL `mtr0types.h` across versions
580    /// 5.7 through 9.1. Unrecognized codes are wrapped in `Unknown(N)`.
581    ///
582    /// # Examples
583    ///
584    /// ```
585    /// use idb::innodb::log::MlogRecordType;
586    ///
587    /// let rec_type = MlogRecordType::from_u8(1);
588    /// assert_eq!(rec_type, MlogRecordType::Mlog1Byte);
589    /// assert_eq!(rec_type.name(), "MLOG_1BYTE");
590    ///
591    /// // Pre-8.0.28 insert at type code 9
592    /// let insert_old = MlogRecordType::from_u8(9);
593    /// assert_eq!(insert_old, MlogRecordType::MlogRecInsert8027);
594    ///
595    /// // Post-8.0.28 insert at type code 67
596    /// let insert_new = MlogRecordType::from_u8(67);
597    /// assert_eq!(insert_new, MlogRecordType::MlogRecInsert);
598    ///
599    /// // Unknown type codes are preserved
600    /// let unknown = MlogRecordType::from_u8(255);
601    /// assert_eq!(unknown, MlogRecordType::Unknown(255));
602    /// assert_eq!(format!("{}", unknown), "UNKNOWN(255)");
603    /// ```
604    pub fn from_u8(val: u8) -> Self {
605        match val {
606            1 => MlogRecordType::Mlog1Byte,
607            2 => MlogRecordType::Mlog2Bytes,
608            4 => MlogRecordType::Mlog4Bytes,
609            8 => MlogRecordType::Mlog8Bytes,
610            9 => MlogRecordType::MlogRecInsert8027,
611            10 => MlogRecordType::MlogRecClustDeleteMark8027,
612            11 => MlogRecordType::MlogRecSecDeleteMark,
613            13 => MlogRecordType::MlogRecUpdateInPlace8027,
614            14 => MlogRecordType::MlogRecDelete8027,
615            15 => MlogRecordType::MlogListEndDelete8027,
616            16 => MlogRecordType::MlogListStartDelete8027,
617            17 => MlogRecordType::MlogListEndCopyCreated8027,
618            18 => MlogRecordType::MlogPageReorganize8027,
619            19 => MlogRecordType::MlogPageCreate,
620            20 => MlogRecordType::MlogUndoInsert,
621            21 => MlogRecordType::MlogUndoEraseEnd,
622            22 => MlogRecordType::MlogUndoInit,
623            24 => MlogRecordType::MlogUndoHdrReuse,
624            25 => MlogRecordType::MlogUndoHdrCreate,
625            26 => MlogRecordType::MlogRecMinMark,
626            27 => MlogRecordType::MlogIbufBitmapInit,
627            28 => MlogRecordType::MlogLsn,
628            29 => MlogRecordType::MlogInitFilePage,
629            30 => MlogRecordType::MlogWriteString,
630            31 => MlogRecordType::MlogMultiRecEnd,
631            32 => MlogRecordType::MlogDummyRecord,
632            33 => MlogRecordType::MlogFileCreate,
633            34 => MlogRecordType::MlogFileRename,
634            35 => MlogRecordType::MlogFileDelete,
635            36 => MlogRecordType::MlogCompRecMinMark,
636            37 => MlogRecordType::MlogCompPageCreate,
637            38 => MlogRecordType::MlogCompRecInsert8027,
638            39 => MlogRecordType::MlogCompRecClustDeleteMark8027,
639            40 => MlogRecordType::MlogCompRecSecDeleteMark,
640            41 => MlogRecordType::MlogCompRecUpdateInPlace8027,
641            42 => MlogRecordType::MlogCompRecDelete8027,
642            43 => MlogRecordType::MlogCompListEndDelete8027,
643            44 => MlogRecordType::MlogCompListStartDelete8027,
644            45 => MlogRecordType::MlogCompListEndCopyCreated8027,
645            46 => MlogRecordType::MlogCompPageReorganize8027,
646            48 => MlogRecordType::MlogZipWriteNodePtr,
647            49 => MlogRecordType::MlogZipWriteBlobPtr,
648            50 => MlogRecordType::MlogZipWriteHeader,
649            51 => MlogRecordType::MlogZipPageCompress,
650            52 => MlogRecordType::MlogZipPageCompressNoData8027,
651            53 => MlogRecordType::MlogZipPageReorganize8027,
652            57 => MlogRecordType::MlogPageCreateRTree,
653            58 => MlogRecordType::MlogCompPageCreateRTree,
654            59 => MlogRecordType::MlogInitFilePage2,
655            61 => MlogRecordType::MlogIndexLoad,
656            62 => MlogRecordType::MlogTableDynamicMeta,
657            63 => MlogRecordType::MlogPageCreateSdi,
658            64 => MlogRecordType::MlogCompPageCreateSdi,
659            65 => MlogRecordType::MlogFileExtend,
660            66 => MlogRecordType::MlogTest,
661            67 => MlogRecordType::MlogRecInsert,
662            68 => MlogRecordType::MlogRecClustDeleteMark,
663            69 => MlogRecordType::MlogRecDelete,
664            70 => MlogRecordType::MlogRecUpdateInPlace,
665            71 => MlogRecordType::MlogListEndCopyCreated,
666            72 => MlogRecordType::MlogPageReorganize,
667            73 => MlogRecordType::MlogZipPageReorganize,
668            74 => MlogRecordType::MlogZipPageCompressNoData,
669            75 => MlogRecordType::MlogListEndDelete,
670            76 => MlogRecordType::MlogListStartDelete,
671            v => MlogRecordType::Unknown(v),
672        }
673    }
674
675    /// Display name for this record type.
676    pub fn name(&self) -> &str {
677        match self {
678            MlogRecordType::Mlog1Byte => "MLOG_1BYTE",
679            MlogRecordType::Mlog2Bytes => "MLOG_2BYTES",
680            MlogRecordType::Mlog4Bytes => "MLOG_4BYTES",
681            MlogRecordType::Mlog8Bytes => "MLOG_8BYTES",
682            MlogRecordType::MlogRecInsert8027 => "MLOG_REC_INSERT_8027",
683            MlogRecordType::MlogRecClustDeleteMark8027 => "MLOG_REC_CLUST_DELETE_MARK_8027",
684            MlogRecordType::MlogRecSecDeleteMark => "MLOG_REC_SEC_DELETE_MARK",
685            MlogRecordType::MlogRecUpdateInPlace8027 => "MLOG_REC_UPDATE_IN_PLACE_8027",
686            MlogRecordType::MlogRecDelete8027 => "MLOG_REC_DELETE_8027",
687            MlogRecordType::MlogListEndDelete8027 => "MLOG_LIST_END_DELETE_8027",
688            MlogRecordType::MlogListStartDelete8027 => "MLOG_LIST_START_DELETE_8027",
689            MlogRecordType::MlogListEndCopyCreated8027 => "MLOG_LIST_END_COPY_CREATED_8027",
690            MlogRecordType::MlogPageReorganize8027 => "MLOG_PAGE_REORGANIZE_8027",
691            MlogRecordType::MlogPageCreate => "MLOG_PAGE_CREATE",
692            MlogRecordType::MlogUndoInsert => "MLOG_UNDO_INSERT",
693            MlogRecordType::MlogUndoEraseEnd => "MLOG_UNDO_ERASE_END",
694            MlogRecordType::MlogUndoInit => "MLOG_UNDO_INIT",
695            MlogRecordType::MlogUndoHdrReuse => "MLOG_UNDO_HDR_REUSE",
696            MlogRecordType::MlogUndoHdrCreate => "MLOG_UNDO_HDR_CREATE",
697            MlogRecordType::MlogRecMinMark => "MLOG_REC_MIN_MARK",
698            MlogRecordType::MlogIbufBitmapInit => "MLOG_IBUF_BITMAP_INIT",
699            MlogRecordType::MlogLsn => "MLOG_LSN",
700            MlogRecordType::MlogInitFilePage => "MLOG_INIT_FILE_PAGE",
701            MlogRecordType::MlogWriteString => "MLOG_WRITE_STRING",
702            MlogRecordType::MlogMultiRecEnd => "MLOG_MULTI_REC_END",
703            MlogRecordType::MlogDummyRecord => "MLOG_DUMMY_RECORD",
704            MlogRecordType::MlogFileCreate => "MLOG_FILE_CREATE",
705            MlogRecordType::MlogFileRename => "MLOG_FILE_RENAME",
706            MlogRecordType::MlogFileDelete => "MLOG_FILE_DELETE",
707            MlogRecordType::MlogCompRecMinMark => "MLOG_COMP_REC_MIN_MARK",
708            MlogRecordType::MlogCompPageCreate => "MLOG_COMP_PAGE_CREATE",
709            MlogRecordType::MlogCompRecInsert8027 => "MLOG_COMP_REC_INSERT_8027",
710            MlogRecordType::MlogCompRecClustDeleteMark8027 => {
711                "MLOG_COMP_REC_CLUST_DELETE_MARK_8027"
712            }
713            MlogRecordType::MlogCompRecSecDeleteMark => "MLOG_COMP_REC_SEC_DELETE_MARK",
714            MlogRecordType::MlogCompRecUpdateInPlace8027 => "MLOG_COMP_REC_UPDATE_IN_PLACE_8027",
715            MlogRecordType::MlogCompRecDelete8027 => "MLOG_COMP_REC_DELETE_8027",
716            MlogRecordType::MlogCompListEndDelete8027 => "MLOG_COMP_LIST_END_DELETE_8027",
717            MlogRecordType::MlogCompListStartDelete8027 => "MLOG_COMP_LIST_START_DELETE_8027",
718            MlogRecordType::MlogCompListEndCopyCreated8027 => {
719                "MLOG_COMP_LIST_END_COPY_CREATED_8027"
720            }
721            MlogRecordType::MlogCompPageReorganize8027 => "MLOG_COMP_PAGE_REORGANIZE_8027",
722            MlogRecordType::MlogZipWriteNodePtr => "MLOG_ZIP_WRITE_NODE_PTR",
723            MlogRecordType::MlogZipWriteBlobPtr => "MLOG_ZIP_WRITE_BLOB_PTR",
724            MlogRecordType::MlogZipWriteHeader => "MLOG_ZIP_WRITE_HEADER",
725            MlogRecordType::MlogZipPageCompress => "MLOG_ZIP_PAGE_COMPRESS",
726            MlogRecordType::MlogZipPageCompressNoData8027 => "MLOG_ZIP_PAGE_COMPRESS_NO_DATA_8027",
727            MlogRecordType::MlogZipPageReorganize8027 => "MLOG_ZIP_PAGE_REORGANIZE_8027",
728            MlogRecordType::MlogPageCreateRTree => "MLOG_PAGE_CREATE_RTREE",
729            MlogRecordType::MlogCompPageCreateRTree => "MLOG_COMP_PAGE_CREATE_RTREE",
730            MlogRecordType::MlogInitFilePage2 => "MLOG_INIT_FILE_PAGE2",
731            MlogRecordType::MlogIndexLoad => "MLOG_INDEX_LOAD",
732            MlogRecordType::MlogTableDynamicMeta => "MLOG_TABLE_DYNAMIC_META",
733            MlogRecordType::MlogPageCreateSdi => "MLOG_PAGE_CREATE_SDI",
734            MlogRecordType::MlogCompPageCreateSdi => "MLOG_COMP_PAGE_CREATE_SDI",
735            MlogRecordType::MlogFileExtend => "MLOG_FILE_EXTEND",
736            MlogRecordType::MlogTest => "MLOG_TEST",
737            MlogRecordType::MlogRecInsert => "MLOG_REC_INSERT",
738            MlogRecordType::MlogRecClustDeleteMark => "MLOG_REC_CLUST_DELETE_MARK",
739            MlogRecordType::MlogRecDelete => "MLOG_REC_DELETE",
740            MlogRecordType::MlogRecUpdateInPlace => "MLOG_REC_UPDATE_IN_PLACE",
741            MlogRecordType::MlogListEndCopyCreated => "MLOG_LIST_END_COPY_CREATED",
742            MlogRecordType::MlogPageReorganize => "MLOG_PAGE_REORGANIZE",
743            MlogRecordType::MlogZipPageReorganize => "MLOG_ZIP_PAGE_REORGANIZE",
744            MlogRecordType::MlogZipPageCompressNoData => "MLOG_ZIP_PAGE_COMPRESS_NO_DATA",
745            MlogRecordType::MlogListEndDelete => "MLOG_LIST_END_DELETE",
746            MlogRecordType::MlogListStartDelete => "MLOG_LIST_START_DELETE",
747            MlogRecordType::Unknown(_) => "UNKNOWN",
748        }
749    }
750}
751
752impl std::fmt::Display for MlogRecordType {
753    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
754        match self {
755            MlogRecordType::Unknown(v) => write!(f, "UNKNOWN({})", v),
756            _ => write!(f, "{}", self.name()),
757        }
758    }
759}
760
761/// Redo log file reader.
762pub struct LogFile {
763    reader: Box<dyn ReadSeek>,
764    file_size: u64,
765}
766
767impl LogFile {
768    /// Open a redo log file.
769    #[cfg(not(target_arch = "wasm32"))]
770    pub fn open(path: &str) -> Result<Self, IdbError> {
771        let file = std::fs::File::open(path)
772            .map_err(|e| IdbError::Io(format!("Cannot open {}: {}", path, e)))?;
773        let file_size = file
774            .metadata()
775            .map_err(|e| IdbError::Io(format!("Cannot stat {}: {}", path, e)))?
776            .len();
777
778        Self::init(Box::new(file), file_size)
779    }
780
781    /// Create a log file reader from an in-memory byte buffer.
782    ///
783    /// The buffer must be at least 2048 bytes (4 blocks of 512 bytes each for
784    /// the header and checkpoint blocks).
785    ///
786    /// # Examples
787    ///
788    /// ```no_run
789    /// use idb::innodb::log::LogFile;
790    ///
791    /// // Build a minimal valid redo log (4 header blocks + 1 data block)
792    /// let data = vec![0u8; 512 * 5];
793    /// let mut log = LogFile::from_bytes(data).unwrap();
794    /// let header = log.read_header().unwrap();
795    /// println!("Created by: {}", header.created_by);
796    /// ```
797    pub fn from_bytes(data: Vec<u8>) -> Result<Self, IdbError> {
798        let file_size = data.len() as u64;
799        Self::init(Box::new(Cursor::new(data)), file_size)
800    }
801
802    fn init(reader: Box<dyn ReadSeek>, file_size: u64) -> Result<Self, IdbError> {
803        if file_size < (LOG_FILE_HDR_BLOCKS as usize * LOG_BLOCK_SIZE) as u64 {
804            return Err(IdbError::Parse(format!(
805                "File is too small for a redo log ({} bytes, minimum {})",
806                file_size,
807                LOG_FILE_HDR_BLOCKS as usize * LOG_BLOCK_SIZE
808            )));
809        }
810
811        Ok(LogFile { reader, file_size })
812    }
813
814    /// Total number of 512-byte blocks in the file.
815    pub fn block_count(&self) -> u64 {
816        self.file_size / LOG_BLOCK_SIZE as u64
817    }
818
819    /// Number of data blocks (excluding the 4 header/checkpoint blocks).
820    pub fn data_block_count(&self) -> u64 {
821        self.block_count().saturating_sub(LOG_FILE_HDR_BLOCKS)
822    }
823
824    /// Read a single 512-byte block by block number.
825    pub fn read_block(&mut self, block_no: u64) -> Result<Vec<u8>, IdbError> {
826        let offset = block_no * LOG_BLOCK_SIZE as u64;
827        if offset + LOG_BLOCK_SIZE as u64 > self.file_size {
828            return Err(IdbError::Io(format!(
829                "Block {} is beyond end of file (offset {}, file size {})",
830                block_no, offset, self.file_size
831            )));
832        }
833
834        self.reader
835            .seek(SeekFrom::Start(offset))
836            .map_err(|e| IdbError::Io(format!("Seek error: {}", e)))?;
837
838        let mut buf = vec![0u8; LOG_BLOCK_SIZE];
839        self.reader
840            .read_exact(&mut buf)
841            .map_err(|e| IdbError::Io(format!("Read error at block {}: {}", block_no, e)))?;
842
843        Ok(buf)
844    }
845
846    /// Read and parse the log file header (block 0).
847    pub fn read_header(&mut self) -> Result<LogFileHeader, IdbError> {
848        let block = self.read_block(0)?;
849        LogFileHeader::parse(&block)
850            .ok_or_else(|| IdbError::Parse("Failed to parse log file header (block 0)".to_string()))
851    }
852
853    /// Read and parse a checkpoint (slot 0 = block 1, slot 1 = block 3).
854    pub fn read_checkpoint(&mut self, slot: u8) -> Result<LogCheckpoint, IdbError> {
855        let block_no = match slot {
856            0 => 1,
857            1 => 3,
858            _ => {
859                return Err(IdbError::Argument(format!(
860                    "Invalid checkpoint slot {} (must be 0 or 1)",
861                    slot
862                )))
863            }
864        };
865        let block = self.read_block(block_no)?;
866        LogCheckpoint::parse(&block).ok_or_else(|| {
867            IdbError::Parse(format!("Failed to parse checkpoint at block {}", block_no))
868        })
869    }
870
871    /// File size in bytes.
872    pub fn file_size(&self) -> u64 {
873        self.file_size
874    }
875}
876
877#[cfg(test)]
878mod tests {
879    use super::*;
880
881    fn make_block() -> Vec<u8> {
882        vec![0u8; LOG_BLOCK_SIZE]
883    }
884
885    #[test]
886    fn test_log_block_header_parse() {
887        let mut block = make_block();
888        // block_no = 42 (no flush bit)
889        BigEndian::write_u32(&mut block[0..], 42);
890        // data_len = 200
891        BigEndian::write_u16(&mut block[4..], 200);
892        // first_rec_group = 50
893        BigEndian::write_u16(&mut block[6..], 50);
894        // epoch_no = 7
895        BigEndian::write_u32(&mut block[8..], 7);
896
897        let hdr = LogBlockHeader::parse(&block).unwrap();
898        assert_eq!(hdr.block_no, 42);
899        assert!(!hdr.flush_flag);
900        assert_eq!(hdr.data_len, 200);
901        assert_eq!(hdr.first_rec_group, 50);
902        assert_eq!(hdr.epoch_no, 7);
903        assert_eq!(hdr.checkpoint_no(), 7);
904        assert!(hdr.has_data());
905    }
906
907    #[test]
908    fn test_log_block_flush_bit() {
909        let mut block = make_block();
910        // Set flush bit (bit 31) + block_no = 100
911        BigEndian::write_u32(&mut block[0..], 0x80000064);
912        BigEndian::write_u16(&mut block[4..], 14); // data_len = header only
913
914        let hdr = LogBlockHeader::parse(&block).unwrap();
915        assert!(hdr.flush_flag);
916        assert_eq!(hdr.block_no, 100);
917        assert!(!hdr.has_data());
918    }
919
920    #[test]
921    fn test_log_block_header_empty() {
922        let block = make_block();
923        let hdr = LogBlockHeader::parse(&block).unwrap();
924        assert_eq!(hdr.block_no, 0);
925        assert!(!hdr.flush_flag);
926        assert_eq!(hdr.data_len, 0);
927        assert_eq!(hdr.first_rec_group, 0);
928        assert_eq!(hdr.epoch_no, 0);
929        assert!(!hdr.has_data());
930    }
931
932    #[test]
933    fn test_log_block_header_too_small() {
934        let block = vec![0u8; 10]; // less than LOG_BLOCK_HDR_SIZE
935        assert!(LogBlockHeader::parse(&block).is_none());
936    }
937
938    #[test]
939    fn test_log_block_trailer_parse() {
940        let mut block = make_block();
941        BigEndian::write_u32(&mut block[LOG_BLOCK_CHECKSUM_OFFSET..], 0xCAFEBABE);
942
943        let trailer = LogBlockTrailer::parse(&block).unwrap();
944        assert_eq!(trailer.checksum, 0xCAFEBABE);
945    }
946
947    #[test]
948    fn test_log_file_header_format_v6() {
949        let mut block = make_block();
950        BigEndian::write_u32(&mut block[LOG_HEADER_FORMAT..], 6);
951        BigEndian::write_u32(&mut block[LOG_HEADER_LOG_UUID..], 0xABCD1234);
952        BigEndian::write_u64(&mut block[LOG_HEADER_START_LSN..], 0x00000000001A2B3C);
953        let creator = b"MySQL 9.0.1";
954        block[LOG_HEADER_CREATED_BY..LOG_HEADER_CREATED_BY + creator.len()]
955            .copy_from_slice(creator);
956
957        let hdr = LogFileHeader::parse(&block).unwrap();
958        assert_eq!(hdr.format_version, 6);
959        assert_eq!(hdr.group_id(), 6);
960        assert_eq!(hdr.start_lsn, 0x1A2B3C);
961        assert_eq!(hdr.log_uuid, 0xABCD1234);
962        assert_eq!(hdr.created_by, "MySQL 9.0.1");
963    }
964
965    #[test]
966    fn test_log_file_header_pre_8030() {
967        let mut block = make_block();
968        // Pre-8.0.30: format_version=1, start_lsn at offset 4
969        BigEndian::write_u32(&mut block[LOG_HEADER_FORMAT..], 1);
970        BigEndian::write_u64(&mut block[4..], 0x00000000001A2B3C); // start_lsn at offset 4
971        let creator = b"MySQL 5.7.44";
972        block[LOG_HEADER_CREATED_BY..LOG_HEADER_CREATED_BY + creator.len()]
973            .copy_from_slice(creator);
974
975        let hdr = LogFileHeader::parse(&block).unwrap();
976        assert_eq!(hdr.format_version, 1);
977        assert_eq!(hdr.start_lsn, 0x1A2B3C);
978        assert_eq!(hdr.log_uuid, 0); // no log_uuid in pre-8.0.30
979        assert_eq!(hdr.created_by, "MySQL 5.7.44");
980    }
981
982    #[test]
983    fn test_log_file_header_format_v5() {
984        let mut block = make_block();
985        // Format version 5 (MySQL 8.0.28): pre-8.0.30 layout
986        BigEndian::write_u32(&mut block[LOG_HEADER_FORMAT..], 5);
987        BigEndian::write_u64(&mut block[4..], 0x00000000DEADBEEF);
988        let creator = b"MySQL 8.0.28";
989        block[LOG_HEADER_CREATED_BY..LOG_HEADER_CREATED_BY + creator.len()]
990            .copy_from_slice(creator);
991
992        let hdr = LogFileHeader::parse(&block).unwrap();
993        assert_eq!(hdr.format_version, 5);
994        assert_eq!(hdr.start_lsn, 0xDEADBEEF);
995        assert_eq!(hdr.log_uuid, 0);
996        assert_eq!(hdr.created_by, "MySQL 8.0.28");
997    }
998
999    #[test]
1000    fn test_log_file_header_empty_created_by() {
1001        let block = make_block();
1002        let hdr = LogFileHeader::parse(&block).unwrap();
1003        assert_eq!(hdr.created_by, "");
1004    }
1005
1006    #[test]
1007    fn test_log_checkpoint_parse() {
1008        let mut block = make_block();
1009        BigEndian::write_u64(&mut block[LOG_CHECKPOINT_NO..], 99);
1010        BigEndian::write_u64(&mut block[LOG_CHECKPOINT_LSN..], 0x00000000DEADBEEF);
1011        BigEndian::write_u32(&mut block[LOG_CHECKPOINT_OFFSET..], 2048);
1012        BigEndian::write_u32(&mut block[LOG_CHECKPOINT_BUF_SIZE..], 65536);
1013        BigEndian::write_u64(
1014            &mut block[LOG_CHECKPOINT_ARCHIVED_LSN..],
1015            0x00000000CAFEBABE,
1016        );
1017
1018        let cp = LogCheckpoint::parse(&block).unwrap();
1019        assert_eq!(cp.number, 99);
1020        assert_eq!(cp.lsn, 0xDEADBEEF);
1021        assert_eq!(cp.offset, 2048);
1022        assert_eq!(cp.buf_size, 65536);
1023        assert_eq!(cp.archived_lsn, 0xCAFEBABE);
1024    }
1025
1026    #[test]
1027    fn test_log_checkpoint_format_v6() {
1028        // In MySQL 8.0.30+ (format v6), only LSN at offset 8 is set
1029        let mut block = make_block();
1030        BigEndian::write_u64(&mut block[LOG_CHECKPOINT_LSN..], 32193931);
1031
1032        let cp = LogCheckpoint::parse(&block).unwrap();
1033        assert_eq!(cp.lsn, 32193931);
1034        assert_eq!(cp.number, 0);
1035        assert_eq!(cp.offset, 0);
1036        assert_eq!(cp.buf_size, 0);
1037        assert_eq!(cp.archived_lsn, 0);
1038    }
1039
1040    #[test]
1041    fn test_mlog_record_type_basic_writes() {
1042        assert_eq!(MlogRecordType::from_u8(1), MlogRecordType::Mlog1Byte);
1043        assert_eq!(MlogRecordType::from_u8(2), MlogRecordType::Mlog2Bytes);
1044        assert_eq!(MlogRecordType::from_u8(4), MlogRecordType::Mlog4Bytes);
1045        assert_eq!(MlogRecordType::from_u8(8), MlogRecordType::Mlog8Bytes);
1046    }
1047
1048    #[test]
1049    fn test_mlog_record_type_pre_8028() {
1050        // Pre-8.0.28 types at old positions
1051        assert_eq!(
1052            MlogRecordType::from_u8(9),
1053            MlogRecordType::MlogRecInsert8027
1054        );
1055        assert_eq!(
1056            MlogRecordType::from_u8(10),
1057            MlogRecordType::MlogRecClustDeleteMark8027
1058        );
1059        assert_eq!(
1060            MlogRecordType::from_u8(18),
1061            MlogRecordType::MlogPageReorganize8027
1062        );
1063        assert_eq!(
1064            MlogRecordType::from_u8(38),
1065            MlogRecordType::MlogCompRecInsert8027
1066        );
1067        assert_eq!(
1068            MlogRecordType::from_u8(52),
1069            MlogRecordType::MlogZipPageCompressNoData8027
1070        );
1071        assert_eq!(
1072            MlogRecordType::from_u8(53),
1073            MlogRecordType::MlogZipPageReorganize8027
1074        );
1075    }
1076
1077    #[test]
1078    fn test_mlog_record_type_corrected_mappings() {
1079        // Types that were previously mapped to wrong codes
1080        assert_eq!(
1081            MlogRecordType::from_u8(25),
1082            MlogRecordType::MlogUndoHdrCreate
1083        );
1084        assert_eq!(MlogRecordType::from_u8(26), MlogRecordType::MlogRecMinMark);
1085        assert_eq!(
1086            MlogRecordType::from_u8(27),
1087            MlogRecordType::MlogIbufBitmapInit
1088        );
1089        assert_eq!(MlogRecordType::from_u8(28), MlogRecordType::MlogLsn);
1090        assert_eq!(
1091            MlogRecordType::from_u8(29),
1092            MlogRecordType::MlogInitFilePage
1093        );
1094        assert_eq!(MlogRecordType::from_u8(30), MlogRecordType::MlogWriteString);
1095        assert_eq!(MlogRecordType::from_u8(31), MlogRecordType::MlogMultiRecEnd);
1096        assert_eq!(MlogRecordType::from_u8(32), MlogRecordType::MlogDummyRecord);
1097        assert_eq!(MlogRecordType::from_u8(33), MlogRecordType::MlogFileCreate);
1098        assert_eq!(MlogRecordType::from_u8(34), MlogRecordType::MlogFileRename);
1099        assert_eq!(MlogRecordType::from_u8(35), MlogRecordType::MlogFileDelete);
1100        assert_eq!(
1101            MlogRecordType::from_u8(36),
1102            MlogRecordType::MlogCompRecMinMark
1103        );
1104        assert_eq!(
1105            MlogRecordType::from_u8(37),
1106            MlogRecordType::MlogCompPageCreate
1107        );
1108    }
1109
1110    #[test]
1111    fn test_mlog_record_type_post_8028() {
1112        // New types added in MySQL 8.0.28+
1113        assert_eq!(MlogRecordType::from_u8(67), MlogRecordType::MlogRecInsert);
1114        assert_eq!(
1115            MlogRecordType::from_u8(68),
1116            MlogRecordType::MlogRecClustDeleteMark
1117        );
1118        assert_eq!(MlogRecordType::from_u8(69), MlogRecordType::MlogRecDelete);
1119        assert_eq!(
1120            MlogRecordType::from_u8(70),
1121            MlogRecordType::MlogRecUpdateInPlace
1122        );
1123        assert_eq!(
1124            MlogRecordType::from_u8(71),
1125            MlogRecordType::MlogListEndCopyCreated
1126        );
1127        assert_eq!(
1128            MlogRecordType::from_u8(72),
1129            MlogRecordType::MlogPageReorganize
1130        );
1131        assert_eq!(
1132            MlogRecordType::from_u8(73),
1133            MlogRecordType::MlogZipPageReorganize
1134        );
1135        assert_eq!(
1136            MlogRecordType::from_u8(74),
1137            MlogRecordType::MlogZipPageCompressNoData
1138        );
1139        assert_eq!(
1140            MlogRecordType::from_u8(75),
1141            MlogRecordType::MlogListEndDelete
1142        );
1143        assert_eq!(
1144            MlogRecordType::from_u8(76),
1145            MlogRecordType::MlogListStartDelete
1146        );
1147    }
1148
1149    #[test]
1150    fn test_mlog_record_type_extended() {
1151        // Types in the 57-66 range
1152        assert_eq!(
1153            MlogRecordType::from_u8(57),
1154            MlogRecordType::MlogPageCreateRTree
1155        );
1156        assert_eq!(
1157            MlogRecordType::from_u8(59),
1158            MlogRecordType::MlogInitFilePage2
1159        );
1160        assert_eq!(MlogRecordType::from_u8(61), MlogRecordType::MlogIndexLoad);
1161        assert_eq!(
1162            MlogRecordType::from_u8(62),
1163            MlogRecordType::MlogTableDynamicMeta
1164        );
1165        assert_eq!(
1166            MlogRecordType::from_u8(63),
1167            MlogRecordType::MlogPageCreateSdi
1168        );
1169        assert_eq!(
1170            MlogRecordType::from_u8(64),
1171            MlogRecordType::MlogCompPageCreateSdi
1172        );
1173        assert_eq!(MlogRecordType::from_u8(65), MlogRecordType::MlogFileExtend);
1174        assert_eq!(MlogRecordType::from_u8(66), MlogRecordType::MlogTest);
1175    }
1176
1177    #[test]
1178    fn test_mlog_record_type_unknown() {
1179        assert_eq!(MlogRecordType::from_u8(0), MlogRecordType::Unknown(0));
1180        assert_eq!(MlogRecordType::from_u8(255), MlogRecordType::Unknown(255));
1181        assert_eq!(MlogRecordType::from_u8(100), MlogRecordType::Unknown(100));
1182        // Gaps in the enum: 3, 5, 6, 7, 12, 23, 47, 54-56, 60, 77+
1183        assert_eq!(MlogRecordType::from_u8(3), MlogRecordType::Unknown(3));
1184        assert_eq!(MlogRecordType::from_u8(12), MlogRecordType::Unknown(12));
1185        assert_eq!(MlogRecordType::from_u8(23), MlogRecordType::Unknown(23));
1186        assert_eq!(MlogRecordType::from_u8(47), MlogRecordType::Unknown(47));
1187        assert_eq!(MlogRecordType::from_u8(60), MlogRecordType::Unknown(60));
1188        assert_eq!(MlogRecordType::from_u8(77), MlogRecordType::Unknown(77));
1189    }
1190
1191    #[test]
1192    fn test_mlog_record_type_name() {
1193        assert_eq!(MlogRecordType::Mlog1Byte.name(), "MLOG_1BYTE");
1194        assert_eq!(MlogRecordType::MlogRecInsert.name(), "MLOG_REC_INSERT");
1195        assert_eq!(
1196            MlogRecordType::MlogRecInsert8027.name(),
1197            "MLOG_REC_INSERT_8027"
1198        );
1199        assert_eq!(MlogRecordType::MlogFileExtend.name(), "MLOG_FILE_EXTEND");
1200        assert_eq!(MlogRecordType::MlogLsn.name(), "MLOG_LSN");
1201        assert_eq!(MlogRecordType::Unknown(99).name(), "UNKNOWN");
1202    }
1203
1204    #[test]
1205    fn test_mlog_record_type_display() {
1206        assert_eq!(format!("{}", MlogRecordType::Mlog1Byte), "MLOG_1BYTE");
1207        assert_eq!(format!("{}", MlogRecordType::Unknown(99)), "UNKNOWN(99)");
1208        assert_eq!(
1209            format!("{}", MlogRecordType::MlogListEndDelete),
1210            "MLOG_LIST_END_DELETE"
1211        );
1212    }
1213
1214    #[test]
1215    fn test_log_block_checksum_validation() {
1216        let mut block = make_block();
1217        // Put some data in the block
1218        BigEndian::write_u32(&mut block[0..], 5); // block_no = 5
1219        BigEndian::write_u16(&mut block[4..], 100); // data_len
1220        BigEndian::write_u16(&mut block[6..], 12); // first_rec_group
1221        block[12] = 0xAB; // some log data
1222
1223        // Calculate and store the correct CRC-32C
1224        let crc = crc32c::crc32c(&block[..LOG_BLOCK_CHECKSUM_OFFSET]);
1225        BigEndian::write_u32(&mut block[LOG_BLOCK_CHECKSUM_OFFSET..], crc);
1226
1227        assert!(validate_log_block_checksum(&block));
1228    }
1229
1230    #[test]
1231    fn test_log_from_bytes_empty() {
1232        let result = LogFile::from_bytes(vec![]);
1233        match result {
1234            Err(e) => assert!(
1235                e.to_string().contains("too small"),
1236                "Expected 'too small' in: {e}"
1237            ),
1238            Ok(_) => panic!("Expected error for empty input"),
1239        }
1240    }
1241
1242    #[test]
1243    fn test_log_from_bytes_too_small() {
1244        let result = LogFile::from_bytes(vec![0u8; 100]);
1245        match result {
1246            Err(e) => assert!(
1247                e.to_string().contains("too small"),
1248                "Expected 'too small' in: {e}"
1249            ),
1250            Ok(_) => panic!("Expected error for 100-byte input"),
1251        }
1252    }
1253
1254    #[test]
1255    fn test_log_block_checksum_invalid() {
1256        let mut block = make_block();
1257        BigEndian::write_u32(&mut block[0..], 5);
1258        // Wrong checksum
1259        BigEndian::write_u32(&mut block[LOG_BLOCK_CHECKSUM_OFFSET..], 0xDEADDEAD);
1260
1261        assert!(!validate_log_block_checksum(&block));
1262    }
1263}