Skip to main content

idb/cli/
checksum.rs

1use std::io::Write;
2
3use colored::Colorize;
4use serde::Serialize;
5
6use crate::cli::{wprintln, create_progress_bar};
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, page_count, page_size
82    )?;
83    wprintln!(writer)?;
84
85    let mut valid_count = 0u64;
86    let mut invalid_count = 0u64;
87    let mut empty_count = 0u64;
88    let mut lsn_mismatch_count = 0u64;
89
90    let pb = create_progress_bar(page_count, "pages");
91
92    for page_num in 0..page_count {
93        pb.inc(1);
94        let page_data = ts.read_page(page_num)?;
95
96        let header = match FilHeader::parse(&page_data) {
97            Some(h) => h,
98            None => {
99                eprintln!("Page {}: Could not parse FIL header", page_num);
100                invalid_count += 1;
101                continue;
102            }
103        };
104
105        // Skip all-zero pages
106        if header.checksum == 0 && page_data.iter().all(|&b| b == 0) {
107            empty_count += 1;
108            if opts.verbose {
109                wprintln!(writer, "Page {}: EMPTY", page_num)?;
110            }
111            continue;
112        }
113
114        let csum_result = validate_checksum(&page_data, page_size);
115        let lsn_valid = validate_lsn(&page_data, page_size);
116
117        if csum_result.valid {
118            valid_count += 1;
119            if opts.verbose {
120                wprintln!(
121                    writer,
122                    "Page {}: {} ({:?}, stored={}, calculated={})",
123                    page_num,
124                    "OK".green(),
125                    csum_result.algorithm,
126                    csum_result.stored_checksum,
127                    csum_result.calculated_checksum,
128                )?;
129            }
130        } else {
131            invalid_count += 1;
132            wprintln!(
133                writer,
134                "Page {}: {} checksum (stored={}, calculated={}, algorithm={:?})",
135                page_num,
136                "INVALID".red(),
137                csum_result.stored_checksum,
138                csum_result.calculated_checksum,
139                csum_result.algorithm,
140            )?;
141        }
142
143        // Check LSN consistency
144        if !lsn_valid {
145            lsn_mismatch_count += 1;
146            if csum_result.valid {
147                wprintln!(
148                    writer,
149                    "Page {}: {} - header LSN low32 does not match trailer",
150                    page_num,
151                    "LSN MISMATCH".yellow(),
152                )?;
153            }
154        }
155    }
156
157    pb.finish_and_clear();
158
159    wprintln!(writer)?;
160    wprintln!(writer, "Summary:")?;
161    wprintln!(writer, "  Total pages: {}", page_count)?;
162    wprintln!(writer, "  Empty pages: {}", empty_count)?;
163    wprintln!(writer, "  Valid checksums: {}", valid_count)?;
164    if invalid_count > 0 {
165        wprintln!(
166            writer,
167            "  Invalid checksums: {}",
168            format!("{}", invalid_count).red()
169        )?;
170    } else {
171        wprintln!(
172            writer,
173            "  Invalid checksums: {}",
174            format!("{}", invalid_count).green()
175        )?;
176    }
177    if lsn_mismatch_count > 0 {
178        wprintln!(
179            writer,
180            "  LSN mismatches: {}",
181            format!("{}", lsn_mismatch_count).yellow()
182        )?;
183    }
184
185    if invalid_count > 0 {
186        std::process::exit(1);
187    }
188
189    Ok(())
190}
191
192fn execute_json(
193    opts: &ChecksumOptions,
194    ts: &mut Tablespace,
195    page_size: u32,
196    page_count: u64,
197    writer: &mut dyn Write,
198) -> Result<(), IdbError> {
199    let mut valid_count = 0u64;
200    let mut invalid_count = 0u64;
201    let mut empty_count = 0u64;
202    let mut lsn_mismatch_count = 0u64;
203    let mut pages = Vec::new();
204
205    for page_num in 0..page_count {
206        let page_data = ts.read_page(page_num)?;
207
208        let header = match FilHeader::parse(&page_data) {
209            Some(h) => h,
210            None => {
211                invalid_count += 1;
212                if opts.verbose {
213                    pages.push(PageChecksumJson {
214                        page_number: page_num,
215                        status: "error".to_string(),
216                        algorithm: "unknown".to_string(),
217                        stored_checksum: 0,
218                        calculated_checksum: 0,
219                        lsn_valid: false,
220                    });
221                }
222                continue;
223            }
224        };
225
226        if header.checksum == 0 && page_data.iter().all(|&b| b == 0) {
227            empty_count += 1;
228            continue;
229        }
230
231        let csum_result = validate_checksum(&page_data, page_size);
232        let lsn_valid = validate_lsn(&page_data, page_size);
233
234        if csum_result.valid {
235            valid_count += 1;
236        } else {
237            invalid_count += 1;
238        }
239        if !lsn_valid {
240            lsn_mismatch_count += 1;
241        }
242
243        // In verbose JSON mode, include all pages; otherwise only invalid
244        if opts.verbose || !csum_result.valid || !lsn_valid {
245            let algorithm_name = match csum_result.algorithm {
246                ChecksumAlgorithm::Crc32c => "crc32c",
247                ChecksumAlgorithm::InnoDB => "innodb",
248                ChecksumAlgorithm::None => "none",
249            };
250            pages.push(PageChecksumJson {
251                page_number: page_num,
252                status: if csum_result.valid {
253                    "valid".to_string()
254                } else {
255                    "invalid".to_string()
256                },
257                algorithm: algorithm_name.to_string(),
258                stored_checksum: csum_result.stored_checksum,
259                calculated_checksum: csum_result.calculated_checksum,
260                lsn_valid,
261            });
262        }
263    }
264
265    let summary = ChecksumSummaryJson {
266        file: opts.file.clone(),
267        page_size,
268        total_pages: page_count,
269        empty_pages: empty_count,
270        valid_pages: valid_count,
271        invalid_pages: invalid_count,
272        lsn_mismatches: lsn_mismatch_count,
273        pages,
274    };
275
276    let json = serde_json::to_string_pretty(&summary)
277        .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
278    wprintln!(writer, "{}", json)?;
279
280    if invalid_count > 0 {
281        std::process::exit(1);
282    }
283
284    Ok(())
285}