1use std::io::Write;
2use std::path::Path;
3use std::sync::Arc;
4
5use colored::Colorize;
6use rayon::prelude::*;
7use serde::Serialize;
8
9use crate::cli::{create_progress_bar, wprintln};
10use crate::innodb::checksum::{validate_checksum, validate_lsn, ChecksumAlgorithm};
11use crate::innodb::write;
12use crate::util::audit::AuditLogger;
13use crate::util::fs::find_tablespace_files;
14use crate::IdbError;
15
16pub struct RepairOptions {
18 pub file: Option<String>,
20 pub batch: Option<String>,
22 pub page: Option<u64>,
24 pub algorithm: String,
26 pub no_backup: bool,
28 pub dry_run: bool,
30 pub verbose: bool,
32 pub json: bool,
34 pub page_size: Option<u32>,
36 pub keyring: Option<String>,
38 pub mmap: bool,
40 pub audit_logger: Option<Arc<AuditLogger>>,
42}
43
44#[derive(Serialize)]
45struct RepairReport {
46 file: String,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 backup_path: Option<String>,
49 algorithm: String,
50 dry_run: bool,
51 total_pages: u64,
52 already_valid: u64,
53 repaired: u64,
54 empty: u64,
55 pages: Vec<PageRepairInfo>,
56}
57
58#[derive(Serialize)]
59struct PageRepairInfo {
60 page_number: u64,
61 action: String,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 old_checksum: Option<u32>,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 new_checksum: Option<u32>,
66}
67
68fn parse_algorithm(s: &str) -> Result<Option<ChecksumAlgorithm>, IdbError> {
70 match s.to_lowercase().as_str() {
71 "auto" => Ok(None),
72 "crc32c" | "crc32" => Ok(Some(ChecksumAlgorithm::Crc32c)),
73 "innodb" | "legacy" => Ok(Some(ChecksumAlgorithm::InnoDB)),
74 "full_crc32" | "mariadb" => Ok(Some(ChecksumAlgorithm::MariaDbFullCrc32)),
75 _ => Err(IdbError::Argument(format!(
76 "Unknown checksum algorithm '{}'. Use: auto, crc32c, innodb, full_crc32",
77 s
78 ))),
79 }
80}
81
82fn algo_name(algorithm: ChecksumAlgorithm) -> &'static str {
83 match algorithm {
84 ChecksumAlgorithm::Crc32c => "crc32c",
85 ChecksumAlgorithm::InnoDB => "innodb",
86 ChecksumAlgorithm::MariaDbFullCrc32 => "full_crc32",
87 ChecksumAlgorithm::None => "none",
88 }
89}
90
91pub fn execute(opts: &RepairOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
93 match (&opts.file, &opts.batch) {
94 (Some(_), Some(_)) => {
95 return Err(IdbError::Argument(
96 "--file and --batch are mutually exclusive".to_string(),
97 ));
98 }
99 (None, None) => {
100 return Err(IdbError::Argument(
101 "Either --file or --batch is required".to_string(),
102 ));
103 }
104 (_, Some(_)) if opts.page.is_some() => {
105 return Err(IdbError::Argument(
106 "--page cannot be used with --batch".to_string(),
107 ));
108 }
109 _ => {}
110 }
111
112 if opts.batch.is_some() {
113 execute_batch(opts, writer)
114 } else {
115 execute_single(opts, writer)
116 }
117}
118
119fn execute_single(opts: &RepairOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
121 let file = opts.file.as_ref().unwrap();
122
123 let mut ts = crate::cli::open_tablespace(file, opts.page_size, opts.mmap)?;
125
126 if let Some(ref keyring_path) = opts.keyring {
127 crate::cli::setup_decryption(&mut ts, keyring_path)?;
128 }
129
130 let page_size = ts.page_size();
131 let page_count = ts.page_count();
132 let vendor_info = ts.vendor_info().clone();
133
134 let explicit_algo = parse_algorithm(&opts.algorithm)?;
136 let algorithm = match explicit_algo {
137 Some(algo) => algo,
138 None => {
139 let page0 = ts.read_page(0)?;
141 write::detect_algorithm(&page0, page_size, Some(&vendor_info))
142 }
143 };
144
145 let backup_path = if !opts.no_backup && !opts.dry_run {
147 let path = write::create_backup(file)?;
148 if !opts.json {
149 wprintln!(writer, "Backup created: {}", path.display())?;
150 }
151 if let Some(ref logger) = opts.audit_logger {
152 let _ = logger.log_backup(file, &path.display().to_string());
153 }
154 Some(path)
155 } else {
156 None
157 };
158
159 let (start_page, end_page) = match opts.page {
161 Some(p) => {
162 if p >= page_count {
163 return Err(IdbError::Argument(format!(
164 "Page {} out of range (tablespace has {} pages)",
165 p, page_count
166 )));
167 }
168 (p, p + 1)
169 }
170 None => (0, page_count),
171 };
172 let scan_count = end_page - start_page;
173
174 let pb = if !opts.json && scan_count > 1 && !opts.dry_run {
175 Some(create_progress_bar(scan_count, "pages"))
176 } else {
177 None
178 };
179
180 let aname = algo_name(algorithm);
181
182 if !opts.json && !opts.dry_run {
183 wprintln!(writer, "Repairing {} using {} algorithm...", file, aname)?;
184 } else if !opts.json && opts.dry_run {
185 wprintln!(
186 writer,
187 "Dry run: scanning {} using {} algorithm...",
188 file,
189 aname
190 )?;
191 }
192
193 let mut already_valid = 0u64;
194 let mut repaired = 0u64;
195 let mut empty = 0u64;
196 let mut page_details: Vec<PageRepairInfo> = Vec::new();
197
198 for page_num in start_page..end_page {
199 let mut page_data = write::read_page_raw(file, page_num, page_size)?;
200
201 if page_data.iter().all(|&b| b == 0) {
203 empty += 1;
204 if let Some(ref pb) = pb {
205 pb.inc(1);
206 }
207 continue;
208 }
209
210 let result = validate_checksum(&page_data, page_size, Some(&vendor_info));
212 let lsn_ok = validate_lsn(&page_data, page_size);
213
214 if result.valid && lsn_ok {
215 already_valid += 1;
216 if let Some(ref pb) = pb {
217 pb.inc(1);
218 }
219 continue;
220 }
221
222 let (old_checksum, new_checksum) =
224 write::fix_page_checksum(&mut page_data, page_size, algorithm);
225
226 if !opts.dry_run {
227 write::write_page(file, page_num, page_size, &page_data)?;
228 if let Some(ref logger) = opts.audit_logger {
229 let _ = logger.log_page_write(
230 file,
231 page_num,
232 "repair",
233 Some(old_checksum),
234 Some(new_checksum),
235 );
236 }
237 }
238
239 repaired += 1;
240
241 if opts.verbose && !opts.json {
242 let action = if opts.dry_run {
243 "would repair"
244 } else {
245 "repaired"
246 };
247 let mut detail = format!(
248 "Page {:>4}: {} (0x{:08X} -> 0x{:08X})",
249 page_num, action, old_checksum, new_checksum
250 );
251 if !result.valid {
252 detail.push_str(" [checksum mismatch]");
253 }
254 if !lsn_ok {
255 detail.push_str(" [LSN mismatch]");
256 }
257 wprintln!(writer, "{}", detail)?;
258 }
259
260 page_details.push(PageRepairInfo {
261 page_number: page_num,
262 action: if opts.dry_run {
263 "would_repair".to_string()
264 } else {
265 "repaired".to_string()
266 },
267 old_checksum: Some(old_checksum),
268 new_checksum: Some(new_checksum),
269 });
270
271 if let Some(ref pb) = pb {
272 pb.inc(1);
273 }
274 }
275
276 if let Some(pb) = pb {
277 pb.finish_and_clear();
278 }
279
280 if opts.json {
281 let report = RepairReport {
282 file: file.clone(),
283 backup_path: backup_path.map(|p| p.display().to_string()),
284 algorithm: aname.to_string(),
285 dry_run: opts.dry_run,
286 total_pages: scan_count,
287 already_valid,
288 repaired,
289 empty,
290 pages: page_details,
291 };
292 let json = serde_json::to_string_pretty(&report)
293 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
294 wprintln!(writer, "{}", json)?;
295 } else {
296 wprintln!(writer)?;
297 wprintln!(writer, "Repair Summary:")?;
298 wprintln!(writer, " Algorithm: {}", aname)?;
299 wprintln!(writer, " Total pages: {:>4}", scan_count)?;
300 wprintln!(writer, " Already valid: {:>4}", already_valid)?;
301 if repaired > 0 {
302 let label = if opts.dry_run {
303 "Would repair"
304 } else {
305 "Repaired"
306 };
307 wprintln!(
308 writer,
309 " {}: {:>4}",
310 label,
311 format!("{}", repaired).green()
312 )?;
313 } else {
314 wprintln!(writer, " Repaired: {:>4}", repaired)?;
315 }
316 wprintln!(writer, " Empty: {:>4}", empty)?;
317 }
318
319 Ok(())
320}
321
322#[derive(Serialize)]
327struct BatchRepairReport {
328 datadir: String,
329 dry_run: bool,
330 algorithm: String,
331 files: Vec<FileRepairResult>,
332 summary: BatchRepairSummary,
333}
334
335#[derive(Serialize, Clone)]
336struct FileRepairResult {
337 file: String,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 backup_path: Option<String>,
340 total_pages: u64,
341 already_valid: u64,
342 repaired: u64,
343 empty: u64,
344 #[serde(skip_serializing_if = "Vec::is_empty")]
345 errors: Vec<String>,
346}
347
348#[derive(Serialize)]
349struct BatchRepairSummary {
350 total_files: usize,
351 files_repaired: usize,
352 files_already_valid: usize,
353 files_error: usize,
354 total_pages_scanned: u64,
355 total_pages_repaired: u64,
356}
357
358#[allow(clippy::too_many_arguments)]
360fn repair_file(
361 path: &Path,
362 datadir: &Path,
363 explicit_algo: Option<ChecksumAlgorithm>,
364 no_backup: bool,
365 dry_run: bool,
366 page_size_override: Option<u32>,
367 keyring: &Option<String>,
368 use_mmap: bool,
369 audit_logger: &Option<Arc<AuditLogger>>,
370) -> FileRepairResult {
371 let display = path.strip_prefix(datadir).unwrap_or(path);
372 let display_str = display.display().to_string();
373 let path_str = path.to_string_lossy().to_string();
374
375 let mut ts = match crate::cli::open_tablespace(&path_str, page_size_override, use_mmap) {
376 Ok(t) => t,
377 Err(e) => {
378 return FileRepairResult {
379 file: display_str,
380 backup_path: None,
381 total_pages: 0,
382 already_valid: 0,
383 repaired: 0,
384 empty: 0,
385 errors: vec![e.to_string()],
386 };
387 }
388 };
389
390 if let Some(ref kp) = keyring {
391 let _ = crate::cli::setup_decryption(&mut ts, kp);
392 }
393
394 let page_size = ts.page_size();
395 let page_count = ts.page_count();
396 let vendor_info = ts.vendor_info().clone();
397
398 let algorithm = match explicit_algo {
400 Some(algo) => algo,
401 None => {
402 let page0 = match ts.read_page(0) {
403 Ok(p) => p,
404 Err(e) => {
405 return FileRepairResult {
406 file: display_str,
407 backup_path: None,
408 total_pages: page_count,
409 already_valid: 0,
410 repaired: 0,
411 empty: 0,
412 errors: vec![e.to_string()],
413 };
414 }
415 };
416 write::detect_algorithm(&page0, page_size, Some(&vendor_info))
417 }
418 };
419
420 let backup_path = if !no_backup && !dry_run {
422 match write::create_backup(&path_str) {
423 Ok(bp) => {
424 if let Some(ref logger) = audit_logger {
425 let _ = logger.log_backup(&path_str, &bp.display().to_string());
426 }
427 Some(bp.display().to_string())
428 }
429 Err(e) => {
430 return FileRepairResult {
431 file: display_str,
432 backup_path: None,
433 total_pages: page_count,
434 already_valid: 0,
435 repaired: 0,
436 empty: 0,
437 errors: vec![format!("Backup failed: {}", e)],
438 };
439 }
440 }
441 } else {
442 None
443 };
444
445 let mut already_valid = 0u64;
446 let mut repaired = 0u64;
447 let mut empty = 0u64;
448 let mut errors = Vec::new();
449
450 for page_num in 0..page_count {
451 let mut page_data = match write::read_page_raw(&path_str, page_num, page_size) {
452 Ok(d) => d,
453 Err(e) => {
454 errors.push(format!("Page {}: {}", page_num, e));
455 continue;
456 }
457 };
458
459 if page_data.iter().all(|&b| b == 0) {
460 empty += 1;
461 continue;
462 }
463
464 let result = validate_checksum(&page_data, page_size, Some(&vendor_info));
465 let lsn_ok = validate_lsn(&page_data, page_size);
466
467 if result.valid && lsn_ok {
468 already_valid += 1;
469 continue;
470 }
471
472 let (old_checksum, new_checksum) =
473 write::fix_page_checksum(&mut page_data, page_size, algorithm);
474
475 if !dry_run {
476 if let Err(e) = write::write_page(&path_str, page_num, page_size, &page_data) {
477 errors.push(format!("Page {}: write failed: {}", page_num, e));
478 continue;
479 }
480 if let Some(ref logger) = audit_logger {
481 let _ = logger.log_page_write(
482 &path_str,
483 page_num,
484 "batch_repair",
485 Some(old_checksum),
486 Some(new_checksum),
487 );
488 }
489 }
490
491 repaired += 1;
492 }
493
494 FileRepairResult {
495 file: display_str,
496 backup_path,
497 total_pages: page_count,
498 already_valid,
499 repaired,
500 empty,
501 errors,
502 }
503}
504
505fn execute_batch(opts: &RepairOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
507 let datadir_str = opts.batch.as_ref().unwrap();
508 let datadir = Path::new(datadir_str);
509
510 if !datadir.is_dir() {
511 return Err(IdbError::Argument(format!(
512 "Data directory does not exist: {}",
513 datadir_str
514 )));
515 }
516
517 let ibd_files = find_tablespace_files(datadir, &["ibd"], None)?;
518
519 if ibd_files.is_empty() {
520 if opts.json {
521 let report = BatchRepairReport {
522 datadir: datadir_str.clone(),
523 dry_run: opts.dry_run,
524 algorithm: opts.algorithm.clone(),
525 files: Vec::new(),
526 summary: BatchRepairSummary {
527 total_files: 0,
528 files_repaired: 0,
529 files_already_valid: 0,
530 files_error: 0,
531 total_pages_scanned: 0,
532 total_pages_repaired: 0,
533 },
534 };
535 let json = serde_json::to_string_pretty(&report)
536 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
537 wprintln!(writer, "{}", json)?;
538 } else {
539 wprintln!(writer, "No .ibd files found in {}", datadir_str)?;
540 }
541 return Ok(());
542 }
543
544 let explicit_algo = parse_algorithm(&opts.algorithm)?;
545
546 let pb = if !opts.json {
547 Some(create_progress_bar(ibd_files.len() as u64, "files"))
548 } else {
549 None
550 };
551
552 let page_size = opts.page_size;
553 let keyring = opts.keyring.clone();
554 let use_mmap = opts.mmap;
555 let no_backup = opts.no_backup;
556 let dry_run = opts.dry_run;
557 let audit_logger = opts.audit_logger.clone();
558
559 let mut results: Vec<FileRepairResult> = ibd_files
560 .par_iter()
561 .map(|path| {
562 let r = repair_file(
563 path,
564 datadir,
565 explicit_algo,
566 no_backup,
567 dry_run,
568 page_size,
569 &keyring,
570 use_mmap,
571 &audit_logger,
572 );
573 if let Some(ref pb) = pb {
574 pb.inc(1);
575 }
576 r
577 })
578 .collect();
579
580 if let Some(ref pb) = pb {
581 pb.finish_and_clear();
582 }
583
584 results.sort_by(|a, b| a.file.cmp(&b.file));
585
586 let total_files = results.len();
588 let files_repaired = results.iter().filter(|r| r.repaired > 0).count();
589 let files_already_valid = results
590 .iter()
591 .filter(|r| r.repaired == 0 && r.errors.is_empty())
592 .count();
593 let files_error = results.iter().filter(|r| !r.errors.is_empty()).count();
594 let total_pages_scanned: u64 = results.iter().map(|r| r.total_pages).sum();
595 let total_pages_repaired: u64 = results.iter().map(|r| r.repaired).sum();
596
597 let aname = match explicit_algo {
598 Some(a) => algo_name(a).to_string(),
599 None => "auto".to_string(),
600 };
601
602 if opts.json {
603 let report = BatchRepairReport {
604 datadir: datadir_str.clone(),
605 dry_run: opts.dry_run,
606 algorithm: aname,
607 files: results,
608 summary: BatchRepairSummary {
609 total_files,
610 files_repaired,
611 files_already_valid,
612 files_error,
613 total_pages_scanned,
614 total_pages_repaired,
615 },
616 };
617 let json = serde_json::to_string_pretty(&report)
618 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
619 wprintln!(writer, "{}", json)?;
620 } else {
621 let mode = if opts.dry_run {
622 "Dry run"
623 } else {
624 "Batch repair"
625 };
626 wprintln!(
627 writer,
628 "{}: {} ({} files)\n",
629 mode,
630 datadir_str,
631 total_files
632 )?;
633
634 for r in &results {
635 if !r.errors.is_empty() {
636 wprintln!(
637 writer,
638 " {:<40} {} {}",
639 r.file,
640 "ERROR".yellow(),
641 r.errors[0]
642 )?;
643 } else if r.repaired > 0 {
644 let label = if opts.dry_run {
645 format!("would repair {} pages", r.repaired)
646 } else {
647 format!("repaired {} pages", r.repaired)
648 };
649 wprintln!(writer, " {:<40} {}", r.file, label.green())?;
650 } else {
651 wprintln!(writer, " {:<40} OK", r.file)?;
652 }
653 }
654
655 wprintln!(writer)?;
656 wprintln!(writer, "Summary:")?;
657 wprintln!(
658 writer,
659 " Files: {} ({} repaired, {} already valid{})",
660 total_files,
661 files_repaired,
662 files_already_valid,
663 if files_error > 0 {
664 format!(", {} error", files_error)
665 } else {
666 String::new()
667 }
668 )?;
669 wprintln!(
670 writer,
671 " Pages: {} scanned, {} repaired",
672 total_pages_scanned,
673 total_pages_repaired
674 )?;
675 }
676
677 Ok(())
678}