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}