Skip to main content

idb/cli/
app.rs

1use clap::{Parser, Subcommand, ValueEnum};
2
3/// Top-level CLI definition for the `inno` binary.
4#[derive(Parser)]
5#[command(name = "inno")]
6#[command(about = "InnoDB file analysis toolkit")]
7#[command(version)]
8pub struct Cli {
9    /// Control colored output
10    #[arg(long, default_value = "auto", global = true)]
11    pub color: ColorMode,
12
13    /// Write output to a file instead of stdout
14    #[arg(short, long, global = true)]
15    pub output: Option<String>,
16
17    #[command(subcommand)]
18    pub command: Commands,
19}
20
21/// Controls when colored output is emitted.
22#[derive(Clone, Copy, ValueEnum)]
23pub enum ColorMode {
24    Auto,
25    Always,
26    Never,
27}
28
29/// Available subcommands for the `inno` CLI.
30#[derive(Subcommand)]
31pub enum Commands {
32    /// Parse .ibd file and display page summary
33    ///
34    /// Reads the 38-byte FIL header of every page in a tablespace, decodes the
35    /// page type, checksum, LSN, prev/next pointers, and space ID, then prints
36    /// a per-page breakdown followed by a page-type frequency summary table.
37    /// Page 0 additionally shows the FSP header (space ID, size, flags).
38    /// Use `--no-empty` to skip zero-checksum allocated pages, or `-p` to
39    /// inspect a single page in detail. With `--verbose`, checksum validation
40    /// and LSN consistency results are included for each page.
41    Parse {
42        /// Path to InnoDB data file (.ibd)
43        #[arg(short, long)]
44        file: String,
45
46        /// Display a specific page number
47        #[arg(short, long)]
48        page: Option<u64>,
49
50        /// Display additional information
51        #[arg(short, long)]
52        verbose: bool,
53
54        /// Skip empty/allocated pages
55        #[arg(short = 'e', long = "no-empty")]
56        no_empty: bool,
57
58        /// Output in JSON format
59        #[arg(long)]
60        json: bool,
61
62        /// Override page size (default: auto-detect)
63        #[arg(long = "page-size")]
64        page_size: Option<u32>,
65
66        /// Path to MySQL keyring file for decrypting encrypted tablespaces
67        #[arg(long)]
68        keyring: Option<String>,
69    },
70
71    /// Detailed page structure analysis
72    ///
73    /// Goes beyond FIL headers to decode the internal structure of each page
74    /// type: INDEX pages show the B+Tree index header, FSEG inode pointers, and
75    /// infimum/supremum system records; UNDO pages show the undo page header
76    /// and segment state; BLOB/LOB pages show chain pointers and data lengths;
77    /// and page 0 shows extended FSP header fields including compression and
78    /// encryption flags. Use `-l` for a compact one-line-per-page listing,
79    /// `-t INDEX` to filter by page type, or `-p` for a single page deep dive.
80    Pages {
81        /// Path to InnoDB data file (.ibd)
82        #[arg(short, long)]
83        file: String,
84
85        /// Display a specific page number
86        #[arg(short, long)]
87        page: Option<u64>,
88
89        /// Display additional information
90        #[arg(short, long)]
91        verbose: bool,
92
93        /// Show empty/allocated pages
94        #[arg(short = 'e', long = "show-empty")]
95        show_empty: bool,
96
97        /// Compact list mode (one line per page)
98        #[arg(short, long)]
99        list: bool,
100
101        /// Filter by page type (e.g., INDEX)
102        #[arg(short = 't', long = "type")]
103        filter_type: Option<String>,
104
105        /// Output in JSON format
106        #[arg(long)]
107        json: bool,
108
109        /// Override page size (default: auto-detect)
110        #[arg(long = "page-size")]
111        page_size: Option<u32>,
112
113        /// Path to MySQL keyring file for decrypting encrypted tablespaces
114        #[arg(long)]
115        keyring: Option<String>,
116    },
117
118    /// Hex dump of raw page bytes
119    ///
120    /// Operates in two modes: **page mode** (default) reads a full page by
121    /// number and produces a formatted hex dump with file-relative offsets;
122    /// **offset mode** (`--offset`) reads bytes at an arbitrary file position,
123    /// useful for inspecting structures that cross page boundaries. Use
124    /// `--length` to limit the number of bytes shown, or `--raw` to emit
125    /// unformatted binary bytes suitable for piping to other tools.
126    Dump {
127        /// Path to InnoDB data file
128        #[arg(short, long)]
129        file: String,
130
131        /// Page number to dump (default: 0)
132        #[arg(short, long)]
133        page: Option<u64>,
134
135        /// Absolute byte offset to start dumping (bypasses page mode)
136        #[arg(long)]
137        offset: Option<u64>,
138
139        /// Number of bytes to dump (default: page size or 256 for offset mode)
140        #[arg(short, long)]
141        length: Option<usize>,
142
143        /// Output raw binary bytes (no formatting)
144        #[arg(long)]
145        raw: bool,
146
147        /// Override page size (default: auto-detect)
148        #[arg(long = "page-size")]
149        page_size: Option<u32>,
150
151        /// Path to MySQL keyring file for decrypting encrypted tablespaces
152        #[arg(long)]
153        keyring: Option<String>,
154
155        /// Decrypt page before dumping (requires --keyring)
156        #[arg(long)]
157        decrypt: bool,
158    },
159
160    /// Intentionally corrupt pages for testing
161    ///
162    /// Writes random bytes into a tablespace file to simulate data corruption.
163    /// Targets can be the FIL header (`-k`), the record data area (`-r`), or
164    /// an absolute byte offset (`--offset`). If no page is specified, one is
165    /// chosen at random. Use `--verify` to print before/after checksum
166    /// comparisons confirming the page is now invalid — useful for verifying
167    /// that `inno checksum` correctly detects the damage.
168    Corrupt {
169        /// Path to data file
170        #[arg(short, long)]
171        file: String,
172
173        /// Page number to corrupt (random if not specified)
174        #[arg(short, long)]
175        page: Option<u64>,
176
177        /// Number of bytes to corrupt
178        #[arg(short, long, default_value = "1")]
179        bytes: usize,
180
181        /// Corrupt the FIL header area
182        #[arg(short = 'k', long = "header")]
183        header: bool,
184
185        /// Corrupt the record data area
186        #[arg(short, long)]
187        records: bool,
188
189        /// Absolute byte offset to corrupt (bypasses page calculation)
190        #[arg(long)]
191        offset: Option<u64>,
192
193        /// Show before/after checksum comparison
194        #[arg(long)]
195        verify: bool,
196
197        /// Output in JSON format
198        #[arg(long)]
199        json: bool,
200
201        /// Override page size (default: auto-detect)
202        #[arg(long = "page-size")]
203        page_size: Option<u32>,
204    },
205
206    /// Search for pages across data directory
207    ///
208    /// Recursively discovers all `.ibd` files under a MySQL data directory,
209    /// opens each as a tablespace, and reads the FIL header of every page
210    /// looking for a matching `page_number` field. Optional `--checksum` and
211    /// `--space-id` filters narrow results when the same page number appears
212    /// in multiple tablespaces. Use `--first` to stop after the first match
213    /// for faster lookups.
214    Find {
215        /// MySQL data directory path
216        #[arg(short, long)]
217        datadir: String,
218
219        /// Page number to search for
220        #[arg(short, long)]
221        page: u64,
222
223        /// Checksum to match
224        #[arg(short, long)]
225        checksum: Option<u32>,
226
227        /// Space ID to match
228        #[arg(short, long)]
229        space_id: Option<u32>,
230
231        /// Stop at first match
232        #[arg(long)]
233        first: bool,
234
235        /// Output in JSON format
236        #[arg(long)]
237        json: bool,
238
239        /// Override page size (default: auto-detect)
240        #[arg(long = "page-size")]
241        page_size: Option<u32>,
242    },
243
244    /// List/find tablespace IDs
245    ///
246    /// Scans `.ibd` and `.ibu` files under a MySQL data directory and reads
247    /// the space ID from the FSP header (page 0, offset 38) of each file.
248    /// In **list mode** (`-l`) it prints every file and its space ID; in
249    /// **lookup mode** (`-t <id>`) it finds the file that owns a specific
250    /// tablespace ID. Useful for mapping a space ID seen in error logs or
251    /// `INFORMATION_SCHEMA` back to a physical file on disk.
252    Tsid {
253        /// MySQL data directory path
254        #[arg(short, long)]
255        datadir: String,
256
257        /// List all tablespace IDs
258        #[arg(short, long)]
259        list: bool,
260
261        /// Find table file by tablespace ID
262        #[arg(short = 't', long = "tsid")]
263        tablespace_id: Option<u32>,
264
265        /// Output in JSON format
266        #[arg(long)]
267        json: bool,
268
269        /// Override page size (default: auto-detect)
270        #[arg(long = "page-size")]
271        page_size: Option<u32>,
272    },
273
274    /// Extract SDI metadata (MySQL 8.0+)
275    ///
276    /// Locates SDI (Serialized Dictionary Information) pages in a tablespace
277    /// by scanning for page type 17853, then reassembles multi-page SDI
278    /// records by following the page chain. The zlib-compressed payload is
279    /// decompressed and printed as JSON. Each tablespace in MySQL 8.0+
280    /// embeds its own table/column/index definitions as SDI records,
281    /// eliminating the need for the `.frm` files used in older versions.
282    /// Use `--pretty` for indented JSON output.
283    Sdi {
284        /// Path to InnoDB data file (.ibd)
285        #[arg(short, long)]
286        file: String,
287
288        /// Pretty-print JSON output
289        #[arg(short, long)]
290        pretty: bool,
291
292        /// Override page size (default: auto-detect)
293        #[arg(long = "page-size")]
294        page_size: Option<u32>,
295
296        /// Path to MySQL keyring file for decrypting encrypted tablespaces
297        #[arg(long)]
298        keyring: Option<String>,
299    },
300
301    /// Analyze InnoDB redo log files
302    ///
303    /// Opens an InnoDB redo log file (`ib_logfile0`/`ib_logfile1` for
304    /// MySQL < 8.0.30, or `#ib_redo*` files for 8.0.30+) and displays
305    /// the log file header, both checkpoint records, and per-block details
306    /// including block number, data length, checkpoint number, and CRC-32C
307    /// checksum status. With `--verbose`, MLOG record types within each
308    /// data block are decoded and summarized. Use `--blocks N` to limit
309    /// output to the first N data blocks, or `--no-empty` to skip blocks
310    /// that contain no redo data.
311    Log {
312        /// Path to redo log file (ib_logfile0, ib_logfile1, or #ib_redo*)
313        #[arg(short, long)]
314        file: String,
315
316        /// Limit to first N data blocks
317        #[arg(short, long)]
318        blocks: Option<u64>,
319
320        /// Skip empty blocks
321        #[arg(long)]
322        no_empty: bool,
323
324        /// Display additional information
325        #[arg(short, long)]
326        verbose: bool,
327
328        /// Output in JSON format
329        #[arg(long)]
330        json: bool,
331    },
332
333    /// Show InnoDB file and system information
334    ///
335    /// Operates in three modes. **`--ibdata`** reads the `ibdata1` page 0
336    /// FIL header and redo log checkpoint LSNs. **`--lsn-check`** compares
337    /// the `ibdata1` header LSN with the latest redo log checkpoint LSN to
338    /// detect whether the system tablespace and redo log are in sync (useful
339    /// for diagnosing crash-recovery state). **`-D`/`-t`** queries a live
340    /// MySQL instance via `INFORMATION_SCHEMA.INNODB_TABLES` and
341    /// `INNODB_INDEXES` for tablespace IDs, table IDs, index root pages,
342    /// and key InnoDB status metrics (requires the `mysql` feature).
343    Info {
344        /// Inspect ibdata1 page 0 header
345        #[arg(long)]
346        ibdata: bool,
347
348        /// Compare ibdata1 and redo log LSNs
349        #[arg(long = "lsn-check")]
350        lsn_check: bool,
351
352        /// MySQL data directory path
353        #[arg(short, long)]
354        datadir: Option<String>,
355
356        /// Database name (for table/index info)
357        #[arg(short = 'D', long)]
358        database: Option<String>,
359
360        /// Table name (for table/index info)
361        #[arg(short, long)]
362        table: Option<String>,
363
364        /// MySQL host
365        #[arg(long)]
366        host: Option<String>,
367
368        /// MySQL port
369        #[arg(long)]
370        port: Option<u16>,
371
372        /// MySQL user
373        #[arg(long)]
374        user: Option<String>,
375
376        /// MySQL password
377        #[arg(long)]
378        password: Option<String>,
379
380        /// Path to MySQL defaults file (.my.cnf)
381        #[arg(long = "defaults-file")]
382        defaults_file: Option<String>,
383
384        /// Output in JSON format
385        #[arg(long)]
386        json: bool,
387
388        /// Override page size (default: auto-detect)
389        #[arg(long = "page-size")]
390        page_size: Option<u32>,
391    },
392
393    /// Recover data from corrupt/damaged tablespace files
394    ///
395    /// Scans a tablespace file and classifies each page as intact, corrupt,
396    /// empty, or unreadable. For INDEX pages, counts recoverable user records
397    /// by walking the compact record chain. Produces a recovery assessment
398    /// showing how many pages and records can be salvaged.
399    ///
400    /// Use `--force` to also extract records from pages with bad checksums
401    /// but valid-looking headers — useful when data is partially damaged
402    /// but the record chain is still intact. Use `--page-size` to override
403    /// page size detection when page 0 is corrupt.
404    ///
405    /// With `--verbose`, per-page details are shown including page type,
406    /// status, LSN, and record count. With `--json`, a structured report
407    /// is emitted including optional per-record detail when combined with
408    /// `--verbose`.
409    Recover {
410        /// Path to InnoDB data file (.ibd)
411        #[arg(short, long)]
412        file: String,
413
414        /// Analyze a single page instead of full scan
415        #[arg(short, long)]
416        page: Option<u64>,
417
418        /// Show per-page details
419        #[arg(short, long)]
420        verbose: bool,
421
422        /// Output in JSON format
423        #[arg(long)]
424        json: bool,
425
426        /// Extract records from corrupt pages with valid headers
427        #[arg(long)]
428        force: bool,
429
430        /// Override page size (critical when page 0 is corrupt)
431        #[arg(long = "page-size")]
432        page_size: Option<u32>,
433
434        /// Path to MySQL keyring file for decrypting encrypted tablespaces
435        #[arg(long)]
436        keyring: Option<String>,
437    },
438
439    /// Validate page checksums
440    ///
441    /// Reads every page in a tablespace and validates its stored checksum
442    /// against both CRC-32C (MySQL 5.7.7+) and legacy InnoDB algorithms.
443    /// Also checks that the header LSN low-32 bits match the FIL trailer.
444    /// All-zero pages are counted as empty and skipped. With `--verbose`,
445    /// per-page results are printed including the detected algorithm and
446    /// stored vs. calculated values. Exits with code 1 if any page has an
447    /// invalid checksum, making it suitable for use in scripts and CI.
448    Checksum {
449        /// Path to InnoDB data file (.ibd)
450        #[arg(short, long)]
451        file: String,
452
453        /// Show per-page checksum details
454        #[arg(short, long)]
455        verbose: bool,
456
457        /// Output in JSON format
458        #[arg(long)]
459        json: bool,
460
461        /// Override page size (default: auto-detect)
462        #[arg(long = "page-size")]
463        page_size: Option<u32>,
464
465        /// Path to MySQL keyring file for decrypting encrypted tablespaces
466        #[arg(long)]
467        keyring: Option<String>,
468    },
469
470    /// Monitor a tablespace file for page-level changes
471    ///
472    /// Polls an InnoDB tablespace file at a configurable interval and reports
473    /// which pages have been modified, added, or removed since the last poll.
474    /// Change detection is based on LSN comparison — if a page's LSN changes
475    /// between polls, it was modified by a write. Checksums are validated for
476    /// each changed page to detect corruption during writes.
477    ///
478    /// The tablespace is re-opened each cycle to detect file growth and avoid
479    /// stale file handles. Use `--verbose` for per-field diffs on changed
480    /// pages, or `--json` for NDJSON streaming output (one JSON object per
481    /// line). Press Ctrl+C for a clean exit with a summary of total changes.
482    Watch {
483        /// Path to InnoDB data file (.ibd)
484        #[arg(short, long)]
485        file: String,
486
487        /// Polling interval in milliseconds
488        #[arg(short, long, default_value = "1000")]
489        interval: u64,
490
491        /// Show per-field diffs for changed pages
492        #[arg(short, long)]
493        verbose: bool,
494
495        /// Output in NDJSON streaming format
496        #[arg(long)]
497        json: bool,
498
499        /// Override page size (default: auto-detect)
500        #[arg(long = "page-size")]
501        page_size: Option<u32>,
502
503        /// Path to MySQL keyring file for decrypting encrypted tablespaces
504        #[arg(long)]
505        keyring: Option<String>,
506    },
507
508    /// Compare two tablespace files page-by-page
509    ///
510    /// Reads two InnoDB tablespace files and compares them page-by-page,
511    /// reporting which pages are identical, modified, or only present in
512    /// one file. With `--verbose`, per-page FIL header field diffs are
513    /// shown for modified pages, highlighting changes to checksums, LSNs,
514    /// page types, and space IDs. Add `--byte-ranges` (with `-v`) to see
515    /// the exact byte offsets where page content differs. Use `-p` to
516    /// compare a single page, or `--json` for machine-readable output.
517    ///
518    /// When files have different page sizes, only FIL headers (first 38
519    /// bytes) are compared and a warning is displayed.
520    Diff {
521        /// First InnoDB data file (.ibd)
522        file1: String,
523
524        /// Second InnoDB data file (.ibd)
525        file2: String,
526
527        /// Show per-page header field diffs
528        #[arg(short, long)]
529        verbose: bool,
530
531        /// Show byte-range diffs for changed pages (requires -v)
532        #[arg(short = 'b', long = "byte-ranges")]
533        byte_ranges: bool,
534
535        /// Compare a single page only
536        #[arg(short, long)]
537        page: Option<u64>,
538
539        /// Output in JSON format
540        #[arg(long)]
541        json: bool,
542
543        /// Override page size (default: auto-detect)
544        #[arg(long = "page-size")]
545        page_size: Option<u32>,
546
547        /// Path to MySQL keyring file for decrypting encrypted tablespaces
548        #[arg(long)]
549        keyring: Option<String>,
550    },
551}