Skip to main content

idb/cli/
pages.rs

1use std::io::Write;
2
3use colored::Colorize;
4use serde::Serialize;
5
6use crate::cli::{wprintln, wprint};
7use crate::innodb::checksum;
8use crate::innodb::compression;
9use crate::innodb::encryption;
10use crate::innodb::index::{FsegHeader, IndexHeader, SystemRecords};
11use crate::innodb::lob::{BlobPageHeader, LobFirstPageHeader};
12use crate::innodb::page::{FilHeader, FspHeader};
13use crate::innodb::page_types::PageType;
14use crate::innodb::tablespace::Tablespace;
15use crate::innodb::undo::{UndoPageHeader, UndoSegmentHeader};
16use crate::util::hex::format_offset;
17use crate::IdbError;
18
19pub struct PagesOptions {
20    pub file: String,
21    pub page: Option<u64>,
22    pub verbose: bool,
23    pub show_empty: bool,
24    pub list_mode: bool,
25    pub filter_type: Option<String>,
26    pub page_size: Option<u32>,
27    pub json: bool,
28}
29
30/// JSON-serializable detailed page info.
31#[derive(Serialize)]
32struct PageDetailJson {
33    page_number: u64,
34    header: FilHeader,
35    page_type_name: String,
36    page_type_description: String,
37    byte_start: u64,
38    byte_end: u64,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    index_header: Option<IndexHeader>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    fsp_header: Option<FspHeader>,
43}
44
45pub fn execute(opts: &PagesOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
46    let mut ts = match opts.page_size {
47        Some(ps) => Tablespace::open_with_page_size(&opts.file, ps)?,
48        None => Tablespace::open(&opts.file)?,
49    };
50
51    let page_size = ts.page_size();
52
53    if opts.json {
54        return execute_json(opts, &mut ts, page_size, writer);
55    }
56
57    if let Some(page_num) = opts.page {
58        let page_data = ts.read_page(page_num)?;
59        print_full_page(&page_data, page_num, page_size, opts.verbose, writer)?;
60        return Ok(());
61    }
62
63    // Print FSP header unless filtering by type
64    if opts.filter_type.is_none() {
65        let page0 = ts.read_page(0)?;
66        if let Some(fsp) = FspHeader::parse(&page0) {
67            print_fsp_header_detail(&fsp, &page0, opts.verbose, writer)?;
68        }
69    }
70
71    for page_num in 0..ts.page_count() {
72        let page_data = ts.read_page(page_num)?;
73        let header = match FilHeader::parse(&page_data) {
74            Some(h) => h,
75            None => continue,
76        };
77
78        // Skip empty pages unless --show-empty
79        if !opts.show_empty && header.checksum == 0 && header.page_type == PageType::Allocated {
80            continue;
81        }
82
83        // Filter by type
84        if let Some(ref filter) = opts.filter_type {
85            if !matches_page_type_filter(&header.page_type, filter) {
86                continue;
87            }
88        }
89
90        if opts.list_mode {
91            print_list_line(&page_data, page_num, page_size, writer)?;
92        } else {
93            print_full_page(&page_data, page_num, page_size, opts.verbose, writer)?;
94        }
95    }
96
97    Ok(())
98}
99
100/// Execute pages in JSON output mode.
101fn execute_json(
102    opts: &PagesOptions,
103    ts: &mut Tablespace,
104    page_size: u32,
105    writer: &mut dyn Write,
106) -> Result<(), IdbError> {
107    let mut pages = Vec::new();
108
109    let range: Box<dyn Iterator<Item = u64>> = if let Some(p) = opts.page {
110        Box::new(std::iter::once(p))
111    } else {
112        Box::new(0..ts.page_count())
113    };
114
115    for page_num in range {
116        let page_data = ts.read_page(page_num)?;
117        let header = match FilHeader::parse(&page_data) {
118            Some(h) => h,
119            None => continue,
120        };
121
122        if !opts.show_empty && header.checksum == 0 && header.page_type == PageType::Allocated {
123            continue;
124        }
125
126        if let Some(ref filter) = opts.filter_type {
127            if !matches_page_type_filter(&header.page_type, filter) {
128                continue;
129            }
130        }
131
132        let pt = header.page_type;
133        let byte_start = page_num * page_size as u64;
134
135        let index_header = if pt == PageType::Index {
136            IndexHeader::parse(&page_data)
137        } else {
138            None
139        };
140
141        let fsp_header = if page_num == 0 {
142            FspHeader::parse(&page_data)
143        } else {
144            None
145        };
146
147        pages.push(PageDetailJson {
148            page_number: page_num,
149            page_type_name: pt.name().to_string(),
150            page_type_description: pt.description().to_string(),
151            byte_start,
152            byte_end: byte_start + page_size as u64,
153            header,
154            index_header,
155            fsp_header,
156        });
157    }
158
159    let json = serde_json::to_string_pretty(&pages)
160        .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
161    wprintln!(writer, "{}", json)?;
162    Ok(())
163}
164
165/// Print a compact one-line summary per page (list mode).
166fn print_list_line(page_data: &[u8], page_num: u64, page_size: u32, writer: &mut dyn Write) -> Result<(), IdbError> {
167    let header = match FilHeader::parse(page_data) {
168        Some(h) => h,
169        None => return Ok(()),
170    };
171
172    let pt = header.page_type;
173    let byte_start = page_num * page_size as u64;
174
175    wprint!(
176        writer,
177        "-- Page {} - {}: {}",
178        page_num,
179        pt.name(),
180        pt.description()
181    )?;
182
183    if pt == PageType::Index {
184        if let Some(idx) = IndexHeader::parse(page_data) {
185            wprint!(writer, ", Index ID: {}", idx.index_id)?;
186        }
187    }
188
189    wprintln!(writer, ", Byte Start: {}", format_offset(byte_start))?;
190    Ok(())
191}
192
193/// Print full detailed information about a page.
194fn print_full_page(page_data: &[u8], page_num: u64, page_size: u32, verbose: bool, writer: &mut dyn Write) -> Result<(), IdbError> {
195    let header = match FilHeader::parse(page_data) {
196        Some(h) => h,
197        None => {
198            eprintln!("Could not parse FIL header for page {}", page_num);
199            return Ok(());
200        }
201    };
202
203    let byte_start = page_num * page_size as u64;
204    let byte_end = byte_start + page_size as u64;
205    let pt = header.page_type;
206
207    // FIL Header
208    wprintln!(writer)?;
209    wprintln!(writer, "=== HEADER: Page {}", header.page_number)?;
210    wprintln!(writer, "Byte Start: {}", format_offset(byte_start))?;
211    wprintln!(
212        writer,
213        "Page Type: {}\n-- {}: {} - {}",
214        pt.as_u16(),
215        pt.name(),
216        pt.description(),
217        pt.usage()
218    )?;
219
220    wprint!(writer, "Prev Page: ")?;
221    if !header.has_prev() {
222        wprintln!(writer, "Not used.")?;
223    } else {
224        wprintln!(writer, "{}", header.prev_page)?;
225    }
226
227    wprint!(writer, "Next Page: ")?;
228    if !header.has_next() {
229        wprintln!(writer, "Not used.")?;
230    } else {
231        wprintln!(writer, "{}", header.next_page)?;
232    }
233
234    wprintln!(writer, "LSN: {}", header.lsn)?;
235    wprintln!(writer, "Space ID: {}", header.space_id)?;
236    wprintln!(writer, "Checksum: {}", header.checksum)?;
237
238    // INDEX-specific headers
239    if pt == PageType::Index {
240        if let Some(idx) = IndexHeader::parse(page_data) {
241            wprintln!(writer)?;
242            print_index_header(&idx, header.page_number, verbose, writer)?;
243
244            wprintln!(writer)?;
245            print_fseg_headers(page_data, header.page_number, &idx, verbose, writer)?;
246
247            wprintln!(writer)?;
248            print_system_records(page_data, header.page_number, writer)?;
249        }
250    }
251
252    // BLOB page-specific headers (old-style)
253    if matches!(pt, PageType::Blob | PageType::ZBlob | PageType::ZBlob2) {
254        if let Some(blob_hdr) = BlobPageHeader::parse(page_data) {
255            wprintln!(writer)?;
256            wprintln!(writer, "=== BLOB Header: Page {}", header.page_number)?;
257            wprintln!(writer, "Data Length: {} bytes", blob_hdr.part_len)?;
258            if blob_hdr.has_next() {
259                wprintln!(writer, "Next BLOB Page: {}", blob_hdr.next_page_no)?;
260            } else {
261                wprintln!(writer, "Next BLOB Page: None (last in chain)")?;
262            }
263        }
264    }
265
266    // LOB first page header (MySQL 8.0+ new-style)
267    if pt == PageType::LobFirst {
268        if let Some(lob_hdr) = LobFirstPageHeader::parse(page_data) {
269            wprintln!(writer)?;
270            wprintln!(writer, "=== LOB First Page Header: Page {}", header.page_number)?;
271            wprintln!(writer, "Version: {}", lob_hdr.version)?;
272            wprintln!(writer, "Flags: {}", lob_hdr.flags)?;
273            wprintln!(writer, "Total Data Length: {} bytes", lob_hdr.data_len)?;
274            if lob_hdr.trx_id > 0 {
275                wprintln!(writer, "Transaction ID: {}", lob_hdr.trx_id)?;
276            }
277        }
278    }
279
280    // Undo log page-specific headers
281    if pt == PageType::UndoLog {
282        if let Some(undo_hdr) = UndoPageHeader::parse(page_data) {
283            wprintln!(writer)?;
284            wprintln!(writer, "=== UNDO Header: Page {}", header.page_number)?;
285            wprintln!(writer, "Undo Type: {} ({})", undo_hdr.page_type.name(), undo_hdr.page_type.name())?;
286            wprintln!(writer, "Log Start Offset: {}", undo_hdr.start)?;
287            wprintln!(writer, "Free Offset: {}", undo_hdr.free)?;
288            wprintln!(
289                writer,
290                "Used Bytes: {}",
291                undo_hdr.free.saturating_sub(undo_hdr.start)
292            )?;
293
294            if let Some(seg_hdr) = UndoSegmentHeader::parse(page_data) {
295                wprintln!(writer, "Segment State: {}", seg_hdr.state.name())?;
296                wprintln!(writer, "Last Log Offset: {}", seg_hdr.last_log)?;
297            }
298        }
299    }
300
301    // FIL Trailer
302    wprintln!(writer)?;
303    let ps = page_size as usize;
304    if page_data.len() >= ps {
305        let trailer_offset = ps - 8;
306        if let Some(trailer) =
307            crate::innodb::page::FilTrailer::parse(&page_data[trailer_offset..])
308        {
309            wprintln!(writer, "=== TRAILER: Page {}", header.page_number)?;
310            wprintln!(writer, "Old-style Checksum: {}", trailer.checksum)?;
311            wprintln!(writer, "Low 32 bits of LSN: {}", trailer.lsn_low32)?;
312            wprintln!(writer, "Byte End: {}", format_offset(byte_end))?;
313
314            if verbose {
315                let csum_result = checksum::validate_checksum(page_data, page_size);
316                let status = if csum_result.valid {
317                    "OK".green().to_string()
318                } else {
319                    "MISMATCH".red().to_string()
320                };
321                wprintln!(
322                    writer,
323                    "Checksum Status: {} ({:?})",
324                    status, csum_result.algorithm
325                )?;
326
327                let lsn_valid = checksum::validate_lsn(page_data, page_size);
328                let lsn_status = if lsn_valid {
329                    "OK".green().to_string()
330                } else {
331                    "MISMATCH".red().to_string()
332                };
333                wprintln!(writer, "LSN Consistency: {}", lsn_status)?;
334            }
335        }
336    }
337
338    Ok(())
339}
340
341/// Print the INDEX page header details.
342fn print_index_header(idx: &IndexHeader, page_num: u32, verbose: bool, writer: &mut dyn Write) -> Result<(), IdbError> {
343    wprintln!(writer, "=== INDEX Header: Page {}", page_num)?;
344    wprintln!(writer, "Index ID: {}", idx.index_id)?;
345    wprintln!(writer, "Node Level: {}", idx.level)?;
346
347    if idx.max_trx_id > 0 {
348        wprintln!(writer, "Max Transaction ID: {}", idx.max_trx_id)?;
349    } else {
350        wprintln!(writer, "-- Secondary Index")?;
351    }
352
353    wprintln!(writer, "Directory Slots: {}", idx.n_dir_slots)?;
354    if verbose {
355        wprintln!(writer, "-- Number of slots in page directory")?;
356    }
357
358    wprintln!(writer, "Heap Top: {}", idx.heap_top)?;
359    if verbose {
360        wprintln!(writer, "-- Pointer to record heap top")?;
361    }
362
363    wprintln!(writer, "Records in Page: {}", idx.n_recs)?;
364    wprintln!(
365        writer,
366        "Records in Heap: {} (compact: {})",
367        idx.n_heap(),
368        idx.is_compact()
369    )?;
370    if verbose {
371        wprintln!(writer, "-- Number of records in heap")?;
372    }
373
374    wprintln!(writer, "Start of Free Record List: {}", idx.free)?;
375    wprintln!(writer, "Garbage Bytes: {}", idx.garbage)?;
376    if verbose {
377        wprintln!(writer, "-- Number of bytes in deleted records.")?;
378    }
379
380    wprintln!(writer, "Last Insert: {}", idx.last_insert)?;
381    wprintln!(
382        writer,
383        "Last Insert Direction: {} - {}",
384        idx.direction,
385        idx.direction_name()
386    )?;
387    wprintln!(writer, "Inserts in this direction: {}", idx.n_direction)?;
388    if verbose {
389        wprintln!(writer, "-- Number of consecutive inserts in this direction.")?;
390    }
391
392    Ok(())
393}
394
395/// Print FSEG (file segment) header details.
396fn print_fseg_headers(page_data: &[u8], page_num: u32, idx: &IndexHeader, verbose: bool, writer: &mut dyn Write) -> Result<(), IdbError> {
397    wprintln!(writer, "=== FSEG_HDR - File Segment Header: Page {}", page_num)?;
398
399    if let Some(leaf) = FsegHeader::parse_leaf(page_data) {
400        wprintln!(writer, "Inode Space ID: {}", leaf.space_id)?;
401        wprintln!(writer, "Inode Page Number: {}", leaf.page_no)?;
402        wprintln!(writer, "Inode Offset: {}", leaf.offset)?;
403    }
404
405    if idx.is_leaf() {
406        if let Some(internal) = FsegHeader::parse_internal(page_data) {
407            wprintln!(writer, "Non-leaf Space ID: {}", internal.space_id)?;
408            if verbose {
409                wprintln!(writer, "Non-leaf Page Number: {}", internal.page_no)?;
410                wprintln!(writer, "Non-leaf Offset: {}", internal.offset)?;
411            }
412        }
413    }
414
415    Ok(())
416}
417
418/// Print system records (infimum/supremum) info.
419fn print_system_records(page_data: &[u8], page_num: u32, writer: &mut dyn Write) -> Result<(), IdbError> {
420    let sys = match SystemRecords::parse(page_data) {
421        Some(s) => s,
422        None => return Ok(()),
423    };
424
425    wprintln!(writer, "=== INDEX System Records: Page {}", page_num)?;
426    wprintln!(
427        writer,
428        "Index Record Status: {} - (Decimal: {}) {}",
429        sys.rec_status,
430        sys.rec_status,
431        sys.rec_status_name()
432    )?;
433    wprintln!(writer, "Number of records owned: {}", sys.n_owned)?;
434    wprintln!(writer, "Deleted: {}", if sys.deleted { "1" } else { "0" })?;
435    wprintln!(writer, "Heap Number: {}", sys.heap_no)?;
436    wprintln!(writer, "Next Record Offset (Infimum): {}", sys.infimum_next)?;
437    wprintln!(writer, "Next Record Offset (Supremum): {}", sys.supremum_next)?;
438    wprintln!(
439        writer,
440        "Left-most node on non-leaf level: {}",
441        if sys.min_rec { "1" } else { "0" }
442    )?;
443
444    Ok(())
445}
446
447/// Print detailed FSP header with additional fields.
448fn print_fsp_header_detail(fsp: &FspHeader, page0: &[u8], verbose: bool, writer: &mut dyn Write) -> Result<(), IdbError> {
449    wprintln!(writer, "=== File Header")?;
450    wprintln!(writer, "Space ID: {}", fsp.space_id)?;
451    if verbose {
452        wprintln!(writer, "-- Offset 38, Length 4")?;
453    }
454    wprintln!(writer, "Size: {}", fsp.size)?;
455    wprintln!(writer, "Flags: {}", fsp.flags)?;
456    wprintln!(writer, "Page Free Limit: {} (this should always be 64 on a single-table file)", fsp.free_limit)?;
457
458    // Compression and encryption detection from flags
459    let comp = compression::detect_compression(fsp.flags);
460    let enc = encryption::detect_encryption(fsp.flags);
461    if comp != compression::CompressionAlgorithm::None {
462        wprintln!(writer, "Compression: {}", comp)?;
463    }
464    if enc != encryption::EncryptionAlgorithm::None {
465        wprintln!(writer, "Encryption: {}", enc)?;
466    }
467
468    // Try to read the first unused segment ID (at FSP offset 72, 8 bytes)
469    let seg_id_offset = crate::innodb::constants::FIL_PAGE_DATA + 72;
470    if page0.len() >= seg_id_offset + 8 {
471        use byteorder::ByteOrder;
472        let seg_id = byteorder::BigEndian::read_u64(&page0[seg_id_offset..]);
473        wprintln!(writer, "First Unused Segment ID: {}", seg_id)?;
474    }
475
476    Ok(())
477}
478
479/// Check if a page type matches the user-provided filter string.
480///
481/// Matches against the page type name (case-insensitive). Supports
482/// short aliases like "index", "undo", "blob", "sdi", etc.
483fn matches_page_type_filter(page_type: &PageType, filter: &str) -> bool {
484    let filter_upper = filter.to_uppercase();
485    let type_name = page_type.name();
486
487    // Exact match on type name
488    if type_name == filter_upper {
489        return true;
490    }
491
492    // Common aliases and prefix matching
493    match filter_upper.as_str() {
494        "UNDO" => *page_type == PageType::UndoLog,
495        "BLOB" => matches!(page_type, PageType::Blob | PageType::ZBlob | PageType::ZBlob2),
496        "LOB" => matches!(page_type, PageType::LobIndex | PageType::LobData | PageType::LobFirst),
497        "SDI" => matches!(page_type, PageType::Sdi | PageType::SdiBlob),
498        "COMPRESSED" | "COMP" => matches!(
499            page_type,
500            PageType::Compressed | PageType::CompressedEncrypted
501        ),
502        "ENCRYPTED" | "ENC" => matches!(
503            page_type,
504            PageType::Encrypted | PageType::CompressedEncrypted | PageType::EncryptedRtree
505        ),
506        _ => type_name.contains(&filter_upper),
507    }
508}