Skip to main content

idb/cli/
parse.rs

1use std::collections::HashMap;
2use std::io::Write;
3
4use byteorder::{BigEndian, ByteOrder};
5use colored::Colorize;
6use indicatif::{ProgressBar, ProgressStyle};
7
8use crate::cli::{wprintln, wprint};
9use crate::innodb::checksum;
10use crate::innodb::page::{FilHeader, FspHeader};
11use crate::innodb::page_types::PageType;
12use crate::innodb::tablespace::Tablespace;
13use crate::util::hex::format_offset;
14use crate::IdbError;
15
16/// Options for the parse subcommand.
17pub struct ParseOptions {
18    pub file: String,
19    pub page: Option<u64>,
20    pub verbose: bool,
21    pub no_empty: bool,
22    pub page_size: Option<u32>,
23    pub json: bool,
24}
25
26/// JSON-serializable page info.
27#[derive(serde::Serialize)]
28struct PageJson {
29    page_number: u64,
30    header: FilHeader,
31    page_type_name: String,
32    page_type_description: String,
33    byte_start: u64,
34    byte_end: u64,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    fsp_header: Option<crate::innodb::page::FspHeader>,
37}
38
39/// Execute the parse subcommand.
40pub fn execute(opts: &ParseOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
41    let mut ts = match opts.page_size {
42        Some(ps) => Tablespace::open_with_page_size(&opts.file, ps)?,
43        None => Tablespace::open(&opts.file)?,
44    };
45
46    let page_size = ts.page_size();
47
48    if opts.json {
49        return execute_json(opts, &mut ts, page_size, writer);
50    }
51
52    if let Some(page_num) = opts.page {
53        // Single page mode
54        let page_data = ts.read_page(page_num)?;
55        print_page_info(writer, &page_data, page_num, page_size, opts.verbose)?;
56    } else {
57        // All pages mode
58        // Print FSP header first
59        let page0 = ts.read_page(0)?;
60        if let Some(fsp) = FspHeader::parse(&page0) {
61            print_fsp_header(writer, &fsp)?;
62            wprintln!(writer)?;
63        }
64
65        wprintln!(
66            writer,
67            "Pages in {} ({} pages, page size {}):",
68            opts.file,
69            ts.page_count(),
70            page_size
71        )?;
72        wprintln!(writer, "{}", "-".repeat(50))?;
73
74        let mut type_counts: HashMap<PageType, u64> = HashMap::new();
75
76        let pb = ProgressBar::new(ts.page_count());
77        pb.set_style(
78            ProgressStyle::default_bar()
79                .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} pages ({eta})")
80                .unwrap()
81                .progress_chars("#>-"),
82        );
83
84        for page_num in 0..ts.page_count() {
85            pb.inc(1);
86            let page_data = ts.read_page(page_num)?;
87            let header = match FilHeader::parse(&page_data) {
88                Some(h) => h,
89                None => continue,
90            };
91
92            *type_counts.entry(header.page_type).or_insert(0) += 1;
93
94            // Skip empty pages if --no-empty
95            if opts.no_empty && header.checksum == 0 && header.page_type == PageType::Allocated {
96                continue;
97            }
98
99            // Skip pages with zero checksum unless they are page 0
100            if header.checksum == 0 && page_num != 0 && !opts.verbose {
101                continue;
102            }
103
104            print_page_info(writer, &page_data, page_num, page_size, opts.verbose)?;
105        }
106
107        pb.finish_and_clear();
108
109        // Print page type summary
110        wprintln!(writer)?;
111        wprintln!(writer, "{}", "Page Type Summary".bold())?;
112        let mut sorted_types: Vec<_> = type_counts.iter().collect();
113        sorted_types.sort_by(|a, b| b.1.cmp(a.1));
114        for (pt, count) in sorted_types {
115            let label = if *count == 1 { "page" } else { "pages" };
116            wprintln!(writer, "  {:20} {:>6} {}", pt.name(), count, label)?;
117        }
118    }
119
120    Ok(())
121}
122
123/// Execute parse in JSON output mode.
124fn execute_json(
125    opts: &ParseOptions,
126    ts: &mut Tablespace,
127    page_size: u32,
128    writer: &mut dyn Write,
129) -> Result<(), IdbError> {
130    let mut pages = Vec::new();
131
132    let range: Box<dyn Iterator<Item = u64>> = if let Some(p) = opts.page {
133        Box::new(std::iter::once(p))
134    } else {
135        Box::new(0..ts.page_count())
136    };
137
138    for page_num in range {
139        let page_data = ts.read_page(page_num)?;
140        let header = match FilHeader::parse(&page_data) {
141            Some(h) => h,
142            None => continue,
143        };
144
145        if opts.no_empty && header.checksum == 0 && header.page_type == PageType::Allocated {
146            continue;
147        }
148
149        let pt = header.page_type;
150        let byte_start = page_num * page_size as u64;
151
152        let fsp_header = if page_num == 0 {
153            FspHeader::parse(&page_data)
154        } else {
155            None
156        };
157
158        pages.push(PageJson {
159            page_number: page_num,
160            page_type_name: pt.name().to_string(),
161            page_type_description: pt.description().to_string(),
162            byte_start,
163            byte_end: byte_start + page_size as u64,
164            header,
165            fsp_header,
166        });
167    }
168
169    wprintln!(
170        writer,
171        "{}",
172        serde_json::to_string_pretty(&pages).unwrap_or_else(|_| "[]".to_string())
173    )?;
174    Ok(())
175}
176
177/// Print detailed information about a single page.
178fn print_page_info(writer: &mut dyn Write, page_data: &[u8], page_num: u64, page_size: u32, verbose: bool) -> Result<(), IdbError> {
179    let header = match FilHeader::parse(page_data) {
180        Some(h) => h,
181        None => {
182            eprintln!("Could not parse FIL header for page {}", page_num);
183            return Ok(());
184        }
185    };
186
187    let byte_start = page_num * page_size as u64;
188    let byte_end = byte_start + page_size as u64;
189
190    let pt = header.page_type;
191
192    wprintln!(writer, "Page: {}", header.page_number)?;
193    wprintln!(writer, "{}", "-".repeat(20))?;
194    wprintln!(writer, "{}", "HEADER".bold())?;
195    wprintln!(writer, "Byte Start: {}", format_offset(byte_start))?;
196    wprintln!(
197        writer,
198        "Page Type: {}\n-- {}: {} - {}",
199        pt.as_u16(),
200        pt.name(),
201        pt.description(),
202        pt.usage()
203    )?;
204
205    if verbose {
206        wprintln!(writer, "PAGE_N_HEAP (Amount of records in page): {}", read_page_n_heap(page_data))?;
207    }
208
209    wprint!(writer, "Prev Page: ")?;
210    if !header.has_prev() {
211        wprintln!(writer, "Not used.")?;
212    } else {
213        wprintln!(writer, "{}", header.prev_page)?;
214    }
215
216    wprint!(writer, "Next Page: ")?;
217    if !header.has_next() {
218        wprintln!(writer, "Not used.")?;
219    } else {
220        wprintln!(writer, "{}", header.next_page)?;
221    }
222
223    wprintln!(writer, "LSN: {}", header.lsn)?;
224    wprintln!(writer, "Space ID: {}", header.space_id)?;
225    wprintln!(writer, "Checksum: {}", header.checksum)?;
226
227    // Checksum validation
228    let csum_result = checksum::validate_checksum(page_data, page_size);
229    if verbose {
230        let status = if csum_result.valid {
231            "OK".green().to_string()
232        } else {
233            "MISMATCH".red().to_string()
234        };
235        wprintln!(
236            writer,
237            "Checksum Status: {} ({:?}, stored={}, calculated={})",
238            status, csum_result.algorithm, csum_result.stored_checksum, csum_result.calculated_checksum
239        )?;
240    }
241
242    wprintln!(writer)?;
243
244    // Trailer
245    let ps = page_size as usize;
246    if page_data.len() >= ps {
247        let trailer_offset = ps - 8;
248        if let Some(trailer) = crate::innodb::page::FilTrailer::parse(&page_data[trailer_offset..]) {
249            wprintln!(writer, "{}", "TRAILER".bold())?;
250            wprintln!(writer, "Old-style Checksum: {}", trailer.checksum)?;
251            wprintln!(writer, "Low 32 bits of LSN: {}", trailer.lsn_low32)?;
252            wprintln!(writer, "Byte End: {}", format_offset(byte_end))?;
253
254            // LSN validation
255            if verbose {
256                let lsn_valid = checksum::validate_lsn(page_data, page_size);
257                let lsn_status = if lsn_valid {
258                    "OK".green().to_string()
259                } else {
260                    "MISMATCH".red().to_string()
261                };
262                wprintln!(writer, "LSN Consistency: {}", lsn_status)?;
263            }
264        }
265    }
266    wprintln!(writer, "{}", "-".repeat(20))?;
267    Ok(())
268}
269
270/// Print FSP header information.
271fn print_fsp_header(writer: &mut dyn Write, fsp: &FspHeader) -> Result<(), IdbError> {
272    wprintln!(writer, "{}", "-".repeat(20))?;
273    wprintln!(writer, "{}", "FSP_HDR - Filespace Header".bold())?;
274    wprintln!(writer, "{}", "-".repeat(20))?;
275    wprintln!(writer, "Space ID: {}", fsp.space_id)?;
276    wprintln!(writer, "Size (pages): {}", fsp.size)?;
277    wprintln!(writer, "Page Free Limit: {}", fsp.free_limit)?;
278    wprintln!(writer, "Flags: {}", fsp.flags)?;
279    Ok(())
280}
281
282/// Read PAGE_N_HEAP from the page header (INDEX page specific).
283fn read_page_n_heap(page_data: &[u8]) -> u16 {
284    let offset = crate::innodb::constants::FIL_PAGE_DATA + 4; // PAGE_N_HEAP is at FIL_PAGE_DATA + 4
285    if page_data.len() < offset + 2 {
286        return 0;
287    }
288    BigEndian::read_u16(&page_data[offset..])
289}