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}