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
21pub struct PagesOptions {
23 pub file: String,
25 pub page: Option<u64>,
27 pub verbose: bool,
29 pub show_empty: bool,
31 pub list_mode: bool,
33 pub filter_type: Option<String>,
35 pub page_size: Option<u32>,
37 pub json: bool,
39 pub keyring: Option<String>,
41 pub mmap: bool,
43 pub deleted: bool,
45 pub csv: bool,
47 pub lob_chain: bool,
49}
50
51#[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
76pub 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 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 if !opts.show_empty && header.checksum == 0 && header.page_type == PageType::Allocated {
149 continue;
150 }
151
152 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
179fn 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
284fn 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
389fn 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
444fn 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 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 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 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 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 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 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 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 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 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
680fn 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
742fn 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
775fn 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
812fn 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 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 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 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
871fn 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 if !is_lob_start {
892 return Ok(());
893 }
894
895 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
930fn 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 if type_name == filter_upper {
940 return true;
941 }
942
943 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}