Skip to main content

idb/cli/
checksum.rs

1use std::io::Write;
2
3use colored::Colorize;
4use serde::Serialize;
5
6use crate::cli::{create_progress_bar, wprintln};
7use crate::innodb::checksum::{validate_checksum, validate_lsn, ChecksumAlgorithm};
8use crate::innodb::page::FilHeader;
9use crate::innodb::tablespace::Tablespace;
10use crate::IdbError;
11
12/// Options for the `inno checksum` subcommand.
13pub struct ChecksumOptions {
14    /// Path to the InnoDB tablespace file (.ibd).
15    pub file: String,
16    /// Show per-page checksum details.
17    pub verbose: bool,
18    /// Emit output as JSON.
19    pub json: bool,
20    /// Override the auto-detected page size.
21    pub page_size: Option<u32>,
22    /// Path to MySQL keyring file for decrypting encrypted tablespaces.
23    pub keyring: Option<String>,
24}
25
26#[derive(Serialize)]
27struct ChecksumSummaryJson {
28    file: String,
29    page_size: u32,
30    total_pages: u64,
31    empty_pages: u64,
32    valid_pages: u64,
33    invalid_pages: u64,
34    lsn_mismatches: u64,
35    #[serde(skip_serializing_if = "Vec::is_empty")]
36    pages: Vec<PageChecksumJson>,
37}
38
39#[derive(Serialize)]
40struct PageChecksumJson {
41    page_number: u64,
42    status: String,
43    algorithm: String,
44    stored_checksum: u32,
45    calculated_checksum: u32,
46    lsn_valid: bool,
47}
48
49/// Validate page checksums for every page in an InnoDB tablespace.
50///
51/// Iterates over all pages and validates the stored checksum (bytes 0–3 of the
52/// FIL header) against two algorithms: **CRC-32C** (MySQL 5.7.7+), which XORs
53/// two independent CRC-32C values computed over bytes \[4..26) and
54/// \[38..page_size-8); and **legacy InnoDB**, which uses `ut_fold_ulint_pair`
55/// with u32 wrapping arithmetic over the same two byte ranges. A page is
56/// considered valid if either algorithm matches the stored value.
57///
58/// Additionally checks **LSN consistency**: the low 32 bits of the header LSN
59/// (bytes 16–23) must match the LSN value in the 8-byte FIL trailer at the
60/// end of the page. All-zero pages are counted as empty and skipped entirely.
61///
62/// Prints a summary with total, empty, valid, and invalid page counts. In
63/// `--verbose` mode, every non-empty page is printed with its algorithm,
64/// stored and calculated checksum values, and LSN status. The process exits
65/// with code 1 if any page has an invalid checksum, making this suitable for
66/// scripted integrity checks.
67pub fn execute(opts: &ChecksumOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
68    let mut ts = match opts.page_size {
69        Some(ps) => Tablespace::open_with_page_size(&opts.file, ps)?,
70        None => Tablespace::open(&opts.file)?,
71    };
72
73    if let Some(ref keyring_path) = opts.keyring {
74        crate::cli::setup_decryption(&mut ts, keyring_path)?;
75    }
76
77    let page_size = ts.page_size();
78    let page_count = ts.page_count();
79
80    if opts.json {
81        return execute_json(opts, &mut ts, page_size, page_count, writer);
82    }
83
84    wprintln!(
85        writer,
86        "Validating checksums for {} ({} pages, page size {})...",
87        opts.file,
88        page_count,
89        page_size
90    )?;
91    wprintln!(writer)?;
92
93    let mut valid_count = 0u64;
94    let mut invalid_count = 0u64;
95    let mut empty_count = 0u64;
96    let mut lsn_mismatch_count = 0u64;
97
98    let pb = create_progress_bar(page_count, "pages");
99
100    for page_num in 0..page_count {
101        pb.inc(1);
102        let page_data = ts.read_page(page_num)?;
103
104        let header = match FilHeader::parse(&page_data) {
105            Some(h) => h,
106            None => {
107                eprintln!("Page {}: Could not parse FIL header", page_num);
108                invalid_count += 1;
109                continue;
110            }
111        };
112
113        // Skip all-zero pages
114        if header.checksum == 0 && page_data.iter().all(|&b| b == 0) {
115            empty_count += 1;
116            if opts.verbose {
117                wprintln!(writer, "Page {}: EMPTY", page_num)?;
118            }
119            continue;
120        }
121
122        let csum_result = validate_checksum(&page_data, page_size, Some(ts.vendor_info()));
123        let lsn_valid = validate_lsn(&page_data, page_size);
124
125        if csum_result.valid {
126            valid_count += 1;
127            if opts.verbose {
128                wprintln!(
129                    writer,
130                    "Page {}: {} ({:?}, stored={}, calculated={})",
131                    page_num,
132                    "OK".green(),
133                    csum_result.algorithm,
134                    csum_result.stored_checksum,
135                    csum_result.calculated_checksum,
136                )?;
137            }
138        } else {
139            invalid_count += 1;
140            wprintln!(
141                writer,
142                "Page {}: {} checksum (stored={}, calculated={}, algorithm={:?})",
143                page_num,
144                "INVALID".red(),
145                csum_result.stored_checksum,
146                csum_result.calculated_checksum,
147                csum_result.algorithm,
148            )?;
149        }
150
151        // Check LSN consistency
152        if !lsn_valid {
153            lsn_mismatch_count += 1;
154            if csum_result.valid {
155                wprintln!(
156                    writer,
157                    "Page {}: {} - header LSN low32 does not match trailer",
158                    page_num,
159                    "LSN MISMATCH".yellow(),
160                )?;
161            }
162        }
163    }
164
165    pb.finish_and_clear();
166
167    wprintln!(writer)?;
168    wprintln!(writer, "Summary:")?;
169    wprintln!(writer, "  Total pages: {}", page_count)?;
170    wprintln!(writer, "  Empty pages: {}", empty_count)?;
171    wprintln!(writer, "  Valid checksums: {}", valid_count)?;
172    if invalid_count > 0 {
173        wprintln!(
174            writer,
175            "  Invalid checksums: {}",
176            format!("{}", invalid_count).red()
177        )?;
178    } else {
179        wprintln!(
180            writer,
181            "  Invalid checksums: {}",
182            format!("{}", invalid_count).green()
183        )?;
184    }
185    if lsn_mismatch_count > 0 {
186        wprintln!(
187            writer,
188            "  LSN mismatches: {}",
189            format!("{}", lsn_mismatch_count).yellow()
190        )?;
191    }
192
193    if invalid_count > 0 {
194        return Err(IdbError::Parse(format!(
195            "{} pages with invalid checksums",
196            invalid_count
197        )));
198    }
199
200    Ok(())
201}
202
203fn execute_json(
204    opts: &ChecksumOptions,
205    ts: &mut Tablespace,
206    page_size: u32,
207    page_count: u64,
208    writer: &mut dyn Write,
209) -> Result<(), IdbError> {
210    let mut valid_count = 0u64;
211    let mut invalid_count = 0u64;
212    let mut empty_count = 0u64;
213    let mut lsn_mismatch_count = 0u64;
214    let mut pages = Vec::new();
215
216    for page_num in 0..page_count {
217        let page_data = ts.read_page(page_num)?;
218
219        let header = match FilHeader::parse(&page_data) {
220            Some(h) => h,
221            None => {
222                invalid_count += 1;
223                if opts.verbose {
224                    pages.push(PageChecksumJson {
225                        page_number: page_num,
226                        status: "error".to_string(),
227                        algorithm: "unknown".to_string(),
228                        stored_checksum: 0,
229                        calculated_checksum: 0,
230                        lsn_valid: false,
231                    });
232                }
233                continue;
234            }
235        };
236
237        if header.checksum == 0 && page_data.iter().all(|&b| b == 0) {
238            empty_count += 1;
239            continue;
240        }
241
242        let csum_result = validate_checksum(&page_data, page_size, Some(ts.vendor_info()));
243        let lsn_valid = validate_lsn(&page_data, page_size);
244
245        if csum_result.valid {
246            valid_count += 1;
247        } else {
248            invalid_count += 1;
249        }
250        if !lsn_valid {
251            lsn_mismatch_count += 1;
252        }
253
254        // In verbose JSON mode, include all pages; otherwise only invalid
255        if opts.verbose || !csum_result.valid || !lsn_valid {
256            let algorithm_name = match csum_result.algorithm {
257                ChecksumAlgorithm::Crc32c => "crc32c",
258                ChecksumAlgorithm::InnoDB => "innodb",
259                ChecksumAlgorithm::MariaDbFullCrc32 => "mariadb_full_crc32",
260                ChecksumAlgorithm::None => "none",
261            };
262            pages.push(PageChecksumJson {
263                page_number: page_num,
264                status: if csum_result.valid {
265                    "valid".to_string()
266                } else {
267                    "invalid".to_string()
268                },
269                algorithm: algorithm_name.to_string(),
270                stored_checksum: csum_result.stored_checksum,
271                calculated_checksum: csum_result.calculated_checksum,
272                lsn_valid,
273            });
274        }
275    }
276
277    let summary = ChecksumSummaryJson {
278        file: opts.file.clone(),
279        page_size,
280        total_pages: page_count,
281        empty_pages: empty_count,
282        valid_pages: valid_count,
283        invalid_pages: invalid_count,
284        lsn_mismatches: lsn_mismatch_count,
285        pages,
286    };
287
288    let json = serde_json::to_string_pretty(&summary)
289        .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
290    wprintln!(writer, "{}", json)?;
291
292    if invalid_count > 0 {
293        return Err(IdbError::Parse(format!(
294            "{} pages with invalid checksums",
295            invalid_count
296        )));
297    }
298
299    Ok(())
300}