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    /// Path to XtraBackup checkpoint file for LSN cross-reference.
39    pub backup_meta: Option<String>,
40}
41
42/// Combined JSON output for verify with redo and/or chain.
43#[derive(Debug, Serialize)]
44struct FullVerifyReport {
45    #[serde(flatten)]
46    structural: crate::innodb::verify::VerifyReport,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    redo: Option<crate::innodb::verify::RedoVerifyResult>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    chain: Option<ChainReport>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    backup_meta: Option<crate::innodb::verify::BackupMetaVerifyResult>,
53}
54
55/// Run structural verification on a tablespace file.
56pub fn execute(opts: &VerifyOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
57    // Handle --chain mode: verify backup chain across multiple files
58    if !opts.chain.is_empty() {
59        return execute_chain(opts, writer);
60    }
61
62    let mut ts = crate::cli::open_tablespace(&opts.file, opts.page_size, opts.mmap)?;
63
64    if let Some(ref keyring_path) = opts.keyring {
65        crate::cli::setup_decryption(&mut ts, keyring_path)?;
66    }
67
68    let page_size = ts.page_size();
69    let page_count = ts.page_count();
70
71    // Read all pages into a flat buffer
72    let mut all_pages = Vec::with_capacity(page_size as usize * page_count as usize);
73    for i in 0..page_count {
74        let page = ts.read_page(i)?;
75        all_pages.extend_from_slice(&page);
76    }
77
78    // Get space_id from page 0
79    let space_id = if all_pages.len() >= page_size as usize {
80        FilHeader::parse(&all_pages[..page_size as usize])
81            .map(|h| h.space_id)
82            .unwrap_or(0)
83    } else {
84        0
85    };
86
87    let config = VerifyConfig::default();
88    let report = verify_tablespace(&all_pages, page_size, space_id, &opts.file, &config);
89
90    // Redo log continuity check
91    let redo_result = if let Some(ref redo_path) = opts.redo {
92        Some(crate::innodb::verify::verify_redo_continuity(
93            redo_path, &all_pages, page_size,
94        )?)
95    } else {
96        None
97    };
98
99    // Backup metadata cross-reference
100    let backup_meta_result = if let Some(ref meta_path) = opts.backup_meta {
101        Some(crate::innodb::verify::verify_backup_meta(
102            meta_path, &all_pages, page_size,
103        )?)
104    } else {
105        None
106    };
107
108    let mut overall_passed = report.passed;
109    if let Some(ref redo) = redo_result {
110        if !redo.covers_tablespace {
111            overall_passed = false;
112        }
113    }
114    if let Some(ref meta) = backup_meta_result {
115        if !meta.passed {
116            overall_passed = false;
117        }
118    }
119
120    if opts.json {
121        let full = FullVerifyReport {
122            structural: report,
123            redo: redo_result,
124            chain: None,
125            backup_meta: backup_meta_result,
126        };
127        let json = serde_json::to_string_pretty(&full)
128            .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
129        wprintln!(writer, "{}", json)?;
130    } else {
131        // Text output
132        wprintln!(writer, "Structural Verification: {}", opts.file)?;
133        wprintln!(writer, "  Page size:   {} bytes", report.page_size)?;
134        wprintln!(writer, "  Total pages: {}", report.total_pages)?;
135        wprintln!(writer)?;
136
137        // Summary table
138        wprintln!(
139            writer,
140            "  {:<30} {:>8} {:>8} {:>8}",
141            "Check",
142            "Checked",
143            "Issues",
144            "Status"
145        )?;
146        wprintln!(writer, "  {}", "-".repeat(60))?;
147        for s in &report.summary {
148            let status = if s.passed {
149                "PASS".green().to_string()
150            } else {
151                "FAIL".red().to_string()
152            };
153            wprintln!(
154                writer,
155                "  {:<30} {:>8} {:>8} {:>8}",
156                s.kind,
157                s.pages_checked,
158                s.issues_found,
159                status
160            )?;
161        }
162        wprintln!(writer)?;
163
164        if opts.verbose && !report.findings.is_empty() {
165            wprintln!(writer, "  Findings:")?;
166            for f in &report.findings {
167                wprintln!(writer, "    Page {:>4}: {}", f.page_number, f.message)?;
168            }
169            wprintln!(writer)?;
170        }
171
172        // Redo log continuity
173        if let Some(ref redo) = redo_result {
174            wprintln!(writer, "  Redo Log Continuity:")?;
175            wprintln!(writer, "    Redo file:        {}", redo.redo_file)?;
176            wprintln!(writer, "    Checkpoint LSN:   {}", redo.checkpoint_lsn)?;
177            wprintln!(writer, "    Tablespace max:   {}", redo.tablespace_max_lsn)?;
178            let redo_status = if redo.covers_tablespace {
179                "PASS".green().to_string()
180            } else {
181                format!("{} (gap: {} bytes)", "FAIL".red(), redo.lsn_gap)
182            };
183            wprintln!(writer, "    Covers tablespace: {}", redo_status)?;
184            wprintln!(writer)?;
185        }
186
187        // Backup metadata cross-reference
188        if let Some(ref meta) = backup_meta_result {
189            wprintln!(writer, "  Backup Metadata Cross-Reference:")?;
190            wprintln!(writer, "    Checkpoint file: {}", meta.checkpoint_file)?;
191            wprintln!(writer, "    Backup type:     {}", meta.backup_type)?;
192            wprintln!(
193                writer,
194                "    LSN window:      {} .. {}",
195                meta.from_lsn,
196                meta.to_lsn
197            )?;
198            wprintln!(
199                writer,
200                "    Tablespace LSNs: {} .. {}",
201                meta.tablespace_min_lsn,
202                meta.tablespace_max_lsn
203            )?;
204            let meta_status = if meta.passed {
205                "PASS".green().to_string()
206            } else {
207                let total_issues = meta.pages_before_window.len() + meta.pages_after_window.len();
208                format!("{} ({} pages outside window)", "FAIL".red(), total_issues)
209            };
210            wprintln!(writer, "    Status:          {}", meta_status)?;
211
212            if opts.verbose {
213                for p in &meta.pages_before_window {
214                    wprintln!(
215                        writer,
216                        "      Page {:>4} ({}): LSN {} < from_lsn {}",
217                        p.page_number,
218                        p.page_type,
219                        p.lsn,
220                        meta.from_lsn
221                    )?;
222                }
223                for p in &meta.pages_after_window {
224                    wprintln!(
225                        writer,
226                        "      Page {:>4} ({}): LSN {} > to_lsn {}",
227                        p.page_number,
228                        p.page_type,
229                        p.lsn,
230                        meta.to_lsn
231                    )?;
232                }
233            }
234            wprintln!(writer)?;
235        }
236
237        let overall = if overall_passed {
238            "PASS".green().to_string()
239        } else {
240            "FAIL".red().to_string()
241        };
242        wprintln!(writer, "  Overall: {}", overall)?;
243    }
244
245    if !overall_passed {
246        return Err(IdbError::Argument("Verification failed".to_string()));
247    }
248
249    Ok(())
250}
251
252/// Execute backup chain verification mode.
253fn execute_chain(opts: &VerifyOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
254    if opts.chain.len() < 2 {
255        return Err(IdbError::Argument(
256            "--chain requires at least 2 files".to_string(),
257        ));
258    }
259
260    let mut files_info = Vec::new();
261
262    for path in &opts.chain {
263        let mut ts = crate::cli::open_tablespace(path, opts.page_size, opts.mmap)?;
264        if let Some(ref keyring_path) = opts.keyring {
265            crate::cli::setup_decryption(&mut ts, keyring_path)?;
266        }
267
268        let page_size = ts.page_size();
269        let page_count = ts.page_count();
270        let mut all_pages = Vec::with_capacity(page_size as usize * page_count as usize);
271        for i in 0..page_count {
272            let page = ts.read_page(i)?;
273            all_pages.extend_from_slice(&page);
274        }
275
276        files_info.push(extract_chain_file_info(&all_pages, page_size, path));
277    }
278
279    let chain_report = verify_backup_chain(files_info);
280
281    if opts.json {
282        let json = serde_json::to_string_pretty(&chain_report)
283            .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
284        wprintln!(writer, "{}", json)?;
285    } else {
286        wprintln!(writer, "Backup Chain Verification")?;
287        wprintln!(writer)?;
288
289        wprintln!(
290            writer,
291            "  {:<40} {:>12} {:>16} {:>16}",
292            "File",
293            "Space ID",
294            "Min LSN",
295            "Max LSN"
296        )?;
297        wprintln!(writer, "  {}", "-".repeat(88))?;
298        for f in &chain_report.files {
299            wprintln!(
300                writer,
301                "  {:<40} {:>12} {:>16} {:>16}",
302                f.file,
303                f.space_id,
304                f.min_lsn,
305                f.max_lsn
306            )?;
307        }
308        wprintln!(writer)?;
309
310        if !chain_report.gaps.is_empty() {
311            wprintln!(writer, "  {} detected:", "Gaps".red())?;
312            for gap in &chain_report.gaps {
313                wprintln!(
314                    writer,
315                    "    {} (max LSN {}) -> {} (min LSN {}): gap of {} bytes",
316                    gap.from_file,
317                    gap.from_max_lsn,
318                    gap.to_file,
319                    gap.to_min_lsn,
320                    gap.gap_size
321                )?;
322            }
323            wprintln!(writer)?;
324        }
325
326        let space_status = if chain_report.consistent_space_id {
327            "PASS".green().to_string()
328        } else {
329            "FAIL (mixed space IDs)".red().to_string()
330        };
331        wprintln!(writer, "  Space ID consistency: {}", space_status)?;
332
333        let chain_status = if chain_report.contiguous {
334            "PASS".green().to_string()
335        } else {
336            "FAIL".red().to_string()
337        };
338        wprintln!(writer, "  Chain continuity:    {}", chain_status)?;
339    }
340
341    if !chain_report.contiguous || !chain_report.consistent_space_id {
342        return Err(IdbError::Argument("Chain verification failed".to_string()));
343    }
344
345    Ok(())
346}