use std::io::Write;
use colored::Colorize;
use serde::Serialize;
use crate::cli::wprintln;
use crate::innodb::page::FilHeader;
use crate::innodb::verify::{
extract_chain_file_info, verify_backup_chain, verify_tablespace, ChainReport, VerifyConfig,
};
use crate::IdbError;
pub struct VerifyOptions {
pub file: String,
pub verbose: bool,
pub json: bool,
pub page_size: Option<u32>,
pub keyring: Option<String>,
pub mmap: bool,
pub redo: Option<String>,
pub chain: Vec<String>,
}
#[derive(Debug, Serialize)]
struct FullVerifyReport {
#[serde(flatten)]
structural: crate::innodb::verify::VerifyReport,
#[serde(skip_serializing_if = "Option::is_none")]
redo: Option<crate::innodb::verify::RedoVerifyResult>,
#[serde(skip_serializing_if = "Option::is_none")]
chain: Option<ChainReport>,
}
pub fn execute(opts: &VerifyOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
if !opts.chain.is_empty() {
return execute_chain(opts, writer);
}
let mut ts = crate::cli::open_tablespace(&opts.file, opts.page_size, opts.mmap)?;
if let Some(ref keyring_path) = opts.keyring {
crate::cli::setup_decryption(&mut ts, keyring_path)?;
}
let page_size = ts.page_size();
let page_count = ts.page_count();
let mut all_pages = Vec::with_capacity(page_size as usize * page_count as usize);
for i in 0..page_count {
let page = ts.read_page(i)?;
all_pages.extend_from_slice(&page);
}
let space_id = if all_pages.len() >= page_size as usize {
FilHeader::parse(&all_pages[..page_size as usize])
.map(|h| h.space_id)
.unwrap_or(0)
} else {
0
};
let config = VerifyConfig::default();
let report = verify_tablespace(&all_pages, page_size, space_id, &opts.file, &config);
let redo_result = if let Some(ref redo_path) = opts.redo {
Some(crate::innodb::verify::verify_redo_continuity(
redo_path, &all_pages, page_size,
)?)
} else {
None
};
let mut overall_passed = report.passed;
if let Some(ref redo) = redo_result {
if !redo.covers_tablespace {
overall_passed = false;
}
}
if opts.json {
let full = FullVerifyReport {
structural: report,
redo: redo_result,
chain: None,
};
let json = serde_json::to_string_pretty(&full)
.map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
wprintln!(writer, "{}", json)?;
} else {
wprintln!(writer, "Structural Verification: {}", opts.file)?;
wprintln!(writer, " Page size: {} bytes", report.page_size)?;
wprintln!(writer, " Total pages: {}", report.total_pages)?;
wprintln!(writer)?;
wprintln!(
writer,
" {:<30} {:>8} {:>8} {:>8}",
"Check",
"Checked",
"Issues",
"Status"
)?;
wprintln!(writer, " {}", "-".repeat(60))?;
for s in &report.summary {
let status = if s.passed {
"PASS".green().to_string()
} else {
"FAIL".red().to_string()
};
wprintln!(
writer,
" {:<30} {:>8} {:>8} {:>8}",
s.kind,
s.pages_checked,
s.issues_found,
status
)?;
}
wprintln!(writer)?;
if opts.verbose && !report.findings.is_empty() {
wprintln!(writer, " Findings:")?;
for f in &report.findings {
wprintln!(writer, " Page {:>4}: {}", f.page_number, f.message)?;
}
wprintln!(writer)?;
}
if let Some(ref redo) = redo_result {
wprintln!(writer, " Redo Log Continuity:")?;
wprintln!(writer, " Redo file: {}", redo.redo_file)?;
wprintln!(writer, " Checkpoint LSN: {}", redo.checkpoint_lsn)?;
wprintln!(writer, " Tablespace max: {}", redo.tablespace_max_lsn)?;
let redo_status = if redo.covers_tablespace {
"PASS".green().to_string()
} else {
format!("{} (gap: {} bytes)", "FAIL".red(), redo.lsn_gap)
};
wprintln!(writer, " Covers tablespace: {}", redo_status)?;
wprintln!(writer)?;
}
let overall = if overall_passed {
"PASS".green().to_string()
} else {
"FAIL".red().to_string()
};
wprintln!(writer, " Overall: {}", overall)?;
}
if !overall_passed {
return Err(IdbError::Argument("Verification failed".to_string()));
}
Ok(())
}
fn execute_chain(opts: &VerifyOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
if opts.chain.len() < 2 {
return Err(IdbError::Argument(
"--chain requires at least 2 files".to_string(),
));
}
let mut files_info = Vec::new();
for path in &opts.chain {
let mut ts = crate::cli::open_tablespace(path, opts.page_size, opts.mmap)?;
if let Some(ref keyring_path) = opts.keyring {
crate::cli::setup_decryption(&mut ts, keyring_path)?;
}
let page_size = ts.page_size();
let page_count = ts.page_count();
let mut all_pages = Vec::with_capacity(page_size as usize * page_count as usize);
for i in 0..page_count {
let page = ts.read_page(i)?;
all_pages.extend_from_slice(&page);
}
files_info.push(extract_chain_file_info(&all_pages, page_size, path));
}
let chain_report = verify_backup_chain(files_info);
if opts.json {
let json = serde_json::to_string_pretty(&chain_report)
.map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
wprintln!(writer, "{}", json)?;
} else {
wprintln!(writer, "Backup Chain Verification")?;
wprintln!(writer)?;
wprintln!(
writer,
" {:<40} {:>12} {:>16} {:>16}",
"File",
"Space ID",
"Min LSN",
"Max LSN"
)?;
wprintln!(writer, " {}", "-".repeat(88))?;
for f in &chain_report.files {
wprintln!(
writer,
" {:<40} {:>12} {:>16} {:>16}",
f.file,
f.space_id,
f.min_lsn,
f.max_lsn
)?;
}
wprintln!(writer)?;
if !chain_report.gaps.is_empty() {
wprintln!(writer, " {} detected:", "Gaps".red())?;
for gap in &chain_report.gaps {
wprintln!(
writer,
" {} (max LSN {}) -> {} (min LSN {}): gap of {} bytes",
gap.from_file,
gap.from_max_lsn,
gap.to_file,
gap.to_min_lsn,
gap.gap_size
)?;
}
wprintln!(writer)?;
}
let space_status = if chain_report.consistent_space_id {
"PASS".green().to_string()
} else {
"FAIL (mixed space IDs)".red().to_string()
};
wprintln!(writer, " Space ID consistency: {}", space_status)?;
let chain_status = if chain_report.contiguous {
"PASS".green().to_string()
} else {
"FAIL".red().to_string()
};
wprintln!(writer, " Chain continuity: {}", chain_status)?;
}
if !chain_report.contiguous || !chain_report.consistent_space_id {
return Err(IdbError::Argument("Chain verification failed".to_string()));
}
Ok(())
}