Skip to main content

idb/cli/
verify.rs

1//! CLI implementation for the `inno verify` subcommand.
2//!
3//! Runs pure structural checks on a tablespace file without requiring
4//! valid checksums. Checks page number sequence, space ID consistency,
5//! LSN monotonicity, B+Tree level validity, page chain bounds, and
6//! trailer LSN matching.
7
8use std::io::Write;
9
10use colored::Colorize;
11use serde::Serialize;
12
13use crate::cli::wprintln;
14use crate::innodb::page::FilHeader;
15use crate::innodb::verify::{
16    extract_chain_file_info, verify_backup_chain, verify_tablespace, ChainReport, VerifyConfig,
17};
18use crate::IdbError;
19
20/// Options for the `inno verify` subcommand.
21pub struct VerifyOptions {
22    /// Path to the InnoDB tablespace file (.ibd).
23    pub file: String,
24    /// Show per-page findings in text output.
25    pub verbose: bool,
26    /// Output in JSON format.
27    pub json: bool,
28    /// Override the auto-detected page size.
29    pub page_size: Option<u32>,
30    /// Path to MySQL keyring file for decrypting encrypted tablespaces.
31    pub keyring: Option<String>,
32    /// Use memory-mapped I/O for file access.
33    pub mmap: bool,
34    /// Path to redo log file for LSN continuity check.
35    pub redo: Option<String>,
36    /// Paths for backup chain verification.
37    pub chain: Vec<String>,
38}
39
40/// Combined JSON output for verify with redo and/or chain.
41#[derive(Debug, Serialize)]
42struct FullVerifyReport {
43    #[serde(flatten)]
44    structural: crate::innodb::verify::VerifyReport,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    redo: Option<crate::innodb::verify::RedoVerifyResult>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    chain: Option<ChainReport>,
49}
50
51/// Run structural verification on a tablespace file.
52pub fn execute(opts: &VerifyOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
53    // Handle --chain mode: verify backup chain across multiple files
54    if !opts.chain.is_empty() {
55        return execute_chain(opts, writer);
56    }
57
58    let mut ts = crate::cli::open_tablespace(&opts.file, opts.page_size, opts.mmap)?;
59
60    if let Some(ref keyring_path) = opts.keyring {
61        crate::cli::setup_decryption(&mut ts, keyring_path)?;
62    }
63
64    let page_size = ts.page_size();
65    let page_count = ts.page_count();
66
67    // Read all pages into a flat buffer
68    let mut all_pages = Vec::with_capacity(page_size as usize * page_count as usize);
69    for i in 0..page_count {
70        let page = ts.read_page(i)?;
71        all_pages.extend_from_slice(&page);
72    }
73
74    // Get space_id from page 0
75    let space_id = if all_pages.len() >= page_size as usize {
76        FilHeader::parse(&all_pages[..page_size as usize])
77            .map(|h| h.space_id)
78            .unwrap_or(0)
79    } else {
80        0
81    };
82
83    let config = VerifyConfig::default();
84    let report = verify_tablespace(&all_pages, page_size, space_id, &opts.file, &config);
85
86    // Redo log continuity check
87    let redo_result = if let Some(ref redo_path) = opts.redo {
88        Some(crate::innodb::verify::verify_redo_continuity(
89            redo_path, &all_pages, page_size,
90        )?)
91    } else {
92        None
93    };
94
95    let mut overall_passed = report.passed;
96    if let Some(ref redo) = redo_result {
97        if !redo.covers_tablespace {
98            overall_passed = false;
99        }
100    }
101
102    if opts.json {
103        let full = FullVerifyReport {
104            structural: report,
105            redo: redo_result,
106            chain: None,
107        };
108        let json = serde_json::to_string_pretty(&full)
109            .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
110        wprintln!(writer, "{}", json)?;
111    } else {
112        // Text output
113        wprintln!(writer, "Structural Verification: {}", opts.file)?;
114        wprintln!(writer, "  Page size:   {} bytes", report.page_size)?;
115        wprintln!(writer, "  Total pages: {}", report.total_pages)?;
116        wprintln!(writer)?;
117
118        // Summary table
119        wprintln!(
120            writer,
121            "  {:<30} {:>8} {:>8} {:>8}",
122            "Check",
123            "Checked",
124            "Issues",
125            "Status"
126        )?;
127        wprintln!(writer, "  {}", "-".repeat(60))?;
128        for s in &report.summary {
129            let status = if s.passed {
130                "PASS".green().to_string()
131            } else {
132                "FAIL".red().to_string()
133            };
134            wprintln!(
135                writer,
136                "  {:<30} {:>8} {:>8} {:>8}",
137                s.kind,
138                s.pages_checked,
139                s.issues_found,
140                status
141            )?;
142        }
143        wprintln!(writer)?;
144
145        if opts.verbose && !report.findings.is_empty() {
146            wprintln!(writer, "  Findings:")?;
147            for f in &report.findings {
148                wprintln!(writer, "    Page {:>4}: {}", f.page_number, f.message)?;
149            }
150            wprintln!(writer)?;
151        }
152
153        // Redo log continuity
154        if let Some(ref redo) = redo_result {
155            wprintln!(writer, "  Redo Log Continuity:")?;
156            wprintln!(writer, "    Redo file:        {}", redo.redo_file)?;
157            wprintln!(writer, "    Checkpoint LSN:   {}", redo.checkpoint_lsn)?;
158            wprintln!(writer, "    Tablespace max:   {}", redo.tablespace_max_lsn)?;
159            let redo_status = if redo.covers_tablespace {
160                "PASS".green().to_string()
161            } else {
162                format!("{} (gap: {} bytes)", "FAIL".red(), redo.lsn_gap)
163            };
164            wprintln!(writer, "    Covers tablespace: {}", redo_status)?;
165            wprintln!(writer)?;
166        }
167
168        let overall = if overall_passed {
169            "PASS".green().to_string()
170        } else {
171            "FAIL".red().to_string()
172        };
173        wprintln!(writer, "  Overall: {}", overall)?;
174    }
175
176    if !overall_passed {
177        return Err(IdbError::Argument("Verification failed".to_string()));
178    }
179
180    Ok(())
181}
182
183/// Execute backup chain verification mode.
184fn execute_chain(opts: &VerifyOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
185    if opts.chain.len() < 2 {
186        return Err(IdbError::Argument(
187            "--chain requires at least 2 files".to_string(),
188        ));
189    }
190
191    let mut files_info = Vec::new();
192
193    for path in &opts.chain {
194        let mut ts = crate::cli::open_tablespace(path, opts.page_size, opts.mmap)?;
195        if let Some(ref keyring_path) = opts.keyring {
196            crate::cli::setup_decryption(&mut ts, keyring_path)?;
197        }
198
199        let page_size = ts.page_size();
200        let page_count = ts.page_count();
201        let mut all_pages = Vec::with_capacity(page_size as usize * page_count as usize);
202        for i in 0..page_count {
203            let page = ts.read_page(i)?;
204            all_pages.extend_from_slice(&page);
205        }
206
207        files_info.push(extract_chain_file_info(&all_pages, page_size, path));
208    }
209
210    let chain_report = verify_backup_chain(files_info);
211
212    if opts.json {
213        let json = serde_json::to_string_pretty(&chain_report)
214            .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
215        wprintln!(writer, "{}", json)?;
216    } else {
217        wprintln!(writer, "Backup Chain Verification")?;
218        wprintln!(writer)?;
219
220        wprintln!(
221            writer,
222            "  {:<40} {:>12} {:>16} {:>16}",
223            "File",
224            "Space ID",
225            "Min LSN",
226            "Max LSN"
227        )?;
228        wprintln!(writer, "  {}", "-".repeat(88))?;
229        for f in &chain_report.files {
230            wprintln!(
231                writer,
232                "  {:<40} {:>12} {:>16} {:>16}",
233                f.file,
234                f.space_id,
235                f.min_lsn,
236                f.max_lsn
237            )?;
238        }
239        wprintln!(writer)?;
240
241        if !chain_report.gaps.is_empty() {
242            wprintln!(writer, "  {} detected:", "Gaps".red())?;
243            for gap in &chain_report.gaps {
244                wprintln!(
245                    writer,
246                    "    {} (max LSN {}) -> {} (min LSN {}): gap of {} bytes",
247                    gap.from_file,
248                    gap.from_max_lsn,
249                    gap.to_file,
250                    gap.to_min_lsn,
251                    gap.gap_size
252                )?;
253            }
254            wprintln!(writer)?;
255        }
256
257        let space_status = if chain_report.consistent_space_id {
258            "PASS".green().to_string()
259        } else {
260            "FAIL (mixed space IDs)".red().to_string()
261        };
262        wprintln!(writer, "  Space ID consistency: {}", space_status)?;
263
264        let chain_status = if chain_report.contiguous {
265            "PASS".green().to_string()
266        } else {
267            "FAIL".red().to_string()
268        };
269        wprintln!(writer, "  Chain continuity:    {}", chain_status)?;
270    }
271
272    if !chain_report.contiguous || !chain_report.consistent_space_id {
273        return Err(IdbError::Argument("Chain verification failed".to_string()));
274    }
275
276    Ok(())
277}