1use 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
20pub struct VerifyOptions {
22 pub file: String,
24 pub verbose: bool,
26 pub json: bool,
28 pub page_size: Option<u32>,
30 pub keyring: Option<String>,
32 pub mmap: bool,
34 pub redo: Option<String>,
36 pub chain: Vec<String>,
38 pub backup_meta: Option<String>,
40}
41
42#[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
55pub fn execute(opts: &VerifyOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
57 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 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 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 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 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 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 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 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 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
252fn 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}