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