Skip to main content

idb/cli/
pages.rs

1use std::io::Write;
2
3use colored::Colorize;
4use serde::Serialize;
5
6use crate::cli::{wprint, wprintln};
7use crate::innodb::checksum;
8use crate::innodb::compression;
9use crate::innodb::encryption;
10use crate::innodb::health::compute_fill_factor;
11use crate::innodb::index::{FsegHeader, IndexHeader, SystemRecords};
12use crate::innodb::lob::{BlobPageHeader, LobChainInfo, LobFirstPageHeader};
13use crate::innodb::page::{FilHeader, FspHeader};
14use crate::innodb::page_types::PageType;
15use crate::innodb::record::walk_compact_records;
16use crate::innodb::tablespace::Tablespace;
17use crate::innodb::undo::{UndoPageHeader, UndoSegmentHeader};
18use crate::util::hex::format_offset;
19use crate::IdbError;
20
21/// Options for the `inno pages` subcommand.
22pub struct PagesOptions {
23    /// Path to the InnoDB tablespace file (.ibd).
24    pub file: String,
25    /// If set, display only this specific page number.
26    pub page: Option<u64>,
27    /// Show additional detail (checksum status, FSEG internals).
28    pub verbose: bool,
29    /// Include empty/allocated pages in the output.
30    pub show_empty: bool,
31    /// Use compact one-line-per-page list format.
32    pub list_mode: bool,
33    /// Filter output to pages matching this type name (e.g. "INDEX", "UNDO").
34    pub filter_type: Option<String>,
35    /// Override the auto-detected page size.
36    pub page_size: Option<u32>,
37    /// Emit output as JSON.
38    pub json: bool,
39    /// Path to MySQL keyring file for decrypting encrypted tablespaces.
40    pub keyring: Option<String>,
41    /// Use memory-mapped I/O for file access.
42    pub mmap: bool,
43    /// Show delete-marked record statistics for INDEX pages.
44    pub deleted: bool,
45    /// Output as CSV.
46    pub csv: bool,
47    /// Traverse and display LOB chain details for BLOB/LOB first pages.
48    pub lob_chain: bool,
49}
50
51/// JSON-serializable detailed page info.
52#[derive(Serialize)]
53struct PageDetailJson {
54    page_number: u64,
55    header: FilHeader,
56    page_type_name: String,
57    page_type_description: String,
58    byte_start: u64,
59    byte_end: u64,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    index_header: Option<IndexHeader>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    fsp_header: Option<FspHeader>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    fill_factor: Option<f64>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    delete_marked_count: Option<usize>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    total_record_count: Option<usize>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    delete_marked_pct: Option<f64>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    lob_chain: Option<LobChainInfo>,
74}
75
76/// Perform deep structural analysis of pages in an InnoDB tablespace.
77///
78/// Unlike `parse` which only decodes FIL headers, this command dives into
79/// page-type-specific internal structures:
80///
81/// - **INDEX pages** (type 17855): Decodes the index header (index ID, B+Tree
82///   level, record counts, heap top, garbage bytes, insert direction), FSEG
83///   inode pointers for leaf and non-leaf segments, and infimum/supremum system
84///   record metadata.
85/// - **UNDO pages** (type 2): Shows the undo page header (type, start/free
86///   offsets, used bytes) and segment header (state, last log offset).
87/// - **BLOB/ZBLOB pages** (types 10, 11, 12): Shows data length and next-page
88///   chain pointer for old-style externally stored columns.
89/// - **LOB_FIRST pages** (MySQL 8.0+): Shows version, flags, total data length,
90///   and transaction ID for new-style LOB first pages.
91/// - **Page 0** (FSP_HDR): Shows extended FSP header fields including
92///   compression algorithm, encryption flags, and first unused segment ID.
93///
94/// In **list mode** (`-l`), output is a compact one-line-per-page summary
95/// showing page number, type, description, and byte offset. In **detail mode**
96/// (the default), each page gets a full multi-section breakdown. Use `-t` to
97/// filter by page type name (supports aliases like "undo", "blob", "lob",
98/// "sdi", "compressed", "encrypted").
99pub fn execute(opts: &PagesOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
100    let mut ts = crate::cli::open_tablespace(&opts.file, opts.page_size, opts.mmap)?;
101
102    if let Some(ref keyring_path) = opts.keyring {
103        crate::cli::setup_decryption(&mut ts, keyring_path)?;
104    }
105
106    let page_size = ts.page_size();
107
108    if opts.json {
109        return execute_json(opts, &mut ts, page_size, writer);
110    }
111
112    if opts.csv {
113        return execute_csv(opts, &mut ts, page_size, writer);
114    }
115
116    if let Some(page_num) = opts.page {
117        let page_data = ts.read_page(page_num)?;
118        print_full_page(
119            &page_data,
120            page_num,
121            page_size,
122            opts.verbose,
123            opts.deleted,
124            writer,
125        )?;
126        if opts.lob_chain {
127            print_lob_chain_if_applicable(&page_data, page_num, &mut ts, writer)?;
128        }
129        return Ok(());
130    }
131
132    // Print FSP header unless filtering by type
133    if opts.filter_type.is_none() {
134        let page0 = ts.read_page(0)?;
135        if let Some(fsp) = FspHeader::parse(&page0) {
136            print_fsp_header_detail(&fsp, &page0, opts.verbose, ts.vendor_info(), writer)?;
137        }
138    }
139
140    for page_num in 0..ts.page_count() {
141        let page_data = ts.read_page(page_num)?;
142        let header = match FilHeader::parse(&page_data) {
143            Some(h) => h,
144            None => continue,
145        };
146
147        // Skip empty pages unless --show-empty
148        if !opts.show_empty && header.checksum == 0 && header.page_type == PageType::Allocated {
149            continue;
150        }
151
152        // Filter by type
153        if let Some(ref filter) = opts.filter_type {
154            if !matches_page_type_filter(&header.page_type, filter) {
155                continue;
156            }
157        }
158
159        if opts.list_mode {
160            print_list_line(&page_data, page_num, page_size, opts.deleted, writer)?;
161        } else {
162            print_full_page(
163                &page_data,
164                page_num,
165                page_size,
166                opts.verbose,
167                opts.deleted,
168                writer,
169            )?;
170            if opts.lob_chain {
171                print_lob_chain_if_applicable(&page_data, page_num, &mut ts, writer)?;
172            }
173        }
174    }
175
176    Ok(())
177}
178
179/// Execute pages in CSV output mode.
180fn execute_csv(
181    opts: &PagesOptions,
182    ts: &mut Tablespace,
183    page_size: u32,
184    writer: &mut dyn Write,
185) -> Result<(), IdbError> {
186    if opts.lob_chain {
187        wprintln!(
188            writer,
189            "page_number,page_type,byte_start,index_id,fill_factor,lob_chain_type,lob_chain_pages,lob_chain_total_bytes"
190        )?;
191    } else {
192        wprintln!(
193            writer,
194            "page_number,page_type,byte_start,index_id,fill_factor"
195        )?;
196    }
197
198    let range: Box<dyn Iterator<Item = u64>> = if let Some(p) = opts.page {
199        Box::new(std::iter::once(p))
200    } else {
201        Box::new(0..ts.page_count())
202    };
203
204    for page_num in range {
205        let page_data = ts.read_page(page_num)?;
206        let header = match FilHeader::parse(&page_data) {
207            Some(h) => h,
208            None => continue,
209        };
210
211        if !opts.show_empty && header.checksum == 0 && header.page_type == PageType::Allocated {
212            continue;
213        }
214
215        if let Some(ref filter) = opts.filter_type {
216            if !matches_page_type_filter(&header.page_type, filter) {
217                continue;
218            }
219        }
220
221        let pt = header.page_type;
222        let byte_start = page_num * page_size as u64;
223
224        let (index_id, fill_factor) = if pt == PageType::Index {
225            let idx = IndexHeader::parse(&page_data);
226            match idx {
227                Some(i) => {
228                    let ff = compute_fill_factor(i.heap_top, i.garbage, page_size);
229                    (Some(i.index_id), Some(format!("{:.4}", ff)))
230                }
231                None => (None, None),
232            }
233        } else {
234            (None, None)
235        };
236
237        if opts.lob_chain {
238            let (lob_type, lob_pages, lob_bytes) = if matches!(
239                pt,
240                PageType::Blob
241                    | PageType::ZBlob
242                    | PageType::ZBlob2
243                    | PageType::LobFirst
244                    | PageType::ZlobFirst
245            ) {
246                match crate::innodb::lob::walk_lob_chain(ts, page_num, 10000) {
247                    Ok(Some(chain)) => (
248                        chain.chain_type,
249                        chain.page_count.to_string(),
250                        chain.total_data_len.to_string(),
251                    ),
252                    _ => (String::new(), String::new(), String::new()),
253                }
254            } else {
255                (String::new(), String::new(), String::new())
256            };
257            wprintln!(
258                writer,
259                "{},{},{},{},{},{},{},{}",
260                page_num,
261                crate::cli::csv_escape(pt.name()),
262                byte_start,
263                index_id.map(|id| id.to_string()).unwrap_or_default(),
264                fill_factor.unwrap_or_default(),
265                crate::cli::csv_escape(&lob_type),
266                lob_pages,
267                lob_bytes
268            )?;
269        } else {
270            wprintln!(
271                writer,
272                "{},{},{},{},{}",
273                page_num,
274                crate::cli::csv_escape(pt.name()),
275                byte_start,
276                index_id.map(|id| id.to_string()).unwrap_or_default(),
277                fill_factor.unwrap_or_default()
278            )?;
279        }
280    }
281    Ok(())
282}
283
284/// Execute pages in JSON output mode.
285fn execute_json(
286    opts: &PagesOptions,
287    ts: &mut Tablespace,
288    page_size: u32,
289    writer: &mut dyn Write,
290) -> Result<(), IdbError> {
291    let mut pages = Vec::new();
292
293    let range: Box<dyn Iterator<Item = u64>> = if let Some(p) = opts.page {
294        Box::new(std::iter::once(p))
295    } else {
296        Box::new(0..ts.page_count())
297    };
298
299    for page_num in range {
300        let page_data = ts.read_page(page_num)?;
301        let header = match FilHeader::parse(&page_data) {
302            Some(h) => h,
303            None => continue,
304        };
305
306        if !opts.show_empty && header.checksum == 0 && header.page_type == PageType::Allocated {
307            continue;
308        }
309
310        if let Some(ref filter) = opts.filter_type {
311            if !matches_page_type_filter(&header.page_type, filter) {
312                continue;
313            }
314        }
315
316        let pt = header.page_type;
317        let byte_start = page_num * page_size as u64;
318
319        let index_header = if pt == PageType::Index {
320            IndexHeader::parse(&page_data)
321        } else {
322            None
323        };
324
325        let fill_factor = index_header.as_ref().map(|idx| {
326            (compute_fill_factor(idx.heap_top, idx.garbage, page_size) * 10000.0).round() / 10000.0
327        });
328
329        let (delete_marked_count, total_record_count, delete_marked_pct) =
330            if pt == PageType::Index && opts.deleted {
331                let recs = walk_compact_records(&page_data);
332                let total = recs.len();
333                let deleted = recs.iter().filter(|r| r.header.delete_mark()).count();
334                let pct = if total > 0 {
335                    (deleted as f64 / total as f64 * 10000.0).round() / 100.0
336                } else {
337                    0.0
338                };
339                (Some(deleted), Some(total), Some(pct))
340            } else {
341                (None, None, None)
342            };
343
344        let fsp_header = if page_num == 0 {
345            FspHeader::parse(&page_data)
346        } else {
347            None
348        };
349
350        let lob_chain = if opts.lob_chain
351            && matches!(
352                pt,
353                PageType::Blob
354                    | PageType::ZBlob
355                    | PageType::ZBlob2
356                    | PageType::LobFirst
357                    | PageType::ZlobFirst
358            ) {
359            crate::innodb::lob::walk_lob_chain(ts, page_num, 10000)
360                .ok()
361                .flatten()
362        } else {
363            None
364        };
365
366        pages.push(PageDetailJson {
367            page_number: page_num,
368            page_type_name: pt.name().to_string(),
369            page_type_description: pt.description().to_string(),
370            byte_start,
371            byte_end: byte_start + page_size as u64,
372            header,
373            index_header,
374            fsp_header,
375            fill_factor,
376            delete_marked_count,
377            total_record_count,
378            delete_marked_pct,
379            lob_chain,
380        });
381    }
382
383    let json = serde_json::to_string_pretty(&pages)
384        .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
385    wprintln!(writer, "{}", json)?;
386    Ok(())
387}
388
389/// Print a compact one-line summary per page (list mode).
390fn print_list_line(
391    page_data: &[u8],
392    page_num: u64,
393    page_size: u32,
394    show_deleted: bool,
395    writer: &mut dyn Write,
396) -> Result<(), IdbError> {
397    let header = match FilHeader::parse(page_data) {
398        Some(h) => h,
399        None => return Ok(()),
400    };
401
402    let pt = header.page_type;
403    let byte_start = page_num * page_size as u64;
404
405    wprint!(
406        writer,
407        "-- Page {} - {}: {}",
408        page_num,
409        pt.name(),
410        pt.description()
411    )?;
412
413    if pt == PageType::Index {
414        if let Some(idx) = IndexHeader::parse(page_data) {
415            wprint!(writer, ", Index ID: {}", idx.index_id)?;
416
417            let ff = compute_fill_factor(idx.heap_top, idx.garbage, page_size);
418            let pct = ff * 100.0;
419            let fill_str = if pct >= 80.0 {
420                format!("{:.1}%", pct).green().to_string()
421            } else if pct >= 50.0 {
422                format!("{:.1}%", pct).yellow().to_string()
423            } else {
424                format!("{:.1}%", pct).red().to_string()
425            };
426            wprint!(writer, ", Fill: {}", fill_str)?;
427
428            if show_deleted {
429                let recs = walk_compact_records(page_data);
430                let total = recs.len();
431                let deleted = recs.iter().filter(|r| r.header.delete_mark()).count();
432                if total > 0 {
433                    let del_pct = deleted as f64 / total as f64 * 100.0;
434                    wprint!(writer, " (del: {}/{}, {:.1}%)", deleted, total, del_pct)?;
435                }
436            }
437        }
438    }
439
440    wprintln!(writer, ", Byte Start: {}", format_offset(byte_start))?;
441    Ok(())
442}
443
444/// Print full detailed information about a page.
445fn print_full_page(
446    page_data: &[u8],
447    page_num: u64,
448    page_size: u32,
449    verbose: bool,
450    show_deleted: bool,
451    writer: &mut dyn Write,
452) -> Result<(), IdbError> {
453    let header = match FilHeader::parse(page_data) {
454        Some(h) => h,
455        None => {
456            eprintln!("Could not parse FIL header for page {}", page_num);
457            return Ok(());
458        }
459    };
460
461    let byte_start = page_num * page_size as u64;
462    let byte_end = byte_start + page_size as u64;
463    let pt = header.page_type;
464
465    // FIL Header
466    wprintln!(writer)?;
467    wprintln!(writer, "=== HEADER: Page {}", header.page_number)?;
468    wprintln!(writer, "Byte Start: {}", format_offset(byte_start))?;
469    wprintln!(
470        writer,
471        "Page Type: {}\n-- {}: {} - {}",
472        pt.as_u16(),
473        pt.name(),
474        pt.description(),
475        pt.usage()
476    )?;
477
478    wprint!(writer, "Prev Page: ")?;
479    if !header.has_prev() {
480        wprintln!(writer, "Not used.")?;
481    } else {
482        wprintln!(writer, "{}", header.prev_page)?;
483    }
484
485    wprint!(writer, "Next Page: ")?;
486    if !header.has_next() {
487        wprintln!(writer, "Not used.")?;
488    } else {
489        wprintln!(writer, "{}", header.next_page)?;
490    }
491
492    wprintln!(writer, "LSN: {}", header.lsn)?;
493    wprintln!(writer, "Space ID: {}", header.space_id)?;
494    wprintln!(writer, "Checksum: {}", header.checksum)?;
495
496    // INDEX-specific headers
497    if pt == PageType::Index {
498        if let Some(idx) = IndexHeader::parse(page_data) {
499            wprintln!(writer)?;
500            print_index_header(&idx, header.page_number, verbose, writer)?;
501
502            // Fill factor
503            let ff = compute_fill_factor(idx.heap_top, idx.garbage, page_size);
504            let pct = ff * 100.0;
505            let fill_str = if pct >= 80.0 {
506                format!("{:.1}%", pct).green().to_string()
507            } else if pct >= 50.0 {
508                format!("{:.1}%", pct).yellow().to_string()
509            } else {
510                format!("{:.1}%", pct).red().to_string()
511            };
512            wprintln!(writer, "Fill Factor: {}", fill_str)?;
513
514            // Delete-marked record stats
515            if show_deleted {
516                let recs = walk_compact_records(page_data);
517                let total = recs.len();
518                let deleted = recs.iter().filter(|r| r.header.delete_mark()).count();
519                wprintln!(writer)?;
520                wprintln!(
521                    writer,
522                    "=== Delete-Marked Records: Page {}",
523                    header.page_number
524                )?;
525                wprintln!(writer, "Total Records: {}", total)?;
526                wprintln!(writer, "Delete-Marked: {}", deleted)?;
527                if total > 0 {
528                    let del_pct = deleted as f64 / total as f64 * 100.0;
529                    wprintln!(writer, "Delete-Marked Ratio: {:.1}%", del_pct)?;
530                }
531            }
532
533            wprintln!(writer)?;
534            print_fseg_headers(page_data, header.page_number, &idx, verbose, writer)?;
535
536            wprintln!(writer)?;
537            print_system_records(page_data, header.page_number, writer)?;
538        }
539    }
540
541    // RTREE page-specific detail
542    if pt == PageType::Rtree {
543        if let Some(info) = crate::innodb::rtree::parse_rtree_page(page_data) {
544            wprintln!(writer)?;
545            wprintln!(writer, "=== RTREE Detail: Page {}", header.page_number)?;
546            wprintln!(
547                writer,
548                "Level: {} ({})",
549                info.level,
550                if info.level == 0 { "leaf" } else { "non-leaf" }
551            )?;
552            wprintln!(writer, "Records: {}", info.record_count)?;
553            wprintln!(writer, "MBRs Extracted: {}", info.mbrs.len())?;
554            if let Some(ref enc) = info.enclosing_mbr {
555                wprintln!(
556                    writer,
557                    "MBR Coverage: ({:.6}, {:.6}) \u{2014} ({:.6}, {:.6})",
558                    enc.min_x,
559                    enc.min_y,
560                    enc.max_x,
561                    enc.max_y
562                )?;
563                wprintln!(writer, "MBR Area: {:.6}", enc.area())?;
564            }
565            if verbose {
566                for (i, mbr) in info.mbrs.iter().enumerate() {
567                    wprintln!(
568                        writer,
569                        "  [{:>3}] ({:.6}, {:.6}) \u{2014} ({:.6}, {:.6})  area={:.6}",
570                        i,
571                        mbr.min_x,
572                        mbr.min_y,
573                        mbr.max_x,
574                        mbr.max_y,
575                        mbr.area()
576                    )?;
577                }
578            }
579        }
580    }
581
582    // BLOB page-specific headers (old-style)
583    if matches!(pt, PageType::Blob | PageType::ZBlob | PageType::ZBlob2) {
584        if let Some(blob_hdr) = BlobPageHeader::parse(page_data) {
585            wprintln!(writer)?;
586            wprintln!(writer, "=== BLOB Header: Page {}", header.page_number)?;
587            wprintln!(writer, "Data Length: {} bytes", blob_hdr.part_len)?;
588            if blob_hdr.has_next() {
589                wprintln!(writer, "Next BLOB Page: {}", blob_hdr.next_page_no)?;
590            } else {
591                wprintln!(writer, "Next BLOB Page: None (last in chain)")?;
592            }
593        }
594    }
595
596    // LOB first page header (MySQL 8.0+ new-style)
597    if pt == PageType::LobFirst {
598        if let Some(lob_hdr) = LobFirstPageHeader::parse(page_data) {
599            wprintln!(writer)?;
600            wprintln!(
601                writer,
602                "=== LOB First Page Header: Page {}",
603                header.page_number
604            )?;
605            wprintln!(writer, "Version: {}", lob_hdr.version)?;
606            wprintln!(writer, "Flags: {}", lob_hdr.flags)?;
607            wprintln!(writer, "Total Data Length: {} bytes", lob_hdr.data_len)?;
608            if lob_hdr.trx_id > 0 {
609                wprintln!(writer, "Transaction ID: {}", lob_hdr.trx_id)?;
610            }
611        }
612    }
613
614    // Undo log page-specific headers
615    if pt == PageType::UndoLog {
616        if let Some(undo_hdr) = UndoPageHeader::parse(page_data) {
617            wprintln!(writer)?;
618            wprintln!(writer, "=== UNDO Header: Page {}", header.page_number)?;
619            wprintln!(
620                writer,
621                "Undo Type: {} ({})",
622                undo_hdr.page_type.name(),
623                undo_hdr.page_type.name()
624            )?;
625            wprintln!(writer, "Log Start Offset: {}", undo_hdr.start)?;
626            wprintln!(writer, "Free Offset: {}", undo_hdr.free)?;
627            wprintln!(
628                writer,
629                "Used Bytes: {}",
630                undo_hdr.free.saturating_sub(undo_hdr.start)
631            )?;
632
633            if let Some(seg_hdr) = UndoSegmentHeader::parse(page_data) {
634                wprintln!(writer, "Segment State: {}", seg_hdr.state.name())?;
635                wprintln!(writer, "Last Log Offset: {}", seg_hdr.last_log)?;
636            }
637        }
638    }
639
640    // FIL Trailer
641    wprintln!(writer)?;
642    let ps = page_size as usize;
643    if page_data.len() >= ps {
644        let trailer_offset = ps - 8;
645        if let Some(trailer) = crate::innodb::page::FilTrailer::parse(&page_data[trailer_offset..])
646        {
647            wprintln!(writer, "=== TRAILER: Page {}", header.page_number)?;
648            wprintln!(writer, "Old-style Checksum: {}", trailer.checksum)?;
649            wprintln!(writer, "Low 32 bits of LSN: {}", trailer.lsn_low32)?;
650            wprintln!(writer, "Byte End: {}", format_offset(byte_end))?;
651
652            if verbose {
653                let csum_result = checksum::validate_checksum(page_data, page_size, None);
654                let status = if csum_result.valid {
655                    "OK".green().to_string()
656                } else {
657                    "MISMATCH".red().to_string()
658                };
659                wprintln!(
660                    writer,
661                    "Checksum Status: {} ({:?})",
662                    status,
663                    csum_result.algorithm
664                )?;
665
666                let lsn_valid = checksum::validate_lsn(page_data, page_size);
667                let lsn_status = if lsn_valid {
668                    "OK".green().to_string()
669                } else {
670                    "MISMATCH".red().to_string()
671                };
672                wprintln!(writer, "LSN Consistency: {}", lsn_status)?;
673            }
674        }
675    }
676
677    Ok(())
678}
679
680/// Print the INDEX page header details.
681fn print_index_header(
682    idx: &IndexHeader,
683    page_num: u32,
684    verbose: bool,
685    writer: &mut dyn Write,
686) -> Result<(), IdbError> {
687    wprintln!(writer, "=== INDEX Header: Page {}", page_num)?;
688    wprintln!(writer, "Index ID: {}", idx.index_id)?;
689    wprintln!(writer, "Node Level: {}", idx.level)?;
690
691    if idx.max_trx_id > 0 {
692        wprintln!(writer, "Max Transaction ID: {}", idx.max_trx_id)?;
693    } else {
694        wprintln!(writer, "-- Secondary Index")?;
695    }
696
697    wprintln!(writer, "Directory Slots: {}", idx.n_dir_slots)?;
698    if verbose {
699        wprintln!(writer, "-- Number of slots in page directory")?;
700    }
701
702    wprintln!(writer, "Heap Top: {}", idx.heap_top)?;
703    if verbose {
704        wprintln!(writer, "-- Pointer to record heap top")?;
705    }
706
707    wprintln!(writer, "Records in Page: {}", idx.n_recs)?;
708    wprintln!(
709        writer,
710        "Records in Heap: {} (compact: {})",
711        idx.n_heap(),
712        idx.is_compact()
713    )?;
714    if verbose {
715        wprintln!(writer, "-- Number of records in heap")?;
716    }
717
718    wprintln!(writer, "Start of Free Record List: {}", idx.free)?;
719    wprintln!(writer, "Garbage Bytes: {}", idx.garbage)?;
720    if verbose {
721        wprintln!(writer, "-- Number of bytes in deleted records.")?;
722    }
723
724    wprintln!(writer, "Last Insert: {}", idx.last_insert)?;
725    wprintln!(
726        writer,
727        "Last Insert Direction: {} - {}",
728        idx.direction,
729        idx.direction_name()
730    )?;
731    wprintln!(writer, "Inserts in this direction: {}", idx.n_direction)?;
732    if verbose {
733        wprintln!(
734            writer,
735            "-- Number of consecutive inserts in this direction."
736        )?;
737    }
738
739    Ok(())
740}
741
742/// Print FSEG (file segment) header details.
743fn print_fseg_headers(
744    page_data: &[u8],
745    page_num: u32,
746    idx: &IndexHeader,
747    verbose: bool,
748    writer: &mut dyn Write,
749) -> Result<(), IdbError> {
750    wprintln!(
751        writer,
752        "=== FSEG_HDR - File Segment Header: Page {}",
753        page_num
754    )?;
755
756    if let Some(leaf) = FsegHeader::parse_leaf(page_data) {
757        wprintln!(writer, "Inode Space ID: {}", leaf.space_id)?;
758        wprintln!(writer, "Inode Page Number: {}", leaf.page_no)?;
759        wprintln!(writer, "Inode Offset: {}", leaf.offset)?;
760    }
761
762    if idx.is_leaf() {
763        if let Some(internal) = FsegHeader::parse_internal(page_data) {
764            wprintln!(writer, "Non-leaf Space ID: {}", internal.space_id)?;
765            if verbose {
766                wprintln!(writer, "Non-leaf Page Number: {}", internal.page_no)?;
767                wprintln!(writer, "Non-leaf Offset: {}", internal.offset)?;
768            }
769        }
770    }
771
772    Ok(())
773}
774
775/// Print system records (infimum/supremum) info.
776fn print_system_records(
777    page_data: &[u8],
778    page_num: u32,
779    writer: &mut dyn Write,
780) -> Result<(), IdbError> {
781    let sys = match SystemRecords::parse(page_data) {
782        Some(s) => s,
783        None => return Ok(()),
784    };
785
786    wprintln!(writer, "=== INDEX System Records: Page {}", page_num)?;
787    wprintln!(
788        writer,
789        "Index Record Status: {} - (Decimal: {}) {}",
790        sys.rec_status,
791        sys.rec_status,
792        sys.rec_status_name()
793    )?;
794    wprintln!(writer, "Number of records owned: {}", sys.n_owned)?;
795    wprintln!(writer, "Deleted: {}", if sys.deleted { "1" } else { "0" })?;
796    wprintln!(writer, "Heap Number: {}", sys.heap_no)?;
797    wprintln!(writer, "Next Record Offset (Infimum): {}", sys.infimum_next)?;
798    wprintln!(
799        writer,
800        "Next Record Offset (Supremum): {}",
801        sys.supremum_next
802    )?;
803    wprintln!(
804        writer,
805        "Left-most node on non-leaf level: {}",
806        if sys.min_rec { "1" } else { "0" }
807    )?;
808
809    Ok(())
810}
811
812/// Print detailed FSP header with additional fields.
813fn print_fsp_header_detail(
814    fsp: &FspHeader,
815    page0: &[u8],
816    verbose: bool,
817    vendor_info: &crate::innodb::vendor::VendorInfo,
818    writer: &mut dyn Write,
819) -> Result<(), IdbError> {
820    wprintln!(writer, "=== File Header")?;
821    wprintln!(writer, "Vendor: {}", vendor_info)?;
822    wprintln!(writer, "Space ID: {}", fsp.space_id)?;
823    if verbose {
824        wprintln!(writer, "-- Offset 38, Length 4")?;
825    }
826    wprintln!(writer, "Size: {}", fsp.size)?;
827    wprintln!(writer, "Flags: {}", fsp.flags)?;
828    wprintln!(
829        writer,
830        "Page Free Limit: {} (this should always be 64 on a single-table file)",
831        fsp.free_limit
832    )?;
833
834    // Compression and encryption detection from flags
835    let comp = compression::detect_compression(fsp.flags, Some(vendor_info));
836    let enc = encryption::detect_encryption(fsp.flags, Some(vendor_info));
837    if comp != compression::CompressionAlgorithm::None {
838        wprintln!(writer, "Compression: {}", comp)?;
839    }
840    if enc != encryption::EncryptionAlgorithm::None {
841        wprintln!(writer, "Encryption: {}", enc)?;
842
843        // Display detailed encryption info if available
844        if let Some(info) = encryption::parse_encryption_info(
845            page0,
846            fsp.page_size_from_flags_with_vendor(vendor_info),
847        ) {
848            let version_desc = match info.magic_version {
849                1 => "V1",
850                2 => "V2",
851                3 => "V3 (MySQL 8.0.5+)",
852                _ => "Unknown",
853            };
854            wprintln!(writer, "  Master Key ID: {}", info.master_key_id)?;
855            wprintln!(writer, "  Server UUID:   {}", info.server_uuid)?;
856            wprintln!(writer, "  Magic:         {}", version_desc)?;
857        }
858    }
859
860    // Try to read the first unused segment ID (at FSP offset 72, 8 bytes)
861    let seg_id_offset = crate::innodb::constants::FIL_PAGE_DATA + 72;
862    if page0.len() >= seg_id_offset + 8 {
863        use byteorder::ByteOrder;
864        let seg_id = byteorder::BigEndian::read_u64(&page0[seg_id_offset..]);
865        wprintln!(writer, "First Unused Segment ID: {}", seg_id)?;
866    }
867
868    Ok(())
869}
870
871/// Print LOB chain information if the given page is a BLOB/LOB first page.
872fn print_lob_chain_if_applicable(
873    page_data: &[u8],
874    page_num: u64,
875    ts: &mut Tablespace,
876    writer: &mut dyn Write,
877) -> Result<(), IdbError> {
878    use crate::innodb::lob;
879
880    let header = match FilHeader::parse(page_data) {
881        Some(h) => h,
882        None => return Ok(()),
883    };
884
885    let is_lob_start = matches!(
886        header.page_type,
887        PageType::Blob | PageType::ZBlob | PageType::LobFirst
888    );
889
890    // Only start chain traversal from chain-start pages
891    if !is_lob_start {
892        return Ok(());
893    }
894
895    // For old-style BLOB, only start from the first page (no prev pointer
896    // indicator in the header, so we always traverse from this page forward).
897    // For LobFirst, it is by definition the first page.
898
899    match lob::walk_lob_chain(ts, page_num, 10000) {
900        Ok(Some(chain)) => {
901            wprintln!(writer)?;
902            wprintln!(
903                writer,
904                "=== LOB Chain: Page {} ({}, {} pages, {} bytes total)",
905                page_num,
906                chain.chain_type,
907                chain.page_count,
908                chain.total_data_len
909            )?;
910            for (i, cp) in chain.pages.iter().enumerate() {
911                wprintln!(
912                    writer,
913                    "  [{:>3}] Page {:<8} {:.<20} {} bytes",
914                    i,
915                    cp.page_no,
916                    cp.page_type,
917                    cp.data_len
918                )?;
919            }
920        }
921        Ok(None) => {}
922        Err(e) => {
923            wprintln!(writer, "  LOB chain error: {}", e)?;
924        }
925    }
926
927    Ok(())
928}
929
930/// Check if a page type matches the user-provided filter string.
931///
932/// Matches against the page type name (case-insensitive). Supports
933/// short aliases like "index", "undo", "blob", "sdi", etc.
934fn matches_page_type_filter(page_type: &PageType, filter: &str) -> bool {
935    let filter_upper = filter.to_uppercase();
936    let type_name = page_type.name();
937
938    // Exact match on type name
939    if type_name == filter_upper {
940        return true;
941    }
942
943    // Common aliases and prefix matching
944    match filter_upper.as_str() {
945        "UNDO" => *page_type == PageType::UndoLog,
946        "BLOB" => matches!(
947            page_type,
948            PageType::Blob | PageType::ZBlob | PageType::ZBlob2
949        ),
950        "LOB" => matches!(
951            page_type,
952            PageType::LobIndex
953                | PageType::LobData
954                | PageType::LobFirst
955                | PageType::ZlobFirst
956                | PageType::ZlobData
957                | PageType::ZlobIndex
958                | PageType::ZlobFrag
959                | PageType::ZlobFragEntry
960        ),
961        "ZLOB" => matches!(
962            page_type,
963            PageType::ZlobFirst
964                | PageType::ZlobData
965                | PageType::ZlobIndex
966                | PageType::ZlobFrag
967                | PageType::ZlobFragEntry
968        ),
969        "SDI" => matches!(
970            page_type,
971            PageType::Sdi | PageType::SdiBlob | PageType::SdiZblob
972        ),
973        "COMPRESSED" | "COMP" => matches!(
974            page_type,
975            PageType::Compressed
976                | PageType::CompressedEncrypted
977                | PageType::PageCompressed
978                | PageType::PageCompressedEncrypted
979        ),
980        "ENCRYPTED" | "ENC" => matches!(
981            page_type,
982            PageType::Encrypted
983                | PageType::CompressedEncrypted
984                | PageType::EncryptedRtree
985                | PageType::PageCompressedEncrypted
986        ),
987        "INSTANT" => *page_type == PageType::Instant,
988        _ => type_name.contains(&filter_upper),
989    }
990}