idb/innodb/vendor.rs
1//! InnoDB vendor detection.
2//!
3//! Identifies whether a tablespace was created by MySQL, Percona XtraDB, or
4//! MariaDB by inspecting FSP flags and redo log creator strings. MariaDB
5//! diverges from MySQL's on-disk format starting from 10.1, especially in FSP
6//! flags layout, checksum algorithm, and page types. Percona XtraDB is
7//! binary-compatible with MySQL at the file level.
8//!
9//! Use [`detect_vendor_from_flags`] for tablespace files and
10//! [`detect_vendor_from_created_by`] for redo log headers.
11
12use serde::Serialize;
13use std::fmt;
14
15/// InnoDB implementation vendor.
16///
17/// # Examples
18///
19/// ```
20/// use idb::innodb::vendor::InnoDbVendor;
21///
22/// let vendor = InnoDbVendor::MySQL;
23/// assert_eq!(format!("{vendor}"), "MySQL");
24///
25/// let vendor = InnoDbVendor::MariaDB;
26/// assert_eq!(format!("{vendor}"), "MariaDB");
27///
28/// let vendor = InnoDbVendor::Percona;
29/// assert_eq!(format!("{vendor}"), "Percona XtraDB");
30/// ```
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
32pub enum InnoDbVendor {
33 /// Oracle MySQL (and binary-compatible forks like AWS Aurora).
34 MySQL,
35 /// Percona Server with XtraDB (binary-compatible with MySQL on disk).
36 Percona,
37 /// MariaDB (divergent on-disk format from 10.1+).
38 MariaDB,
39}
40
41impl fmt::Display for InnoDbVendor {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 InnoDbVendor::MySQL => write!(f, "MySQL"),
45 InnoDbVendor::Percona => write!(f, "Percona XtraDB"),
46 InnoDbVendor::MariaDB => write!(f, "MariaDB"),
47 }
48 }
49}
50
51/// MariaDB tablespace format variant.
52///
53/// MariaDB 10.5+ introduced `full_crc32`, a simplified format with a single
54/// CRC-32C checksum over the entire page (minus the last 4 bytes). Older
55/// MariaDB versions use the "original" format, which is closer to MySQL but
56/// with different flag bit assignments.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
58pub enum MariaDbFormat {
59 /// Original MariaDB format (10.1–10.4). FSP flags layout differs from
60 /// MySQL in compression (bit 16) and encryption handling.
61 Original,
62 /// Full CRC-32C format (MariaDB 10.5+). FSP flags bit 4 is the marker.
63 /// Page size in bits 0-3, compression algo in bits 5-7.
64 FullCrc32,
65}
66
67impl fmt::Display for MariaDbFormat {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self {
70 MariaDbFormat::Original => write!(f, "original"),
71 MariaDbFormat::FullCrc32 => write!(f, "full_crc32"),
72 }
73 }
74}
75
76/// Vendor and format information for a tablespace or redo log.
77///
78/// # Examples
79///
80/// ```
81/// use idb::innodb::vendor::{VendorInfo, InnoDbVendor, MariaDbFormat};
82///
83/// // Create VendorInfo for each vendor
84/// let mysql = VendorInfo::mysql();
85/// assert_eq!(mysql.vendor, InnoDbVendor::MySQL);
86/// assert!(!mysql.is_full_crc32());
87///
88/// let percona = VendorInfo::percona();
89/// assert_eq!(percona.vendor, InnoDbVendor::Percona);
90///
91/// let maria = VendorInfo::mariadb(MariaDbFormat::FullCrc32);
92/// assert_eq!(maria.vendor, InnoDbVendor::MariaDB);
93/// assert!(maria.is_full_crc32());
94/// assert_eq!(format!("{maria}"), "MariaDB (full_crc32)");
95/// ```
96#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
97pub struct VendorInfo {
98 /// The InnoDB implementation vendor.
99 pub vendor: InnoDbVendor,
100 /// MariaDB-specific format variant, if applicable.
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub mariadb_format: Option<MariaDbFormat>,
103}
104
105impl VendorInfo {
106 /// Create a VendorInfo for MySQL (the default).
107 ///
108 /// # Examples
109 ///
110 /// ```
111 /// use idb::innodb::vendor::{VendorInfo, InnoDbVendor};
112 ///
113 /// let info = VendorInfo::mysql();
114 /// assert_eq!(info.vendor, InnoDbVendor::MySQL);
115 /// assert_eq!(info.mariadb_format, None);
116 /// ```
117 pub fn mysql() -> Self {
118 VendorInfo {
119 vendor: InnoDbVendor::MySQL,
120 mariadb_format: None,
121 }
122 }
123
124 /// Create a VendorInfo for Percona.
125 ///
126 /// # Examples
127 ///
128 /// ```
129 /// use idb::innodb::vendor::{VendorInfo, InnoDbVendor};
130 ///
131 /// let info = VendorInfo::percona();
132 /// assert_eq!(info.vendor, InnoDbVendor::Percona);
133 /// assert_eq!(info.mariadb_format, None);
134 /// ```
135 pub fn percona() -> Self {
136 VendorInfo {
137 vendor: InnoDbVendor::Percona,
138 mariadb_format: None,
139 }
140 }
141
142 /// Create a VendorInfo for MariaDB with a specific format.
143 ///
144 /// # Examples
145 ///
146 /// ```
147 /// use idb::innodb::vendor::{VendorInfo, InnoDbVendor, MariaDbFormat};
148 ///
149 /// let info = VendorInfo::mariadb(MariaDbFormat::FullCrc32);
150 /// assert_eq!(info.vendor, InnoDbVendor::MariaDB);
151 /// assert_eq!(info.mariadb_format, Some(MariaDbFormat::FullCrc32));
152 /// assert!(info.is_full_crc32());
153 /// ```
154 pub fn mariadb(format: MariaDbFormat) -> Self {
155 VendorInfo {
156 vendor: InnoDbVendor::MariaDB,
157 mariadb_format: Some(format),
158 }
159 }
160
161 /// Returns true if this is a MariaDB full_crc32 tablespace.
162 ///
163 /// # Examples
164 ///
165 /// ```
166 /// use idb::innodb::vendor::{VendorInfo, MariaDbFormat};
167 ///
168 /// assert!(VendorInfo::mariadb(MariaDbFormat::FullCrc32).is_full_crc32());
169 /// assert!(!VendorInfo::mariadb(MariaDbFormat::Original).is_full_crc32());
170 /// assert!(!VendorInfo::mysql().is_full_crc32());
171 /// ```
172 pub fn is_full_crc32(&self) -> bool {
173 self.mariadb_format == Some(MariaDbFormat::FullCrc32)
174 }
175}
176
177impl fmt::Display for VendorInfo {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 match self.mariadb_format {
180 Some(format) => write!(f, "{} ({})", self.vendor, format),
181 None => write!(f, "{}", self.vendor),
182 }
183 }
184}
185
186/// Detect the InnoDB vendor from FSP flags on page 0.
187///
188/// Detection logic:
189/// - If bit 4 is set → MariaDB full_crc32 (unambiguous marker)
190/// - If bit 16 is set and bits 11-14 are zero → likely MariaDB original
191/// (PAGE_COMPRESSION flag in a position MySQL doesn't use)
192/// - Otherwise → MySQL (Percona is binary-compatible, indistinguishable from
193/// flags alone)
194///
195/// # Examples
196///
197/// ```
198/// use idb::innodb::vendor::{detect_vendor_from_flags, InnoDbVendor, MariaDbFormat};
199///
200/// // Zero flags → MySQL (the default)
201/// let info = detect_vendor_from_flags(0);
202/// assert_eq!(info.vendor, InnoDbVendor::MySQL);
203///
204/// // Bit 4 set → MariaDB full_crc32
205/// let info = detect_vendor_from_flags(0x10);
206/// assert_eq!(info.vendor, InnoDbVendor::MariaDB);
207/// assert_eq!(info.mariadb_format, Some(MariaDbFormat::FullCrc32));
208///
209/// // Bit 16 set (PAGE_COMPRESSION) with MySQL compression bits zero → MariaDB original
210/// let info = detect_vendor_from_flags(1 << 16);
211/// assert_eq!(info.vendor, InnoDbVendor::MariaDB);
212/// assert_eq!(info.mariadb_format, Some(MariaDbFormat::Original));
213/// ```
214pub fn detect_vendor_from_flags(fsp_flags: u32) -> VendorInfo {
215 use crate::innodb::constants::*;
216
217 // Check for MariaDB full_crc32 marker (bit 4)
218 if fsp_flags & MARIADB_FSP_FLAGS_FCRC32_MARKER_MASK != 0 {
219 return VendorInfo::mariadb(MariaDbFormat::FullCrc32);
220 }
221
222 // Check for MariaDB original format: bit 16 set (PAGE_COMPRESSION)
223 // and MySQL compression bits (11-12) are zero. MySQL never uses bit 16.
224 if fsp_flags & MARIADB_FSP_FLAGS_PAGE_COMPRESSION != 0 {
225 let mysql_comp_bits = (fsp_flags >> 11) & 0x03;
226 if mysql_comp_bits == 0 {
227 return VendorInfo::mariadb(MariaDbFormat::Original);
228 }
229 }
230
231 // Default: MySQL (Percona is indistinguishable from flags)
232 VendorInfo::mysql()
233}
234
235/// Detect the InnoDB vendor from a redo log `created_by` string.
236///
237/// Redo log block 0 contains a creator string (e.g., "MySQL 8.0.32",
238/// "MariaDB 10.11.4", "Percona Server 8.0.32-24").
239///
240/// # Examples
241///
242/// ```
243/// use idb::innodb::vendor::{detect_vendor_from_created_by, InnoDbVendor};
244///
245/// assert_eq!(detect_vendor_from_created_by("MySQL 8.0.32"), InnoDbVendor::MySQL);
246/// assert_eq!(detect_vendor_from_created_by("MariaDB 10.11.4"), InnoDbVendor::MariaDB);
247/// assert_eq!(detect_vendor_from_created_by("Percona Server 8.0.32-24"), InnoDbVendor::Percona);
248///
249/// // Unknown or empty strings default to MySQL
250/// assert_eq!(detect_vendor_from_created_by(""), InnoDbVendor::MySQL);
251/// ```
252pub fn detect_vendor_from_created_by(created_by: &str) -> InnoDbVendor {
253 let lower = created_by.to_lowercase();
254 if lower.contains("mariadb") {
255 InnoDbVendor::MariaDB
256 } else if lower.contains("percona") {
257 InnoDbVendor::Percona
258 } else {
259 InnoDbVendor::MySQL
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_default_flags_are_mysql() {
269 let info = detect_vendor_from_flags(0);
270 assert_eq!(info.vendor, InnoDbVendor::MySQL);
271 assert_eq!(info.mariadb_format, None);
272 }
273
274 #[test]
275 fn test_mysql_flags_with_page_size() {
276 // ssize=5 (16K) at bits 6-9
277 let flags = 5 << 6;
278 let info = detect_vendor_from_flags(flags);
279 assert_eq!(info.vendor, InnoDbVendor::MySQL);
280 }
281
282 #[test]
283 fn test_mysql_flags_with_compression() {
284 // zlib compression at bits 11-12
285 let flags = 1 << 11;
286 let info = detect_vendor_from_flags(flags);
287 assert_eq!(info.vendor, InnoDbVendor::MySQL);
288 }
289
290 #[test]
291 fn test_mariadb_full_crc32_detected() {
292 // Bit 4 set = full_crc32 marker
293 let flags = 0x10;
294 let info = detect_vendor_from_flags(flags);
295 assert_eq!(info.vendor, InnoDbVendor::MariaDB);
296 assert_eq!(info.mariadb_format, Some(MariaDbFormat::FullCrc32));
297 assert!(info.is_full_crc32());
298 }
299
300 #[test]
301 fn test_mariadb_full_crc32_with_page_size() {
302 // Bit 4 (marker) + ssize=5 in bits 0-3
303 let flags = 0x10 | 5;
304 let info = detect_vendor_from_flags(flags);
305 assert_eq!(info.vendor, InnoDbVendor::MariaDB);
306 assert_eq!(info.mariadb_format, Some(MariaDbFormat::FullCrc32));
307 }
308
309 #[test]
310 fn test_mariadb_original_page_compression() {
311 // Bit 16 set, MySQL compression bits (11-12) zero
312 let flags = 1 << 16;
313 let info = detect_vendor_from_flags(flags);
314 assert_eq!(info.vendor, InnoDbVendor::MariaDB);
315 assert_eq!(info.mariadb_format, Some(MariaDbFormat::Original));
316 }
317
318 #[test]
319 fn test_vendor_from_created_by_mysql() {
320 assert_eq!(
321 detect_vendor_from_created_by("MySQL 8.0.32"),
322 InnoDbVendor::MySQL
323 );
324 }
325
326 #[test]
327 fn test_vendor_from_created_by_mariadb() {
328 assert_eq!(
329 detect_vendor_from_created_by("MariaDB 10.11.4"),
330 InnoDbVendor::MariaDB
331 );
332 }
333
334 #[test]
335 fn test_vendor_from_created_by_percona() {
336 assert_eq!(
337 detect_vendor_from_created_by("Percona Server 8.0.32-24"),
338 InnoDbVendor::Percona
339 );
340 }
341
342 #[test]
343 fn test_vendor_from_empty_string() {
344 assert_eq!(detect_vendor_from_created_by(""), InnoDbVendor::MySQL);
345 }
346
347 #[test]
348 fn test_vendor_info_display() {
349 assert_eq!(format!("{}", VendorInfo::mysql()), "MySQL");
350 assert_eq!(format!("{}", VendorInfo::percona()), "Percona XtraDB");
351 assert_eq!(
352 format!("{}", VendorInfo::mariadb(MariaDbFormat::FullCrc32)),
353 "MariaDB (full_crc32)"
354 );
355 assert_eq!(
356 format!("{}", VendorInfo::mariadb(MariaDbFormat::Original)),
357 "MariaDB (original)"
358 );
359 }
360}