Skip to main content

idb/cli/
find.rs

1use std::io::Write;
2use std::path::Path;
3
4use serde::Serialize;
5
6use crate::cli::{wprintln, create_progress_bar};
7use crate::innodb::page::FilHeader;
8use crate::innodb::tablespace::Tablespace;
9use crate::util::fs::find_tablespace_files;
10use crate::IdbError;
11
12/// Options for the `inno find` subcommand.
13pub struct FindOptions {
14    /// MySQL data directory path to search.
15    pub datadir: String,
16    /// Page number to search for across all tablespace files.
17    pub page: u64,
18    /// Optional checksum filter — only match pages with this stored checksum.
19    pub checksum: Option<u32>,
20    /// Optional space ID filter — only match pages in this tablespace.
21    pub space_id: Option<u32>,
22    /// Stop searching after the first match.
23    pub first: bool,
24    /// Emit output as JSON.
25    pub json: bool,
26    /// Override the auto-detected page size.
27    pub page_size: Option<u32>,
28}
29
30#[derive(Serialize)]
31struct FindResultJson {
32    datadir: String,
33    target_page: u64,
34    matches: Vec<FindMatchJson>,
35    files_searched: usize,
36}
37
38#[derive(Serialize)]
39struct FindMatchJson {
40    file: String,
41    page_number: u64,
42    checksum: u32,
43    space_id: u32,
44}
45
46/// Search a MySQL data directory for pages matching a given page number.
47///
48/// Recursively discovers all `.ibd` files under the specified data directory
49/// using [`find_tablespace_files`],
50/// opens each as a [`Tablespace`], and
51/// iterates over every page reading the FIL header. A page is considered a
52/// match when its stored `page_number` field (bytes 4–7 of the FIL header)
53/// equals the target value.
54///
55/// Optional filters narrow the results:
56/// - `--checksum`: only match pages whose stored checksum (bytes 0–3) equals
57///   the given value.
58/// - `--space-id`: only match pages whose space ID (bytes 34–37) equals the
59///   given value, useful when the same page number exists in multiple
60///   tablespaces.
61///
62/// With `--first`, searching stops after the first match across all files,
63/// providing a fast lookup when only one hit is expected. A progress bar is
64/// displayed for the file-level scan (suppressed in `--json` mode).
65pub fn execute(opts: &FindOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
66    let datadir = Path::new(&opts.datadir);
67    if !datadir.is_dir() {
68        return Err(IdbError::Argument(format!(
69            "Data directory does not exist: {}",
70            opts.datadir
71        )));
72    }
73
74    // Find all .ibd files in subdirectories
75    let ibd_files = find_tablespace_files(datadir, &["ibd"])?;
76
77    if ibd_files.is_empty() {
78        if opts.json {
79            let result = FindResultJson {
80                datadir: opts.datadir.clone(),
81                target_page: opts.page,
82                matches: Vec::new(),
83                files_searched: 0,
84            };
85            let json = serde_json::to_string_pretty(&result)
86                .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
87            wprintln!(writer, "{}", json)?;
88        } else {
89            wprintln!(writer, "No .ibd files found in {}", opts.datadir)?;
90        }
91        return Ok(());
92    }
93
94    let mut matches: Vec<FindMatchJson> = Vec::new();
95    let mut files_searched = 0;
96
97    let pb = if !opts.json {
98        Some(create_progress_bar(ibd_files.len() as u64, "files"))
99    } else {
100        None
101    };
102
103    for ibd_path in &ibd_files {
104        if let Some(ref pb) = pb {
105            pb.inc(1);
106        }
107        let display_path = ibd_path.strip_prefix(datadir).unwrap_or(ibd_path);
108        if !opts.json {
109            wprintln!(writer, "Checking {}.. ", display_path.display())?;
110        }
111
112        let ts_result = match opts.page_size {
113            Some(ps) => Tablespace::open_with_page_size(ibd_path, ps),
114            None => Tablespace::open(ibd_path),
115        };
116        let mut ts = match ts_result {
117            Ok(t) => t,
118            Err(_) => continue,
119        };
120
121        files_searched += 1;
122
123        for page_num in 0..ts.page_count() {
124            let page_data = match ts.read_page(page_num) {
125                Ok(d) => d,
126                Err(_) => continue,
127            };
128
129            let header = match FilHeader::parse(&page_data) {
130                Some(h) => h,
131                None => continue,
132            };
133
134            if header.page_number as u64 == opts.page {
135                // If checksum filter specified, must also match
136                if let Some(expected_csum) = opts.checksum {
137                    if header.checksum != expected_csum {
138                        continue;
139                    }
140                }
141                // If space_id filter specified, must also match
142                if let Some(expected_sid) = opts.space_id {
143                    if header.space_id != expected_sid {
144                        continue;
145                    }
146                }
147
148                if !opts.json {
149                    wprintln!(
150                        writer,
151                        "Found page {} in {} (checksum: {}, space_id: {})",
152                        opts.page,
153                        display_path.display(),
154                        header.checksum,
155                        header.space_id
156                    )?;
157                }
158
159                matches.push(FindMatchJson {
160                    file: display_path.display().to_string(),
161                    page_number: header.page_number as u64,
162                    checksum: header.checksum,
163                    space_id: header.space_id,
164                });
165
166                if opts.first {
167                    break;
168                }
169            }
170        }
171
172        if opts.first && !matches.is_empty() {
173            break;
174        }
175    }
176
177    if let Some(pb) = pb {
178        pb.finish_and_clear();
179    }
180
181    if opts.json {
182        let result = FindResultJson {
183            datadir: opts.datadir.clone(),
184            target_page: opts.page,
185            matches,
186            files_searched,
187        };
188        let json = serde_json::to_string_pretty(&result)
189            .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
190        wprintln!(writer, "{}", json)?;
191    } else if matches.is_empty() {
192        wprintln!(writer, "Page {} not found in any .ibd file.", opts.page)?;
193    } else {
194        wprintln!(writer)?;
195        wprintln!(
196            writer,
197            "Found {} match(es) in {} file(s) searched.",
198            matches.len(),
199            files_searched
200        )?;
201    }
202
203    Ok(())
204}
205