Skip to main content

idb/cli/
sdi.rs

1use std::io::Write;
2
3use crate::cli::wprintln;
4use crate::innodb::sdi;
5use crate::IdbError;
6
7/// Options for the `inno sdi` subcommand.
8pub struct SdiOptions {
9    /// Path to the InnoDB tablespace file (.ibd).
10    pub file: String,
11    /// Pretty-print the extracted JSON metadata.
12    pub pretty: bool,
13    /// Override the auto-detected page size.
14    pub page_size: Option<u32>,
15    /// Path to MySQL keyring file for decrypting encrypted tablespaces.
16    pub keyring: Option<String>,
17    /// Use memory-mapped I/O for file access.
18    pub mmap: bool,
19}
20
21/// Extract SDI metadata from a MySQL 8.0+ tablespace.
22///
23/// SDI (Serialized Dictionary Information) is MySQL 8.0's mechanism for
24/// embedding the data dictionary (table definitions, column metadata, index
25/// descriptions) directly inside each `.ibd` file, replacing the `.frm` files
26/// used in MySQL 5.x. SDI data is stored in pages of type 17853
27/// (`FIL_PAGE_SDI`). The SDI binary format and JSON schema are identical
28/// across MySQL 8.0, 9.0, and 9.1.
29///
30/// This command first scans the tablespace for SDI pages by checking page
31/// types, then uses [`sdi::extract_sdi_from_pages`]
32/// to reassemble records that may span multiple pages via the next-page chain.
33/// Each record's zlib-compressed payload is decompressed into a JSON string
34/// containing the full table/column/index definition.
35///
36/// With `--pretty`, the JSON is re-parsed and re-serialized with indentation
37/// for readability. If a tablespace has no SDI pages (e.g., pre-8.0 files),
38/// a message is printed indicating that SDI is unavailable.
39pub fn execute(opts: &SdiOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
40    let mut ts = crate::cli::open_tablespace(&opts.file, opts.page_size, opts.mmap)?;
41
42    if let Some(ref keyring_path) = opts.keyring {
43        crate::cli::setup_decryption(&mut ts, keyring_path)?;
44    }
45
46    // MariaDB does not use SDI — return a clear error
47    if ts.vendor_info().vendor == crate::innodb::vendor::InnoDbVendor::MariaDB {
48        return Err(IdbError::Argument(
49            "SDI is not available for MariaDB tablespaces. MariaDB does not use \
50             Serialized Dictionary Information (SDI); table metadata is stored \
51             in the data dictionary (mysql.* tables) or .frm files."
52                .to_string(),
53        ));
54    }
55
56    // Find SDI pages
57    let sdi_pages = sdi::find_sdi_pages(&mut ts)?;
58
59    if sdi_pages.is_empty() {
60        wprintln!(writer, "No SDI pages found in {}.", opts.file)?;
61        wprintln!(writer, "SDI is only available in MySQL 8.0+ tablespaces.")?;
62        return Ok(());
63    }
64
65    wprintln!(
66        writer,
67        "Found {} SDI page(s): {:?}",
68        sdi_pages.len(),
69        sdi_pages
70    )?;
71
72    // Use multi-page reassembly to extract records
73    let records = sdi::extract_sdi_from_pages(&mut ts, &sdi_pages)?;
74
75    if records.is_empty() {
76        wprintln!(
77            writer,
78            "No SDI records found (pages may be non-leaf or empty)."
79        )?;
80        return Ok(());
81    }
82
83    for rec in &records {
84        wprintln!(writer)?;
85        wprintln!(
86            writer,
87            "=== SDI Record: type={} ({}), id={}",
88            rec.sdi_type,
89            sdi::sdi_type_name(rec.sdi_type),
90            rec.sdi_id
91        )?;
92        wprintln!(
93            writer,
94            "Compressed: {} bytes, Uncompressed: {} bytes",
95            rec.compressed_len,
96            rec.uncompressed_len
97        )?;
98
99        if rec.data.is_empty() {
100            wprintln!(
101                writer,
102                "(Data could not be decompressed - may span multiple pages)"
103            )?;
104            continue;
105        }
106
107        if opts.pretty {
108            // Pretty-print JSON
109            match serde_json::from_str::<serde_json::Value>(&rec.data) {
110                Ok(json) => {
111                    wprintln!(
112                        writer,
113                        "{}",
114                        serde_json::to_string_pretty(&json).unwrap_or(rec.data.clone())
115                    )?;
116                }
117                Err(_) => {
118                    wprintln!(writer, "{}", rec.data)?;
119                }
120            }
121        } else {
122            wprintln!(writer, "{}", rec.data)?;
123        }
124    }
125
126    wprintln!(writer)?;
127    wprintln!(writer, "Total SDI records: {}", records.len())?;
128
129    Ok(())
130}