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