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