Skip to main content

idb/cli/
checksum.rs

1use std::io::Write;
2
3use colored::Colorize;
4use serde::Serialize;
5
6use indicatif::{ProgressBar, ProgressStyle};
7
8use crate::cli::wprintln;
9use crate::innodb::checksum::{validate_checksum, validate_lsn, ChecksumAlgorithm};
10use crate::innodb::page::FilHeader;
11use crate::innodb::tablespace::Tablespace;
12use crate::IdbError;
13
14pub struct ChecksumOptions {
15    pub file: String,
16    pub verbose: bool,
17    pub json: bool,
18    pub page_size: Option<u32>,
19}
20
21#[derive(Serialize)]
22struct ChecksumSummaryJson {
23    file: String,
24    page_size: u32,
25    total_pages: u64,
26    empty_pages: u64,
27    valid_pages: u64,
28    invalid_pages: u64,
29    lsn_mismatches: u64,
30    #[serde(skip_serializing_if = "Vec::is_empty")]
31    pages: Vec<PageChecksumJson>,
32}
33
34#[derive(Serialize)]
35struct PageChecksumJson {
36    page_number: u64,
37    status: String,
38    algorithm: String,
39    stored_checksum: u32,
40    calculated_checksum: u32,
41    lsn_valid: bool,
42}
43
44pub fn execute(opts: &ChecksumOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
45    let mut ts = match opts.page_size {
46        Some(ps) => Tablespace::open_with_page_size(&opts.file, ps)?,
47        None => Tablespace::open(&opts.file)?,
48    };
49
50    let page_size = ts.page_size();
51    let page_count = ts.page_count();
52
53    if opts.json {
54        return execute_json(opts, &mut ts, page_size, page_count, writer);
55    }
56
57    wprintln!(
58        writer,
59        "Validating checksums for {} ({} pages, page size {})...",
60        opts.file, page_count, page_size
61    )?;
62    wprintln!(writer)?;
63
64    let mut valid_count = 0u64;
65    let mut invalid_count = 0u64;
66    let mut empty_count = 0u64;
67    let mut lsn_mismatch_count = 0u64;
68
69    let pb = ProgressBar::new(page_count);
70    pb.set_style(
71        ProgressStyle::default_bar()
72            .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} pages ({eta})")
73            .unwrap()
74            .progress_chars("#>-"),
75    );
76
77    for page_num in 0..page_count {
78        pb.inc(1);
79        let page_data = ts.read_page(page_num)?;
80
81        let header = match FilHeader::parse(&page_data) {
82            Some(h) => h,
83            None => {
84                eprintln!("Page {}: Could not parse FIL header", page_num);
85                invalid_count += 1;
86                continue;
87            }
88        };
89
90        // Skip all-zero pages
91        if header.checksum == 0 && page_data.iter().all(|&b| b == 0) {
92            empty_count += 1;
93            if opts.verbose {
94                wprintln!(writer, "Page {}: EMPTY", page_num)?;
95            }
96            continue;
97        }
98
99        let csum_result = validate_checksum(&page_data, page_size);
100        let lsn_valid = validate_lsn(&page_data, page_size);
101
102        if csum_result.valid {
103            valid_count += 1;
104            if opts.verbose {
105                wprintln!(
106                    writer,
107                    "Page {}: {} ({:?}, stored={}, calculated={})",
108                    page_num,
109                    "OK".green(),
110                    csum_result.algorithm,
111                    csum_result.stored_checksum,
112                    csum_result.calculated_checksum,
113                )?;
114            }
115        } else {
116            invalid_count += 1;
117            wprintln!(
118                writer,
119                "Page {}: {} checksum (stored={}, calculated={}, algorithm={:?})",
120                page_num,
121                "INVALID".red(),
122                csum_result.stored_checksum,
123                csum_result.calculated_checksum,
124                csum_result.algorithm,
125            )?;
126        }
127
128        // Check LSN consistency
129        if !lsn_valid {
130            lsn_mismatch_count += 1;
131            if csum_result.valid {
132                wprintln!(
133                    writer,
134                    "Page {}: {} - header LSN low32 does not match trailer",
135                    page_num,
136                    "LSN MISMATCH".yellow(),
137                )?;
138            }
139        }
140    }
141
142    pb.finish_and_clear();
143
144    wprintln!(writer)?;
145    wprintln!(writer, "Summary:")?;
146    wprintln!(writer, "  Total pages: {}", page_count)?;
147    wprintln!(writer, "  Empty pages: {}", empty_count)?;
148    wprintln!(writer, "  Valid checksums: {}", valid_count)?;
149    if invalid_count > 0 {
150        wprintln!(
151            writer,
152            "  Invalid checksums: {}",
153            format!("{}", invalid_count).red()
154        )?;
155    } else {
156        wprintln!(
157            writer,
158            "  Invalid checksums: {}",
159            format!("{}", invalid_count).green()
160        )?;
161    }
162    if lsn_mismatch_count > 0 {
163        wprintln!(
164            writer,
165            "  LSN mismatches: {}",
166            format!("{}", lsn_mismatch_count).yellow()
167        )?;
168    }
169
170    if invalid_count > 0 {
171        std::process::exit(1);
172    }
173
174    Ok(())
175}
176
177fn execute_json(
178    opts: &ChecksumOptions,
179    ts: &mut Tablespace,
180    page_size: u32,
181    page_count: u64,
182    writer: &mut dyn Write,
183) -> Result<(), IdbError> {
184    let mut valid_count = 0u64;
185    let mut invalid_count = 0u64;
186    let mut empty_count = 0u64;
187    let mut lsn_mismatch_count = 0u64;
188    let mut pages = Vec::new();
189
190    for page_num in 0..page_count {
191        let page_data = ts.read_page(page_num)?;
192
193        let header = match FilHeader::parse(&page_data) {
194            Some(h) => h,
195            None => {
196                invalid_count += 1;
197                if opts.verbose {
198                    pages.push(PageChecksumJson {
199                        page_number: page_num,
200                        status: "error".to_string(),
201                        algorithm: "unknown".to_string(),
202                        stored_checksum: 0,
203                        calculated_checksum: 0,
204                        lsn_valid: false,
205                    });
206                }
207                continue;
208            }
209        };
210
211        if header.checksum == 0 && page_data.iter().all(|&b| b == 0) {
212            empty_count += 1;
213            continue;
214        }
215
216        let csum_result = validate_checksum(&page_data, page_size);
217        let lsn_valid = validate_lsn(&page_data, page_size);
218
219        if csum_result.valid {
220            valid_count += 1;
221        } else {
222            invalid_count += 1;
223        }
224        if !lsn_valid {
225            lsn_mismatch_count += 1;
226        }
227
228        // In verbose JSON mode, include all pages; otherwise only invalid
229        if opts.verbose || !csum_result.valid || !lsn_valid {
230            let algorithm_name = match csum_result.algorithm {
231                ChecksumAlgorithm::Crc32c => "crc32c",
232                ChecksumAlgorithm::InnoDB => "innodb",
233                ChecksumAlgorithm::None => "none",
234            };
235            pages.push(PageChecksumJson {
236                page_number: page_num,
237                status: if csum_result.valid {
238                    "valid".to_string()
239                } else {
240                    "invalid".to_string()
241                },
242                algorithm: algorithm_name.to_string(),
243                stored_checksum: csum_result.stored_checksum,
244                calculated_checksum: csum_result.calculated_checksum,
245                lsn_valid,
246            });
247        }
248    }
249
250    let summary = ChecksumSummaryJson {
251        file: opts.file.clone(),
252        page_size,
253        total_pages: page_count,
254        empty_pages: empty_count,
255        valid_pages: valid_count,
256        invalid_pages: invalid_count,
257        lsn_mismatches: lsn_mismatch_count,
258        pages,
259    };
260
261    let json = serde_json::to_string_pretty(&summary)
262        .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
263    wprintln!(writer, "{}", json)?;
264
265    if invalid_count > 0 {
266        std::process::exit(1);
267    }
268
269    Ok(())
270}