Skip to main content

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}