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
67    /// Detailed page structure analysis
68    ///
69    /// Goes beyond FIL headers to decode the internal structure of each page
70    /// type: INDEX pages show the B+Tree index header, FSEG inode pointers, and
71    /// infimum/supremum system records; UNDO pages show the undo page header
72    /// and segment state; BLOB/LOB pages show chain pointers and data lengths;
73    /// and page 0 shows extended FSP header fields including compression and
74    /// encryption flags. Use `-l` for a compact one-line-per-page listing,
75    /// `-t INDEX` to filter by page type, or `-p` for a single page deep dive.
76    Pages {
77        /// Path to InnoDB data file (.ibd)
78        #[arg(short, long)]
79        file: String,
80
81        /// Display a specific page number
82        #[arg(short, long)]
83        page: Option<u64>,
84
85        /// Display additional information
86        #[arg(short, long)]
87        verbose: bool,
88
89        /// Show empty/allocated pages
90        #[arg(short = 'e', long = "show-empty")]
91        show_empty: bool,
92
93        /// Compact list mode (one line per page)
94        #[arg(short, long)]
95        list: bool,
96
97        /// Filter by page type (e.g., INDEX)
98        #[arg(short = 't', long = "type")]
99        filter_type: Option<String>,
100
101        /// Output in JSON format
102        #[arg(long)]
103        json: bool,
104
105        /// Override page size (default: auto-detect)
106        #[arg(long = "page-size")]
107        page_size: Option<u32>,
108    },
109
110    /// Hex dump of raw page bytes
111    ///
112    /// Operates in two modes: **page mode** (default) reads a full page by
113    /// number and produces a formatted hex dump with file-relative offsets;
114    /// **offset mode** (`--offset`) reads bytes at an arbitrary file position,
115    /// useful for inspecting structures that cross page boundaries. Use
116    /// `--length` to limit the number of bytes shown, or `--raw` to emit
117    /// unformatted binary bytes suitable for piping to other tools.
118    Dump {
119        /// Path to InnoDB data file
120        #[arg(short, long)]
121        file: String,
122
123        /// Page number to dump (default: 0)
124        #[arg(short, long)]
125        page: Option<u64>,
126
127        /// Absolute byte offset to start dumping (bypasses page mode)
128        #[arg(long)]
129        offset: Option<u64>,
130
131        /// Number of bytes to dump (default: page size or 256 for offset mode)
132        #[arg(short, long)]
133        length: Option<usize>,
134
135        /// Output raw binary bytes (no formatting)
136        #[arg(long)]
137        raw: bool,
138
139        /// Override page size (default: auto-detect)
140        #[arg(long = "page-size")]
141        page_size: Option<u32>,
142    },
143
144    /// Intentionally corrupt pages for testing
145    ///
146    /// Writes random bytes into a tablespace file to simulate data corruption.
147    /// Targets can be the FIL header (`-k`), the record data area (`-r`), or
148    /// an absolute byte offset (`--offset`). If no page is specified, one is
149    /// chosen at random. Use `--verify` to print before/after checksum
150    /// comparisons confirming the page is now invalid — useful for verifying
151    /// that `inno checksum` correctly detects the damage.
152    Corrupt {
153        /// Path to data file
154        #[arg(short, long)]
155        file: String,
156
157        /// Page number to corrupt (random if not specified)
158        #[arg(short, long)]
159        page: Option<u64>,
160
161        /// Number of bytes to corrupt
162        #[arg(short, long, default_value = "1")]
163        bytes: usize,
164
165        /// Corrupt the FIL header area
166        #[arg(short = 'k', long = "header")]
167        header: bool,
168
169        /// Corrupt the record data area
170        #[arg(short, long)]
171        records: bool,
172
173        /// Absolute byte offset to corrupt (bypasses page calculation)
174        #[arg(long)]
175        offset: Option<u64>,
176
177        /// Show before/after checksum comparison
178        #[arg(long)]
179        verify: bool,
180
181        /// Output in JSON format
182        #[arg(long)]
183        json: bool,
184
185        /// Override page size (default: auto-detect)
186        #[arg(long = "page-size")]
187        page_size: Option<u32>,
188    },
189
190    /// Search for pages across data directory
191    ///
192    /// Recursively discovers all `.ibd` files under a MySQL data directory,
193    /// opens each as a tablespace, and reads the FIL header of every page
194    /// looking for a matching `page_number` field. Optional `--checksum` and
195    /// `--space-id` filters narrow results when the same page number appears
196    /// in multiple tablespaces. Use `--first` to stop after the first match
197    /// for faster lookups.
198    Find {
199        /// MySQL data directory path
200        #[arg(short, long)]
201        datadir: String,
202
203        /// Page number to search for
204        #[arg(short, long)]
205        page: u64,
206
207        /// Checksum to match
208        #[arg(short, long)]
209        checksum: Option<u32>,
210
211        /// Space ID to match
212        #[arg(short, long)]
213        space_id: Option<u32>,
214
215        /// Stop at first match
216        #[arg(long)]
217        first: bool,
218
219        /// Output in JSON format
220        #[arg(long)]
221        json: bool,
222
223        /// Override page size (default: auto-detect)
224        #[arg(long = "page-size")]
225        page_size: Option<u32>,
226    },
227
228    /// List/find tablespace IDs
229    ///
230    /// Scans `.ibd` and `.ibu` files under a MySQL data directory and reads
231    /// the space ID from the FSP header (page 0, offset 38) of each file.
232    /// In **list mode** (`-l`) it prints every file and its space ID; in
233    /// **lookup mode** (`-t <id>`) it finds the file that owns a specific
234    /// tablespace ID. Useful for mapping a space ID seen in error logs or
235    /// `INFORMATION_SCHEMA` back to a physical file on disk.
236    Tsid {
237        /// MySQL data directory path
238        #[arg(short, long)]
239        datadir: String,
240
241        /// List all tablespace IDs
242        #[arg(short, long)]
243        list: bool,
244
245        /// Find table file by tablespace ID
246        #[arg(short = 't', long = "tsid")]
247        tablespace_id: Option<u32>,
248
249        /// Output in JSON format
250        #[arg(long)]
251        json: bool,
252
253        /// Override page size (default: auto-detect)
254        #[arg(long = "page-size")]
255        page_size: Option<u32>,
256    },
257
258    /// Extract SDI metadata (MySQL 8.0+)
259    ///
260    /// Locates SDI (Serialized Dictionary Information) pages in a tablespace
261    /// by scanning for page type 17853, then reassembles multi-page SDI
262    /// records by following the page chain. The zlib-compressed payload is
263    /// decompressed and printed as JSON. Each tablespace in MySQL 8.0+
264    /// embeds its own table/column/index definitions as SDI records,
265    /// eliminating the need for the `.frm` files used in older versions.
266    /// Use `--pretty` for indented JSON output.
267    Sdi {
268        /// Path to InnoDB data file (.ibd)
269        #[arg(short, long)]
270        file: String,
271
272        /// Pretty-print JSON output
273        #[arg(short, long)]
274        pretty: bool,
275
276        /// Override page size (default: auto-detect)
277        #[arg(long = "page-size")]
278        page_size: Option<u32>,
279    },
280
281    /// Analyze InnoDB redo log files
282    ///
283    /// Opens an InnoDB redo log file (`ib_logfile0`/`ib_logfile1` for
284    /// MySQL < 8.0.30, or `#ib_redo*` files for 8.0.30+) and displays
285    /// the log file header, both checkpoint records, and per-block details
286    /// including block number, data length, checkpoint number, and CRC-32C
287    /// checksum status. With `--verbose`, MLOG record types within each
288    /// data block are decoded and summarized. Use `--blocks N` to limit
289    /// output to the first N data blocks, or `--no-empty` to skip blocks
290    /// that contain no redo data.
291    Log {
292        /// Path to redo log file (ib_logfile0, ib_logfile1, or #ib_redo*)
293        #[arg(short, long)]
294        file: String,
295
296        /// Limit to first N data blocks
297        #[arg(short, long)]
298        blocks: Option<u64>,
299
300        /// Skip empty blocks
301        #[arg(long)]
302        no_empty: bool,
303
304        /// Display additional information
305        #[arg(short, long)]
306        verbose: bool,
307
308        /// Output in JSON format
309        #[arg(long)]
310        json: bool,
311    },
312
313    /// Show InnoDB file and system information
314    ///
315    /// Operates in three modes. **`--ibdata`** reads the `ibdata1` page 0
316    /// FIL header and redo log checkpoint LSNs. **`--lsn-check`** compares
317    /// the `ibdata1` header LSN with the latest redo log checkpoint LSN to
318    /// detect whether the system tablespace and redo log are in sync (useful
319    /// for diagnosing crash-recovery state). **`-D`/`-t`** queries a live
320    /// MySQL instance via `INFORMATION_SCHEMA.INNODB_TABLES` and
321    /// `INNODB_INDEXES` for tablespace IDs, table IDs, index root pages,
322    /// and key InnoDB status metrics (requires the `mysql` feature).
323    Info {
324        /// Inspect ibdata1 page 0 header
325        #[arg(long)]
326        ibdata: bool,
327
328        /// Compare ibdata1 and redo log LSNs
329        #[arg(long = "lsn-check")]
330        lsn_check: bool,
331
332        /// MySQL data directory path
333        #[arg(short, long)]
334        datadir: Option<String>,
335
336        /// Database name (for table/index info)
337        #[arg(short = 'D', long)]
338        database: Option<String>,
339
340        /// Table name (for table/index info)
341        #[arg(short, long)]
342        table: Option<String>,
343
344        /// MySQL host
345        #[arg(long)]
346        host: Option<String>,
347
348        /// MySQL port
349        #[arg(long)]
350        port: Option<u16>,
351
352        /// MySQL user
353        #[arg(long)]
354        user: Option<String>,
355
356        /// MySQL password
357        #[arg(long)]
358        password: Option<String>,
359
360        /// Path to MySQL defaults file (.my.cnf)
361        #[arg(long = "defaults-file")]
362        defaults_file: Option<String>,
363
364        /// Output in JSON format
365        #[arg(long)]
366        json: bool,
367
368        /// Override page size (default: auto-detect)
369        #[arg(long = "page-size")]
370        page_size: Option<u32>,
371    },
372
373    /// Recover data from corrupt/damaged tablespace files
374    ///
375    /// Scans a tablespace file and classifies each page as intact, corrupt,
376    /// empty, or unreadable. For INDEX pages, counts recoverable user records
377    /// by walking the compact record chain. Produces a recovery assessment
378    /// showing how many pages and records can be salvaged.
379    ///
380    /// Use `--force` to also extract records from pages with bad checksums
381    /// but valid-looking headers — useful when data is partially damaged
382    /// but the record chain is still intact. Use `--page-size` to override
383    /// page size detection when page 0 is corrupt.
384    ///
385    /// With `--verbose`, per-page details are shown including page type,
386    /// status, LSN, and record count. With `--json`, a structured report
387    /// is emitted including optional per-record detail when combined with
388    /// `--verbose`.
389    Recover {
390        /// Path to InnoDB data file (.ibd)
391        #[arg(short, long)]
392        file: String,
393
394        /// Analyze a single page instead of full scan
395        #[arg(short, long)]
396        page: Option<u64>,
397
398        /// Show per-page details
399        #[arg(short, long)]
400        verbose: bool,
401
402        /// Output in JSON format
403        #[arg(long)]
404        json: bool,
405
406        /// Extract records from corrupt pages with valid headers
407        #[arg(long)]
408        force: bool,
409
410        /// Override page size (critical when page 0 is corrupt)
411        #[arg(long = "page-size")]
412        page_size: Option<u32>,
413    },
414
415    /// Validate page checksums
416    ///
417    /// Reads every page in a tablespace and validates its stored checksum
418    /// against both CRC-32C (MySQL 5.7.7+) and legacy InnoDB algorithms.
419    /// Also checks that the header LSN low-32 bits match the FIL trailer.
420    /// All-zero pages are counted as empty and skipped. With `--verbose`,
421    /// per-page results are printed including the detected algorithm and
422    /// stored vs. calculated values. Exits with code 1 if any page has an
423    /// invalid checksum, making it suitable for use in scripts and CI.
424    Checksum {
425        /// Path to InnoDB data file (.ibd)
426        #[arg(short, long)]
427        file: String,
428
429        /// Show per-page checksum details
430        #[arg(short, long)]
431        verbose: bool,
432
433        /// Output in JSON format
434        #[arg(long)]
435        json: bool,
436
437        /// Override page size (default: auto-detect)
438        #[arg(long = "page-size")]
439        page_size: Option<u32>,
440    },
441}