Skip to main content

idb/binlog/
header.rs

1//! Binary log file header and format description event parsing.
2//!
3//! MySQL binary logs start with a 4-byte magic number (`\xfe\x62\x69\x6e`)
4//! followed by a Format Description Event that describes the binlog version,
5//! server version, and event header length.
6
7use byteorder::{ByteOrder, LittleEndian};
8use serde::Serialize;
9
10use super::constants::{BINLOG_MAGIC, COMMON_HEADER_SIZE};
11
12/// Validate that the first 4 bytes match the binlog magic number.
13///
14/// # Examples
15///
16/// ```
17/// use idb::binlog::header::validate_binlog_magic;
18///
19/// assert!(validate_binlog_magic(&[0xfe, 0x62, 0x69, 0x6e]));
20/// assert!(!validate_binlog_magic(&[0x00, 0x00, 0x00, 0x00]));
21/// assert!(!validate_binlog_magic(&[0xfe, 0x62])); // too short
22/// ```
23pub fn validate_binlog_magic(data: &[u8]) -> bool {
24    data.len() >= 4 && data[..4] == BINLOG_MAGIC
25}
26
27/// Parsed binlog event header (19 bytes, common to all events).
28///
29/// # Examples
30///
31/// ```
32/// use idb::binlog::header::BinlogEventHeader;
33/// use byteorder::{LittleEndian, ByteOrder};
34///
35/// let mut buf = vec![0u8; 19];
36/// LittleEndian::write_u32(&mut buf[0..], 1700000000); // timestamp
37/// buf[4] = 15; // FORMAT_DESCRIPTION_EVENT
38/// LittleEndian::write_u32(&mut buf[5..], 1); // server_id
39/// LittleEndian::write_u32(&mut buf[9..], 100); // event_length
40/// LittleEndian::write_u32(&mut buf[13..], 119); // next_position
41/// LittleEndian::write_u16(&mut buf[17..], 0); // flags
42///
43/// let hdr = BinlogEventHeader::parse(&buf).unwrap();
44/// assert_eq!(hdr.timestamp, 1700000000);
45/// assert_eq!(hdr.type_code, 15);
46/// assert_eq!(hdr.server_id, 1);
47/// assert_eq!(hdr.event_length, 100);
48/// assert_eq!(hdr.next_position, 119);
49/// ```
50#[derive(Debug, Clone, Serialize)]
51pub struct BinlogEventHeader {
52    /// Unix timestamp of the event.
53    pub timestamp: u32,
54    /// Event type code.
55    pub type_code: u8,
56    /// Server ID that produced the event.
57    pub server_id: u32,
58    /// Total length of the event (header + data + checksum).
59    pub event_length: u32,
60    /// Position of the next event in the binlog file.
61    pub next_position: u32,
62    /// Event flags.
63    pub flags: u16,
64}
65
66impl BinlogEventHeader {
67    /// Parse a binlog event header from at least 19 bytes.
68    pub fn parse(data: &[u8]) -> Option<Self> {
69        if data.len() < COMMON_HEADER_SIZE {
70            return None;
71        }
72
73        Some(BinlogEventHeader {
74            timestamp: LittleEndian::read_u32(&data[0..]),
75            type_code: data[4],
76            server_id: LittleEndian::read_u32(&data[5..]),
77            event_length: LittleEndian::read_u32(&data[9..]),
78            next_position: LittleEndian::read_u32(&data[13..]),
79            flags: LittleEndian::read_u16(&data[17..]),
80        })
81    }
82}
83
84/// Parsed Format Description Event (type 15).
85///
86/// The first real event in any v4 binlog file. Describes the binlog format
87/// version, server version string, creation timestamp, and event header length.
88///
89/// # Examples
90///
91/// ```
92/// use idb::binlog::header::FormatDescriptionEvent;
93/// use byteorder::{LittleEndian, ByteOrder};
94///
95/// let mut buf = vec![0u8; 100];
96/// // binlog_version = 4
97/// LittleEndian::write_u16(&mut buf[0..], 4);
98/// // server_version (50 bytes, null-padded)
99/// let ver = b"8.0.35";
100/// buf[2..2 + ver.len()].copy_from_slice(ver);
101/// // create_timestamp
102/// LittleEndian::write_u32(&mut buf[52..], 1700000000);
103/// // header_length
104/// buf[56] = 19;
105///
106/// let fde = FormatDescriptionEvent::parse(&buf).unwrap();
107/// assert_eq!(fde.binlog_version, 4);
108/// assert_eq!(fde.server_version, "8.0.35");
109/// assert_eq!(fde.create_timestamp, 1700000000);
110/// assert_eq!(fde.header_length, 19);
111/// ```
112#[derive(Debug, Clone, Serialize)]
113pub struct FormatDescriptionEvent {
114    /// Binlog format version (always 4 for MySQL 5.0+).
115    pub binlog_version: u16,
116    /// Server version string (e.g. "8.0.35").
117    pub server_version: String,
118    /// Timestamp when the binlog was created.
119    pub create_timestamp: u32,
120    /// Length of each event header (19 for v4).
121    pub header_length: u8,
122    /// Checksum algorithm (0 = none, 1 = CRC32).
123    pub checksum_alg: u8,
124}
125
126impl FormatDescriptionEvent {
127    /// Whether CRC-32 event checksums are enabled (checksum_alg == CRC32).
128    pub fn has_checksum(&self) -> bool {
129        self.checksum_alg == 1
130    }
131
132    /// Parse a Format Description Event from the event data (after the 19-byte common header).
133    pub fn parse(data: &[u8]) -> Option<Self> {
134        // Minimum: 2 (version) + 50 (server_version) + 4 (timestamp) + 1 (header_length) = 57
135        if data.len() < 57 {
136            return None;
137        }
138
139        let binlog_version = LittleEndian::read_u16(&data[0..]);
140
141        // Server version is 50 bytes, null-terminated
142        let ver_bytes = &data[2..52];
143        let server_version = std::str::from_utf8(ver_bytes)
144            .unwrap_or("")
145            .trim_end_matches('\0')
146            .to_string();
147
148        let create_timestamp = LittleEndian::read_u32(&data[52..]);
149        let header_length = data[56];
150
151        // Checksum algorithm is the last byte before the 4-byte checksum
152        // In FDE, it's at a variable position based on the post-header lengths array.
153        // For simplicity, try to read it from the end of the data.
154        let checksum_alg = if data.len() >= 58 {
155            // After header_length byte, there's a variable-length array of post-header
156            // lengths. The checksum_alg byte is at the end of this array.
157            // For FDE v4, it's typically at offset 57 + (number_of_event_types)
158            // A reasonable heuristic: last 5 bytes are [checksum_alg, crc32_checksum(4)]
159            if data.len() >= 5 {
160                data[data.len() - 5]
161            } else {
162                0
163            }
164        } else {
165            0
166        };
167
168        Some(FormatDescriptionEvent {
169            binlog_version,
170            server_version,
171            create_timestamp,
172            header_length,
173            checksum_alg,
174        })
175    }
176}
177
178/// Parsed ROTATE_EVENT (type 4).
179///
180/// Signals rotation to the next binary log file.
181///
182/// # Examples
183///
184/// ```
185/// use idb::binlog::header::RotateEvent;
186/// use byteorder::{LittleEndian, ByteOrder};
187///
188/// let mut buf = vec![0u8; 24];
189/// LittleEndian::write_u64(&mut buf[0..], 4); // position
190/// buf[8..].copy_from_slice(b"mysql-bin.000002");
191///
192/// let re = RotateEvent::parse(&buf).unwrap();
193/// assert_eq!(re.position, 4);
194/// assert_eq!(re.next_filename, "mysql-bin.000002");
195/// ```
196#[derive(Debug, Clone, Serialize)]
197pub struct RotateEvent {
198    /// Position in the next binlog file to start reading from.
199    pub position: u64,
200    /// Filename of the next binlog file.
201    pub next_filename: String,
202}
203
204impl RotateEvent {
205    /// Parse a ROTATE_EVENT from the event payload (after the 19-byte common header).
206    pub fn parse(data: &[u8]) -> Option<Self> {
207        if data.len() < 8 {
208            return None;
209        }
210        let position = LittleEndian::read_u64(&data[0..]);
211        let next_filename = std::str::from_utf8(&data[8..])
212            .unwrap_or("")
213            .trim_end_matches('\0')
214            .to_string();
215        Some(RotateEvent {
216            position,
217            next_filename,
218        })
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_binlog_magic_valid() {
228        assert!(validate_binlog_magic(&BINLOG_MAGIC));
229        assert!(validate_binlog_magic(&[0xfe, 0x62, 0x69, 0x6e, 0x00]));
230    }
231
232    #[test]
233    fn test_binlog_magic_invalid() {
234        assert!(!validate_binlog_magic(&[0x00, 0x00, 0x00, 0x00]));
235        assert!(!validate_binlog_magic(&[0xfe, 0x62])); // too short
236        assert!(!validate_binlog_magic(&[]));
237    }
238
239    #[test]
240    fn test_binlog_event_header_parse() {
241        let mut buf = vec![0u8; 19];
242        LittleEndian::write_u32(&mut buf[0..], 1700000000);
243        buf[4] = 15; // FORMAT_DESCRIPTION_EVENT
244        LittleEndian::write_u32(&mut buf[5..], 1);
245        LittleEndian::write_u32(&mut buf[9..], 100);
246        LittleEndian::write_u32(&mut buf[13..], 119);
247        LittleEndian::write_u16(&mut buf[17..], 0);
248
249        let hdr = BinlogEventHeader::parse(&buf).unwrap();
250        assert_eq!(hdr.timestamp, 1700000000);
251        assert_eq!(hdr.type_code, 15);
252        assert_eq!(hdr.server_id, 1);
253        assert_eq!(hdr.event_length, 100);
254        assert_eq!(hdr.next_position, 119);
255        assert_eq!(hdr.flags, 0);
256    }
257
258    #[test]
259    fn test_binlog_event_header_too_short() {
260        let buf = vec![0u8; 10];
261        assert!(BinlogEventHeader::parse(&buf).is_none());
262    }
263
264    #[test]
265    fn test_format_description_event_parse() {
266        let mut buf = vec![0u8; 100];
267        LittleEndian::write_u16(&mut buf[0..], 4);
268        let ver = b"8.0.35";
269        buf[2..2 + ver.len()].copy_from_slice(ver);
270        LittleEndian::write_u32(&mut buf[52..], 1700000000);
271        buf[56] = 19;
272        // checksum_alg at buf[95] (100 - 5)
273        buf[95] = 1; // CRC32
274
275        let fde = FormatDescriptionEvent::parse(&buf).unwrap();
276        assert_eq!(fde.binlog_version, 4);
277        assert_eq!(fde.server_version, "8.0.35");
278        assert_eq!(fde.create_timestamp, 1700000000);
279        assert_eq!(fde.header_length, 19);
280        assert_eq!(fde.checksum_alg, 1);
281    }
282
283    #[test]
284    fn test_format_description_event_too_short() {
285        let buf = vec![0u8; 30];
286        assert!(FormatDescriptionEvent::parse(&buf).is_none());
287    }
288}