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    wprintln!(
160        writer,
161        "{}",
162        serde_json::to_string_pretty(&pages).unwrap_or_else(|_| "[]".to_string())
163    )?;
164    Ok(())
165}
166
167/// Print a compact one-line summary per page (list mode).
168fn print_list_line(page_data: &[u8], page_num: u64, page_size: u32, writer: &mut dyn Write) -> Result<(), IdbError> {
169    let header = match FilHeader::parse(page_data) {
170        Some(h) => h,
171        None => return Ok(()),
172    };
173
174    let pt = header.page_type;
175    let byte_start = page_num * page_size as u64;
176
177    wprint!(
178        writer,
179        "-- Page {} - {}: {}",
180        page_num,
181        pt.name(),
182        pt.description()
183    )?;
184
185    if pt == PageType::Index {
186        if let Some(idx) = IndexHeader::parse(page_data) {
187            wprint!(writer, ", Index ID: {}", idx.index_id)?;
188        }
189    }
190
191    wprintln!(writer, ", Byte Start: {}", format_offset(byte_start))?;
192    Ok(())
193}
194
195/// Print full detailed information about a page.
196fn print_full_page(page_data: &[u8], page_num: u64, page_size: u32, verbose: bool, writer: &mut dyn Write) -> Result<(), IdbError> {
197    let header = match FilHeader::parse(page_data) {
198        Some(h) => h,
199        None => {
200            eprintln!("Could not parse FIL header for page {}", page_num);
201            return Ok(());
202        }
203    };
204
205    let byte_start = page_num * page_size as u64;
206    let byte_end = byte_start + page_size as u64;
207    let pt = header.page_type;
208
209    // FIL Header
210    wprintln!(writer)?;
211    wprintln!(writer, "=== HEADER: Page {}", header.page_number)?;
212    wprintln!(writer, "Byte Start: {}", format_offset(byte_start))?;
213    wprintln!(
214        writer,
215        "Page Type: {}\n-- {}: {} - {}",
216        pt.as_u16(),
217        pt.name(),
218        pt.description(),
219        pt.usage()
220    )?;
221
222    wprint!(writer, "Prev Page: ")?;
223    if !header.has_prev() {
224        wprintln!(writer, "Not used.")?;
225    } else {
226        wprintln!(writer, "{}", header.prev_page)?;
227    }
228
229    wprint!(writer, "Next Page: ")?;
230    if !header.has_next() {
231        wprintln!(writer, "Not used.")?;
232    } else {
233        wprintln!(writer, "{}", header.next_page)?;
234    }
235
236    wprintln!(writer, "LSN: {}", header.lsn)?;
237    wprintln!(writer, "Space ID: {}", header.space_id)?;
238    wprintln!(writer, "Checksum: {}", header.checksum)?;
239
240    // INDEX-specific headers
241    if pt == PageType::Index {
242        if let Some(idx) = IndexHeader::parse(page_data) {
243            wprintln!(writer)?;
244            print_index_header(&idx, header.page_number, verbose, writer)?;
245
246            wprintln!(writer)?;
247            print_fseg_headers(page_data, header.page_number, &idx, verbose, writer)?;
248
249            wprintln!(writer)?;
250            print_system_records(page_data, header.page_number, writer)?;
251        }
252    }
253
254    // BLOB page-specific headers (old-style)
255    if matches!(pt, PageType::Blob | PageType::ZBlob | PageType::ZBlob2) {
256        if let Some(blob_hdr) = BlobPageHeader::parse(page_data) {
257            wprintln!(writer)?;
258            wprintln!(writer, "=== BLOB Header: Page {}", header.page_number)?;
259            wprintln!(writer, "Data Length: {} bytes", blob_hdr.part_len)?;
260            if blob_hdr.has_next() {
261                wprintln!(writer, "Next BLOB Page: {}", blob_hdr.next_page_no)?;
262            } else {
263                wprintln!(writer, "Next BLOB Page: None (last in chain)")?;
264            }
265        }
266    }
267
268    // LOB first page header (MySQL 8.0+ new-style)
269    if pt == PageType::LobFirst {
270        if let Some(lob_hdr) = LobFirstPageHeader::parse(page_data) {
271            wprintln!(writer)?;
272            wprintln!(writer, "=== LOB First Page Header: Page {}", header.page_number)?;
273            wprintln!(writer, "Version: {}", lob_hdr.version)?;
274            wprintln!(writer, "Flags: {}", lob_hdr.flags)?;
275            wprintln!(writer, "Total Data Length: {} bytes", lob_hdr.data_len)?;
276            if lob_hdr.trx_id > 0 {
277                wprintln!(writer, "Transaction ID: {}", lob_hdr.trx_id)?;
278            }
279        }
280    }
281
282    // Undo log page-specific headers
283    if pt == PageType::UndoLog {
284        if let Some(undo_hdr) = UndoPageHeader::parse(page_data) {
285            wprintln!(writer)?;
286            wprintln!(writer, "=== UNDO Header: Page {}", header.page_number)?;
287            wprintln!(writer, "Undo Type: {} ({})", undo_hdr.page_type.name(), undo_hdr.page_type.name())?;
288            wprintln!(writer, "Log Start Offset: {}", undo_hdr.start)?;
289            wprintln!(writer, "Free Offset: {}", undo_hdr.free)?;
290            wprintln!(
291                writer,
292                "Used Bytes: {}",
293                undo_hdr.free.saturating_sub(undo_hdr.start)
294            )?;
295
296            if let Some(seg_hdr) = UndoSegmentHeader::parse(page_data) {
297                wprintln!(writer, "Segment State: {}", seg_hdr.state.name())?;
298                wprintln!(writer, "Last Log Offset: {}", seg_hdr.last_log)?;
299            }
300        }
301    }
302
303    // FIL Trailer
304    wprintln!(writer)?;
305    let ps = page_size as usize;
306    if page_data.len() >= ps {
307        let trailer_offset = ps - 8;
308        if let Some(trailer) =
309            crate::innodb::page::FilTrailer::parse(&page_data[trailer_offset..])
310        {
311            wprintln!(writer, "=== TRAILER: Page {}", header.page_number)?;
312            wprintln!(writer, "Old-style Checksum: {}", trailer.checksum)?;
313            wprintln!(writer, "Low 32 bits of LSN: {}", trailer.lsn_low32)?;
314            wprintln!(writer, "Byte End: {}", format_offset(byte_end))?;
315
316            if verbose {
317                let csum_result = checksum::validate_checksum(page_data, page_size);
318                let status = if csum_result.valid {
319                    "OK".green().to_string()
320                } else {
321                    "MISMATCH".red().to_string()
322                };
323                wprintln!(
324                    writer,
325                    "Checksum Status: {} ({:?})",
326                    status, csum_result.algorithm
327                )?;
328
329                let lsn_valid = checksum::validate_lsn(page_data, page_size);
330                let lsn_status = if lsn_valid {
331                    "OK".green().to_string()
332                } else {
333                    "MISMATCH".red().to_string()
334                };
335                wprintln!(writer, "LSN Consistency: {}", lsn_status)?;
336            }
337        }
338    }
339
340    Ok(())
341}
342
343/// Print the INDEX page header details.
344fn print_index_header(idx: &IndexHeader, page_num: u32, verbose: bool, writer: &mut dyn Write) -> Result<(), IdbError> {
345    wprintln!(writer, "=== INDEX Header: Page {}", page_num)?;
346    wprintln!(writer, "Index ID: {}", idx.index_id)?;
347    wprintln!(writer, "Node Level: {}", idx.level)?;
348
349    if idx.max_trx_id > 0 {
350        wprintln!(writer, "Max Transaction ID: {}", idx.max_trx_id)?;
351    } else {
352        wprintln!(writer, "-- Secondary Index")?;
353    }
354
355    wprintln!(writer, "Directory Slots: {}", idx.n_dir_slots)?;
356    if verbose {
357        wprintln!(writer, "-- Number of slots in page directory")?;
358    }
359
360    wprintln!(writer, "Heap Top: {}", idx.heap_top)?;
361    if verbose {
362        wprintln!(writer, "-- Pointer to record heap top")?;
363    }
364
365    wprintln!(writer, "Records in Page: {}", idx.n_recs)?;
366    wprintln!(
367        writer,
368        "Records in Heap: {} (compact: {})",
369        idx.n_heap(),
370        idx.is_compact()
371    )?;
372    if verbose {
373        wprintln!(writer, "-- Number of records in heap")?;
374    }
375
376    wprintln!(writer, "Start of Free Record List: {}", idx.free)?;
377    wprintln!(writer, "Garbage Bytes: {}", idx.garbage)?;
378    if verbose {
379        wprintln!(writer, "-- Number of bytes in deleted records.")?;
380    }
381
382    wprintln!(writer, "Last Insert: {}", idx.last_insert)?;
383    wprintln!(
384        writer,
385        "Last Insert Direction: {} - {}",
386        idx.direction,
387        idx.direction_name()
388    )?;
389    wprintln!(writer, "Inserts in this direction: {}", idx.n_direction)?;
390    if verbose {
391        wprintln!(writer, "-- Number of consecutive inserts in this direction.")?;
392    }
393
394    Ok(())
395}
396
397/// Print FSEG (file segment) header details.
398fn print_fseg_headers(page_data: &[u8], page_num: u32, idx: &IndexHeader, verbose: bool, writer: &mut dyn Write) -> Result<(), IdbError> {
399    wprintln!(writer, "=== FSEG_HDR - File Segment Header: Page {}", page_num)?;
400
401    if let Some(leaf) = FsegHeader::parse_leaf(page_data) {
402        wprintln!(writer, "Inode Space ID: {}", leaf.space_id)?;
403        wprintln!(writer, "Inode Page Number: {}", leaf.page_no)?;
404        wprintln!(writer, "Inode Offset: {}", leaf.offset)?;
405    }
406
407    if idx.is_leaf() {
408        if let Some(internal) = FsegHeader::parse_internal(page_data) {
409            wprintln!(writer, "Non-leaf Space ID: {}", internal.space_id)?;
410            if verbose {
411                wprintln!(writer, "Non-leaf Page Number: {}", internal.page_no)?;
412                wprintln!(writer, "Non-leaf Offset: {}", internal.offset)?;
413            }
414        }
415    }
416
417    Ok(())
418}
419
420/// Print system records (infimum/supremum) info.
421fn print_system_records(page_data: &[u8], page_num: u32, writer: &mut dyn Write) -> Result<(), IdbError> {
422    let sys = match SystemRecords::parse(page_data) {
423        Some(s) => s,
424        None => return Ok(()),
425    };
426
427    wprintln!(writer, "=== INDEX System Records: Page {}", page_num)?;
428    wprintln!(
429        writer,
430        "Index Record Status: {} - (Decimal: {}) {}",
431        sys.rec_status,
432        sys.rec_status,
433        sys.rec_status_name()
434    )?;
435    wprintln!(writer, "Number of records owned: {}", sys.n_owned)?;
436    wprintln!(writer, "Deleted: {}", if sys.deleted { "1" } else { "0" })?;
437    wprintln!(writer, "Heap Number: {}", sys.heap_no)?;
438    wprintln!(writer, "Next Record Offset (Infimum): {}", sys.infimum_next)?;
439    wprintln!(writer, "Next Record Offset (Supremum): {}", sys.supremum_next)?;
440    wprintln!(
441        writer,
442        "Left-most node on non-leaf level: {}",
443        if sys.min_rec { "1" } else { "0" }
444    )?;
445
446    Ok(())
447}
448
449/// Print detailed FSP header with additional fields.
450fn print_fsp_header_detail(fsp: &FspHeader, page0: &[u8], verbose: bool, writer: &mut dyn Write) -> Result<(), IdbError> {
451    wprintln!(writer, "=== File Header")?;
452    wprintln!(writer, "Space ID: {}", fsp.space_id)?;
453    if verbose {
454        wprintln!(writer, "-- Offset 38, Length 4")?;
455    }
456    wprintln!(writer, "Size: {}", fsp.size)?;
457    wprintln!(writer, "Flags: {}", fsp.flags)?;
458    wprintln!(writer, "Page Free Limit: {} (this should always be 64 on a single-table file)", fsp.free_limit)?;
459
460    // Compression and encryption detection from flags
461    let comp = compression::detect_compression(fsp.flags);
462    let enc = encryption::detect_encryption(fsp.flags);
463    if comp != compression::CompressionAlgorithm::None {
464        wprintln!(writer, "Compression: {}", comp)?;
465    }
466    if enc != encryption::EncryptionAlgorithm::None {
467        wprintln!(writer, "Encryption: {}", enc)?;
468    }
469
470    // Try to read the first unused segment ID (at FSP offset 72, 8 bytes)
471    let seg_id_offset = crate::innodb::constants::FIL_PAGE_DATA + 72;
472    if page0.len() >= seg_id_offset + 8 {
473        use byteorder::ByteOrder;
474        let seg_id = byteorder::BigEndian::read_u64(&page0[seg_id_offset..]);
475        wprintln!(writer, "First Unused Segment ID: {}", seg_id)?;
476    }
477
478    Ok(())
479}
480
481/// Check if a page type matches the user-provided filter string.
482///
483/// Matches against the page type name (case-insensitive). Supports
484/// short aliases like "index", "undo", "blob", "sdi", etc.
485fn matches_page_type_filter(page_type: &PageType, filter: &str) -> bool {
486    let filter_upper = filter.to_uppercase();
487    let type_name = page_type.name();
488
489    // Exact match on type name
490    if type_name == filter_upper {
491        return true;
492    }
493
494    // Common aliases and prefix matching
495    match filter_upper.as_str() {
496        "UNDO" => *page_type == PageType::UndoLog,
497        "BLOB" => matches!(page_type, PageType::Blob | PageType::ZBlob | PageType::ZBlob2),
498        "LOB" => matches!(page_type, PageType::LobIndex | PageType::LobData | PageType::LobFirst),
499        "SDI" => matches!(page_type, PageType::Sdi | PageType::SdiBlob),
500        "COMPRESSED" | "COMP" => matches!(
501            page_type,
502            PageType::Compressed | PageType::CompressedEncrypted
503        ),
504        "ENCRYPTED" | "ENC" => matches!(
505            page_type,
506            PageType::Encrypted | PageType::CompressedEncrypted | PageType::EncryptedRtree
507        ),
508        _ => type_name.contains(&filter_upper),
509    }
510}