Skip to main content

idb/cli/
repair.rs

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
16/// Options for the `inno repair` subcommand.
17pub struct RepairOptions {
18    /// Path to the InnoDB tablespace file (.ibd).
19    pub file: Option<String>,
20    /// Repair all .ibd files under a data directory.
21    pub batch: Option<String>,
22    /// Repair only a specific page number.
23    pub page: Option<u64>,
24    /// Checksum algorithm to use: "auto", "crc32c", "innodb", "full_crc32".
25    pub algorithm: String,
26    /// Skip creating a backup before repair.
27    pub no_backup: bool,
28    /// Show what would be repaired without modifying the file.
29    pub dry_run: bool,
30    /// Show per-page details.
31    pub verbose: bool,
32    /// Emit output as JSON.
33    pub json: bool,
34    /// Override the auto-detected page size.
35    pub page_size: Option<u32>,
36    /// Path to MySQL keyring file for decrypting encrypted tablespaces.
37    pub keyring: Option<String>,
38    /// Use memory-mapped I/O for file access.
39    pub mmap: bool,
40    /// Audit logger for recording write operations.
41    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
68/// Parse an algorithm string into a ChecksumAlgorithm.
69fn 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
91/// Recalculate and write correct checksums for corrupt pages.
92pub 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
119/// Repair a single tablespace file.
120fn execute_single(opts: &RepairOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
121    let file = opts.file.as_ref().unwrap();
122
123    // Open tablespace read-only to get metadata
124    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    // Determine algorithm
135    let explicit_algo = parse_algorithm(&opts.algorithm)?;
136    let algorithm = match explicit_algo {
137        Some(algo) => algo,
138        None => {
139            // Auto-detect from page 0
140            let page0 = ts.read_page(0)?;
141            write::detect_algorithm(&page0, page_size, Some(&vendor_info))
142        }
143    };
144
145    // Create backup unless --no-backup or --dry-run
146    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    // Determine page range
160    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        // Check if page is empty (all zeros)
202        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        // Validate current checksum
211        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        // Page needs repair
223        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// ---------------------------------------------------------------------------
323// Batch repair mode (#89)
324// ---------------------------------------------------------------------------
325
326#[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/// Repair a single file for batch mode. Returns the result for aggregation.
359#[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    // Determine algorithm
399    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    // Create backup unless --no-backup or --dry-run
421    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
505/// Repair all .ibd files under a data directory.
506fn 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    // Compute summary
587    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}